常见的排序算法有7种,如下图:
这7种算一个大类,叫做比较类排序,通过比较的方法来进行排序。
而非比较类有计数排序、桶排序、基数排序。
注意:本文使用C语言编写程序,且全部默认升序排序。
一、插入排序
1、插入排序
插入排序程序:
void InsertSort(int* a, int n) { for (int i = 0; i < n - 1; ++i) { int end = i; int tmp = a[end + 1]; while (end >= 0 && tmp < a[end]) { a[end + 1] = a[end]; end--; } a[end + 1] = tmp; } }
排序arr[0, n-1],元素共有n个。
插入排序是把排序分成arr[0,end](有序)和arr[end+1,n-1](无序),然后在把这个arr[end+1]插入到前面arr[0,end](有序)中,使arr[0,end+1]变得有序。
原理动态图:(自己做)
特性:
时间复杂度:平均O(N^2),最好时O(N),最坏时O(N^2)
空间复杂度:O(1)
稳定
元素集合越接近有序,直接插入排序的时间效率就越高
2、希尔排序
希尔排序程序:
void ShellSort(int* a, int n) { int gap = n; while (gap > 1) { gap = gap / 3 + 1; for (int i = 0; i < n - gap; i += gap) { int end = i; int tmp = a[end + gap]; while (end >= 0 && tmp < a[end]) { a[end + gap] = a[end]; end -= gap; } a[end + gap] = tmp; } } }
希尔排序是直接插入排序的优化,它是把每个间隔为gap的元素分成一组,然后先对这组进行直接插入排序,这样让整体接近有序,经过多组排序后,最后gap为1时,整体几乎为有序,此时时间效率几乎接近O(N)。
原理动态图:(自己做)
特性:
时间复杂度:平均O(N^1.3) ,最好时O(N),最坏时O(N^2)
空间复杂度:O(1)
不稳定
gap>1时都是预排序,让整体变得接近有序,gap == 1时,整体已经接近有序,很快排完序
希尔排序是直接插入排序的优化
二、选择排序
1、选择排序
选择排序程序:
void SelectSort(int* a, int n) { for (int i = 0; i < n-1; ++i) { int num = i; for (int j = i+1; j < n; ++j) { if (a[j] < a[num]) { num = j; } } int tmp = a[i]; a[i] = a[num]; a[num] = tmp; } }
选择排序也叫简单选择排序,或者直接选择排序
原理图:(自己做)
特性:
时间复杂度:O(N^2)
空间复杂度:O(1)
不稳定
选择排序思路很好理解,但效率不好,实际中还少用
优化后的选择排序:
void SelectSort2(int* a, int n) { int begin = 0, end = n - 1; while (begin < end) { int mini = begin, maxi = end; for (int i = begin; i <= end; ++i) { if (a[i] > a[maxi]) maxi = i; if (a[i] < a[mini]) mini = i; } int tmp = a[begin]; a[begin] = a[mini]; a[mini] = tmp; //如果beign原位置为maxi,就把交换后的位置给maxi if (maxi == begin) maxi = mini; tmp = a[end]; a[end] = a[maxi]; a[maxi] = tmp; ++begin; --end; } }
每次循环都能找到最小值和最大值,一次循环找两个极值,减少一半的时间复杂度。但是平均时间复杂度还是O(N^2),
2、堆排序
堆排序程序:
void Swap(int* q, int* p) { int tmp = *q; *q = *p; *p = tmp; } void AdjustDown(int* a, int n, int parent) { int LargerChild = parent * 2 + 1; while (LargerChild < n) { //找到最大的孩子:如果右孩子存在且大于左孩子,则选右孩子 if (LargerChild + 1 < n && a[LargerChild + 1] > a[LargerChild]) { ++LargerChild; } if (a[LargerChild] > a[parent]) { Swap(&a[LargerChild], &a[parent]); parent = LargerChild; LargerChild = parent * 2 + 1; }else { break; } } } void HeapSort(int* a, int n) { //建大堆 (整体大的在高层) for (int i = (n - 1 - 1) / 2; i >= 0; --i) { AdjustDown(a, n, i); } //把最大的数放在最后,且最后这位不在参与前面的排序 for (int i = n - 1; i > 0; --i) { Swap(&a[0], &a[i]); AdjustDown(a, i, 0); } }
原理图:(出于https://www.cnblogs.com/jx-dx/p/3756248.html)
特性:
时间复杂度:O(N*logN)
空间复杂度:O(1)
不稳定
堆排序使用堆来选数,效率高了许多。
三、交换排序
1、冒泡排序
冒泡排序程序:
void BubbleSort(int* a, int n) { for (int i = 0; i < n - 1; ++i) { for (int j = 0; j < n - 1 - i; ++j) { if (a[j + 1] < a[j]) { int tmp = a[j + 1]; a[j + 1] = a[j]; a[j] = tmp; } } } }
冒泡排序是一种交换排序,通过不断的交换,把最大值放在最后,或最小值放在最前。
原理动态图: (出于知乎十大经典排序算法及动图演示 - 知乎)
特性:
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定
冒泡排序是很容易理解的排序
2、快速排序
快速排序程序:
void Swap(int* q, int* p) { int tmp = *q; *q = *p; *p = tmp; } int PartitionSort(int* a, int left, int right) { int keyi = left; while (left < right) { while(left < right && a[right] >= a[keyi]) { --right;//一直找到小于a[keyi]的位置 } while (left < right && a[left] <= a[keyi]) { ++left;//一直找到大于a[keyi]的位置 }//把小于a[keyi]放在a[keyi]左边,大于a[keyi]放在a[keyi]左边 if(left < right) Swap(&a[left], &a[right]); }//把a[keyi]的值放在小于a[keyi]和大于a[keyi]的中间,之后a[left]表示这个中间值 Swap(&a[left], &a[keyi]); //传回中间值的位置,即left(left位置和keyi位置的值换了,但是指向中间值的是left) return left; } void QuickSort(int* a, int left, int right) { if (left < right) { int keyi = PartitionSort(a, left, right); QuickSort(a, left, keyi - 1); QuickSort(a, keyi + 1, right); } return; }
快速排序是采用分治的思想:(hoare版)
选取一个关键值,把小于关键值的值放在关键值的左边,大于关键值的值放在关键值的右边
然后让小于关键值的这部分快速排序,让大于关键值的这部分快速排序
最后小于关键值的部分是有序的,大于关键值的部分也是有序,即整体变得有序。
原理动态图: (出于知乎十大经典排序算法及动图演示 - 知乎)
特性:
时间复杂度:O(N*logN)
空间复杂度:O(logN)
不稳定
快速排序的整体综合性能和使用场景都是比较好。
(C++中algorthm库中sort使用的就是快速排序)
四、归并排序
1、归并排序
归并排序程序:(二路归并)
void _MergeSort(int* a, int begin, int end, int* tmp) { if (begin >= end) return; int mid = (begin + end) / 2; _MergeSort(a, begin, mid, tmp); _MergeSort(a, mid + 1, end, tmp); int begin1 = begin, end1 = mid; int begin2 = mid + 1, end2 = end; int i = begin; while (begin1 <= end1 && begin2 <= end2) { if (a[begin1] < a[begin2]) tmp[i++] = a[begin1++]; else tmp[i++] = a[begin2++]; } while (begin1 <= end1) tmp[i++] = a[begin1++]; while (begin2 <= end2) tmp[i++] = a[begin2++]; //归并的那部分,就拷贝回那部分 memcpy(a+begin, tmp+begin, (end - begin + 1)*sizeof(int)); //用循环拷贝 //for (int i = begin; i <= end; ++i) a[i] = tmp[i]; } void MergeSort(int* a, int n) { int* tmp = (int*)malloc(n * sizeof(int)); if (tmp == NULL) { perror("malloc fail!"); return; } _MergeSort(a, 0, n-1, tmp); free(tmp); tmp = NULL; }
原理图:(出于https://www.cnblogs.com/pierceming/p/12775351.html)
原理动态图:(出于知乎十大经典排序算法及动图演示 - 知乎)
特性:
时间复杂度:O(N*logN)
空间复杂度:O(N)
稳定
归并排序的缺点是需要O(N) 的空间复杂度,归并排序更多是解决磁盘中的外排序问题