zoukankan      html  css  js  c++  java
  • 排序算法

    衡量排序算法的几个概念

    衡量排序算法的几个概念,如下:

    执行效率(时间复杂度)

    内存消耗(空间复杂度)

    原地排序/Sorted in place (空间复杂度为 O(1) )

    原地排序算法,就是特指空间复杂度是 O(1) 的排序算法。(即,在排序时,不产生新的数列,只在原数列对数列中的元素进行交换位置)

    属于原地排序算法有:冒泡排序、选择排序、插入排序(希尔排序)、快排

    稳定性

    简单的说,两个值相等的元素在排序后前后顺序不变,称为稳定;反之(即,不一定不变),称为不稳定。

    一、冒泡排序

    每一次遍历,较大数都要向右(or 向左)移动(像冒泡效果)。

    时间复杂度为:

    • 最优时间复杂度 -- O(n),即只遍历一遍
    • 最坏时间复杂度 -- O(n^2),跑完所有循环

    稳定性:稳定

    public static void main(String[] args) {
        int[] disorderArray = {54, 26, 93, 17, 77, 31, 44, 55, 20};   // 常规打乱
        int[] disorderArray2 = {17, 20, 26, 31, 44, 54, 55, 77, 93};  // 正序
        int[] disorderArray3 = {93, 77, 55, 54, 44, 31, 26, 20, 17};  // 倒序
        bubbleSort(disorderArray);  
        bubbleSort(disorderArray2);
        bubbleSort(disorderArray3);
    }
    
    public static void bubbleSort(int[] disorderArray) {
        int k = 0;  // 循环的次数
        for (int i = disorderArray.length - 1; i > 0; i--) {
            // i 为下面循环的终点。下面每次循环后,最大的数就放在最后了,所以终点减1
            boolean flag = true;
            for (int j = 0; j < i; j++) {  // 使用小于号,所以 j 只到 i - 1, 而最后的 j + 1 刚好是 i
                k++;
                if (disorderArray[j] > disorderArray[j + 1]) {
                    // 每一次将较大的数后移,即冒泡效果
                    int temp = disorderArray[j];
                    disorderArray[j] = disorderArray[j + 1];
                    disorderArray[j + 1] = temp;
                    flag = false;
                }
            }
            if (flag) {  // 优化排序
                // 内层没有发生交换即这一次的循环是顺的,所以整个已经顺了,则退出循环;
                break;
            }
        }
        System.out.println("循环的次数:" + k);
    }
    

    二、选择排序

    移动次数最少。

    选择一个最小(or 最大)数与首位(or 末位)交换位置,放好的数在下一次内存层循环时剔除。

    时间复杂度:

    • 最优时间复杂度-- O(n^2)
    • 最坏时间复杂度-- O(n^2)

    稳定性:不稳定

    public static void main(String[] args) {
        int[] disorderArray = {54, 26, 93, 17, 77, 31, 44, 55, 20};   // 常规打乱
        int[] disorderArray2 = {17, 20, 26, 31, 44, 54, 55, 77, 93};  // 正序
        int[] disorderArray3 = {93, 77, 55, 54, 44, 31, 26, 20, 17};  // 倒序
        selectionSort(disorderArray);  
        selectionSort(disorderArray2);
        selectionSort(disorderArray3);
    }
    
    public static void selectionSort(int[] disorderArray) {
        // 其中外层循环中的 i 表示:待排数列的 首位(or 末位)
        int k = 0;
        int n = disorderArray.length;
        for (int i = 0; i < n - 1; i++) { // 需要进行 n - 1 次交换位置
            int minNumIndex = i;
            // 每完成内层循环,都找出当前情况下最小数所在的位置
            for (int j = i + 1; j < n; j++) {  // j 的最大值为 n - 1
                k++;
                if (disorderArray[j] < disorderArray[minNumIndex]) {
                    minNumIndex = j;
                }
            }
            // 将最小数所在的位置放置在第 i 位
            if (minNumIndex != i) {
                int temp = disorderArray[i];
                disorderArray[i] = disorderArray[minNumIndex];
                disorderArray[minNumIndex] = temp;
            }
        }
        System.out.println("循环的次数:" + k);
    }
    

    三、插入排序

    选择一个数,插入到有序数列中

    思路步骤:

    1. 先将第一个位置的元素是为有序序列,
    2. 从第二个位置(即下标为1的元素)开始取出(即,待插入的数),
    3. 向前比较插入有序序列里,此时有序序列的长度加一。

    其中

    边界条件:待插入的数,比较完有序序列的所有数后,仍然没找到位置,那说明它的位置应该是放在首位

    时间复杂度:和冒泡一样

    • 最优时间复杂度:O(n)
    • 最坏时间复杂度:O(n2)

    稳定性:稳定

    public static void main(String[] args) {
        int[] disorderArray = {54, 26, 93, 17, 77, 31, 44, 55, 20};   // 常规打乱
        int[] disorderArray2 = {17, 20, 26, 31, 44, 54, 55, 77, 93};  // 正序
        int[] disorderArray3 = {93, 77, 55, 54, 44, 31, 26, 20, 17};  // 倒序
        insertSort(disorderArray);  
        insertSort(disorderArray2);
        insertSort(disorderArray3);
    }
    
    public static void insertSort(int[] disorderArray) {
        int k = 0;
        for (int i = 1; i < disorderArray.length; i++) {
            int temp = disorderArray[i];  // 待插入的数为:disorderArray[i],并将其取出,取出后该位置可用,后续较大的数可往此方向移动 或 为插入使用
            for (int j = i - 1; j >= 0; j--) {  // 往前插入,往前算,所以使用 --
                k++;
                if (temp < disorderArray[j]) {
                    disorderArray[j + 1] = disorderArray[j];
                    if (j == 0) {  // 已经没得比较了,就把它放在首位。(边界条件)
                        disorderArray[0] = temp;
                    }
                } else {
                    disorderArray[j + 1] = temp;
                    break;
                }
            }
        }
        System.out.println("循环的次数:" + k);
    }
    

    四、快速排序

    这里举例是从小到大排序。快排使用的递归实现,体现分治思想。

    思路步骤:

    1. (分治思想的执行方式之一)选择一个基准(一般选择数列中的第一元素),然后遍历数列,

      ​ 将不小于基准的数放右边(称为--较大数列),小于基准的放左边(较小数列),基准放中间。

    2. 然后再分别对 较大数列 和 较小数列 执行上面一步(即,递归的方式)。

    3. 递归结束的条件:数列只有1个数或0个数时,即数列 的 开始位置start >= 结束位置end

    问题1(步骤2的细节):如何切出 较大(小)数列---从原数列开始位置start(or 结束位置end) 到 基准位置来切

    问题2(步骤1的细节):如何将较小数抛到左边,较大数抛到右边 --- 定义两个游标,轮流从数列的左右两边遍历数列

    1. 将最左边的数(即,数列的开始位置)取出,作为基准数。此时左边该位置可用。
    2. 从右边遍历,直到找到一个较小数抛到左边可用的位置。此时右边便有一个可用位置(找到就较小数就停止遍历)
    3. 接着从左边遍历,直到找到一个较小数抛到右边可用的位置。此时左边便有一个可用位置。(找到就较大数就停止遍历)
    4. 2 和 3 循环轮流,直到 左右两边的游标重合时,结束所有循环(2、3的遍历,以及2、3之间的循环轮流)

    问题3(步骤1的细节):基准数放在哪个位置 --- 当两个游标重合时,该位置就是基准数的位置。

    时间复杂度:

    • 最优时间复杂度:O(nlogn)
    • 最坏时间复杂度:O(n2)

    稳定性:不稳定

    public static void main(String[] args) {
        int[] disorderArray = {54, 26, 93, 17, 77, 31, 44, 55, 20};   // 常规打乱
        int[] disorderArray2 = {17, 20, 26, 31, 44, 54, 55, 77, 93};  // 正序
        int[] disorderArray3 = {93, 77, 55, 54, 44, 31, 26, 20, 17};  // 倒序
        quickSort(disorderArray, 0, disorderArray.length-1);
        quickSort(disorderArray2, 0, disorderArray.length-1);
        quickSort(disorderArray3, 0, disorderArray.length-1);
    }
    
    public static void quickSort(int[] disorderArray, int start, int end) {
        if (start >= end) {  // 步骤3
            return;
        }
        int low = start;  // 问题2
        int high = end;  // 问题2
        int mid = disorderArray[start];  // 问题2.1
    
        while (low < high) {  // 问题2.4
            while (low < high && disorderArray[high] >= mid) {  // 问题2.2
                times++;
                high--;
            }
            disorderArray[low] = disorderArray[high];  // 问题2.2
    
            while (low < high && disorderArray[low] < mid) {  // 问题2.3
                times++;
                low++;
            }
            disorderArray[high] = disorderArray[low];  // 问题2.3
        }
        disorderArray[low] = mid;  // 问题3
    
        // 步骤2、问题1
        quickSort(disorderArray, start, low - 1);
        quickSort(disorderArray, low + 1, end);
    }
    

    合理选择分区点

    如果数据原来就是有序的或者接近有序的,每次分区点都选择最后一个数据,那快速排序算法就会变得非常糟糕,时间复杂度就会退化为 O(n2)。 最理想的分区点是:被分区点分开的两个分区中,数据的数量差不多。

    三数取中法
    从区间的首、尾、中间,分别取出一个数,然后对比大小,取这 3 个数的中间值作为分区点。这样每间隔某个固定的长度,取数据出来比较,将中间值作为分区点的分区算法,肯定要比单纯取某一个数据更好。 但是,如果要排序的数组比较大,那“三数取中”可能就不够了,可能要“五数取中”或者“十数取中”。

    随机法
    每次从要排序的区间中,随机选择一个元素作为分区点。这种方法并不能保证每次分区点都选的比较好,但是从概率的角度来看,也不大可能会出现每次分区点都选的很差的情况,所以平均情况下,这样选的分区点是比较好的。时间复杂度退化为最糟糕的 O(n2) 的情况,出现的可能性不大。

    警惕堆栈溢出

    快排是用递归实现的,递归要警惕堆栈溢出。为了避免快速排序里,递归过深而堆栈过小,导致堆栈溢出,我们有两种解决办法:
    第一种是限制递归深度。一旦递归过深,超过了我们事先设定的阈值,就停止递归。 第二种是通过在堆上模拟实现一个函数调用栈,手动模拟递归压栈、出栈的过程,这样就没有了系统栈大小的限制。

    五、归并排序

    也是分治的思想(二分法来分)。将数列递归分成最小数列(一个元素的数列,视为有序数列),再有序两两合并成有序数列(递归到将所有数列合成为一个)

    思路步骤: 先分后合

    1. 先将数列分为两个子数列,再将子数列分为两个子子数列(即,递归),直到细分后的数列元素小于等于1个。

      1-1. 方式一、将拆分的数列放在新的数列里

      1-2.方式二:用游标来表示查分,(没有新的数列产生)---- 代码采用次方式

    2. 将切分后的数列排序 并且向上合并。(关键点:最小的元素为1个,视为有序。合并操作要保证 合并后的数列也要有序)

    时间复杂度:

    • 最优时间复杂度:O(nlogn)
    • 最坏时间复杂度:O(nlogn)

    稳定性:稳定

    public static void main(String[] args) {
        int[] disorderArray = {54, 26, 93, 17, 77, 31, 44, 55, 20};   // 常规打乱
        int[] disorderArray2 = {17, 20, 26, 31, 44, 54, 55, 77, 93};  // 正序
        int[] disorderArray3 = {93, 77, 55, 54, 44, 31, 26, 20, 17};  // 倒序
        mergeSort(disorderArray, 0, disorderArray.length-1);
        mergeSort(disorderArray2, 0, disorderArray.length-1);
        mergeSort(disorderArray3, 0, disorderArray.length-1);
    }
    
    public static void mergeSort(int[] disorderArray, int leftIndex, int rightIndex) {
    
        int mid = leftIndex + (rightIndex - leftIndex) / 2;
        if (leftIndex < rightIndex) {  //
            mergeSort(disorderArray, leftIndex, mid);  // 步骤1
            mergeSort(disorderArray, mid + 1, rightIndex);  // 步骤1
            merge(disorderArray, leftIndex, mid, rightIndex);  // 步骤2
        }
    }
    
    public static void merge(int[] disorderArray, int leftIndex, int mid, int rightIndex) {
        int i = leftIndex;
        int j = mid + 1;
        int k = 0;
        int[] tmp = new int[rightIndex - leftIndex + 1];
        while (i <= mid && j <= rightIndex) {
            tmp[k++] = disorderArray[i] < disorderArray[j] ? disorderArray[i++] : disorderArray[j++];
        }
        while (i <= mid) {
            tmp[k++] = disorderArray[i++];
        }
        while (j <= rightIndex) {
            tmp[k++] = disorderArray[j++];
        }
        // 将tmp中的数据拷贝到a[low...high]
        for (int x = 0; x < tmp.length; ++x) {
            disorderArray[x + leftIndex] = tmp[x];
        }
    }
    

    补充:

    合并后的数列是新的数列。也可将新数列拷贝到原数列上,但本质还是产生了新数列。所以空间复杂度高(是 O(n))(即,不是原地排序)

    总结:常见排序算法效率比较

    排序算法 平均情况 最好情况 最差情况 稳定性 备注
    冒泡排序 O(n^2) O(n) O(n^2) 稳定 适合数据量小
    选择排序 O(n^2) O(n^2) O(n^2) 不稳定 适合数据量小
    插入排序 O(n^2) O(n) O(n^2) 稳定 适合数列原时序较好的
    希尔排序 不稳定 特殊的插入排序,与步长有关
    快速排序 O(nlogn) O(nlogn) O(n^2) 不稳定 适合数据量大的
    归并排序 O(nlogn) O(nlogn) O(nlogn) 稳定 适合数据量大的
  • 相关阅读:
    GetTickCount 和getTickCount
    载入其他同名源文件导致vs编译错误
    opencv的配置
    VS05错误:部署WEB文件失败
    c++移动文件夹
    opencv2.4.0版本不支持Mat的大小自动调整?
    关于c++中public & private方法调用问题
    c++读取文件夹及子文件夹数据
    深入理解java虚拟机-第四章
    深入理解java虚拟机-第三章
  • 原文地址:https://www.cnblogs.com/roronoa-wang/p/13340375.html
Copyright © 2011-2022 走看看