(数据结构与算法)排序算法

分类

在这里插入图片描述

O(n2)级别的排序

冒泡排序(Bubble Sort)

废话不多说,直接贴代码

public static void BubbleSort(int[] array){
       int length = array.length;
       //交换元素开始
        for(int i=0; i<length; i++){
            //每次排完一遍之后可以少做一次交换
            boolean flag = false;
            for(int j=0; j<length-i-1; j++){
                if(array[j]>array[j+1]){
                    int temp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = temp;
                    flag = true;
                }
            }
            if (!flag)//当发现元素没有交换时,直接停止
                break;
        }
    }

复杂度分析:

  1. 冒泡的过程只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度为
    O(1),是一个原地排序算法
  2. 在冒泡排序中,只有交换才可以改变两个元素的前后顺序。为了保证冒泡排序算法的稳定性,当
    有相邻的两个元素大小相等的时候,我们不做交换,相同大小的数据在排序前后不会改变顺序,
    所以冒泡排序是稳定的排序算法
  3. 最好情况下,要排序的数据已经是有序的了,我们只需要进行一次冒泡操作,就可以结束了,所
    以最好情况时间复杂度是 O(n)。而最坏的情况是,要排序的数据刚好是倒序排列的,我们需要
    进行 n 次冒泡操作,所以最坏情况时间复杂度为 **O(n2)**平均也是。

插入排序

这是一个动态排序的过程,即动态地往有序集合中添加数据。

public static void insertSort(int[] arrays){
        int n = arrays.length;
        for(int i=1; i<n; i++){
            int temp = arrays[i];
            int j=i-1;
            for(; j>=0; j--){
                if(arrays[j]>temp){
                   arrays[j+1] = arrays[j];
                }else {
                    break;
                }
            }
            arrays[j+1] = temp;
        }
    }

复杂度分析:

  1. 原地排序算法
  2. 稳定的排序算法
  3. 最好是时间复杂度为 O(n)。最坏情况时间复杂度为 O(n2)以平均时间复杂度为 O(n2)。

选择排序(Selection Sort)

选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次
会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。
在这里插入图片描述

public static void selectSort(int[] arr){
        int temp = 0;
        int flag = 0;
        int n = arr.length;
        for (int i = 0; i < n; i++) {
            temp = arr[i];
            for(int j=i+1; j<n; j++){
                if(arr[j] < temp){
                    temp = arr[j];
                    flag = j;
                }
            }
            arr[flag] = arr[i];
            arr[i] = temp;
        }
    }

空间复杂度为 O(1)
不是稳定的排序算法
选择排序的最好情况时间复杂度、最坏情况和平均情况时间复杂度都为 O(n2)

小结

冒泡排序、选择排序,可能就纯粹停留在理论的层面了,学习的目的也只是为了开拓思维,实际开发中应用并不多,但是插入排序还是挺有用的。后面讲排序优化的时候,我会讲到,有些编程语言中的排序函数的实现原理会用到插入排序算法。
在这里插入图片描述

O(nlogn)级别的排序

归并排序(Merge Sort)

归并排序的核心思想还是蛮简单的。如果要排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。

public static void mergeSort(int[] arr, int p, int r){
        if(p<r){
            int q = p+((r-p)>>1);
            mergeSort(arr, p, q);
            mergeSort(arr, q+1, r);
            merge(arr, p, q, r);

        }
    }
    public static void merge(int[] arr, int p, int q, int r){
        int i,j,k,n1,n2;
        n1 = q-p+1;
        n2 = r-q;
        int[] left = new int[n1];
        int[] right = new int[n2];
        //拷贝数组
        for(i=0,k=p; i<n1; i++,k++)
            left[i] = arr[k];
        for (i=0,k=q+1; i<n2; i++,k++)
            right[i] = arr[k];
        //比较并拷贝到原数组中
        for(k=p,i=0,j=0; i<n1 && j<n2; k++){
            if(left[i]<=right[j]){
                arr[k] = left[i];
                i++;
            }else {
                arr[k] = right[j];
                j++;
            }
        }
        if(i<n1){
            for(;i<n1;i++,k++)
                arr[k] = left[i];
        }
        if(j<n2){
            for(;j<n2;j++,k++)
                arr[k] = right[j];
        }
    }

复杂度分析:

  1. 归并排序是一个稳定的排序算法
  2. 归并排序的时间复杂度任何情况下都是 O(nlogn)
  3. 空间复杂度是 O(n)

快速排序算法(Quicksort)

快排的思想是这样的:如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点)。
我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。
根据分治、递归的处理思想,我们可以用递归排序下标从 p 到 q-1 之间的数据和下标从 q+1 到r 之间的数据,直到区间缩小为 1,就说明所有的数据都有序了。

public static void quickSort(int[] arr, int p, int r){
        if (p >= r)
            return;
        int q = partition(arr, p, r);
        quickSort(arr, p,q-1);
        quickSort(arr,q+1,r);

    }
    public static int partition(int[] arr, int p, int r){
        int index = arr[r];
        int i = p;
        int j = r;
        while(i<j){
            while(i<j && arr[i]<index){
                i++;
            }
            if(i<j){
                arr[j--] = arr[i];
            }
            while(i<j && arr[j]>index){
                j--;
            }
            if(i<j){
                arr[i++] = arr[j];
            }
        }
        arr[i] = index;
        return i;
    }

复杂度分析:

  1. 不稳定排序
  2. 原地排序
  3. 大部分情况下的时间复杂度都可以做到 O(nlogn),只有在极端情况
    下,才会退化到 O(n2)

番外篇

O(n) 时间复杂度内求无序数组中的第 K 大元素。比如,4, 2, 5, 12, 3 这样一组数据,第 3 大元素就是
4。
我们选择数组区间 A[0…n-1] 的最后一个元素 A[n-1] 作为 pivot,对数组 A[0…n-1] 原地分区,这样数组就分成了三部分,A[0…p-1]、A[p]、A[p+1…n-1]。
如果 p+1=K,那 A[p] 就是要求解的元素;如果 K>p+1, 说明第 K 大元素出现在 A[p+1…n-1]区间,我们再按照上面的思路递归地在 A[p+1…n-1] 这个区间内查找。同理,如果 K<p+1,那我们就在 A[0…p-1] 区间查找。
这是用了快排的思想。

O(n) 级别的排序算法

都不是原地排序

桶排序(Bucket sort)

首先,我们来看桶排序。桶排序,顾名思义,会用到“桶”,核心思想是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。
当桶的个数 m 接近数据个数 n 时,这个时候桶排序的时间复杂度接近 O(n)。

桶排序看起来很优秀,那它是不是可以替代我们之前讲的排序算法呢?

答案当然是否定的。数据在各个桶之间的分布是比较均匀的。如果数据经过桶的划分之后,有些桶里的数据非常多,有些非常少,很不平均,那桶内数据排序的时间复杂度就不是常量级了。在极端情况下,如果数据都被划分到一个桶里,那就退化为 O(nlogn) 的排序算法了。

举个例子:

比如说我们有 10GB 的订单数据,我们希望按订单金额(假设金额都是正整数)进行排序,但
是我们的内存有限,只有几百 MB,没办法一次性把 10GB 的数据都加载到内存中。这个时候该
怎么办呢?
现在我来讲一下,如何借助桶排序的处理思想来解决这个问题。
我们可以先扫描一遍文件,看订单金额所处的数据范围。假设经过扫描之后我们得到,订单金额
最小是 1 元,最大是 10 万元。我们将所有订单根据金额划分到 100 个桶里,第一个桶我们存
储金额在 1 元到 1000 元之内的订单,第二桶存储金额在 1001 元到 2000 元之内的订单,以此
类推。每一个桶对应一个文件,并且按照金额范围的大小顺序编号命名(00,01,02…99)。
理想的情况下,如果订单金额在 1 到 10 万之间均匀分布,那订单会被均匀划分到 100 个文件
中,每个小文件中存储大约 100MB 的订单数据,我们就可以将这 100 个小文件依次放到内存
中,用快排来排序。等所有文件都排好序之后,我们只需要按照文件编号,从小到大依次读取每
个小文件中的订单数据,并将其写入到一个文件中,那这个文件中存储的就是按照金额从小到大
排序的订单数据了。
不过,你可能也发现了,订单按照金额在 1 元到 10 万元之间并不一定是均匀分布的 ,所以
10GB 订单数据是无法均匀地被划分到 100 个文件中的。有可能某个金额区间的数据特别多,
划分之后对应的文件就会很大,没法一次性读入内存。这又该怎么办呢?
针对这些划分之后还是比较大的文件,我们可以继续划分,比如,订单金额在 1 元到 1000 元
之间的比较多,我们就将这个区间继续划分为 10 个小区间,1 元到 100 元,101 元到 200
元,201 元到 300 元…901 元到 1000 元。如果划分之后,101 元到 200 元之间的订单还是太
多,无法一次性读入内存,那就继续再划分,直到所有的文件都能读入内存为止。

计数排序(Counting sort)

计数排序其实是桶排序的一种特殊情况。当要排序的 n 个数据,所处的范围并不大的时候,比如最大值是 k,我们就可以把数据划分成 k+1 个桶。每个桶内的数据值都是相同的,省掉了桶内排序的时间。
比如我们对 A[8] 进行排序,它们分别是:2,5,3,0,2,3,0,3。
我们使用大小为 6 的数组 C[6] 表示桶,其中下标对应分数。不过,
C[6] 内存储的并不是考生,而是对应的考生个数。像我刚刚举的那个例子,我们只需要遍历一
遍考生分数,就可以得到 C[6] 的值。
在这里插入图片描述
我们对 C[6] 数组顺序求和,C[6] 存储的数据就变成了下面这样子。C[k] 里存储
小于等于分数 k 的考生个数。
在这里插入图片描述
我们从后到前依次扫描数组 A。比如,当扫描到 3 时,我们可以从数组 C 中取出下标为 3 的值
7,也就是说,到目前为止,包括自己在内,分数小于等于 3 的考生有 7 个,也就是说 3 是数
组 R 中的第 7 个元素(也就是数组 R 中下标为 6 的位置)。当 3 放入到数组 R 中后,小于等
于 3 的元素就只剩下了 6 个了,所以相应的 C[3] 要减 1,变成 6。
以此类推,当我们扫描到第 2 个分数为 3 的考生的时候,就会把它放入数组 R 中的第 6 个元素
的位置(也就是下标为 5 的位置)。当我们扫描完整个数组 A 后,数组 R 内的数据就是按照分
数从小到大有序排列的了。
在这里插入图片描述
计数排序只能用在数据范围不大的场景中,如果数据范围 k 比要排序的数据 n 大很多,就不适合用计数排序了。而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。

基数排序(Radix sort)

假设我们有 10 万个手机号码,希望将这 10 万个手机号码从小到大排序,你有什么比较快速的排序方法呢?我们之前讲的快排,时间复杂度可以做到 O(nlogn),还有更高效的排序算法吗?桶排序、计数排序能派上用场吗?手机号码有 11 位,范围太大,显然不适合用这两种排序算法。针对这个排序问题,有没有时间复杂度是 O(n) 的算法呢?现在我就来介绍一种新的排序算法,基数排序。

先按照最后一位来排序手机号码,然后,再按照倒数第二位重新排序,以此类推,最后按照第一位重新排序。经过 11 次排序之后,手机号码就都有序了。

基数排序对要排序的数据是有要求的,需要可以分割出独立的“位”来比较,而且位之间有递进的关系,如果 a 数据的高位比 b 数据大,那剩下的低位就不用比较了。除此之外,每一位的数据范围不能太大,要可以用线性排序算法来排序,否则,基数排序的时间复杂度就无法做到 O(n) 了。

如何实现一个通用的、高性能的排序函数

在这里插入图片描述

发布了14 篇原创文章 · 获赞 0 · 访问量 773
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览