[ 数据结构 ] 排序算法--------冒泡、选择、插入、希尔、快排、归并、基数、堆

0 前言

1.1 排序分类

  • 内部排序和外部排序,前者数据加载到内存,后者数据量大需借助外部文件.

  • 内部排序包含:

    插入排序:直接插入排序,希尔排序

    选择排序:简单选择排序,堆排序--------- 补充:堆排序

    交换排序:冒泡排序,快速排序

    归并排序

    基数排序

1.2 复杂度

1)度量一个程序时间有两种方法,事后统计或事前估算,事前估算就需要分析时间复杂度

2)时间复杂度:算法中的基本操作语句的重复执行次数是问题规模 n 的某个函数,

​ 计算方法:去常数阶–>保留最高阶项–>去除最高阶项系数

​ 常见时间复杂度:常数阶 O(1)、对数阶 O(log2n)、线性阶 O(n)、线性对数阶 O(nlog2n)、平方阶 O(n^2)、立方阶 O(n^3)、k 次方阶 O(n^k)、指数阶 O(2^n)

​ 对应结构:普通无循环结构语句、while循环、for循环、外层for内部while结构、2层for循环…

3)空间复杂度:度量程序占用的存储空间大小,比如基数排序的空间换时间

1 冒泡排序

[ 数据结构 ] 排序算法--------冒泡、选择、插入、希尔、快排、归并、基数、堆_第1张图片
思路:

  1. 说明:本文以从小到大排序作为顺序排序进行举例说明,print(int[] arr)为打印方法,使用面向过程编程,没有面向对象和封装操作
  2. 代码核心:两层for循环+交换
  3. 微观理解:外层for:表示进行第几轮排序,内层for:比较相邻元素并将较大值交换到右边
  4. 宏观理解:每一轮的作用可以简单认为将最大值放在最右边,数组有n个元素,因此需要求n-1次最大值,即需要arr.length-1轮
  5. 对比说明:这里的交换法需要区别插入排序和希尔排序中的交换法,这里的交换法使用的目的是将一个无序数组的最大值通过两两交换最终弄到最右边,原来的无序数组仍然很可能是无序的,而希尔/插入中交换法的目的是将一个任意值从有序数组的最右边,通过交换的手段插入到正确位置,原来的有序数组仍然一定是有序的

优化:

​ 如果本轮没有进入交换操作的代码,说明数组已经有序,退出循环

private static void sort(int[] arr) {
        int temp;
        boolean flag = true;
        for (int i = 0; i < arr.length - 1; i++) {//每完成一轮排序则排除一个最大值,剩余的是无序数组,注意区别插入排序中的交换法
            for (int j = 0; j < arr.length-i-1; j++) {
                if (arr[j] > arr[j + 1]) {
                    flag = false;
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
            System.out.println("完成第"+(i+1)+"轮排序");
            print(arr);
            //优化:如果本轮没有换位则无需再排
            if (flag) {
                break;
            } else {
                flag = true;
            }

        }
    }

2 选择排序

[ 数据结构 ] 排序算法--------冒泡、选择、插入、希尔、快排、归并、基数、堆_第2张图片
思路:

  1. 代码核心:两层for循环
  2. 微观理解:外层for:表示进行第几轮排序,内层for:辅助变量收集最小值赋给第一个元素
  3. 宏观理解:每一轮的作用可以简单认为将最小值放在最左边,数组有n个元素,因此需要求n-1次最大值,即需要arr.length-1轮
  4. 对比说明:冒泡排序是比较相邻元素并交换,选择排序是与极值比较且一轮仅交换一次

优化:

​ 如果本轮的第一个元素就是最小值,则不用操作

private static void sort(int[] arr) {

        int min = 0;
        int cor=0;

        for (int j = 0; j < arr.length - 1; j++) {
            //每一轮都默认乱序数组第一个数为该轮最小值
            min = arr[j];
            cor=j;
            for (int i = j; i < arr.length; i++) {
                if (arr[i] < min) {
                    //找到更小的则更新最小值和角标
                    cor = i;
                    min = arr[i];
                }
            }
            //最小值已保存,将第一个数与最小处赋值
            if (cor!=j)//优化:第一个数为最小则无需赋值
                arr[cor] = arr[j];
            arr[j] = min;
            print(arr);
        }

    }

3 插入排序

[ 数据结构 ] 排序算法--------冒泡、选择、插入、希尔、快排、归并、基数、堆_第3张图片
(移位法)思路:

  1. 代码核心:外层for循环+内层while循环
  2. 微观理解:外层for表示需要几轮插入操作,内部:将待插入数取出,开始从后往前遍历(while循环)有序数组,指针指向待插入数的前一个,只要索引不越界同时待插入数比指针所在数小,就将该数(指针所在)后移并继续前移指针,一旦不满足(待插入数大于指针数)说明插入位置找到,插入则本轮结束
  3. 宏观理解:每一轮的作用就是在左侧的有序数组中插入一个数到正确位置,因为有序数组长度从1到arr.length,所以需要arr.length-1轮

(交换法)思路:

  1. 代码核心:两层for循环
  2. 微观理解:外层for表示需要几轮插入操作,内部:在有序数组的后面多取一个数(待插入数),从后往前遍历(for循环)有序数组,如果发现逆序则执行交换
  3. 宏观理解:每一轮的作用就是在左侧的有序数组中插入一个数到正确位置,因为有序数组长度从1到arr.length,所以需要arr.length-1轮
  4. 交换法对比移位法:移位法用的while循环,原理是没找到就后移,一旦找到就退出循环并插入,交换法用的是for循环,没找到位置就一直交换,一旦不再交换了就说明本轮数组已经有序了
  5. 效率比较:交换法因为需要使用中间变量实现待插入数+遍历数+中间变量间相互赋值而且遍历不会终止,而移位法只需移位(赋值)且一旦找到正确位置遍历当即终止,所以移位法效率更高
private static void sort(int[] arr) {
        //变量声明在循环外更好
        int insertVal =0;
        int insertIndex= 0;
        for (int i = 1; i < arr.length; i++) {
            insertVal = arr[i];//取出待插入数,保证该位置可被后推覆盖
            insertIndex= i-1;//有序数组下界
            //索引不越界下,逆序遍历有序数组,只要待插入值比遍历值小(小到大排序)就将遍历值后推
            while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
                arr[insertIndex + 1] = arr[insertIndex];
                insertIndex--;
            }
            //位置找到,则先补上遍历附带的索引自减1,再将待插入值放入有序数组
            //赋值优化:只有待插入数和有序数组之间已经有序则无需赋值,既然有序则没有上面的后推和遍历,
            //          即索引自减没执行过,因此
            if (insertIndex + 1 != i) {
                arr[insertIndex+1] = insertVal;
                System.out.print("该轮赋值了");
            }
            System.out.print("第"+i+"轮: ");
            print(arr);
        }

    }
    
    
    //等价于sort方法,只是for代替while循环,交换代替了插入的后推及赋值
    //后续的希尔排序就是基于这两种方法的优化
    private static void sort2(int[] arr) {
        int temp = 0;
        for (int i = 0; i < arr.length-1; i++) {
            for (int j = i; j >= 0; j--) {
                if (arr[j] > arr[j + 1]) {
                    temp = arr[j + 1];
                    arr[j + 1] = arr[j];
                    arr[j] = temp;
                }
            }
        }
        print(arr);

    }

4 希尔排序

[ 数据结构 ] 排序算法--------冒泡、选择、插入、希尔、快排、归并、基数、堆_第4张图片

思路:

  1. 插入排序问题:如果待插入数最小,则后移次数明显增多,arr = {2,3,4,5,6,1},当将1插入时需要后移5次
  2. 希尔排序:基于简单插入排序,加入了分组和步长(增量)的概念,不断缩小组内增量
  3. 简单来讲,就是将数据分组后实现组内排序,步长不断缩小表示组内元素不断增加,数据整体越逆序效果越明显
  4. 简单插入排序就是步长为一所有数据都在一组的希尔排序,也就是希尔的最后一波排序
  5. 代码理解:就是在插入排序的最外层套上for循环,以不断缩小增量直到步长为1
//参考插入排序的交换式,加入分组和步长的概念,不断缩小组内增量
    private static void sort1(int[] arr) {
        int temp = 0;
        int count = 0;
        for (int gap = arr.length/2; gap >0 ; gap/=2) {
            for (int i = 0; i < arr.length - gap; i++) {//这里使用i++而不是加步长,才能保证每个组内排序
                for (int j = i; j >=0; j-=gap) {
                    if (arr[j] > arr[j + gap]) {
                        temp = arr[j];
                        arr[j] = arr[j + gap];
                        arr[j + gap] = temp;
                    }
                }
            }
            System.out.print("第"+(++count)+"轮");
            print(arr);
        }

    }

    //参考插入排序的移位法,加入分组和步长的概念,不断缩小组内增量
    private static void sort2(int[] arr) {
        int value = 0;
        int key = 0;
        int count = 0;

        for (int gap = arr.length/2; gap >0 ; gap/=2) {

            for (int i = gap; i < arr.length ; i++) {
                value = arr[i];
                key = i - gap;
                while (key >= 0 && value < arr[key]) {
                    arr[key + gap] = arr[key];
                    key-=gap;
                }
                if (key != i - gap) {
                    arr[key + gap] = value;
                }
            }

            System.out.print("第"+(++count)+"轮");
            print(arr);
        }

    }

5 归并排序

[ 数据结构 ] 排序算法--------冒泡、选择、插入、希尔、快排、归并、基数、堆_第5张图片

思路:

  1. 代码核心:递归,分治策略
  2. 微观理解:
    • 传参:数组arr,low(排序区间下界索引),high(排序区间上界索引)
    • 终止:上下界相交(即只对组内唯一元素排序)
    • 分:将数据从区间正中分为两组
    • 治:三个指针,顺序遍历两个子数组并将较小值顺序赋给辅助数组,最终将辅助数组(有序)的元素同步到待排序数组arr
  3. 宏观理解:先分后治,先递归分组将数组一直分到每组仅有一个元素,此时递归终止且组内有序(一个元素),层层返回(类似回溯),执行分组操作下面的合并操作(辅助数组+2个子数组+3个指针实现归并)
//分治方法,分为递归分组,治为该层的归并排序
    private static void sort(int[] arr,int low ,int high) {
        //辅助数组,用于复制归并后的有序数组
        int[] assist = new int[arr.length];

        //安全性校验,表示该组只有一个元素
        if (high <= low) {
            return;
        }

        //分治法中的分,即分组,也是递归调用,上面的健壮判断可以看成递归终止条件
        int mid = low + ((high - low) >> 1);
        sort(arr, low, mid);
        sort(arr,mid+1,high);

        //当分组完毕(递归终止)后,最终的两组内只有一个元素(即有序),才会执行下面代码,即层层排序返回上层,这里为界,上面为分组,下面为(归并)排序
        //个人理解为:在上面的分组操作没有终止前,一直在做分的操作,直到分组完毕才开始治,治完再治上层
        //举例:本层的上下界为0和1,则上面的分组防止直接不做操作返回,而对两个单元素的子数组做如下的归并排序,返回上层

        //使用三个指针,表示两个子数组的起点索引和归并数组的起点索引
        int i = low;
        int p1 = low;
        int p2 = mid + 1;

        //比较子有序数组的起点值,不断复制最小值到辅助数组并后移指针,直到某一子数组复制完毕
        while (p1 <= mid && p2 <= high) {
            if (arr[p1] < arr[p2]) {
                assist[i++] = arr[p1++];
            } else {
                assist[i++] = arr[p2++];
            }
        }

        //将未复制完的子数组弄完
        while (p1 <= mid) {
            assist[i++] = arr[p1++];
        }
        while (p2 <= high) {
            assist[i++] = arr[p2++];
        }

        //将归并后的顺序同步给请求者
        for (int j = low; j <= high; j++) {
            arr[j] = assist[j];
        }
    }

6 快速排序

[ 数据结构 ] 排序算法--------冒泡、选择、插入、希尔、快排、归并、基数、堆_第6张图片

思路:

  1. 代码核心:递归
  2. 微观理解:
    • 传参:数组arr,low(排序区间下界索引),high(排序区间上界索引)
    • 终止:上下界相交(即只对组内唯一元素排序)
    • 单层逻辑:取下界值arr[low]为基准值,左右指针最初为区间上界和下界+1,即待分组元素的边界外,开始相向移动,遇到对方的元素则做交换操作,直到两指针相交,交换右指针元素和区间上界(基准值),此时单层排序完成只需对左右子组递归排序即可
  3. 宏观理解:不同于归并排序的分治思想,归并操作(排序)是在分组后才完成的,而快速排序的交换操作(排序)是在分组前就完成了

个人理解+优化:

//问题1:理想条件下,基准值两边交换完后,刚好剩一个小的与基准值交换,如果不剩呢
//  退出while前r指针做了自减,所以指向的一定是左子组的数据
//问题2:如果只有一侧数据呢,怎么交换
//  若只有右组数据,则r最终指向上界即自己换自己,若只有左组数据,l和r在下界相交,只需交换一次基准值即可
//问题3:分组结束的状况是怎么样的,也就是最底层和上两层的情况举例
//  最底层区间只有一个元素,上层则2/3个
//问题4:指针遇到与基准值等值的怎么办?
//	正常交换操作即可,交换后等基准值分到哪一组都不影响正确性
//问题5:什么情况下左右指针相遇?
//	左右指针相交,说明交换完毕/只有一侧有数据,需退出,否则持续交换
//	至于最终l==r还是l>r,看谁先找,以及是否能找到,结果复杂不好判断
//问题6:为什么指针相交后,基准值的交换操作用的是右指针r指的元素,而不是左指针?
//	由问题1知r最终指向左子组,即r的元素比基准值小,因此使用r和lo交换
//优化:
//	基准值的交换操作可以加判断条件,比如该组就两个数且有序,即lo==r,则不用交换

最终代码:

//问题1:理想条件下,基准值两边交换完后,刚好剩一个小的与基准值交换,如果不剩呢
    //  退出while前r指针做了自减,所以指向的一定是左子组的数据
    //问题2:如果只有一侧数据呢,怎么交换
    //  若只有右组数据,则r最终指向上界即自己换自己,若只有左组数据,l和r在下界相交,只需交换一次基准值即可
    //问题3:分组结束的状况是怎么样的,也就是最底层和上两层的情况举例
    //  最底层区间只有一个元素,上层则2/3个
    //函数功能:对数组的某一区间进行排序
    private static void sort(int[] arr,int lo ,int hi) {

        //安全性校验:保证区间内不止一个元素
        if (lo >= hi) {
            return;
        }

        //分组:将区间元素(按序)分为左右子组,并拿到基准值最终的角标
        //左右指针最初为区间上界和下界+1,即待分组元素的边界外,开始相向移动
        int l = lo;
        int r = hi+1;
        int temp = 0;
        int pivot = arr[lo];//基准值

        while (true) {
            //右指针向左移,遇到左元素(基准值)停下,或遇到区间上界停下
            //说明:如果指针遇到等基准值,则交换后等基准值分到哪一组都不影响正确性
            while (arr[--r] > pivot) {
                if (r == lo) {
                    break;
                }
            }
            while (arr[++l] < pivot) {
                if (l == hi) {
                    break;
                }
            }
            //左右指针相交,说明交换完毕/只有一侧有数据,需退出,否则持续交换
            //至于最终l==r还是l>r,看谁先找,以及是否能找到,结果复杂不好判断
            if (l >= r) {
                break;
            } else {
                temp = arr[l];
                arr[l] = arr[r];
                arr[r] = temp;
            }
        }
        //使用r作为基准值角标是,因为r最终指向左子组元素
        //当然r也可能是lo上界(本身有序没有交换操作),因此可做优化if(lo===r)
        temp = arr[lo];
        arr[lo] = arr[r];
        arr[r] = temp;

        //左右子组排序
        sort(arr, lo, r-1);
        sort(arr, r+1, hi);
    }

7 基数排序

[ 数据结构 ] 排序算法--------冒泡、选择、插入、希尔、快排、归并、基数、堆_第7张图片

  1. 代码核心:两层for循环
  2. 微观理解:外层for:表示进行第几轮排序(依次针对个位、十位、百位…),内层for:二维数组表示10个桶,将元素依次放入桶中,顺序遍历桶元素依次同步到原数组
  3. 宏观理解:每一轮的作用可以简单认为针对不同位做桶操作,数组中最大元素有n位,因此需要n轮
  4. 总结:桶排序虽然是按个位十位百位依次排序,但是道理就像先比百位,再比十位,最后个位,得到正确顺序,核心在于辅助数组(二维数组即十个桶,一维数组即十个桶的偏移量)的使用,属于空间换时间,还有,基数排序具有稳定性,因为相同值间的前后顺序不会改变,这是因为所有操作都是基于原本的顺序遍历的
private static void sort(int[] arr) {

        //准备工作:
        // 1.开辟一个二维数组(10个桶),每个桶大小和arr保持一致
        // 2.计算数组中最大位数
        int[][] buckets = new int[10][arr.length];
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
        }
        int rounds = (max + "").length();

        //元素最高n位,就需要进行n轮排序,依次对个位,十位,百位操作
        for (int round = 0,n=1; round < rounds; round++,n*=10) {

            //每一轮需先重置桶偏移量
            int[] bucketOffset = new int[10];

            //遍历数组元素,计算桶编号,放入对应桶中并更新桶偏移量
            for (int i = 0; i < arr.length; i++) {
                int bucketNo = arr[i]/n % 10;
                buckets[bucketNo][bucketOffset[bucketNo]] = arr[i];
                bucketOffset[bucketNo]++;
            }

            //顺序遍历每个桶中元素,并同步到原数组
            int index = 0;
            for (int i = 0; i < 10; i++) {
                if (bucketOffset[i] != 0) {
                    for (int j = 0; j < bucketOffset[i]; j++) {
                        arr[index++] = buckets[i][j];
                    }
                }
            }
            System.out.print("第" + (round+1) + "轮:");
            print(arr);
        }

    }

8 总结对比

[ 数据结构 ] 排序算法--------冒泡、选择、插入、希尔、快排、归并、基数、堆_第8张图片

你可能感兴趣的:(数据结构与算法,排序算法,数据结构,算法)