** 排序算法**
排序算法是《数据结构与算法》中最基本的算法之一。
排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。用下面的表概括:
排序算法 | 时间复杂度(平均) | 时间复杂度(最好情况) | 时间复杂度(最坏情况) | 空间复杂度 | 排序方式 | 稳定性 |
---|---|---|---|---|---|---|
冒泡排序 | O(n²) | O(n) | O(n²) | O(1) | In-place | 稳定 |
选择排序 | O(n²) | O(n²) | O(n²) | O(1) | In-place | 不稳定 |
插入排序 | O(n²) | O(n) | O(n²) | O(1) | In-place | 稳定 |
希尔排序 | O(n log n) | O(n log² n) | O(n log² n) | O(1) | In-place | 不稳定 |
归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | Out-place | 稳定 |
快速排序 | O(n log n) | O(n log n) | O(n²) | O(log n) | In-place | 不稳定 |
堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | In-place | 不稳定 |
计数排序 | O(n + k) | O(n + k) | O(n + k) | O(k) | Out-place | 稳定 |
桶排序 | O(n + k) | O(n + k) | O(n²) | O(n + k) | Out-place | 稳定 |
基数排序 | O(n * k) | O(n * k) | O(n * k) | O(n + k) | Out-place | 稳定 |
0.1 算法分类
十种常见排序算法可以分为两大类:
- 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(n log n),因此也称为非线性时间比较类排序。
- 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ofoys7DG-1636987687191)(D:\txozhou\Documents\排序算法.jpg)]
02.关于稳定性
- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
- 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
03.关于时间复杂度
- 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
- **空间复杂度:**是指算法在计算机
平方阶 (O(n²)) 排序 各类简单排序:直接插入、直接选择和冒泡排序。
线性对数阶 (O(n log² n)) 排序 快速排序、堆排序和归并排序;
O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数。 希尔排序
线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序。
名词解释:
- n:数据规模
- k:"桶"的个数
- In-place:占用常数内存,不占用额外内存
- Out-place:占用额外内存
- 稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同
内执行时所需存储空间的度量,它也是数据规模n的函数。
1、冒泡排序(Bubble Sort)
1.1 算法描述
冒泡排序是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢*“浮”*到数列的顶端。
1.2算法分析
1.1.1原理
冒泡排序在扫描过程中两两比较相邻记录,如果反序则交换,最终,最大记录就被“沉到”了序列的最后一个位置,第二遍扫描将第二大记录“沉到”了倒数第二个位置,重复上述操作,直到n-1 遍扫描后,整个序列就排好序了。
1.1.2 什么时候最快
当输入的数据已经是正序时(都已经是正序了,我还要你冒泡排序有何用啊)。
1.1.3 什么时候最慢
当输入的数据是反序时(写一个 for 循环反序输出数据不就行了,干嘛要用你冒泡排序呢,我是闲的吗)。
平均时间复杂度: T(n) = O(n²)
最坏时间复杂度: T(n) = O(n²)
:当输入的数据是反序时
最好时间复杂度: T(n) = O(n)
:当输入的数据已经有序时,只需遍历一遍用于确认数据已有序。
空间复杂度: O(1)
稳定性: 稳定
1.3 算法步骤
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 持续每次对越来越少的元素重复以上步骤1~3,直到排序完成。
1.4 代码演示
C语言
#include <stdio.h> void bubble_sort(int arr[], int len) { int i, j, temp; for (i = 0; i < len - 1; i++) for (j = 0; j < len - 1 - i; j++) if (arr[j] > arr[j + 1]) { temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } int main() { int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70 }; int len = (int) sizeof(arr) / sizeof(*arr); //计算数组的长度 Sizeof(arr)数组的字节,*arr相当于a[0] 就是一个存储单元的内容 for (int i = 0; i < len; i++) printf("%d ", arr[i]); printf("\n"); bubble_sort(arr, len); //冒泡排序 int i; for (int i = 0; i < len; i++) printf("%d ", arr[i]); return 0; }
运行结果:
输入:22 34 3 32 82 55 89 50 37 5 64 35 9 70
输出:3 5 9 22 32 34 35 37 50 55 64 70 82 89
C++
#include <iostream> using namespace std; template<typename T> //整数或浮点数皆可使用,若要使用类(class)或结构体(struct)时必须重载大于(>)运算符 void bubble_sort(T arr[], int len) { int i, j; for (i = 0; i < len - 1; i++) for (j = 0; j < len - 1 - i; j++) if (arr[j] > arr[j + 1]) swap(arr[j], arr[j + 1]); } int main() { int arr[] = { 61, 17, 29, 22, 34, 60, 72, 21, 50, 1, 62 }; int len = (int) sizeof(arr) / sizeof(*arr); bubble_sort(arr, len); //冒泡排序 for (int i = 0; i < len; i++) cout << arr[i] << ' '; cout << endl; float arrf[] = { 17.5, 19.1, 0.6, 1.9, 10.5, 12.4, 3.8, 19.7, 1.5, 25.4, 28.6, 4.4, 23.8, 5.4 }; len = (float) sizeof(arrf) / sizeof(*arrf); for (int i = 0; i < len; i++) cout << arrf[i] << ' '; cout<<endl; bubble_sort(arrf, len); for (int i = 0; i < len; i++) cout << arrf[i] << ' '; cout<<endl; return 0; }
运行结果:
输入:61 17 29 22 34 60 72 21 50 1 62
输出:1 17 21 22 29 34 50 60 61 62 72
输入:17.5 19.1 0.6 1.9 10.5 12.4 3.8 19.7 1.5 25.4 28.6 4.4 23.8 5.4
输出:0.6 1.5 1.9 3.8 4.4 5.4 10.5 12.4 17.5 19.1 19.7 23.8 25.4 28.6
python
def bubbleSort(arr): n = len(arr) # 遍历所有数组元素 for i in range(n): # Last i elements are already in place for j in range(0, n-i-1): if arr[j] > arr[j+1] : arr[j], arr[j+1] = arr[j+1], arr[j] arr = [64, 34, 25, 12, 22, 11, 90, 56,78] print ("排序前的数组:") for i in range(len(arr)): print ("%d" %arr[i]) print("\n") bubbleSort(arr) print ("排序后的数组:") for i in range(len(arr)): print ("%d" %arr[i])
运行结果:
输入:排序前的数组:
64 34 25 12 22 11 90 56 78
输出:排序后的数组:
11 12 22 25 34 56 64 78 90
1.5 动图显示
1.6算法改进思路
- 改进思路一:设置一标志flag,当一趟遍历过程中发生元素交换时改变flag值,而某趟当flag值没有改变,则代表数组已经有序,无需再继续排序。
- 改进思路二: 设置一标志性变量pos,用于记录每趟排序中最后一次进行交换的位置。由于pos位置之后的记录均已交换到位,故在进行下一趟排序时只要扫描到pos位置即可。
- 改进思路三:传统冒泡排序中每一趟排序操作只能找到一个最大值或最小值,我们考虑利用在每趟排序中进行正向和反向两遍冒泡的方法一次可以得到两个最终值(最大者和最小者) , 从而使排序趟数几乎减少了一半。
2、选择排序(Selection Sort)
2.1 算法描述
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。 无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
注意选择排序与冒泡排序的区别:冒泡排序通过依次交换相邻两个顺序不合法的元素位置,从而将当前最小(大)元素放到合适的位置;而选择排序每遍历一次都记住了当前最小(大)元素的位置,最后仅需一次交换操作即可将其放到合适的位置。
2.2 算法分析
n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:
- 初始状态:无序区为R[1…n],有序区为空;
- 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R(i…n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1…i]和R[i+1…n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
- n-1趟结束,数组有序化了。
选择排序是时间复杂度表现最稳定的排序算法之一,因为无论什么数据进去都是O(n²) 的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。
平均时间复杂度: T(n) = O(n²)
最坏时间复杂度: T(n) = O(n²)
最好时间复杂度: T(n) = O(n²)
空间复杂度: O(1)
稳定性: 不稳定
2.3 算法步骤
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
2.4代码演示
C语言
void swap(int *a,int *b) //交换两个数 { int temp = *a; *a = *b; *b = temp; } void selection_sort(int arr[], int len) { int i,j; for (i = 0 ; i < len - 1 ; i++) { int min = i; for (j = i + 1; j < len; j++) //走访未排序的元素 if (arr[j] < arr[min]) //找到目前最小值 min = j; //记录最小值 swap(&arr[min], &arr[i]); //做交换 } }
C++
void SelectSort(int *arr, int size) { if (arr == NULL) return; //1.找到无序区中最小的元素和它的下标 int i, j; for (i = 0; i < size - 1; i++) { int k = i; for (j = i + 1; j < size; j++) { if (arr[j] < arr[k]) { k = j; } } //2.把最小的元素与无序区第一个元素交换 //swap(arr[i], arr[k]); if (k != i) { int tmp = arr[i]; arr[i] = arr[k]; arr[k] = tmp; } } }
python
def selectionSort(arr): for i in range(len(arr) - 1): # 记录最小数的索引 minIndex = i for j in range(i + 1, len(arr)): if arr[j] < arr[minIndex]: minIndex = j # i 不是最小数时,将 i 和最小数进行交换 if i != minIndex: arr[i], arr[minIndex] = arr[minIndex], arr[i] return arr arr = [64, 34, 25, 12, 22, 11, 90, 56,78] print ("排序前的数组:") for i in range(len(arr)): print ("%d" %arr[i]), print("\n") #bubbleSort(arr) selectionSort(arr) print ("排序后的数组:") for i in range(len(arr)): print ("%d" %arr[i]),
2.5 动图演示
2.6算法改进思路
性能表现实在太稳定了,一般改进思路可以从空间换时间角度切入,减少比较次数。
3、插入排序(Insertion Sort)
3.1 算法描述
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
3.2 算法分析
直接插入排序(straight insertion sort),有时也简称为插入排序(insertion sort),是减治法的一种典型应用。其基本思想如下:
- 对于一个数组A[0,n]的排序问题,假设认为数组在A[0,n-1]排序的问题已经解决了。
- 考虑A[n]的值,从右向左扫描有序数组A[0,n-1],直到第一个小于等于A[n]的元素,将A[n]插在这个元素的后面。
很显然,基于增量法的思想在解决这个问题上拥有更高的效率。
直接插入排序对于最坏情况(严格递减的数组),需要比较和移位的次数为n(n-1)/2;对于最好的情况(严格递增的数组),需要比较的次数是n-1,需要移位的次数是0。当然,对于最好和最坏的研究其实没有太大的意义,因为实际情况下,一般不会出现如此极端的情况。然而,直接插入排序对于基本有序的数组,会体现出良好的性能,这一特性,也给了它进一步优化的可能性。(希尔排序)。直接插入排序的时间复杂度是O(n²),空间复杂度是O(1),同时也是稳定排序。
下面用一个具体的场景,直观地体会一下直接插入排序的过程:
场景:
现有一个无序数组,共7个数:89 45 54 29 90 34 68。
使用直接插入排序法,对这个数组进行升序排序。
89 45 54 29 90 34 68
45 89 54 29 90 34 68
45 54 89 29 90 34 68
29 45 54 89 90 34 68
29 45 54 89 90 34 68
29 34 45 54 89 90 68
29 34 45 54 68 89 90
插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
平均时间复杂度: T(n) = O(n²)
最坏时间复杂度: T(n) = O(n²)
:输入数组按降序排列(完全逆序)
最好时间复杂度: T(n) = O(n)
:输入数组按升序排列(基本有序)
空间复杂度: O(1)
稳定性:稳定
3.3算法步骤
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
3.4 代码演示
C语言
void insertion_sort(int arr[], int len){ int i,j,key; for (i=1;i<len;i++){ key = arr[i]; j=i-1; while((j>=0) && (arr[j]>key)) { arr[j+1] = arr[j]; j--; } arr[j+1] = key; } }
C++
void insertion_sort(int arr[],int len){ for(int i=1;i<len;i++){ int key=arr[i]; int j=i-1; while((j>=0) && (key<arr[j])){ arr[j+1]=arr[j]; j--; } arr[j+1]=key; } }
python
def insertionSort(arr): for i in range(len(arr)): preIndex = i-1 current = arr[i] while preIndex >= 0 and arr[preIndex] > current: arr[preIndex+1] = arr[preIndex] preIndex-=1 arr[preIndex+1] = current return arr arr = [64, 34, 25, 12, 22, 11, 90, 56,78] print ("排序前的数组:") for i in range(len(arr)): print ("%d" %arr[i]) print("\n") #bubbleSort(arr) #selectionSort(arr) insertionSort(arr) print ("排序后的数组:") for i in range(len(arr)): print ("%d" %arr[i])
3.5动图演示
3.6算法改进思路
- 改进思路一:查找插入位置时使用二分查找的方式,减少比较次数。
4、希尔排序(Shell Sort)
4.1算法描述
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称递减增量排序算法。但希尔排序是非稳定排序算法。同时该算法是冲破O(n²)的第一批算法之一。本文会以图解的方式详细介绍希尔排序的基本思想及其代码实现。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
4.2算法分析
希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的。
-
基本思想
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。
我们来看下希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。
平均时间复杂度:
T(n) = O(n^1.5)
最坏时间复杂度:
T(n) = O(nlog²n)
空间复杂度:
O(1)
稳定性:
不稳定
,由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
4.3 算法步骤
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
4.4 代码演示
C语言
void Shell_sort(int arr[], int len) { int gap, temp, i, j; for (gap = len >> 1; gap > 0; gap >>= 1){ for (i = gap; i < len; i++) { temp = arr[i]; for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap) arr[j + gap] = arr[j]; arr[j + gap] = temp; } } } int main() { int arr[] = {11, 33, 35, 38, 23, 29, 89, 69, 99, 56, 68, 39, 78}; int len = (int)sizeof(arr) / sizeof(*arr);//计算数组的长度 Sizeof(arr)数组的字节,*arr相当于a[0] 就是一个存储单元的内容 for (int i = 0; i < len; i++){ printf("%d ", arr[i]); } printf("\n"); Shell_sort(arr, len); for (int i = 0; i < len; i++) { printf("%d ", arr[i]); } printf("\n"); return 0; }
C++
template<typename T> //整数或浮点数皆可使用,若要使用类(class)或结构体(struct)时必须重载大于(>)运算符 void shell_sort(T array[], int length) { int h = 1; while (h < length / 3) { h = 3 * h + 1; } while (h >= 1) { for (int i = h; i < length; i++) { for (int j = i; j >= h && array[j] < array[j - h]; j -= h) { std::swap(array[j], array[j - h]); } } h = h / 3; } } int main() { int array[] = {33, 35, 59, 19, 56, 78, 96, 66, 45, 72, 39, 11}; int length = (int)sizeof(array) / sizeof(*array);//计算数组的长度 Sizeof(arr)数组的字节,*arr相当于a[0] 就是一个存储单元的内容 for (int i = 0; i < length; i++) cout << array[i] <<" "; cout << endl; shell_sort(array, length); for (int i = 0; i < length; i++) cout << array[i] << " "; cout << endl; return 0; }
python
def shellSort(arr): import math gap=1 while(gap < len(arr)/3): gap = gap*3+1 while gap > 0: for i in range(gap,len(arr)): temp = arr[i] j = i-gap while j >=0 and arr[j] > temp: arr[j+gap]=arr[j] j-=gap arr[j+gap] = temp gap = math.floor(gap/3) return arr arr = [64, 34, 25, 12, 22, 11, 90, 56,78] print ("排序前的数组:") for i in range(len(arr)): print ("%d" %arr[i]) print("\n") #bubbleSort(arr) #selectionSort(arr) insertionSort(arr) print ("排序后的数组:") for i in range(len(arr)): print ("%d" %arr[i])
4.5动图演示
4.6算法改进思路
Shell排序的执行时间依赖于增量序列,好的增量序列的共同特征:
① 最后一个增量必须为1;
② 应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况。
有人通过大量的实验,给出了较好的结果:当n较大时,比较和移动的次数约在nl.25到1.6n1.25之间。
但是Shell排序的时间性能显然优于直接插入排序,希尔排序的时间性能优于直接插入排序的原因:
- 当文件初态基本有序时直接插入排序所需的比较和移动次数均较少。
- 当n值较小时,n 和 n² 的差别也较小,即直接插入排序的最好时间复杂度 O(n) 和最坏时间复杂度 O(n²) 差别不大。
- 在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。
因此,希尔排序在效率上较直接插入排序有较大的改进。
使用建议:
不需要大量的辅助空间,和归并排序一样容易实现。希尔排序是基于插入排序的一种算法, 在此算法基础之上增加了一个新的特性,提高了效率。希尔排序没有快速排序算法快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比O(n²)复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。 此外,希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。专家们提倡,几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快,再改成快速排序这样更高级的排序算法
5、归并排序(Merge Sort)
5.1 算法描述
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
- 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
- 自下而上的迭代;
在《数据结构与算法 JavaScript 描述》中,作者给出了自下而上的迭代方法。但是对于递归法,作者却认为:
However, it is not possible to do so in JavaScript, as the recursion goes too deep for the language to handle.
然而,在 JavaScript 中这种方式不太可行,因为这个算法的递归深度对它来讲太深了。
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(n log n) 的时间复杂度。代价是需要额外的内存空间。
5.2 算法分析
归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而**治(conquer)**的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
分而治之
可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log²n。
合并相邻有序子序列
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。
平均情况:T(n) = O(nlogn)
最差情况:T(n) = O(nlogn)
最佳情况:T(n) = O(n)
空间复杂度: O(n)
,归并排序需要一个与原数组相同长度的数组做辅助来排序
稳定性: 稳定
5.3算法步骤
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
5.4 代码实现
C语言
普通模式
void merge_sort(int* arr, int length) { int step = 1; //归并区间步长 int l[length], r[length]; //gcc, 两个临时数组,分别保存待归并的两个区间 //int l[100], r[100]; //vc while(step < length) { int start = 0; //归并区间的开始下标 while(start < length - step) { //归 int len_l, len_r; //左右待归并区间的长度 len_l = len_r = step; memcpy(l, arr + start, sizeof(int) * len_l); if(start + 2 * step > length) { len_r = length - start - step; } memcpy(r, arr + start + step, sizeof(int) * len_r); //并 int i = 0, j = 0, k = start; while(i < len_l && j < len_r) { arr[k++] = l[i] < r[j] ? l[i++] : r[j++]; } while(i < len_l) { arr[k++] = l[i++]; } start += 2 * step; } step *= 2; } }
使用递归模式
void merge_sort_recursive(int arr[], int reg[], int start, int end) { if (start >= end) return; int len = end - start, mid = (len >> 1) + start; int start1 = start, end1 = mid; int start2 = mid + 1, end2 = end; merge_sort_recursive(arr, reg, start1, end1); merge_sort_recursive(arr, reg, start2, end2); int k = start; while (start1 <= end1 && start2 <= end2) reg[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++]; //比较两个数的大小,如果a[start2]大于a[start1]则返回a[start1],否则返回a[start2] while (start1 <= end1) reg[k++] = arr[start1++]; while (start2 <= end2) reg[k++] = arr[start2++]; for (k = start; k <= end; k++) arr[k] = reg[k]; } void merge_sort(int arr[], const int len) { int reg[len]; merge_sort_recursive(arr, reg, 0, len - 1); }
C++
template<typename T> // 整数或浮点皆可使用,若要使用物件(class)时必须定"小于"(<)的运算子功能 void merge_sort(T arr[], int len) { T *a = arr; T *b = new T[len]; for (int seg = 1; seg < len; seg += seg) { for (int start = 0; start < len; start += seg + seg) { int low = start, mid = min(start + seg, len), high = min(start + seg + seg, len); int k = low; int start1 = low, end1 = mid; int start2 = mid, end2 = high; while (start1 < end1 && start2 < end2) b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++]; //比较两个数的大小,如果a[start2]大于a[start1]则返回a[start1],否则返回a[start2] while (start1 < end1) b[k++] = a[start1++]; while (start2 < end2) b[k++] = a[start2++]; } T *temp = a; a = b; b = temp; } if (a != arr) { for (int i = 0; i < len; i++) b[i] = a[i]; b = a; } delete[] b; }
python
def merge_subsequence(arr, start, m, end): # 重新开两个数组,用于要合并的两个子序列 left = arr[start: m + 1] right = arr[m + 1: end + 1] # 滑动指针,对比left 和 right两个子序列 l_index, r_index = 0, 0 while l_index < len(left) and r_index < len(right): if left[l_index] > right[r_index]: arr[start] = left[l_index] l_index += 1 else: arr[start] = right[r_index] r_index += 1 start += 1 # 对没有滑动完的序列进行赋值处理 if l_index == len(left): arr[start: end + 1] = right[r_index: len(right)] else: arr[start: end + 1] = left[l_index: len(left)] def merge_sort(arr, start, end): m = int((start + end) / 2) if start < end: merge_sort(arr, start, m) merge_sort(arr, m + 1, end) merge_subsequence(arr, start, m, end)
5.5 动图演示
5.6算法改进思路
- 对小规模子数组使用插入排序。用不同的方法处理小规模数组能改进大多递归算法的性能,在小数组上上,插入排序可能比并归排序更快。
- 测试数组是否有序。根据归并排序的特点,每次归并的两个小数组都是有序的,当 a[mid] <= a[mid + 1]时我们可以跳过merge方法,这样并不影响排序的递归调用。
- 不将元素复制到辅助数组。我们可以节省将数组复制到辅助数组的时间,这需要一些技巧。先克隆原数组到辅助数组,然后在之后的递归交换输入数组和辅助数组的角色(通过看代码更容易理解)
6、快速排序(Quick Sort)
6.1 算法描述
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),在大多数情况下都比平均时间复杂度为 O(n log n) 的排序算法表现要更好。
6.2算法分析
- 最佳情况:
T(n) = O(n log n)
,快速排序最优的情况就是每一次取到的元素都刚好平分整个数组 - 最差情况:
T(n) = O(n²)
,最差的情况就是每一次取到的元素就是数组中最小/最大的,这种情况其实就是冒泡排序了(每一次都排好一个元素的顺序) - 平均情况:
T(n) = O(n log n)
- 稳定性:
不稳定
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
6.3 算法步骤
- 从数列中挑出一个元素做为 “基准”(pivot)值,一般选第一个数,或者最后一个数。
- 采用双指针(头尾两端)遍历,从左往右找到比基准值大的第一个数,从右往左找到比基准值小的第一个数,交换两数位置,直到头尾指针相等或头指针大于尾指针,把基准值与头指针的数交换。这样一轮之后,左边的数就比基准值小,右边的数就比基准值大。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
- 左右两边数列递归结束后,排序完成。
6.4代码演示
C语言
typedef struct _Range { int start, end; } Range; Range new_Range(int s, int e) { Range r; r.start = s; r.end = e; return r; } void swap(int *x, int *y) { int t = *x; *x = *y; *y = t; } void quick_sort(int arr[], const int len) { if (len <= 0) return ; //避免len等于负值时引发段错误(Segment Fault) //r[]模拟列表,p为数量,r[p++]为pop且取得元素 Range r[len]; int p = 0; r[p++] = new_Range(0, len -1); while(p){ Range range = r[--p]; if (range.start >= range.end) continue; int mid = arr[(range.start + range.end) / 2]; //选择中间点为基准点 int left = range.start, right = range.end; do { while (arr[left] < mid) ++left; //检索基准点左侧是否符合要求 while (arr[right] > mid) --right; //检索基准点左侧是否符合要求 if (left <= right) { swap(&arr[left], &arr[right]); left++; right--; //移动指针以继续 } } while (left <= right); if (range.start < right) r[p++] = new_Range(range.start, right); if (range.end > left) r[p++] = new_Range(left, range.end); } }
C++
int Paritition1(int A[], int low, int high) { int pivot = A[low]; while (low < high) { while (low < high && A[high] >= pivot) { --high; } A[low] = A[high]; while (low < high && A[low] <= pivot) { ++low; } A[high] = A[low]; } A[low] = pivot; return low; } void QuickSort(int A[], int low, int high) //快排母函数 { if (low < high) { int pivot = Paritition1(A, low, high); QuickSort(A, low, pivot - 1); QuickSort(A, pivot + 1, high); } }
python
def partion(list,p,r): i=p-1 for j in range(p,r): if list[j]<=list[r]: i+=1 list[i],list[j]=list[j],list[i] list[i+1],list[r]=list[r],list[i+1] return i def quicksort(list,p,r): if p<r: q=partion(list,p,r) quicksort(list,p,q) quicksort(list,q+1,r)
6.5 动图演示
6.6算法改进思路
改进思路:改进选取枢轴的方法
- 选取随机数作为枢轴。但是随机数的生成本身是一种代价,根本减少不了算法其余部分的平均运行时间。
- 使用左端,右端和中心的中值做为枢轴元。经验得知,选取左端,右端,中心元素的中值会减少了快排大约 14%的比较。
- 每次选取数据集中的中位数做枢轴。选取中位数的可以在 O(n)时间内完成。(证明见
《算法导论(第二版)》
) P111 第九章中位数和顺序统计学:在平均情况下,任何顺序统计量(特别是中位数)都可以在线性时间内得到。
其他改进思路:
- 快速排序在处理小规模数据时的表现不好。这个时候可以改用插入排序。当数据规模小于一定程度时,改用插入排序。具体小到何种规模时,采用插入排序,这个理论上还不解,一些文章中说是 5 到 25 之间。SGI STL 中的快速排序采用的值是 10。
- 对于一个每个元素都完全相同的一个序列来讲,快速排序也会退化到 O(n²)。要将这种情况避免到,可以这样做:在分区的时候,将序列分为 3 堆,一堆小于中轴元素,一堆等于中轴元素,一堆大于中轴元素,下次递归调用快速排序的时候,只需对小于和大于中轴元素的两堆数据进行排序,中间等于中轴元素的一堆已经放好
7、堆排序(Heap Sort)
7.1算法描述
堆排序与快速排序,归并排序一样都是时间复杂度为 O ( N ∗ l o g N ) O(N*logN) O(N∗logN)的几种常见排序方法。学习堆排序前,先讲解下什么是数据结构中的二叉堆。
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
- 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
- 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
堆排序的平均时间复杂度为 Ο(n log n)。
7.2算法分析
调堆:O(h)
建堆:O(n)
循环调堆:O(n log n)
总运行时间T(n) = O(nlogn) + O(n) = O(nlogn)。对于堆排序的最好情况与最坏情况的运行时间,因为最坏与最好的输入都只是影响建堆的运行时间O(1)或者O(n),而在总体时间中占重要比例的是循环调堆的过程,即O(nlogn) + O(1) =O(nlogn) + O(n) = O(nlogn)。因此最好或者最坏情况下,堆排序的运行时间都是O(nlogn)。而且堆排序还是 原地算法(in-place algorithm) 。
- 平均情况:
T(n) = O(n log n)
- 最差情况:
T(n) = O(n log n)
- 最佳情况:
T(n) = O(n log n)
- 空间复杂度:
O(1)
- 稳定性:
不稳定
堆的存储:
一般都用数组来表示堆,i结点的父结点下标就为(i – 1) / 2。它的左右子结点下标分别为2 * i + 1和2 * i + 2。如下图所示:
堆排序:
由上面的介绍我们可以看出堆的第一个元素要么是最大值(大顶堆),要么是最小值(小顶堆),这样在排序的时候(假设共n个节点),直接将第一个元素和最后一个元素进行交换,然后从第一个元素开始进行向下调整至第n-1个元素。所以,如果需要升序,就建一个大堆,需要降序,就建一个小堆。
堆排序的步骤分为三步:
1、建堆(升序建大堆,降序建小堆);
2、交换数据;
3、向下调整。
假设我们现在要对数组arr[]={8,5,0,3,7,1,2}进行排序(降序):
首先要先建小堆:
堆建好了下来就要开始排序了:
现在这个数组就已经是有序的了。
7.3算法步骤
- 构造最大堆(Build_Max_Heap):若数组下标范围为0~n,考虑到单独一个元素是大根堆,则从下标n/2开始的元素均为大根堆。于是只要从n/2-1开始,向前依次构造大根堆,这样就能保证,构造到某个节点时,它的左右子树都已经是大根堆。
- 堆排序(HeapSort):由于堆是用数组模拟的。得到一个大根堆后,数组内部并不是有序的。因此需要将堆化数组有序化。思想是移除根节点,并做最大堆调整的递归运算。第一次将heap[0]与heap[n-1]交换,再对heap[0…n-2]做最大堆调整。第二次将heap[0]与heap[n-2]交换,再对heap[0…n-3]做最大堆调整。重复该操作直至heap[0]和heap[1]交换。由于每次都是将最大的数并入到后面的有序区间,故操作完后整个数组就是有序的了。
- 最大堆调整(Max_Heapify):该方法是提供给上述两个过程调用的。目的是将堆的末端子节点作调整,使得子节点永远小于父节点 。
7.4代码演示
C语言
void swap(int *a, int *b) { int temp = *b; *b = *a; *a = temp; } void max_heapify(int arr[], int start, int end) { // 建立父节点指标和子节点指标 int dad = start; int son = dad * 2 + 1; while (son <= end) { // 若子节点指标在范围内才做比较 if (son + 1 <= end && arr[son] < arr[son + 1]) // 先比较两个子节点大小,选择最大的 son++; if (arr[dad] > arr[son]) //如果父节点大于子节点代表调整完毕,直接跳出函数 return; else { // 否则交换父子内容再继续子节点和孙节点比较 swap(&arr[dad], &arr[son]); dad = son; son = dad * 2 + 1; } } } void heap_sort(int arr[], int len) { int i; // 初始化,i从最后一个父节点开始调整 for (i = len / 2 - 1; i >= 0; i--) max_heapify(arr, i, len - 1); // 先将第一个元素和已排好元素前一位做交换,再重新调整,直接排序完毕 for (i = len - 1; i > 0; i--) { swap(&arr[0], &arr[i]); max_heapify(arr, 0, i - 1); } }
C++
void max_heapify(int arr[], int start, int end) { // 建立父节点指标和子节点指标 int dad = start; int son = dad * 2 + 1; while (son <= end) { // 若子节点指标在范围内才做比较 if (son + 1 <= end && arr[son] < arr[son + 1]) // 先比較兩個子節點大小,選擇最大的 son++; if (arr[dad] > arr[son]) // 先比较两个子节点大小,选择最大的 return; else { // 否则交换父子内容再继续子节点和孙节点比较 swap(arr[dad], arr[son]); dad = son; son = dad * 2 + 1; } } } void heap_sort(int arr[], int len) { // 初始化,i从最后一个父节点开始调整 for (int i = len / 2 - 1; i >= 0; i--) max_heapify(arr, i, len - 1); // 先将第一个元素和已排好元素前一位做交换,再重新调整(刚调整的元素之前的元素),直接排序完毕 for (int i = len - 1; i > 0; i--) { swap(arr[0], arr[i]); max_heapify(arr, 0, i - 1); } }
Python
def heap_sort(ary): n = len(ary) first = int(n/2-1) #最后一个非叶子节点 for start in range(first,-1,-1): #构建最大堆 max_heapify(ary,start,n-1) for end in range(n-1,0,-1): #堆排,将最大跟堆转换成有序数组 ary[end],ary[0] = ary[0], ary[end] #将根节点元素与最后叶子节点进行互换,取出最大根节点元素,对剩余节点重新构建最大堆 max_heapify(ary,0,end-1) #因为end上面取的是n-1,故而这里直接放end-1,相当于忽略了最后最大根节点元素ary[n-1] return ary #最大堆调整:将堆的末端子节点作调整,使得子节点永远小于父节点 #start为当前需要调整最大堆的位置,end为调整边界 def max_heapify(ary,start,end): root = start while True: child = root * 2 + 1 #调整节点的子节点 if child > end: break if child + 1 <= end and ary[child] < ary[child+1]: child = child + 1 #取较大的子节点 if ary[root] < ary[child]: #较大的子节点成为父节点 ary[root], ary[child] = ary[child], ary[root] #交换 root = child else: break
7.5 动图演示
8、计数排序(Counting Sort)
8.1算法描述
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
- 计数排序的特征
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。
通俗地理解,例如有 10 个年龄不同的人,统计出有 8 个人的年龄比 A 小,那 A 的年龄就排在第 9 位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去 1 的原因。
8.2算法分析
- 平均情况:
T(n) = O(n+k)
- 最差情况:
T(n) = O(n+k)
- 最佳情况:
T(n) = O(n+k)
- 空间复杂度:
O(n+k)
- 稳定性:
稳定
当输入的元素是n 个0到k之间的整数时,它的运行时间是 O(n + k),空间复杂度也是O(n+k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。
8.3算法的步骤
- (1)找出待排序的数组中最大和最小的元素
- (2)统计数组中每个值为i的元素出现的次数,存入数组C的第i项
- (3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
- (4)反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
8.4代码演示
C语言
#include <stdio.h> #include <stdlib.h> #define random(x) rand()%(x) #define NUM 10 // 产生10个随机数 #define MAXNUM 100 //待排序的数字范围是0-100 void countingSort(int A[], int n, int k){ int *c, *b; int i; c = (int *)malloc(sizeof(int)*k);/*临时数组,注意它的大小是待排序序列中值最大的那个。如假定该排序序列中最大值为1000000,则该数组需要1000000*sizeof(int)个存储单元*/ b = (int *)malloc(sizeof(int)*n); /*存放排序结果的数组*/ for (i = 0; i < k; i++) c[i] = 0; for (i = 0; i < n; i++) c[A[i]] += 1; /*统计数组A中每个值为i的元素出现的次数*/ for (i = 1; i < k; i++) c[i] = c[i - 1] + c[i]; /*确定值为i的元素在数组c中出现的位置*/ for (i = n - 1; i >= 0; i--) { b[c[A[i]] - 1] = A[i]; /*对A数组,从后向前确定每个元素所在的最终位置;*/ c[A[i]] -= 1; } for (i = 0; i < n; i++) A[i] = b[i]; /*这个目的是返回A数组作为有序序列*/ free(c); free(b); } void printArray(int A[], int n){ int i = 0; for (i = 0; i < n; i++){ printf("%4d", A[i]); } printf("\n"); } int main() { int A[NUM]; int i; for (i = 0; i < NUM; i++) A[i] = random(MAXNUM); printf("随机产生一个数组:\n"); printArray(A, NUM); countingSort(A, NUM, MAXNUM); printf("排序后的数组:\n"); printArray(A, NUM); return 0; }
C++
vector<int> CountSort(const vector<int> & vec) { int length = vec.size(); if (length <= 1) { return vec; } else { // 1、计算最大值和最小值 int max = *max_element(vec.begin(), vec.end()); int min = *min_element(vec.begin(), vec.end()); // 2、开辟长度为(max - min + 1)的数组,即为计数排序的空间复杂度 vector<int> countArray (max - min + 1, 0); ///3、遍历数组计数 for (int i = 0; i < length; ++i) { ++countArray[vec[i] - min]; } // 4、排序 vector<int> sortArray; for (int i = 0; i < countArray.size(); ++i) { for (int j = 0; j < countArray[i]; ++j) { sortArray.push_back(min + i); } } return sortArray; } }
Python
def countingSort(arr, maxValue): bucketLen = maxValue+1 bucket = [0]*bucketLen sortedIndex =0 arrLen = len(arr) for i in range(arrLen): if not bucket[arr[i]]: bucket[arr[i]]=0 bucket[arr[i]]+=1 for j in range(bucketLen): while bucket[j]>0: arr[sortedIndex] = j sortedIndex+=1 bucket[j]-=1 return arr
8.5 动图演示
9、桶排序(Bucket Sort)
9.1算法描述
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
9.2算法分析
- 平均情况:
T(n) = O(n+k)
- 最差情况:
T(n) = O(n²)
- 最佳情况:
T(n) = O(n)
- 空间复杂度:
O(n+k)
- 稳定性:
稳定
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
9.3 算法步骤
- 设置一个定量的数组当作空桶;
- 遍历输入数据,并且把数据一个一个放到对应的桶里去;
- 对每个不是空的桶进行排序;
- 从不是空的桶里把排好序的数据拼接起来。
9.4代码演示
C语言
typedef struct node { int key; struct node *next; }KeyNode; void bucket_sort(int keys[],int size,int bucket_size) { int i,j; KeyNode **bucket_table = (KeyNode **)malloc(bucket_size * sizeof(KeyNode*)); for(i = 0;i < bucket_size;i++) { bucket_table[i] = (KeyNode*)malloc(sizeof(KeyNode)); bucket_table[i]->key = 0; bucket_table[i]->next = NULL; } for(j = 0;j < size;j++) { KeyNode *node = (KeyNode *)malloc(sizeof(KeyNode)); node->key = keys[j]; node->next = NULL; int index = keys[j]/10; KeyNode *p = bucket_table[index]; if(p->key == 0) { bucket_table[index]->next = node; (bucket_table[index]->key)++; }else { while(p->next != NULL && p->next->key <= node->key) p = p->next; node->next = p->next; p->next = node; (bucket_table[index]->key)++; } } //print result KeyNode * k = NULL; for(i = 0;i < bucket_size;i++) for(k = bucket_table[i]->next;k!=NULL;k=k->next) printf("%d ",k->key); printf("\n"); }
Python
def bucketSort(nums): # 选择一个最大的数 max_num = max(nums) # 创建一个元素全是0的列表, 当做桶 bucket = [0]*(max_num+1) # 把所有元素放入桶中, 即把对应元素个数加一 for i in nums: bucket[i] += 1 # 存储排序好的元素 sort_nums = [] # 取出桶中的元素 for j in range(len(bucket)): if bucket[j] != 0: for y in range(bucket[j]): sort_nums.append(j) return sort_nums nums = [5,6,3,2,1,65,2,0,8,0] print bucketSort(nums)
9.5图片演示
10、基数排序(Radix Sort)
10.1算法描述
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
基数排序 vs 计数排序 vs 桶排序
基数排序有两种方法:
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
- 基数排序:根据键值的每位数字来分配桶;
- 计数排序:每个桶只存储单一键值;
- 桶排序:每个桶存储一定范围的数值;
10.2算法分析
- 平均情况:
T(n) = O(n*k)
- 最差情况:
T(n) = O(n*K)
- 最佳情况:
T(n) = O(n*K)
- 空间复杂度:
O(n+k)
- 稳定性:
稳定
基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。
基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。
10.3代码演示
C语言
void print(int *a, int n) { int i; for (i = 0; i < n; i++) { printf("%d\t", a[i]); } } void radixsort(int *a, int n) { int i, b[MAX], m = a[0], exp = 1; for (i = 1; i < n; i++) { if (a[i] > m) { m = a[i]; } } while (m / exp > 0) { int bucket[BASE] = { 0 }; for (i = 0; i < n; i++) { bucket[(a[i] / exp) % BASE]++; } for (i = 1; i < BASE; i++) { bucket[i] += bucket[i - 1]; } for (i = n - 1; i >= 0; i--) { b[--bucket[(a[i] / exp) % BASE]] = a[i]; } for (i = 0; i < n; i++) { a[i] = b[i]; } exp *= BASE; #ifdef SHOWPASS printf("\nPASS : "); print(a, n); #endif } }
C++
int maxbit(int data[], int n) //辅助函数,求数据的最大位数 { int maxData = data[0]; ///< 最大数 /// 先求出最大数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。 for (int i = 1; i < n; ++i) { if (maxData < data[i]) maxData = data[i]; } int d = 1; int p = 10; while (maxData >= p) { //p *= 10; // Maybe overflow maxData /= 10; ++d; } return d; /* int d = 1; //保存最大的位数 int p = 10; for(int i = 0; i < n; ++i) { while(data[i] >= p) { p *= 10; ++d; } } return d;*/ } void radixsort(int data[], int n) //基数排序 { int d = maxbit(data, n); int *tmp = new int[n]; int *count = new int[10]; //计数器 int i, j, k; int radix = 1; for(i = 1; i <= d; i++) //进行d次排序 { for(j = 0; j < 10; j++) count[j] = 0; //每次分配前清空计数器 for(j = 0; j < n; j++) { k = (data[j] / radix) % 10; //统计每个桶中的记录数 count[k]++; } for(j = 1; j < 10; j++) count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶 for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中 { k = (data[j] / radix) % 10; tmp[count[k] - 1] = data[j]; count[k]--; } for(j = 0; j < n; j++) //将临时数组的内容复制到data中 data[j] = tmp[j]; radix = radix * 10; } delete []tmp; delete []count; }
Python
from random import randint def RadixSort( arr,d ): for k in range(d): #d为最大数的位数 0,1,2 s=[ [] for i in range(10) ] for i in arr: # /结果为float 结果为int s[ i//(10**k)%10 ].append(i) #比如345,k为此时的位数 0表示个位 通过 i//(10**K)%10就可以取到个位,在插入到第一的数组中 arr = [ j for i in s for j in i ] return arr if __name__=='__main__': #主函数 a = [randint(1,999) for i in range(10)] #列表推到论 print('待排序列:',a) # max(集合) ->去最大数 # str(数字) ->转成字符串 # len(字符串) ->取长度 num = len( str(max(a)) ) #取出集合中最大值的位数 a = RadixSort(a,num) print('排序以后的数组为',a)
10.4LSD 基数排序动图演示
小结
文章最后再对十大经典排序算法性能分析做一次小结,加深记忆。
稳定的排序:冒泡排序,插入排序,归并排序
不稳定的排序:选择排序,堆排序,快速排序,希尔排序
平均时间复杂度T(n) = O(n log n)
:希尔排序,归并排序,快速排序,堆排序
平均时间复杂度T(n) = O(n²)
:冒泡排序,简单选择排序,插入排序
最好时间复杂度T(n) = O(n)
:冒泡排序,插入排序
最好时间复杂度T(n) = O(n log n)
:归并排序,快速排序,堆排序
最好时间复杂度T(n) = O(n²)
:简单选择排序
最坏时间复杂度T(n) = O(n log n)
:归并排序,堆排序
最坏时间复杂度T(n) = O(n²)
:冒泡排序,简单选择排序,插入排序,快速排序
空间复杂度O(1)
:冒泡排序,简单选择排序,插入排序,希尔排序,堆排序
空间复杂度O(n)
:归并排序
空间复杂度O(n log n)
:快速排序
参考链接:
https://www.runoob.com/w3cnote/ten-sorting-algorithm.html
https://www.cnblogs.com/onepixel/articles/7674659.html
https://zhuanlan.zhihu.com/p/42818629