常见的排序算法包括:
冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序和堆排序。本文默认讨论由小到大排序。
排序算法 | 最好时间 | 最差时间 | 平均时间 | 空间 | 是否稳定 |
---|---|---|---|---|---|
冒泡排序 | O(n)或者O(n^2) | O(n^2) | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
希尔排序 | O(n) | O(n(log n)^2) | 依据步长变化 | O(1) | 不稳定 |
归并排序 | O(nlog n) | O(nlog n) | O(nlog n) | O(n) | 稳定 |
快速排序 | O(nlog n) | O(n^2) | O(nlog n) | O(log n~n) | 不稳定 |
堆排序 | O(nlog n) | O(nlog n) | O(nlog n) | O(1) | 不稳定 |
排序算法的稳定性的概念:假设两个相等的数ri=rj
,在排序之前ri
在rj
的前面,经过排序之后ri
还是在rj
的前面,那么该排序算法就是稳定的,否则就是不稳定的。
1.冒泡排序
冒泡排序是最简单的一种排序算法。
逆序对的概念:如果i<j
,但是在A[i] > A[j]
,那么称A[i],A[j]
为一组逆序对,如果i+1=j
,也称之为相邻的逆序对。
冒泡排序的思路很简单:存在相邻的逆序对,则交换相邻的两个元素,每一轮比较交换之后,必然相对的最后一个元素就绪。
一般冒泡排序的空间复杂度为O(1)
,最好的时间复杂度为O(n^2)
,平均时间复杂度为O(n^2)
,最坏的时间复杂度也为O(n^2)
。
稳定性取决于相等的相邻元素是否交换,由于是相邻的逆序对才交换,所以这种情况下的冒泡排序算法是稳定的。
具体代码如下:
//Bubble sort
//average:O(n^2);
//best:O(n^2);
//worst:O(n^2);
//space complication:O(1);
//stable sort
void BubbleSort(int A[], int n){
for(int i = 0; i < n-1; ++i){
for(int j = 0; j < n-i-1; ++j){
if(A[j] > A[j+1]) //>= 则冒泡排序不稳定
std::swap(A[j], A[j+1]);
}
}
}
冒泡排序还可以加以优化,使得最好的时间复杂度为O(n)
,其余无法改变。优化的手段就是添加一个全局的排序标志,一旦这个标志显示有序,那么整体就有序。
最好的情况发生在最开始就是整体有序的,经过一轮比较就直接完成。
具体代码如下:
//same
//best:O(n)
void BubbleSort(int A[], int n) {
bool sorted = false; //全局有序标志
while(!sorted) {
sorted = true; //假定已经有序
for(int i = 0; i < n - 1; ++i) {
if(A[i] > A[i + 1]) {
std::swap(A[i], A[i + 1]);
sorted = false; //清楚全局有序标志
}
}
--n; //相对末尾元素就位
}
}
2.选择排序
选择排序的思路:每一次找到最大元素的下标,找到后将其与最后一个元素交换,这样每次也使得最后一个元素就绪。
选择排序的空间复杂度为O(1)
,最好的时间复杂度为O(n^2)
,平均时间复杂度为O(n^2)
,最坏的时间复杂度也为O(n^2)
。并且是不稳定的,不稳定的原因就在于那个交换的操作。
代码如下:
//Selection sort
//average:O(n^2);
//best:O(n^2);
//worst:O(n^2);
//space complication:O(1);
//unstable sort
void SelectionSort(int A[], int n) {
for(int i = n - 1; i >= 0; --i) {
int max_index = i;
for(int j = i; j >= 0; --j) {
if(A[j] > A[max_index]) {
max_index = j;
}
}
std::swap(A[max_index], A[i]);//这一步会导致不稳定
}
}
3.插入排序
插入排序的思路:和我们平常打桥牌一样,每摸到一张牌就将其插入到合适的位置,为了给插入的元素腾位置,需要将其余的所有元素在插入之前往后移动一个位置。
选择排序的空间复杂度为O(1)
,最好的时间复杂度为O(n)
,平均时间复杂度为O(n^2)
,最坏的时间复杂度也为O(n^2)
。插入排序最好的时间复杂度发生在一开始就有序的情况。
插入排序是稳定的。
具体代码如下:
//Insertion sort
//average:O(n^2);
//best:O(n);
//worst:O(n^2);
//space complication:O(1);
//stable sort
//适用于数据量较小的;
//在STL的sort算法和stdlib的qsort算法,将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)
void InsertionSort(int A[], int n) {
for(int i = 1; i < n; ++i){
int get = A[i]; //手牌
int j = i - 1;
//将get插入到A[0],A[1],...,A[i-1]中
while(j >=0 && A[j] > get){
A[j+1] = A[j]; //往右边移
--j;
}
A[j+1] = get;
}
}
4.希尔排序
希尔排序是一种基于插入排序的快速的排序算法。
//Shell sort
//average:依据步长不同而不同;
//best:O(n);
//worst:O(n(log n)^2);
//space complication:O(1);
//unstable sort
void ShellSort(int A[], int n){
int h = 1;
while(h < n/3)
h = 3 * h + 1; //步长序列:1,4,13,40...
while(h >= 1){
for(int i = h; i < n; i++){
int get = A[i];
int j = i - h;
//将get插入到A[i-h],A[i-2h]...中
while(j >= 0 && A[j] > get){
A[j + h] = A[j];
j -= h;
}
A[j+h] = get;
}
h /= 3; //减小步长
}
}
5.归并排序
//Merge sort
//average:O(nlog n);
//best:O(nlog n);
//worst:O(nlog n);
//space complication:O(n);
//stable sort
void MergeSort(int A[], int lo, int hi){
//bound A[lo, hi];
if(lo >= hi)
return;
int mid = lo + (hi - lo)/2;
MergeSort(A, lo, mid);
MergeSort(A, mid + 1, hi);
Merge(A, lo, mid, hi);
}
void Merge(int A[], int lo, int mid, int hi){
int len = hi - lo + 1;
int *temp = new int[len];
int i = lo, j = mid + 1, index = 0;
while(i <= mid && j <= hi)
temp[index++] = A[i] <= A[j] ? A[i++] : A[j++];
while(i <= mid)
temp[index++] = A[i++];
while(j <= hi)
temp[index++] = A[j++];
for(int k = 0; k < len; ++k)
A[lo++] = temp[k];
delete []temp;
}
6.快速排序
//Quick sort
//average:O(nlog n);
//best:O(nlog n);
//worst:O(n^2);
//space complication:O(log n)~O(n);
//unstable sort
void QuickSort(int A[], int lo, int hi){
if(lo >= hi)
return;
int pivot_index = Partition(A, lo, hi);
QuickSort(A, lo, pivot_index - 1);
QuickSort(A, pivot_index + 1, hi);
}
int Partition(int A[], int lo, int hi){
int pivot = A[hi]; //选择最后一个元素作为基准
int tail = lo - 1; //tail为小于基准的子数组的最后一个元素索引
for(int i = lo; i < hi; ++i){
if(A[i] <= pivot)
std::swap(A[++tail], A[i]); //将小于基准的元素放到子数组末尾
}
std::swap(A[tail + 1], A[hi]); //将基准放置在子数组末尾
return tail + 1; //返回基准的索引
}
7.堆排序
//Heap sort
//average:O(nlog n);
//best:O(nlog n);
//worst:O(nlog n);
//space complication:O(1);
//unstable sort
void HeapSort(int A[], int n){
int heap_size = BuildHeap(A, n);
while(heap_size > 1){
std::swap(A[0], A[--heap_size]); //最后一个元素和当前最大的堆顶元素互换,并将堆的规模减1
Heapify(A, 0, heap_size);
}
}
void Heapify(int A[], int i, int size){ //从A[i]向下堆调整
int left_child = 2 * i + 1; //左孩子索引
int right_child = 2 * i + 2; //右孩子索引
int max = i; //选出父节点和子节点中最大的索引
if(left_child < size && A[left_child] > A[max])
max = left_child;
if(right_child < size && A[right_child] > A[max])
max = right_child;
if(max != i){
std::swap(A[i], A[max]);
Heapify(A, max, size);
}
}
int BuildHeap(int A[], int n){ //O(n)
int heap_size = n;
for(int i = heap_size / 2 - 1; i >= 0; --i){ //从每个非叶节点向下堆调整
Heapify(A, i, heap_size);
}
return heap_size;
}
非比较排序
计数排序(CountSort)
计数排序主要针对于一定范围的内的整数排序,时间复杂度为:O(n + k)
,其中k
为整数的范围,计数牺牲空间来换取时间。
步骤:
1. 找到最大最小元素记为max, min
,计算范围k = max - min + 1
,申请一个临时数组A
,其长度为k
。
2. 统计待排序数组中值为i
的元素出现的次数,存入数组A
的第i - min
项。
3. 遍历计数数组A
,反向填充原数组。
举个例子:原数组B=[2,1,-1,4,2]
,max=4,min=-1,k=6
,则A=[1,0,1,2,0,1]
,分别表示-1,0,1,2,3,4
在原数组中出现的个数。
遍历A
,反向填充:
index = 0
i = 0, A[0] = 1, B[index++] = i + min = -1, --A[0], judge A[0] == 0, pass
i = 1, A[1] = 0, judge A[0] == 0, pass
i = 2, A[2] = 1, B[index++] = i + min = 1, --A[2], judge A[2] == 0, pass
i = 3, A[3] = 2, B[index++] = 3 + min = 2, --A[3], judge A[3] == 0, continue
B[index++] = 3 + min = 2, --A[3], judge A[3] == 0, pass
i = 4, A[4] = 0, judge A[4] == 0, pass
i = 5, A[5] = 1, B[index++] = 4 + min = 4, --A[5], judge A[5] == 0, pass
具体代码如下:
//CountSort
//Time: O(n + k)
template <typename RandomItWithTypeInt>
void CountSort(RandomItWithTypeInt first, RandomItWithTypeInt last) {
if (last < first) throw "Interval is wrong";
int maxElem = *max_element(first, last);
int minElem = *min_element(first, last);
vector<int> count_array(maxElem - minElem + 1, 0);
for (auto iter = first; iter != last; ++iter) {
++count_array[*iter];
}
auto iter = first;
for (int i = 0; i <= maxElem - minElem; ++i) {
while (count_array[i]) {
*first++ = i + minElem;
--count_array[i];
}
}
return;
}
基数排序(RadixSort)
基数排序通常适用于非负整数,是一种分配排序,通过数某一位的将其分配至某一个桶,再重排重新分配,时间复杂度为:O(k*n)
,其中k为最大位数。
步骤:
1. 计算最大位数,假设为k。
2. 声明一个临时数组记录待排序数组temp,和m
个桶bucket,通常m=10
,从最低位开始,按位放入相应的bucket,重复这个操作。
3. 将temp放回原数组。
举个例子:原数组A=[11,10,8,279,9]
,首先计算最大位数是279的3位,即k=3
。
//按各位
temp: 11,10,8,279,9
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
10 11 8 279
9
//按十位
temp:10,11,8,279,9
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
8 10 279
9 11
//按百位
temp:10,11,8,279,9
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
8 279
9
10
11
//放回元素组
B = [8,9,10,11,279]
代码如下:
template <typename RandomItWithTypeUnsigned>
void RadixSort(RandomItWithTypeUnsigned first, RandomItWithTypeUnsigned last) {
if (last < first) throw "Interval is wrong";
int max_digits = 1;
int digit = 10;
for (auto iter = first; iter != last; ++iter) {
if (*iter >= digit) {
digit *= 10;
++max_digits;
}
}
int radix = 1, max_index = last - first;
vector<int> temp(first, last);
for (int i = 0; i < 10; ++i) {
vector<int> count(10);
for (auto iter = first; iter != last; ++iter) {
int k = (*iter / radix) % 10;
++count[k];
}
for (int j = 1; j < 10; ++j) {
count[j] += count[j - 1];
}
for (int n = max_index - 1; n >= 0; --n) {
int k = (*(first + n) / radix) % 10;
temp[count[k] - 1] = *(first + n);
--count[k];
}
for (auto i = 0; i < max_index; ++i) {
*(first + i) = temp[i];
}
radix *= 10;
}
return;
}
总结
稳定的排序算法只有归并、插入、选择排序以及非比较排序,非比较排序的算法时间复杂度通常可以达到O(N)
,不受比较排序O(NlogN)
的影响,但是限制也比较明显。