程序员必会的五个排序

本文深入探讨了冒泡排序、插入排序、选择排序、快速排序和归并排序五种核心排序算法,详细讲解了每种算法的时间复杂度、实现原理及应用场景。同时,通过实例演示了如何使用快速排序在O(n)时间内找到无序数组中的第k大元素。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

千里之行始于足下。我个人认为,无论是前端也好,后端也罢,掌握一些基本的算法还是必不可少的。代码的实现只是思想的具现化,关键还在于思想,实现的语言可以千变万化,但是思想还是一力降十会,以不变应万变。 

本文主要分享了冒泡排序,插入排序,选择排序,快速排序,归并排序五大排序,以及如何使用快排在O(n)内找到一个无序数组中的第k大元素。话不多说,先上图(图中罗列了常用的排序,主要讲述其中的五个高频的,其中三个简单的,两个稍复杂一点的)。所有源码均已上传至github:链接


排序数组:[2, 4, 1, 3, 6, 5]复制代码

冒泡排序

时间复杂度:最好O(n),平均O(n2),最差O(n2)

冒泡排序是最最基本的排序了。它的原理比较简单,每一次的冒泡操作都是对相邻数据的两两比较,看是否满足大小关系要求,不满足则互换,以此类推,下面是代码

    private int[] bubbleSort(int[] arrays) {
        //第一个循环遍历,第二个循环比较
        for (int i = 0; i < arrays.length; i++) {
            for (int j = i + 1; j < arrays.length; j++) {
                if (arrays[i] > arrays[j]) {
                    int tmp = arrays[i];
                    arrays[i] = arrays[j];
                    arrays[j] = tmp;
                }
            }
            System.out.print("第" + i + "次交换");
            printAll(arrays);
        }
        return arrays;
    }复制代码

它的执行情况是这样的:


但是发现一个问题,做了好多重复操作,因为将代码改造如下(手动加了一个flag的标识位,手动跳出循环)

    private int[] bubbleSort(int[] arrays) {
        //第一个循环遍历,第二个循环比较
        for (int i = 0; i < arrays.length; i++) {
            //退出标记
            boolean flag = false;
            for (int j = i + 1; j < arrays.length; j++) {
                if (arrays[i] > arrays[j]) {
                    int tmp = arrays[i];
                    arrays[i] = arrays[j];
                    arrays[j] = tmp;
                    flag = true;
                }
            }
            if (!flag) break;
            System.out.print("第" + i + "次交换");
            printAll(arrays);
        }
        return arrays;
    }复制代码

再次执行,结果如下,明显少了几次循环


get:遇到问题多多考虑加标识

插入排序

时间复杂度:最好O(n),平均O(n2),最差O(n2)

插入排序的思想有点类似扑克牌思想。默认arrays[0]为第一张扑克牌,并且第一个循环从1开始,每次揭一张牌然后和手里得牌进行比较,插入相应的位置。

    private int[] insertionSort(int[] arrays) {
        for (int i = 1; i < arrays.length; i++) {
            int value = arrays[i];
            int j = i - 1;
            for (; j >= 0; --j) {
                if (value < arrays[j]) {
                    arrays[j + 1] = arrays[j];
                } else {
                    break;
                }
            }
            arrays[j + 1] = value;
            System.out.print("第" + i + "次交换");
            printAll(arrays);
        }
        return arrays;
    }复制代码

它的执行情况是这样的:


选择排序

时间复杂度:最好O(n2),平均O(n2),最差O(n2)

选择排序的思想有点像插入排序,在第一个循环里,它都默认当前的值是最小,然后取出它的下标,在第二个循环里找到比自己还要小的,并赋值,然后再做交换操作,以此类推。它的代码实现如下:

    private int[] selectionSort(int[] arrays) {
        for (int i = 0; i < arrays.length; i++) {
            int minIndex = i;
            for (int j = i; j < arrays.length; j++) {
                if (arrays[j] < arrays[minIndex]) {
                    minIndex = j;
                }
            }
            int tmp = arrays[i];
            arrays[i] = arrays[minIndex];
            arrays[minIndex] = tmp;
            System.out.print("第" + i + "次交换:");
            printAll(arrays);
        }
        return arrays;
    }复制代码

它的执行情况是这样的:


快速排序

时间复杂度:最好O(nlogn),平均O(n2),最差O(n2)

快排和归并排序可能要比前面三个排序要稍微复杂一点。这两种排序适合大规模数据的排序。

他们都用到了分治算法的思想:分而治之

什么是分治算法?

分而治之。先分解再解决然后合并:

  1. 将原问题划分成 n 个规模较小,并且结构与原问题相似的子问题
  2. 解决这些子问题
  3. 合并其结果,就得到原问题的解

快排的思想是取arrays数组中一组数组,取这个区间范围内任意一个数据作为它的分区点pivot,然后遍历数据将小的放在pivot左边,大的放在右边。

比较简单的实现就是如果空间足够,申请两个临时数组 A 和 B,遍历 arrays,将小于 pivot 的元素都拷贝到临时数组 A,将大于 pivot 的元素都拷贝到临时数组 B,最后再将数组A和数组B 中数据顺序拷回arrays。

但是这样就不是原地排序了,并且浪费空间,所以我们应该是原地分区,具体实现如下:

    private void quickSort(int[] arrays, int head, int tail) {
        if (head >= tail) return;
        int pivot = partition(arrays, head, tail);
        quickSort(arrays, head, tail - 1);
        quickSort(arrays, pivot + 1, tail);
    }复制代码

关键在于partition函数,它是用来获取分区点,并且交换元素。

这里注意一个细节,对分区点pivot的选择是当前arrays的tail位,而不是head位,这样的好处是会避免数据的重复交换,这个pivot的选择一定要注意,选择的不合理,时间复杂度会直接退化到O(n2)具体代码实现如下

    private int partition(int[] arrays, int head, int tail) {
        int pivot = arrays[tail];
        int i = head;
        for (int j = head; j < tail; j++) {
            if (pivot > arrays[j]) {
                int tmp = arrays[i];
                arrays[i] = arrays[j];
                arrays[j] = tmp;
                ++i;
            }
        }
        int tmp = arrays[i];
        arrays[i] = arrays[tail];
        arrays[tail] = tmp;
        return pivot;
    }复制代码

归并排序

时间复杂度:最好O(nlogn),平均O(nlogn),最差O(nlogn)

快排是自上而下先分区处理子问题后合并,而归并正合适相反,归并是自下而上先处理子问题再合并。归并的缺点是比快排占内存。

归并是思路是把需要排序的数组从中间分成两部分,对每一部分分别排序,排好序然后再合并在一起就可以具体代码如下:

    private void mergeSort(int[] arrays, int head, int tail) {
        if (head >= tail) return;
        int pivot = (head + tail) / 2;
        //分而治之
        mergeSort(arrays, head, pivot);
        mergeSort(arrays, pivot + 1, tail);
        //合并
        merge(arrays, head, pivot, tail);
    }复制代码

关键点在于这个merge方法。

它声明了一个tmp数组用来存储分解的子数组,第一个while循环,遍历部分arrays数组,并且根据大小关系来移动p或者q,然后将满足划分关系的数据存入tmp。

    private void merge(int[] arrays, int head, int pivot, int tail) {
        System.out.println("head=" + head + ",pivot=" + pivot + "tail=" + tail);
        int p = head;
        int q = pivot + 1;
        int k = 0;
        int[] tmp = new int[tail - head + 1];
        while (p <= pivot && q <= tail) {
            if (arrays[p] <= arrays[q]) {
                tmp[k++] = arrays[p++];
            } else {
                tmp[k++] = arrays[q++];
            }
        }

        // 判断哪个子数组中有剩余的数据
        int start = p;
        int end = pivot;
        if (q <= tail) {
            start = q;
            end = tail;
        }

        // 将剩余的数据拷贝到临时数组tmp
        while (start <= end) {
            tmp[k++] = arrays[start++];
        }

        // 将tmp中的数组拷贝回arrays
//        for (int i = 0; i <= tail - head; ++i) {
//            arrays[head + i] = tmp[i];
//        }
        //上述代码与下面代码等同,只不过idea有个警告,我就转换了一下
        if (tail - head + 1 >= 0)
            System.arraycopy(tmp, 0, arrays, head, tail - head + 1);
    }复制代码

并且我将每一次执行merge方法的时候,head,piovt,tail的执行情况打印了出来。


排序测试结果

    public static void main(String[] args) {
        BaseSort baseSort = new BaseSort();
        int[] arrays = {2, 4, 1, 3, 6, 5};
        int[] bubbleRes = baseSort.bubbleSort(arrays);
        System.out.print("冒泡排序:");
        baseSort.printAll(bubbleRes);
        int[] insertRes = baseSort.insertionSort(arrays);
        System.out.print("插入排序:");
        baseSort.printAll(insertRes);
        int[] selectRes = baseSort.selectionSort(arrays);
        System.out.print("选择排序:");
        baseSort.printAll(selectRes);

        int[] quickArray = {2, 4, 1, 3, 6, 5};
        baseSort.quickSort(quickArray, 0, quickArray.length - 1);
        System.out.print("快速排序:");
        baseSort.printAll(quickArray);
        int[] mergeArray = {2, 4, 1, 3, 6, 5};
        baseSort.mergeSort(mergeArray, 0, mergeArray.length - 1);
        System.out.print("归并排序:");
        baseSort.printAll(mergeArray);
    }复制代码


如何使用快排在O(n)内找到一个无序数组中第k大元素

分析:快排的思想是分而治之,还是以arrays{2,4,1,3,6,5}为例,当k=3时,期望结果是4,这里的关键还是分区点的选择,这里以arrays最后一个元素的下标作为pivot。这样数组就分成了三部分,然后再遍历查找k的值所在的位置即可。具体实现代码如下:

    private int findKByQuick(int[] arrays,int head,int tail,int k){
        int pivot = partition(arrays, head, tail);
        if(pivot > k - 1){
            return findKByQuick(arrays,head,pivot - 1,k);
        }else if (pivot < k - 1){
            return  findKByQuick(arrays,pivot + 1,tail,k);
        }
//        pivot == k - 1
        System.out.println("k = " + (pivot + 1));
        printAll(arrays);
        return arrays[pivot];
    }复制代码

关键点还在于分区函数,这里将常规排序的分区函数改造了一下,返回下标即可。

    private int partition(int[] arrays, int head, int tail) {
        int last = arrays[tail];
        int pivot = head;
        for (int j = head; j < tail; j++) {
            if (last < arrays[j]) {
                int tmp = arrays[pivot];
                arrays[pivot] = arrays[j];
                arrays[j] = tmp;
                ++pivot;
            }
        }
        int tmp = arrays[pivot];
        arrays[pivot] = arrays[tail];
        arrays[tail] = tmp;
        return pivot;
    }复制代码

测试结果

根据测试结果可以得知,利用快排查找第K大元素并没有将数组完全排序,而是只进行了部分排序,因此时间复杂度为O(n)。

    public static void main(String[] args) {
        FindKthLargest findKthLargest = new FindKthLargest();
        int[] findArray = {2, 4, 1, 3, 6, 5};
        int k = 2;
        int kValue = findKthLargest.findKByQuick(findArray,0,findArray.length - 1,k);
        System.out.print("第" + k + "大元素为" + kValue);
    }复制代码



end


您的点赞和关注是对我最大的支持,谢谢!


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值