算法~十大经典排序算法

本文详细介绍了十大经典排序算法,包括冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序、计数排序、桶排序和基数排序。内容涵盖算法描述、动图演示、代码实现、时间复杂度、空间复杂度、稳定性以及额外内存占用。在面试中,快速排序和归并排序尤其重要,而稳定性和空间复杂度是评估排序算法性能的关键因素。

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

0、算法概述


查找和排序算法是算法的入门知识,其经典思想可以用于很多算法当中。因为其实现代码较短,应用较常见。所以在面试中经常会问到排序算法及其相关的问题。但万变不离其宗,只要熟悉了思想,灵活运用也不是难事。

一般在面试中最常考的是快速排序归并排序,并且经常有面试官要求现场写出这两种排序的代码。对这两种排序的代码一定要信手拈来才行。还有插入排序、冒泡排序、堆排序、基数排序、桶排序等。面试官对于这些排序可能会要求比较各自的优劣、各种算法的思想及其使用场景,分析算法的时间和空间复杂度

0.1 算法分类

十种常见排序算法可以分为两大类:

  • 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
  • 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。 

0.2 算法复杂度

0.3 相关概念

  • 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
  • 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
  • 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
  • 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。 
  • 原地排序(In-place):原地排序就是指在排序过程中不申请多余的存储空间,只利用原来存储待排数据的存储空间进行比较和交换的数据排序。
  • 非原地排序(Out-place):需要利用额外的数组来辅助排序。

 

1、冒泡排序(Bubble Sort)


冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。 

1.1 算法描述

  • 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  • 针对所有的元素重复以上的步骤,除了最后一个;
  • 重复步骤1~3,直到排序完成。

1.2 动图演示

1.3 代码实现

void BubbleSort(int array[], int length) {
    for (int i = 0; i < length - 1; i++) {
        for (int j = 0; j < length - 1 - i; j++) {
            if (array[j] > array[j + 1]) {
                int temp = array[j + 1];
                array[j + 1] = array[j];
                array[j] = temp;
            }
        }
    }
}

1.4 时间复杂度

最好情况下:正序有序,则只需要比较n次。故,为O(n) 
最坏情况下:  逆序有序,则需要比较(n-1)+(n-2)+……+1,故,为O(N*N)

1.5 空间复杂度

O(1) 

1.6 稳定性

排序过程中只交换相邻两个元素的位置。因此,当两个数相等时,是没必要交换两个数的位置的。所以,它们的相对位置并没有改变,冒泡排序算法是稳定的。

1.7 占用额外内存

不占用,原地排序

 

2、选择排序(Selection Sort)


2.1 算法描述

  • 找到数组中最小的元素,将它和数组的第一个元素交换位置
  • 在剩下的元素中继续寻找最小的元素,和数组的第二个元素交换位置
  • 重复1~2,直到整个数组排序完成

2.2 动图演示

2.3 代码实现

void SelectionSort(int array[], int length) {
    int minIndex, temp;
    for (int i = 0; i < length - 1; i++) {
        minIndex = i;
        for (int j = i + 1; j < length; j++) {
            if (array[j] < array[minIndex]) {
                minIndex = j;
            }
        }
        temp = array[i];
        array[i] = array[minIndex];
        array[minIndex] = temp;
    }
}

2.4 时间复杂度

最好情况下:交换0次,但是每次都要找到最小的元素,因此大约必须遍历N*N次,因此为O(N*N)。减少了交换次数!
最坏情况下,平均情况下:O(N*N)

2.5 空间复杂度

O(1) 

2.6 稳定性

由于每次都是选取未排序序列A中的最小元素x与A中的第一个元素交换,因此跨距离了,很可能破坏了元素间的相对位置,因此选择排序是不稳定的!

2.7 占用额外内存

不占用,原地排序

 

3、插入排序(Insertion Sort)


插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,通过比较找到合适的位置插入元素来达到排序的目的(不是通过交换位置而是通过比较)。

3.1 算法描述

一般来说,插入排序都采用原地排序(In-place)在数组上实现。具体算法描述如下:

  • 从第一个元素开始,该元素可以认为已经被排序;
  • 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  • 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  • 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  • 将新元素插入到该位置后;
  • 重复步骤2~5。

3.2 动图演示

3.3 代码实现

void InsertionSort(int array[], int length) {
    int preIndex, current;
    for (int i = 1; i < length; i++) {
        preIndex = i - 1;
        current = array[i];
        while (preIndex >= 0 && array[preIndex] > current) {
            array[preIndex + 1] = array[preIndex];
            preIndex--;
        }
        array[preIndex + 1] = current;
    }
}

3.4 时间复杂度

最好的情况下:正序有序(从小到大),这样只需要比较n次,不需要移动。因此时间复杂度为O(n) 
最坏的情况下:逆序有序,这样每一个元素就需要比较n次,共有n个元素,因此实际复杂度为O(n­2) 
平均情况下:O(n­2)

3.5 空间复杂度

插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

3.6 稳定性

理解性记忆比死记硬背要好。因此,我们来分析下。稳定性,就是有两个相同的元素,排序先后的相对位置是否变化,主要用在排序时有多个排序规则的情况下。在插入排序中,preIndex是已排序部分中的元素,当current和array[preIndex]比较时,直接插到array[preIndex]的后面(没有必要插到array[preIndex]的前面,这样做还需要不必要的移动),因此,插入排序是稳定的。

3.7 占用额外内存

不占用,原地排序

 

4、希尔排序(Shell Sort)


1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。我们知道,插入排序对于大规模的乱序数组的时候效率是比较慢的,因为它每次只能将数据移动一位,希尔排序为了加快插入的速度,让数据移动的时候可以实现跳跃移动,节省了一部分的时间开支。

4.1 算法描述

  • 希尔排序的思想是采用插入排序的方法
  • 先让数组中任意间隔为 h 的元素有序,刚开始 h 的大小可以是 h = n / 2
  • 接着让 h = n / 4,让 h 一直缩小
  • 当 h = 1 时,也就是此时数组中任意间隔为1的元素有序,此时的数组就是有序的了

4.2 动图演示

4.3 代码实现

void ShellSort(int array[], int length) {
    for (int gap = length/2; gap > 0; gap = gap/2) {
        for (int i = gap; i < length; i++) {
            int j = i;
            int current = array[i];
            while (j - gap >= 0 && current < array[j - gap]) {
                array[j] = array[j - gap];
                j = j - gap;
            }
            array[j] = current;
        }
    }
}

4.4 时间复杂度

最好情况:由于希尔排序的好坏和步长d的选择有很多关系,因此,目前还没有得出最好的步长如何选择(现在有些比较好的选择了,但不确定是否是最好的)。所以,不知道最好的情况下的算法时间复杂度。 
最坏情况下:O(N*logN),最坏的情况下和平均情况下差不多。 
平均情况下:O(N*logN)

4.5 空间复杂度

空间复杂度:O(1)

4.6 稳定性

非稳定排序

4.7 占用额外内存

原地排序

 

5、归并排序(Merge Sort)


归并排序是另一种不同的排序方法,因为归并排序使用了递归分治的思想,所以理解起来比较容易。先递归划分子问题,然后合并结果。把待排序列看成由两个有序的子序列,然后合并两个子序列,然后把子序列看成由两个有序序列。倒着来看,其实就是先两两合并,然后四四合并,最终形成有序序列。

5.1 算法描述

  • 把长度为n的输入序列分成两个长度为n/2的子序列;
  • 对这两个子序列分别采用归并排序;
  • 将两个排序好的子序列合并成一个最终的排序序列。

5.2 动图演示

5.3 代码实现

 

5.4 时间复杂度

最好的情况下:一趟归并需要n次,总共需要logN次,因此为O(N*logN)
最坏的情况下,接近于平均情况下,为O(N*logN)
说明:对长度为n的文件,需进行logN 趟二路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlgn)。

5.5 空间复杂度

O(n)

5.6 稳定性

归并排序最大的特色就是它是一种稳定的排序算法。归并过程中是不会改变元素的相对位置的。

5.7 占用额外内存

需要O(n)的额外空间。但是很适合于多链表排序。

 

6、快速排序(Quick Sort)


快速排序的核心思想也是分治法,分而治之。它的实现方式是每次从序列中选出一个基准值,把这个元素称之为中轴元素,其他数依次和基准值做比较,比基准值大的放右边,比基准值小的放左边,然后再对左边和右边的两组数分别选出一个基准值,进行同样的比较移动,重复步骤,直到最后都变成单个元素,整个数组就成了有序的序列。

6.1 算法描述

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

6.2 动图演示

6.3 代码实现

 

6.4 时间复杂度

最好的情况下:因为每次都将序列分为两个部分(一般二分都复杂度都和logN相关),故为 O(N*logN) 
最坏的情况下:基本有序时,退化为冒泡排序,几乎要比较N*N次,故为O(N*N)

6.5 空间复杂度

 

6.6 稳定性

由于每次都需要和中轴元素交换,因此原来的顺序就可能被打乱。如序列为 5 3 3 4 3 8 9 10 11会将3的顺序打乱。所以说,快速排序是不稳定的!

6.7 占用额外内存

 

7、堆排序(Heap Sort)


堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆的特点就是堆顶的元素是一个最值,大顶堆的堆顶是最大值,小顶堆则是最小值。堆排序就是把堆顶的元素与最后一个元素交换,交换之后破坏了堆的特性,我们再把堆中剩余的元素再次构成一个大顶堆,然后再把堆顶元素与最后第二个元素交换....如此往复下去,等到剩余的元素只有一个的时候,此时的数组就是有序的了。

7.1 算法描述

  • 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
  • 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
  • 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

7.2 动图演示

7.3 代码实现

 

7.4 时间复杂度

最坏情况下,接近于最差情况下:O(N*logN),因此它是一种效果不错的排序算法。

7.5 空间复杂度

 

7.6 稳定性

堆排序需要不断地调整堆,因此它是一种不稳定的排序。

7.7 占用额外内存

 

8、计数排序(Counting Sort)


计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

8.1 算法描述

  • 找出待排序的数组中最大和最小的元素;
  • 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
  • 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
  • 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。

8.2 动图演示

8.3 代码实现

 

8.4 时间复杂度和空间复杂度

当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。

8.6 稳定性

稳定的排序算法

8.7 占用额外内存

占用

 

9、桶排序(Bucket Sort)


桶排序就是把最大值和最小值之间的数进行瓜分,例如分成 10 个区间,10个区间对应10个桶,我们把各元素放到对应区间的桶中去,再对每个桶中的数进行排序,可以采用归并排序,也可以采用快速排序之类的。之后每个桶里面的数据就是有序的了,我们在进行合并汇总。

9.1 算法描述

  • 设置一个定量的数组当作空桶;
  • 遍历输入数据,并且把数据一个一个放到对应的桶里去;
  • 对每个不是空的桶进行排序;
  • 从不是空的桶里把排好序的数据拼接起来。 

9.2 动图演示

9.3 代码实现

 

9.4 时间复杂度和空间复杂度

桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。 

9.5 稳定性

 

96 占用额外内存

 

10、基数排序(Radix Sort)


基数排序是一种非比较排序,按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

例如:先以个位数的大小来对数据进行排序,接着以十位数的大小来多数进行排序,接着以百位数的大小......

排到最后,就是一组有序的元素了。不过,他在以某位数进行排序的时候,是用“桶”来排序的。

由于某位数(个位/十位....,不是一整个数)的大小范围为0-9,所以我们需要10个桶,然后把具有相同数值的数放进同一个桶里,之后再把桶里的数按照0号桶到9号桶的顺序取出来,这样一趟下来,按照某位数的排序就完成了。

10.1 算法描述

  • 取得数组中的最大数,并取得位数;
  • arr为原始数组,从最低位开始取每个位组成radix数组;
  • 对radix进行计数排序(利用计数排序适用于小范围数的特点);

10.2 动图演示

10.3 代码实现

 

10.4 时间复杂度

基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。

10.5 空间复杂度

基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。

10.6 稳定性

基数排序基于分别排序,分别收集,所以是稳定的。

10.7 占用额外内存

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zfpp25_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值