【尚硅谷数据结构】第 7 章 排序算法

参考:https://blog.csdn.net/oneby1314/category_10231585.html

1、排序算法介绍

1.1、排序算法的简介

  • 排序也称排序算法(Sort Algorithm), 排序是将一组数据, 依指定的顺序进行排列的过程。

1.2、排序算法的分类

  • 内部排序:指将需要处理的所有数据都加载到内部存储器(内存)中进行排序。

  • 外部排序法:数据量过大, 无法全部加载到内存中, 需要借助外部存储(文件等)进行排序。

  • 常见的排序算法分类

在这里插入图片描述

2、算法的复杂度

2.1、时间复杂度的度量方法

  • 事后统计的方法:这种方法可行, 但是有两个问题:
    • 一是要想对设计的算法的运行性能进行评测, 需要实际运行该程序;
    • 二是所得时间的统计量依赖于计算机的硬件、 软件等环境因素, 这种方式, 要在同一台计算机的相同状态下运行, 才能比较哪个算法速度更快。
  • 事前估算的方法:通过分析某个算法的时间复杂度来判断哪个算法更优

2.2、时间频度

  • 基本介绍时间频度: 一个算法花费的时间与算法中语句的执行次数成正比例, 哪个算法中语句执行次数多, 它花费时间就多。 一个算法中的语句执行次数称为语句频度或时间频度。 记为 T(n)。 [举例说明]

  • 举例说明-基本案例:比如计算 1-100 所有数字之和,我们设计两种算法:

在这里插入图片描述

举例说明-忽略常数项:

  • 2n+20 和 2n 随着 n 变大, 执行曲线无限接近, 20 可以忽略

  • 3n+10 和 3n 随着 n 变大, 执行曲线无限接近, 10 可以忽略

在这里插入图片描述

举例说明-忽略低次项:

  • 2n^2+3n+10 和 2n^2 随着 n 变大, 执行曲线无限接近, 可以忽略 3n+10

  • n^2+5n+20 和 n^2 随着 n 变大,执行曲线无限接近, 可以忽略 5n+20

在这里插入图片描述

2.3、时间复杂度

  • 一般情况下, 算法中的基本操作语句的重复执行次数是问题规模 n 的某个函数, 用 T(n)表示, 若有某个辅助函数 f(n), 使得当 n 趋近于无穷大时, T(n) / f(n) 的极限值为不等于零的常数, 则称 f(n)是 T(n)的同数量级函数。记作 T(n)=O ( f(n) ), 称O ( f(n) ) 为算法的渐进时间复杂度, 简称时间复杂度。
  • T(n) 不同, 但时间复杂度可能相同。 如: T(n)=n²+7n+6 与 T(n)=3n²+2n+2 它们的 T(n) 不同, 但时间复杂度相同, 都为 O(n²)。
  • 计算时间复杂度的方法:
    • 用常数 1 代替运行时间中的所有加法常数 T(n)=n²+7n+6 => T(n)=n²+7n+1
    • 修改后的运行次数函数中, 只保留最高阶项 T(n)=n²+7n+1 => T(n) = n²
    • 去除最高阶项的系数 T(n) = n² => T(n) = n² => O(n²)

2.4、常见的时间复杂度

2.4.1、常见时间复杂度概述

  • 常见时间复杂度

    • 常数阶 O(1)
    • 对数阶 O(log2n)
    • 线性阶 O(n)
    • 线性对数阶 O(nlog2n)
    • 平方阶 O(n^2)
    • 立方阶 O(n^3)
    • k 次方阶 O(n^k)
    • 指数阶 O(2^n)
  • 结论:

    • 常见的算法时间复杂度由小到大依次为: Ο (1)<Ο (log2n)<Ο (n)<Ο (nlog2n)<Ο (n2)<Ο (n3)< Ο (nk) < Ο (2n) , 随着问题规模 n 的不断增大, 上述时间复杂度不断增大, 算法的执行效率越低

    • 从图中可见, 我们应该尽可能避免使用指数阶的算法

在这里插入图片描述

2.4.2、常数阶 O(1)

  • 无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1)

  • 代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。

在这里插入图片描述

2.4.3、对数阶 O(log2n)

在这里插入图片描述

2.4.4、线性阶 O(n)

  • 说明:这段代码,for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度

在这里插入图片描述

2.4.5、线性对数阶 O(nlogN)

  • 说明:线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)

在这里插入图片描述

2.4.6、平方阶 O(n²)

  • 说明:平方阶O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²),这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(nn),即 O(n²) 如果将其中一层循环的n改成m,那它的时间复杂度就变成了 O(mn)

2.4.7、其他阶

  • 立方阶 O(n³)、 K 次方阶 O(n^k)
  • 说明: 参考上面的 O(n²) 去理解就好了, O(n³)相当于三层 n 循环, 其它的类似

2.5、平均和最坏时间复杂度

  • 平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下, 该算法的运行时间。

  • 最坏情况下的时间复杂度称最坏时间复杂度。 一般讨论的时间复杂度均是最坏情况下的时间复杂度。 这样做的原因是: 最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限, 这就保证了算法的运行时间不会比最坏情况更长。

  • 平均时间复杂度和最坏时间复杂度是否一致, 和算法有关(如图)。

在这里插入图片描述

2.6、算法的空间复杂度

  • 类似于时间复杂度的讨论, 一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间, 它也是问题规模 n 的函数。
  • 空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。 有的算法需要占用的临时工作单元数与解决问题的规模 n 有关, 它随着 n 的增大而增大, 当 n 较大时, 将占用较多的存储单元, 例如快速排序和归并排序算法, 基数排序就属于这种情况
  • 在做算法分析时, 主要讨论的是时间复杂度。 从用户使用体验上看, 更看重的程序执行的速度。 一些缓存产品(redis, memcache)和算法(基数排序)本质就是用空间换时间

3、冒泡排序

3.1、基本介绍

  • 冒泡排序(Bubble Sorting) 的基本思想是: 通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值, 若发现逆序则交换, 使值较大的元素逐渐从前移向后部, 就象水底下的气泡一样逐渐向上冒。
  • 优化:因为排序的过程中, 各元素不断接近自己的位置, 如果一趟比较下来没有进行过交换, 就说明序列有序, 因此要在排序过程中设置一个标志 flag 判断元素是否进行过交换。 从而减少不必要的比较冒泡排序的优化

3.2、冒泡排序图解

  1. 一共比较n-1趟
  2. 每趟比较 n - i 次

在这里插入图片描述

3.3、代码实现

    // 老师上课时候的版本
    public static void bubbleSort(int[] arr) {
    
    
        int temp = 0;
        // 循环n-1趟
        for (int i = 0; i < arr.length - 1; i++) {
    
    
            // 优化冒泡排序算法
            // 若在一趟排序中没有发生元素位置发生变化则提前结束循环
            boolean flag = false; // 表示变量,表示是否进行过交换
            for (int j = 0; j < arr.length - 1 - i; j++) {
    
    
                if (arr[j] > arr[j+1]) {
    
    
                    flag = true;
                    temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
            //System.out.println("第" + (i+1) + "趟排序后的数组");
            //System.out.println(Arrays.toString(arr));
            if (!flag) {
    
    
                break;
            }
        }
    }

    /**
     * @Description: 我自己写的冒泡排序,i从1开始表示第一趟
     * @Param: [arr]
     * @Return: void
     * @Author: Daniel
     * @Date: 2020/11/20
     */
    public static void bubbleSort_mine(int[] arr) {
    
    
        int len = arr.length;
        // i 代表循环趟数,共循环n-1趟
        for (int i = 1; i < len; i++) {
    
    
            for (int j = 0; j < len - i; j++) {
    
    
                // 这里写的有冗余问题,因为这样写会新建 (n-1)*(n+1)/2 个temp对象
                // 完全没有必要!直接把temp放在外面即可
                // temp只是一个临时变量而已
                int temp;
                if (arr[j] > arr[j+1]) {
    
    
                    temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
        }
    }
  • 测试用例

    使用Math.random( )生成80000个随机数,进行冒泡排序;

        public static void main(String[] args) {
          
          
            int[] arr = new int[80000];
            for (int i = 0; i < arr.length; i++) {
          
          
                arr[i] = (int) (Math.random()*8000000); // 生成一个[0,800000)的数
            }
    
            Date date1 = new Date();
            SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String date1Str = sf.format(date1);
            System.out.println("排序前的时间是 = " + date1Str);
    
            bubbleSort(arr);
    
            Date date2 = new Date();
            String date2Str = sf.format(date2);
            System.out.println("排序后的时间是= " + date2Str);
        }
    
  • 试验结果

    排序前的时间是 = 2020-11-20 10:18:11
    排序后的时间是= 2020-11-20 10:18:21
    

4、选择排序

4.1、选择排序基本介绍

  • 选择排序也属于内部排序法,是从欲排序的数组中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。

4.2、选择排序思想

对n个数进行选择排序:

​ 外循环n-1趟:

​ 每一趟找到剩下的未排序的最小值,记录其下标,然后将 arr[i] 与 该最小值互换位置。

  • 选择排序(select sorting) 也是一种简单的排序方法。 它的基本思想是(n 是数组大小):
    • 第一次从 arr[0]~arr[n-1]选取最小值,与 arr[0] 交换
    • 第二次从 arr[1]~arr[n-1]中选取最小值, 与 arr[1] 交换
    • 第三次从 arr[2]~arr[n-1]中选取最小值, 与 arr[2] 交换, …,
    • 第 i 次从 arr[i-1]~arr[n-1]中选取最小值, 与 arr[i-1] 交换, …,
    • 第 n-1 次从 arr[n-2]~arr[n-1]中选取最小值,与 arr[n-2] 交换,
    • 总共通过 n-1 次, 得到一个按排序码从小到大排列的有序序列。

4.3、选择排序图解

在这里插入图片描述

  1. 选择排序一共有数组大小减1轮排序
  2. 每1轮排序,又是一个循环,循环的规则
    • 先假定当前这个数是最小的
    • 然后和后面的每个数进行比较,如果发现有比当前更小的数,就重新确定最小数,得到下标
    • 当遍历到数组最后时,就得到本轮最小数和下标
    • 将第i个数和选出来的最小数互换位置

4.4、代码实现

    public static void selectSort(int[] arr) {
    
    
        for (int i = 0; i < arr.length - 1; i++) {
    
    
            int minIndex = i;
            int min = arr[i];
            for (int j = i + 1; j < arr.length; j ++) {
    
    
                if (min > arr[j]) {
    
     // 说明假定的最小值,并不是最小
                    min = arr[j]; //重置min
                    minIndex = j;
                }
            }
            // 将最小值,放在arr[i],即交换
            if (minIndex != i) {
    
    
                arr[minIndex] = arr[i];
                arr[i] = min;
            }
        }
    }

4.5、总结

  • 由于选择排序算法在最内层的 for 循环中,满足 if (min > arr[j]) { 条件后,只需要记录最小值和最小值在数组中的索引,无需像冒泡排序那样每次都要执行交换操作,所以选择排序算法的执行速度比冒泡排序算法快一些

5、插入排序

5.1、插入排序基本介绍

  • 插入式排序属于内部排序法, 是对于欲排序的元素以插入的方式找寻该元素的适当位置, 以达到排序的目的。

5.2、插入排序思想

  • 插入排序(Insertion Sorting)的基本思想是:把n个待排序的元素看成一个有序表一个无序表
  • 开始时,有序表中只含有一个元素,无序表中含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表

5.3、插入排序图解

  • 插入排序逻辑:

    • 首先,将数组分为两个数组,前部分有序数组,后部分是无序数组,我们的目的就是一点一点取出无序数组中的值,将其放到有序数组中区
    • 第一趟:arr[0] 作为有序数组的元素,arr[1] 作为无序数组中第一个元素,将 arr[1] 与 arr[0] 比较,目标是将 arr[1] 插入到有序数组中
    • 第一趟:arr[0] 和 arr[1] 作为有序数组的元素,arr[2] 作为无序数组中第一个元素,将 arr[2] 与 arr[0] 和 arr[1] 比较,目标是将 arr[2] 插入到有序数组中
    • 第 i 趟:arr[0]~arr[i] 作为有序数组的元素,arr[i+1] 作为无序数组中第一个元素,将 arr[i+1] 与 arr[0]~arr[i] 比较,目标是将 arr[i+1] 插入到有序数组中
    • 第 n-1 趟:此时有序数组为 arr[0]~arr[n-2] ,无序数组为 arr[n-1] ,将无序数组中最后一个元素插入到有序数组中即可
    • 如何进行插入?
      • 假设有个指针(index),指向无序数组中的第一个元素,即 arr[index] 是无序数组中的第一个元素,我们定义一个变量来存储该值:int insertVal = arr[index];,现在要将其插入到前面的有序数组中
      • 将 index 前移一步,则指向有序数组最后一个元素,我们定义一个新的变量来存储该指针:insertIndex = index - 1; ,即 arr[insertIndex] 是有序数组最后一个元素
      • 我们需要找到一个比 insertVal 小的值,并将 insertVal 插入在该值后面:
        • 如果 insertVal > arr[insertIndex] ,执行插入
        • 如果 insertVal < arr[insertIndex] ,将有序数组后移,腾出插入空间,insertIndex 指针前移,再看看前一个元素满不满足条件,直到找到插入位置
        • 即循环终止条件为找到插入位置,又分为两种情况:
          • 在有序数组中间找到插入位置
          • insertVal 比有序数组中所有的数都小,插入在数组第一个位置(insertIndex = 0 的情况)
  • 总结:两层循环

    • for 循环控制走多少趟:for(int i = 1; i < arr.length; i++) { ,从数组第一个元素开始到数组最后一个元素结束

    • while 循环不断将指针前移,在有序数组中寻找插入位置,并执行插入:

      while (insertIndex >= 0 && insertVal < arr[insertIndex]) {

5.43、代码实现

    // 插入排序
    public static void insertSort(int[] arr) {
    
    
        int insertVal = 0;
        int insertIndex = 0;
        // 使用for循环来把代码简化
        for (int i = 1; i < arr.length; i++) {
    
    
            // 定义待插入的数
            insertVal = arr[i];
            insertIndex = i - 1;

            // 给insertValue找到插入的位置
            // 说明
            // 1. insertIndex >= 0 保证在给 insertVal 找插入位置,不越界
            // 2. insertVal < arr[insertIndex] 待插入的数,还没有找到插入位置
            // 3. 就需要将arr[insertIndex] 后移
            while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
    
    
                arr[insertIndex + 1] = arr[insertIndex];
                insertIndex--;
            }

            // 优化
            if (insertIndex != i) {
    
    
                arr[insertIndex + 1] = insertVal;
            }

            //System.out.println("第" + i + "轮插入");
            //System.out.println(Arrays.toString(arr));
        }
    }

6、希尔排序

希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。

6.1、简单插入排序问题

  • 我们看简单的插入排序可能存在的问题,数组 arr = { 2, 3, 4, 5, 6, 1 } 这时需要插入的数 1(最小),简单插入排序的过程如下

  • 结论: 当需要插入的数是较小的数时, 后移的次数明显增多, 对效率有影响

    {2,3,4,5,6,6}
    {2,3,4,5,5,6}
    {2,3,4,4,5,6}
    {2,3,3,4,5,6}
    {2,2,3,4,5,6}
    {1,2,3,4,5,6}
    

6.2、希尔排序基本介绍

  • 希尔排序是希尔(Donald Shell) 于 1959 年提出的一种排序算法。 希尔排序也是一种插入排序, 它是简单插入排序经过改进之后的一个更高效的版本, 也称为缩小增量排序。

6.3、希尔排序基本思想

  • 希尔排序按照增量将数组进行分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止

6.4、希尔排序图解(交换法)

第一次:gap = arr.length/5 = 5 , 将数组分为五组,每个数组元素的索引相差 5

  • 如何完成第一次的排序?

    • 仔细想想,我们需要用一次循环将每组中的元素排序
    • 总共有五组,我们又需要一次循环
    • 所以完成每次排序,需要两层循环
  • 程序代码如下,把 i ,j 都看作是辅助指针:

    • i 与 j 配合使用,可以将指针从数组第一个元素,移动至最后一个元素,目的:把数组遍历一遍

    • j 与 i 配合使用,每次都从数组索引 i 处往前遍历,每次向前移动 gap 个位置,然后进行交换(冒泡排序的意思):看看前面的元素有没有比我的值大,如果前面的元素比我的值大,我就要和他交换位置,跑到前面去

      // 希尔排序的第1轮排序
      // 因为第1轮排序,是将10个数据分成了 5组
      /* 为什么从5开始?
         因为从5到10有5趟循环,5组会产生5个最小值;
      */
      for (int i = 5; i < arr.length; i++) {
              
              
          // 遍历各组中所有的元素(共5组,每组有2个元素), 步长5
          for (int j = i - 5; j >= 0; j -= 5) {
              
              
              // 如果当前元素大于加上步长后的那个元素,说明交换
              if (arr[j] > arr[j + 5]) {
              
              
                  temp = arr[j];
                  arr[j] = arr[j + 5];
                  arr[j + 5] = temp;
              }
          }
      }
      
    • 第二次:gap = gap /2 = 2; , 将数组分为两组,每个数组元素的索引相差 2

      • 第一组:
        • i = 2 时,数组从索引 2 处往前遍历,间隔为 2 :将 arr[0]、arr[2] 排序
        • i = 4 时,数组从索引 4 处往前遍历,间隔为 2 :将 arr[0]、arr[2]、arr[4] 排序
        • i = 6 时,数组从索引 6 处往前遍历,间隔为 2 :将 arr[0]、arr[2]、arr[4]、arr[6] 排序
        • i = 8 时,数组从索引 8 处往前遍历,间隔为 2 :将 arr[0]、arr[2]、arr[4]、arr[6]、arr[8] 排序
      • 第二组:
        • i = 3 时,数组从索引 3 处往前遍历,间隔为 2 :将 arr[1]、arr[3] 排序
        • i = 5 时,数组从索引 5 处往前遍历,间隔为 2 :将 arr[1]、arr[3]、arr[5] 排序
        • i = 7 时,数组从索引 7 处往前遍历,间隔为 2 :将 arr[1]、arr[3]、arr[5]、arr[7] 排序
        • i = 9 时,数组从索引 9 处往前遍历,间隔为 2 :将 arr[1]、arr[3]、arr[5]、arr[7]、arr[9] 排序
      // 希尔排序的第2轮排序
      // 因为第2轮排序,是将10个数据分成了 5/2 = 2组
      for (int i = 2; i < arr.length; i++) {
              
              
          // 遍历各组中所有的元素(共5组,每组有2个元素), 步长5
          for (int j = i - 2; j >= 0; j -= 2) {
              
              
              // 如果当前元素大于加上步长后的那个元素,说明交换
              if (arr[j] > arr[j + 2]) {
              
              
                  temp = arr[j];
                  arr[j] = arr[j + 2];
                  arr[j + 2] = temp;
              }
          }
      }
      System.out.println("希尔排序2轮后=" + Arrays.toString(arr));
      
    • 第三次:gap = gap /2 = 1; , 将数组分为一组,每个数组元素的索引相差 1 ,对于交换法而言,这就是异常冒泡排序

      • i = 1 时,数组从索引 1 处往前遍历,间隔为 1 :将 arr[0]、arr[1] 排序
      • i = 2 时,数组从索引 2 处往前遍历,间隔为 1 :将 arr[0]、arr[1]、arr[2] 排序
      • i = 3 时,数组从索引 3 处往前遍历,间隔为 1 :将 arr[0]、arr[1]、arr[2]、arr[3] 排序
      // 希尔排序的第3轮排序
      // 因为第3轮排序,是将10个数据分成了 2/2 = 1组
      for (int i = 1; i < arr.length; i++) {
              
              
          // 遍历各组中所有的元素(共5组,每组有2个元素), 步长5
          for (int j = i - 1; j >= 0; j -= 1) {
              
              
              // 如果当前元素大于加上步长后的那个元素,说明交换
              if (arr[j] > arr[j + 1]) {
              
              
                  temp = arr[j];
                  arr[j] = arr[j + 1];
                  arr[j + 1] = temp;
              }
          }
      }
      System.out.println("希尔排序3轮后=" + Arrays.toString(arr));
      
    • **总结:**每次使用循环改变gap的值(初始值:数组大小/2,之后:gap=gap/2),然后在改变gap的循环中嵌套上面的双层for循环

      • 改变gap:**for(int gap = arr.length/2;gap>0;gap/2) **

      • 内存循环:实现对每组数组的排序

        for(int i = gap; i < arr.length; i++) {
                  
                  
          // 遍历各组中所有的元素(共gap组,每组有?个元素),步长为gap
          for(int j = i - gap; j >=0 ; j-=gap){
                  
                  
        }
        
    • 希尔排序伪代码

          for (int gap = arr.length / 2; gap > 0; gap /= 2) {
              
              
              for (int i = gap; i < arr.length; i++) {
              
              
                  for (int j = i- gap ;j > 0; j -= gap) {
              
              
                      // 对每组进行冒泡排序
                  }
              }
          }
      

在这里插入图片描述

6.5、代码实现

6.5.1、写希尔排序(交换法)

    // 希尔排序时,对有序序列在插入时采用【交换法】
    public static void shellSort(int[] arr) {
    
    
        int temp = 0;
        int count = 0;
        // 使用循环处理
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
    
    
            for (int i = gap; i < arr.length; i++) {
    
    
                // 遍历各组中所有的元素(共gap组,每组有?个元素),步长gap
                for (int j = i - gap; j >= 0; j -= gap) {
    
    
                    // 如果当前元素大于加上步长后的那个元素,就要交换
                    if (arr[j] > arr[j + gap]) {
    
    
                        temp = arr[j];
                        arr[j] = arr[j + gap];
                        arr[j + gap] = temp;
                    }
                }
            }
            // System.out.println("希尔排序第"+ (++count) +"轮 =" + Arrays.toString(arr));
        }

6.5.2、希尔排序(移位法)

  • 编写基于插入法的希尔排序算法:

    • 记录当前位置的元素值 int temp = arr[j];,从当前元素前一个位置开始,往前寻找,每次移动gap个距离
      • 如果temp < arr [ j - gap]:
        • 将数组元素往后移,腾出插入空间 : arr[j] = arr [j - gap];
        • 然后继续往前找:j -= gap;
      • 如果 temp > arr[j - gap],找到插入位置,执行插入 arr[j] = temp; , 因为在上一步已经腾出了空间,并且将指针 j 前移,所以可以直接插入
      • 如果 找到数组最前面还是没有找到插入位置:j - gap < 0 ,则证明 temp 需要插入在数组最前面
    • 仅仅就是将之前交换法的冒泡操作替换成了插入操作
        // 增量gap,并逐步的缩小增量
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
          
          
          // 从第gap个元素,逐个对其所在的组进行直接插入排序
          for (int i = gap; i < arr.length; i++) {
          
          
            int j = i;
            int temp = arr[j];
            if (arr[j] < arr[j - gap]) {
          
          
              while (j - gap >= 0 && temp < arr[j - gap]) {
          
          
                // 移动
                arr[j] = arr[j - gap];
                j -= gap;
              }
              // 当退出while后,就给temp找到插入的位置
              arr[j] = temp;
            }
          }
        }
    
    
      // 我自己写的,根据前面插入排序算法,更好理解
      public static void shellSort_2(int[] arr) {
          
          
          int len = arr.length;
          int insertVal; // 待插入的值
          int insertIndex; // 插入位置
          for (int gap = len / 2; gap > 0; gap /= 2) {
          
          
              for (int i = gap; i < len; i++) {
          
          
                  insertVal = arr[i];
                  insertIndex = i - gap;
                  while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
          
          
                      arr[insertIndex + gap] = arr[insertIndex]; // 将有序数组后移
                      insertIndex -= gap; // 插入指针前移
                  }
                  arr[insertIndex + gap] = insertVal;
              }
          }
      }
    
    

移位法的排序速度非常快,80000个数据只需要1ms。

7、快速排序

快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

7.1、快排简介

  1. 快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
  2. 快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
  3. 快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
  4. 快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。
  5. 虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好,可是这是为什么呢,我也不知道。好在我的强迫症又犯了,查了 N 多资料终于在《算法艺术与信息学竞赛》上找到了满意的答案:
  6. 快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

7.2、代码思路

  1. 从数列中挑出一个元素,称为 “基准”(pivot);
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。
  3. 在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  4. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;

在这里插入图片描述

快排流程分析

以 {25, 84, 21, 47, 15, 27, 68, 35, 20} 数列为例(下面的流程和上面的动图其实不太一样,不过大体思想是一样的)

  1. 第一趟:val = 25; 先取出来保存着
    • { 20, 84, 21, 47, 15, 27, 68, 35, 20}
    • {20, 84, 21, 47, 15, 27, 68, 35, 84}
    • {20, 15, 21, 47, 15, 27, 68, 35, 84}
    • {20, 15, 21, 47, 47, 27, 68, 35, 84}
    • {20, 15, 21, 25, 47, 27, 68, 35, 84}
  2. 第二趟:val = 20; 先取出来保存着
    • { 15, 15, 21}
    • {15, 20, 21}
  3. 以此类推 …

7.3、代码实现

    public static void quickSort(int[] arr, int left, int right) {
    
    
        int l = left; // 左下标
        int r = right; // 右下标

        // pivot 中轴值
        int pivot = arr[(left + right) / 2];
        int temp = 0; // 临时变量,交换的时候使用
        // while循环的目的是让比pivot值小的放到左边
        // 比pivot值大的放到右边
        while ( l < r ) {
    
    
            // 在pivot的左边一直找,找到大于等于pivot值,才退出
            while (arr[l] < pivot) {
    
    
                l += 1;
            }
            // 在pivot的右边一直找,找到小于等于pivot值,才退出
            while (arr[r] > pivot) {
    
    
                r -= 1;
            }
            // 如果l >= r 说明pivot的左右两边的值,已经按照
            // 左边全部是小于等于pivot值,右边全部是大于等于pivot值
            if ( l >= r) {
    
    
                break;
            }

            // 交换
            temp = arr[l];
            arr[l] = arr[r];
            arr[r] = temp;

            // 如果交换完成后,发现这个arr[l] == pivot 值相等 r--, 前移
            if (arr[l] == pivot) {
    
    
                r -= 1;
            }
            // 如果交换完后,发现这个arr[r] == pivot 值相等 l--, 后移
            if (arr[r] == pivot) {
    
    
                l += 1;
            }
        }
        // 如果 l == r, 必须 l++, r--,否则会出现栈溢出
        if (l == r) {
    
    
            l += 1;
            r -= 1;
        }
        // 向左递归
        if (left < r) {
    
    
            quickSort(arr, left, r);
        }
        // 向右递归
        if (right > l) {
    
    
            quickSort(arr, l, right);
        }
    }

归并排序基数排序没有看!

猜你喜欢

转载自blog.csdn.net/DDDDeng_/article/details/110421793