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) 稳定 适合数据量大的
  • 相关阅读:
    形象理解ERP(转)
    禁用windows server 2008 域密码复杂性要求策略
    How to adding find,filter,remove filter on display method Form
    Windows Server 2008 R2激活工具
    How to using bat command running VS development SSRS report
    Creating Your First Mac AppGetting Started
    Creating Your First Mac AppAdding a Track Object 添加一个 Track 对象
    Creating Your First Mac AppImplementing Action Methods 实现动作方法
    Creating Your First Mac AppReviewing the Code 审查代码
    Creating Your First Mac AppConfiguring the window 设置窗口
  • 原文地址:https://www.cnblogs.com/roronoa-wang/p/13340375.html
Copyright © 2011-2022 走看看