排序算法(1)

c实例请参考 排序算法-c实现


术语记录:

稳定(Stability) - 如果一个排序算法是得一个对象排序后,相同 key 的元素顺序不变,则我们称此排序算法是稳定的

基于比较排序算法(comparison based sorting algorithms) - 该排序算法需要有元素之间的比较,任何需要基于比较排序算法最少需要 nlogn 次比较

原位置排序(Sorting In Place) - 即排序的时候不需要额外的空间,但是需要挪位

尾递归(Tail Recursion):如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归,尾递归可以被消除



常用的排序算法有:

  1. 选择排序(Selection Sort)
  2. 冒泡排序(Bubble Sort)
  3. 插入排序(Insertion Sort)
  4. 归并排序(Merge Sort)
  5. 堆排序(Heap Sort)
  6. 快速排序(Quick Sort)
  7. 基数排序(Radix Sort)
  8. 计数排序(Counting Sort)
  9. 桶排序(Bucket Sort)
  10. 鸽巢排序(Pigeonhole Sort)
  11. 希尔排序(Shell Sort)
  12. 梳子排序(Comb Sort)
  13. 圈排序(Cycle Sort)


备注:下面默认都是从小到大排序


选择排序(Selection Sort)

重复查找最小值放在开头,下面是算法模拟

假设数组 arr[] = {5,3,10,6,8},i = 0
1.从 i = 0 开始遍历5次找到最小值 3 跟 arr[i] 交换 - {3,5,10,6,8}
2.从 i = 1 开始遍历4次找到最小值 5,无需交换 - {3,5,10,6,8}
3.从 i = 2 开始遍历3次找到最小值 6 跟 arr[i] 交换变为 {3,5,6,10,8}
4.从 i = 3 开始遍历2次找到最小值 8 跟 arr[i] 交换变为 {3,5,6,8,10}

时间复杂度:O(n^2),因为需要2个循环语句

空间复杂度:O(1)


冒泡排序(Bubble Sort)

是最简单的排序,自然稳定排序算法,原位置排序(Sorting In Place)

重复交换相邻的顺序不对的元素,下面是未优化的算法模拟

假设数组 arr[] = {5,3,10,6}
5 3 10 6 -> 3 5 10 6
3 5 10 6 -> 3 5 10 6
3 5 10 6 -> 3 5 6 10

3 5 6 10 -> 3 5 6 10
3 5 6 10 -> 3 5 6 10

3 5 6 10 -> 3 5 6 10

优化前:

    时间复杂度:O(n^2)

    空间复杂度:O(1)

优化后:

    最差情况时间复杂度为:O(n^2)

    最好情况:O(n) - 当排序已经排序好了的数组

如何优化请看下一节实际例子


插入排序(Insertion Sort)

是一种简单直观的排序算法,工作原理是在左边是已排序的序列,右边是未排序的序列,在已排序的序列从后向前扫描,找到相应位置并插入。通常采用in-place排序,因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间

时间复杂度:O(n^2)

边界情况:如果元序列是逆序的,那么需要最大的时间,如果已经排好序,那么只需要最小时间即n

该算法可以将从后往前搜索比较改为二分查找,称为 二分查找插入排序 这样可以减少 比较操作提升速度,但是 交换操作 的数目没变,所以时间复杂度还是O(n^2)

插入排序对于列表排序支持非常友好


归并排序(Merge Sort)

与 快速排序 一样,归并排序属于分治法(分而治之)(Divide and Conquer)

递归地把当前序列平均分割成两半,在保持元素顺序的同时将上一步得到的子序列集成到一起(归并)

下图来自于维基百科,递归的分割到长度为1,此时合并动作开始,直到合并成完整的数组

时间复杂度:T(n) = 2T(n/2) + Θ(n) = Θ(nLogn)

空间复杂度:O(n),由于使用递归

归并排序一般是按顺序访问数据,所以对随机访问要求比较低。


快速排序(Quick Sort)

与 归并 一样,快速排序属于分治法(分而治之)(Divide and Conquer),简称快排

挑出一个元素作为基准值(pivot),比基准值小的放到左边,比基准值大的放到右边,如此重复操作,基准值有很多挑选法,比如

1.总是挑选第一个

2.总是挑选最后一个

3.随机挑选

4.挑选中间一个

快排算法里最主要的是分割算法(partition)

时间复杂度:

一般情况下为 T(n) = T(k) + T(n-k-1) + Θ(n),k < 基准值

当基准值总是最大值或者最小值时为最差情况,在递归最差的情况下时间复杂度可以表示为 T(n) = T(0) + T(n-1) + Θ(n) 即 T(n) = T(n-1) + Θ(n) 即 Θ(n^2)

当基准值总是挑选中间值时为最优情况,在递归最优情况下时间复杂度为 T(n) = 2T(n/2) + Θ(n) 即 Θ(nlogn)

平均情况为 O(nLogn)

由于在内层循环里在大多数数据结构里都可以实施,快速排序算法非常快,而且通过修改基准值来改变其行为,一般情况下在特定数据结构上最差情况很难发生,但是在数据量非常大并且是外部存储时,归并算法更好。

稳定性:默认不稳定,牺牲性能和空间来维护稳定性

in-place:一般情况下是

排序数组上,快速排序比归并排序更首选,但是对于排列列表,归并排序更优先

以下三种将会是快速排序的最差情况(其实就是每次的基准值(pivot)都取到最大值或者最小值):

1.正序排列好了

2.逆序排列好了

3.所有元素都相同(即为1,2的特殊情况)



堆排序(Heap Sort)

堆排序是在 二叉堆 数据结构上基于比较的排序技术,跟 选择排序 有点像,二叉堆一般用数组来表示

二叉堆是完全二叉树或者是近似完全二叉树,父节点总是比两个子节点大(大顶堆 max heap)或者小(小顶堆 min heap)

Input data: 4, 10, 3, 5, 1
         4(0)
        /   \
     10(1)   3(2)
    /   \
 5(3)    1(4)

下标为 i 的父节点的子节点为 arr[2*i+1] 和 arr[2*i+2]

下标为 i 的子节点的父节点为 (i - 1)/2

排序步骤:

1.先构造大顶堆,然后将堆顶元素与末尾元素交换,此时最大数已经在末尾

2.排除掉已经排序在末尾的大元素,继续构造大顶堆,然后将堆顶元素与末尾元素继续交换

3.重复以上动作即可完成堆排序

时间复杂度:O(nlogn)

in-place

默认实现不稳定


基数排序(Radix Sort)

基数排序是一种非比较型排序,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数

时间复杂度:O(k*n),其中 n 是排序元素个数,k 是数字位数。注意这不是说这个时间复杂度一定优于 O(n*log(n)),k 的大小取决于数字位的选择(比如比特位数),和待排序数据所属数据类型的全集的大小;k 决定了进行多少轮处理,而 n 是每轮处理的操作数目

如果考虑和比较排序进行对照,基数排序的形式复杂度虽然不一定更小,但由于不进行比较,因此其基本操作的代价较小,而且在适当选择的 底数(决定 k 的值) 之下,k 一般不大于 log n,所以基数排序一般要快过基于比较的排序,比如快速排序


计数排序(Counting Sort)

计数排序使用一个额外的数组 C ,其中第i个元素是待排序数组 A 中值等于 i 的元素的个数。然后根据数组 C 来将 A 中的元素排到正确的位置

计数排序不是比较排序,排序的速度快于任何比较排序算法,所以一般可以作为其他排序算法的子排序比如 基数排序,扩展下可以支持负数排序,但是不支持小数

计数排序对于数据范围很大的数组,需要大量时间和内存,计数排序是用来排序0到100之间的数字的最好的算法

步骤:

1.找出待排序的数组中最大和最小的元素

2.统计数组中每个值为 i 的元素出现的次数,存入数组C 的第 i 项

3.对所有的计数累加(C 中的第一个元素开始,每一项和前一项相加)

4.反向填充目标数组:将每个元素 i 放在新数组的第 C[i] 项,每放一个元素就将 C[i] 减去1

时间复杂度:O(n+k),n为数组个数,k为元素范围

空间复杂度:O(n+k)


桶排序(Bucket Sort)

工作的原理是将数组分到有限数量的桶里。每个桶再个别排序。当要被排序的数组内的数值是 均匀分配(uniformly distributed) 的时候,桶排序使用线性时间O(n)

1.设置一个定量的数组当作空桶子

2.寻访序列,并且把项目一个一个放到对应的桶子去

3.对每个不是空的桶子进行排序

4.从不是空的桶子里把项目再放回原来的序列中。


鸽巢排序(Pigeonhole Sort)

对桶排序的改进,适用情况变得更窄,时间复杂度为O(n),在元素个数和可能的key值大致一样时适用。

1.遍历查找最大值(max)和最小值(min)并算出范围 R

2.初始化大小为 R 的空鸽巢

3.遍历每个元素,将 arr[i] 放入索引为 arr[i] - min 的巢洞

4.遍历所有非空的巢洞按序放回原始数组



希尔排序(Shell Sort)

递减增量排序算法,是 插入排序 的一种更高效的改进版本,希尔排序是非稳定排序算法

在步长gap的选择上一般选择 len/2,其实还有更好的选择办法具体可以参考 wiki_shellsort

假设有数组 [ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],我们以步长为 5 进行排序,大致看起来是这样的

13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10

我们对每列进行 插入排序

10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45

此时数组看起来是这样的 [ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ],然后再以步长为3进行 插入排序

10 14 73
25 23 13
27 94 33
39 25 59
94 65 82
45

排序之后为

10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94

最后一步长为1进行排序,此时即为简单的 插入排序


梳子排序(Comb Sort)

对于冒泡排序的改进,在冒泡排序中,只比较数组中相邻的二项,即比较的二项的间距 (Gap) 是1,梳排序提出此间距其实可大于1。

梳排序中,开始时的间距设置为数组长度,并在循环中以固定比率递减,通常递减率设置为 1.3(前人通过经验总结出来)。

在一次循环中,梳排序如同冒泡排序一样把数组从首到尾扫描一次,比较及交换两项,不同的是两项的间距不固定于1。

如果间距递减至1,梳排序假定输入数组大致排序好,并以 冒泡排序 作最后检查及修正

假设输入为

8 4 3 7 6 5 2 1
目标为将之变成递增排序。因为输入长度=8,开始时设置间距为8/1.3≒6,则比较8和2、4和1,并作交换两次。

8 4 3 7 6 5 2 1
2 4 3 7 6 5 8 1
2 1 3 7 6 5 8 4
第二次循环,更新间距为6/1.3≒4。比较2和6、1和5,直至7和4,此循环中只须交换一次。

2 1 3 7 6 5 8 4
2 1 3 4 6 5 8 7
接下来的每次循环,间距依次递减为3 → 2 → 1,

间距=3时,不须交换

2 1 3 4 6 5 8 7
间距=2时,不须交换

2 1 3 4 6 5 8 7
间距h=1时,交换三次

2 1 3 4 6 5 8 7
1 2 3 4 6 5 8 7
1 2 3 4 5 6 8 7
1 2 3 4 5 6 7 8
上例中,共作了六次交换以完成排序。

时间复杂度:最好情况下 O(n),最差情况下 O(n^2)


圈排序(Cycle Sort)

in-place 不稳定排序算法,只适用对于内存写入和交换特别消耗的情况,比如当每个元素特别大,那么每次交换将会是大量的写入。圈排序可以保证在正确位置的元素不动。

arr[] = {10, 5, 2, 3}
 index =  0   1   2   3
cycle_start = 0 
item = 10 = arr[0]

Find position where we put the item  
pos = cycle_start
while (arr[i] < item)  
    pos++;

We put 10 at arr[3] and change item to 
old value of arr[3].
arr[] = {10, 5, 2, 10} 
item = 3 

Again rotate rest cycle that start with index '0' 
Find position where we put the item = 3 
we swap item with element at arr[1] now 
arr[] = {10, 3, 2, 10} 
item = 5

Again rotate rest cycle that start with index '0' and item = 5 
we swap item with element at arr[2].
arr[] = {10, 3, 5, 10 } 
item = 2

Again rotate rest cycle that start with index '0' and item = 2
arr[] = {2, 3,  5, 10}  

Above is one iteration for cycle_stat = 0.
Repeat above steps for cycle_start = 1, 2, ..n-2

时间复杂度:O(n^2)


c实例请参考 排序算法-c实现 


参考:

geeksforgeeks-algorithms

维基百科

上一篇: 搜索算法
下一篇: 排序算法(2)-c实现
作者邮箱: 203328517@qq.com