算法与数据结构(一) -- 冒泡,插入,希尔,选择,归并,快速,堆排序

作者:opLW
参考:

  1. 王争老师的 《数据结构与算法之美》
  2. 程序员小灰的文章
  3. 厘米姑娘的算法面试总结
目录

1.概览
2.冒泡排序
3.插入排序
4.希尔排序(插入排序升级版)
5.选择排序
6.归并排序
7.快速排序
8.堆排序
9.快速排序,归并排序与堆排序的比较,及部分应用场景
可视化算法学习链接

1.概览
  • 稳定性 针对排序算法,我们还有一个重要的度量指标,稳定性。这个概念是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。

例子 来解释一下。比如我们有一组数据2,9,3,4,8,3,按照大小排序之后就是2,3,3,4,8,9。这组数据里有两个3。经过某种排序算法排序之后,如果两个3的前后顺序没有改变,那我们就把这种排序算法叫作稳定的排序算法;如果前后顺序发生变化,那对应的排序算法就叫作不稳定的排序算法

稳定排序有什么用? 在实际开发中,被比较的往往是一个对象的某一个属性,而不是单纯的数列,所以我们应该保证该属性相同的对象之间,保持排序前的顺序。比如:银行取款,我们要按用户的等级来排序,等级高的在前,与此同时我们要让等级相同的用户按先来先服务的顺序排,这个时候稳定排序的重要性就体现了。

  • 在这里插入图片描述
名字大致操作时间复杂度最好/最坏/平均空间复杂度稳定性
冒泡排序正如其名:从头开始至有序区,两两比较,如果前者大于后者则交换两者的位置。每一轮过后,无序区的最大值都会上浮到至末尾,从而形成有序的数列。O(n) / O(n^2) / O(n^2)O(1)稳定
插入排序从第二个元素开始,每次与前面的元素比较寻找插入的位置,每插入一个数,都会使前面的有序区增加一个数。O(n) / O(n^2) / O(n^2)O(1)稳定
希尔排序先将整个待排记录序列分割成若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次插入排序O(nlogn)/O(n^2)/O(1)不稳定
选择排序选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。O(n^2) / O(n^2) / O(n^2)O(1)稳定
归并排序先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起。复杂度稳定均为 O(nlogn)O(n)稳定
快速排序取一个记录作为枢轴,经过一趟排序将整段序列分为两个部分,使得数轴左侧都小于枢轴、右侧都大于枢轴;再对这两部分继续进行排序使整个序列达到有序O(nlogn)/O(n^2)/O(nlogn)O(1)不稳定
堆排序近似完全二叉树的结构,子结点的键值或索引总是小于(或大于)其父节点O(nlogn)O(1)不稳定

下面仅贴出代码,便于有一定基础的同学复习。对于没有基础的同学,我也会相应的贴出详细介绍算法的链接。(来源于程序员小灰

2.冒泡排序

在这里插入图片描述

public static void bubbleSort(int[] ary) {
        int length = ary.length;
        // 用于记录最后一个比较交换的位置
        int lastExchangeIndex = ary.length - 1;
        // 记录无序区的最后一个
        int unsortedBorder = lastExchangeIndex;
        for (int i = 0; i < length; i++) {
            //记录这一次遍历是否是全部有序的,有序的话则不用再比较,直接跳出
            boolean isSorted = true;
            for (int j = 0; j < unsortedBorder; j++) {
                if (ary[j + 1] < ary[j]) {
                    int tmp = ary[j];
                    ary[j] = ary[j + 1];
                    ary[j + 1] = tmp;
                    //有交换代表这一次遍历不是全部有序
                    isSorted = false;
                    //更新无序区的边界
                    lastExchangeIndex = j;
                }
            }
            if (isSorted) {
                break;
            }
            unsortedBorder = lastExchangeIndex;
        }
    }
  • 两个优化
    • 使用unsortedBorder记录无序区的最后一个,减少比较的次数。
    • 使用isSorted来标记某一次遍历是否全部有序,有则直接跳出,减少比较的次数。

3.插入排序

在这里插入图片描述

public static void insertSort(int[] ary) {
        int length = ary.length;
        //待插入的值
        int insertVal;
        for (int i = 1; i < length; i++) {
            insertVal = ary[i];
            int j = i;
            //寻找插入的位置
            while (j > 0 && ary[j - 1] > insertVal) {
                ary[j] = ary[j - 1];
                j --;
            }
            ary[j] = insertVal;
        }
    }
  • 为什么最好情况下是O(n) 呢? 当数列有序时,每一个待插入的数都大于前面有序区所有的数,从而不用移动位置。
  • 选择冒泡排序还是插入排序 从上面的表格可以看出,两者的时间复杂度一样,但是更多情况下,选择的还是插入排序。因为在交换无序数据次数一样的情况下,插入排序交换数据的速度更快:插入排序只需要一条赋值语句ary[j] = ary[j - 1];,而冒泡排序需要int tmp = ary[j]; ary[j] = ary[j + 1]; ary[j + 1] = tmp;三条赋值语句。

4.希尔排序(插入排序升级版 – 跳跃式交换数据)
public static void shellSort(int[] ary) {
       int length = ary.length;
       int insertVal;
       // 记录每一次跳跃式比较的增量
       int step = length / 2;
       while (step >= 1) {
           for (int i = step; i < length; i += step) {
               insertVal = ary[i];
               int j = i;
               // 注意点,与插入排序不同的是这里要">=",因为step最小为1
               while (j >= step && ary[j - step] > insertVal) {
                   ary[j] = ary[j - step];
                   j -= step;
               }
               ary[j] = insertVal;
           }
           // 缩小跳跃式增量的大小为原来的一半
           step /= 2;
       }
   }
  • 希尔排序的基本思想是实现跳跃式的数据交换,而不是像直接插入排序一样一个一个的比较和移动过。在前面跳跃式交换之后,数据基本呈现有序的状态,所以最后一遍增量为1的插入排序,只要做少量的比较和交换即可完成排序。

5.选择排序

在这里插入图片描述

public static void selectSort(int[] ary) {
        int indexOfMin;
        int length = ary.length;
        for (int i = 0; i < length; i ++) {
            indexOfMin = i;
            for (int j = i + 1; j < length; j ++) {
                if (ary[j] < ary[indexOfMin]) {
                    indexOfMin = j;
                }
            }
            if (indexOfMin != i) {
                int t = ary[indexOfMin];
                ary[indexOfMin] = ary[i];
                ary[i] = t;
            }
        }
    }

总结 以上算法比较简单,适合数量规模较小的排序。当涉及到规模大的排序时,使用以下算法较为合适。

6.归并排序

在这里插入图片描述

//这里的end是待排序列的最后一个元素的下标,不是我们习惯的ary.length
public static void mergeSort(int[] ary, int start, int end) {
        if (start < end) {
            int mid = start + (end - start) / 2;
            mergeSort(ary, start, mid);
            mergeSort(ary, mid + 1, end);
            merge(ary, start, mid, end);
        }
    }

    public static void merge(int[] ary, int start, int mid, int end) {
        int[] tmp = new int[ary.length];
        int i = start, j = mid + 1, k = start;
        while (i != mid + 1 && j != end + 1) {
        	//决定归并排序是稳定排序的关键,当==的时候我们用的还是处于前面的数据
            if (ary[i] <= ary[j]) {
                tmp[k++] = ary[i++];
            } else {
                tmp[k++] = ary[j++];
            }
        }
        while (i != mid + 1) {
            tmp[k++] = ary[i++];
        }
        while (j != end + 1) {
            tmp[k++] = ary[j++];
        }
        for (i = start; i <= end; i++) {
            ary[i] = tmp[i];
        }
    }

7.快速排序
public static void quickSort(int[] ary, int start, int end) {
        if (start < end) {
        	//取得中心点,中心点左边的数据小于中心点,中心点右边的数据大于中心点
            int pivot = partition(ary, start, end);
            quickSort(ary, start, pivot - 1);
            quickSort(ary, pivot + 1, end);
        }
    }

    public static int partition(int[] ary, int startIndex, int endIndex) {
        int pivotVal = ary[startIndex];
        int left = startIndex;
        int right = endIndex;
        while (left != right) { // ==0==
        	// ==1==
            while (left < right && ary[right] >= pivotVal) {
                right --;
            }
            // ==2==
            while (left < right && ary[left] <= pivotVal) {
                left ++;
            }
            if (left < right) {
                int tmp = ary[left];
                ary[left] = ary[right];
                ary[right] = tmp;
            }
        }
        // ==3==
        int tmp2 = ary[left];
        ary[left] = ary[startIndex];
        ary[startIndex] = tmp2;

        return left;
    }
  • 注意 12 这两个while语句的先后顺序关系很大。看下面的示意图:
    在这里插入图片描述
    • 显而易见,区别是致命的。那为什么呢?因为我们选择的pivotVal,其原始下标是在最左端,也就是说最后和他交换的数据应该是一个小于pivotVal的值。那么当我们先执行1时,right往左移动,重叠退出循环,此时的left指向的是比pivotVal小的值,交换正确;那么当我们先执行2时,left往右移动,重叠退出循环,此时的left指向的是比pivotVal大的值,把一个比pivotVal大的值放到pivotVal的前面显然是不行的。 总结 当我们选择最左端作为参照点时,应该先执行1,即让right指针左移;同理当我们选择最右端作为参照点时,应该先执行2,即让left指针右移。
  • 快速排序最坏的时间复杂度为O(n^2) 举一个比较极端的例子。如果数组中的数据原来已经是有序的了,比如1,3,5,6,8。如果我们每次选择最后一个元素作为pivot,那每次分区得到的两个区间都是不均等的。我们需要进行大约n次分区操作,才能完成快排的整个过程。每次分区我们平均要扫描大约n/2个元素,这种情况下,快排的时间复杂度就从O(nlogn)退化成了O(n2)。
  • 详细的学习链接 小灰老师的漫画算法 – 快速排序

8.堆排序
public static void heapSort(int[] ary) {
        //int i = ary.length / 2 - 1 因为下标从0开始
        for (int i = ary.length / 2 - 1; i >= 0; i--) {
            headAdjust(ary, i, ary.length);
        }
        for (int i = ary.length - 1; i >= 0; i--) {
        	//取出大顶堆顶部的值放到后面
            int tmp = ary[0];
            ary[0] = ary[i];
            ary[i] = tmp;
            //重新调整大顶堆
            headAdjust(ary, 0, i);
        }
    }

    public static void headAdjust(int[] ary, int parent, int length) {
        //取得左子节点
        int child = parent * 2 + 1;
        int tmp = ary[parent];
        while (child < length) {
            //判断左,右子节点的值谁更大
            if (child + 1 < length && ary[child + 1] > ary[child]) {
                child ++;
            }
            if(tmp > ary[child]) {
                break;
            }
            //大于tmp的子节点的值上移
            ary[parent] = ary[child];
            parent = child;
            child = child * 2 + 1;
        }
        ary[parent] = tmp;
    }

9.快速排序,归并排序与堆排序的比较,及部分应用场景
  • 快速排序比归并排序常用 归并排序的时间复杂度任何情况下都是O(nlogn)而且是稳定排序,看起来非常优秀。而快速排序,正常情况下是O(nlogn),最坏情况下,时间复杂度是O(n2),但是出现的概率比较小。看起来快速排序好像由于归并排序。但是,归并排序并没有像快排那样应用广泛,这是为什么呢?因为它有一个致命的“弱点”,那就是归并排序不是原地排序算法。想象下当数据量很大的时候,归并排序会浪费很多空间
  • 快速排序比堆排序常用 堆排序不像快速排序会出现最坏情况,其时间复杂度为O(nlogn),并且不需要太多额外的空间。1.快速排序比较数据时是顺序访问,而堆排序比较数据时是跳跃式的访问,不利于cpu缓存2.快速排序的比较和交换的次数比堆排序少,因为堆排序初始化建堆的时候可能会打乱已有的顺序,使得数组比之前无序,增加了交换的次数
  • 部分应用场景(记录一个大体的思路)
    • 在大量数据中查找第k大的数据。 利用快排每一次交换之后,会以pivot为中心,形成小于pivot和大于pivot的两部分,从而快速的排序,如果pivot + 1 == k 则返回pivot对应的值。
    • 在大量数据中查找前k大的数据 利用堆排序的优点,先取k个数建立一个小顶堆,然后依次遍历剩下的数据。如果比堆顶大,则替换堆顶,重新调整该小顶堆。最终小顶堆的k个数据,就是前k大的数据。

可视化算法学习链接
  1. 十大经典排序算法(动画解析)
  2. VisuAlgo
  3. algorithm-visualizer

万水千山总是情,麻烦手下别留情。
如若讲得有不妥,文末留言告知我,
如若觉得还可以,收藏点赞要一起。

opLW原创七言律诗,转载请注明出处

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是对比冒泡排序插入排序快速排序堆排序希尔排序归并排序的结论: 1. 冒泡排序冒泡排序是一种简单直观的排序算法,它通过不断交换相邻元素的位置来将最大的元素逐渐移动到数组的末尾。冒泡排序的时间复杂度为O(n^2),其n是数组的长度。冒泡排序是稳定的排序算法,即相等元素的相对顺序在排序后保持不变。 2. 插入排序插入排序是一种简单且高效的排序算法,它通过将元素逐个插入到已排序的部分数组来构建有序数组。插入排序的时间复杂度为O(n^2),其n是数组的长度。插入排序是稳定的排序算,即相等元素的相对顺序在排序后保持不变。 3 快速排序快速排序是一种高效的排序算法,它通过选择一个基准元素,将数组分成两个子数组,其一个子数组的所有元素都小于基准元素,另一个子数组的所有元素都大于基准元素,然后递归地对子数组进行排序。速排序的平均时间复杂度为O(nlogn),最坏情况下的时间复杂度为O(n^2),其n是数组的长度。快速排序是不稳定的排序算法。 4. 堆排序堆排序是一种高效的排序算法,它利用堆这种数据结构来进行排序堆排序的时间复杂度为O(nlogn),其n是数组的长度。堆排序是不稳定的排序算法。 5. 希尔排序希尔排序是一种改进的插入排序算法,它通过将数组分成多个子数组,并对每个子数组进行插入排序,最后再对整个数组进行一次插入排序希尔排序的时间复杂度取决于选取的间隔序列,最坏情况下的时间复杂度为O(n^2),平均情况下的时间复杂度为O(nlogn)。希尔排序是不稳定的排序算法。 6. 归并排序归并排序是一种高效的排序算法,它通过将数组分成两个子数组,分别对子数组进行排序,然后将两个有序子数组合并成一个有序数组。归并排序的时间复杂度为O(nlogn),其n是数组的长度。归并排序是稳定的排序算法。 综上所述,冒泡排序插入排序是简单但效率较低的排序算法,适用于小规模的数据;快速排序归并排序是高效的排序算法,适用于大规模的数据;堆排序希尔排序是介于两者之间的排序算法,适用于等规模的数据。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值