起因
虽然C++的STL给我们直接提供了快速排序
算法(std::sort()
),刷题的时候大多数题也不要求自己手写排序算法,但是熟悉它们的原理以及自己动手实现一遍(以非递减序列为例),能够加深理解以及更好地应用。特别是对于那些时间复杂度要求严苛,进阶类的题目,理解好这些排序算法将会写出更高效的代码。
万一在面试时被问到也不至于懵逼
总览
图来自参考链接[1]
P.S.
- 其中排序方法的稳定性指的是当两个相同数值的数在排序前后的相对位置是否一致;
in-place
占用常数内存,不占用额外内存;out-place
占用额外内存
1. 冒泡排序
关键点:将剩余数组中的最大(最小)的元素交替冒泡到尾部(首部)
算法详细原理见:https://www.runoob.com/w3cnote/bubble-sort.html
以将最大数值冒泡到尾部为例:
template<typename T>
void bubble_sort(vector<T>& arr, int len){
bool swapped = false;
for(int i=0; i<len; i++){
for(int j=1; j<len-i; j++){
if(arr[j-1] > arr[j]){
swap(arr[j-1], arr[j]);
swapped = true;
}
}
if(!swapped) break; // 如果没有产生交换,证明目前数组已有序
}
}
2. 选择排序
关键点:将剩余数组中的最大值(最小值)交换到尾部(首部)
详细原理见https://www.runoob.com/w3cnote/selection-sort.html
此处以将最小元素交换到首部为例:
template<typename T>
void selection_sort(vector<T>& arr, int len){
for(int i=0; i<len; i++){
for(int j=i+1; j<len; j++){
if(arr[i] > arr[j])
swap(arr[i], arr[j]);
}
}
}
3. 插入排序
关键点:数组分成已排序 / 未排序两部分,将未排序部分的元素插入已排序部分。
详细原理见https://www.runoob.com/w3cnote/insertion-sort.html
template<typename T>
void insert_sort(vector<T>& arr, int len){
for(int i=1; i<len; i++){
for(int j=i; j>0; j--){
if(arr[j-1]>arr[j]) // 以交换的方式进行插入
swap(arr[j-1], arr[j]);
else
break;
}
}
}
4. 希尔排序
关键点:数组按照某个增量分成子序列,每个子序列进行插入排序,逐步减少增量值。
详细原理见https://www.runoob.com/w3cnote/shell-sort.html以及参考链接【3】
template<typename T>
void shell_sort(vector<T>& arr, int len){
int gap = 1; // 增量间隔
while(gap<len/3) gap = gap*3+1; // 为3的倍数,也可以为其他值
while(gap>=1){
// 分组进行插入排序
for(int i=gap; i<len; i++){
for(int j=i; j>=gap && arr[j-gap]>arr[j]; j-=gap)
swap(arr[j-gap], arr[j]);
}
gap = gap/3; // 缩小增量
}
}
5. 归并排序
关键点:递归的思想,将两个有序数组归并为一个有序数组。
详细原理见https://www.runoob.com/w3cnote/merge-sort.html
template<typename T>
// 将两个有序数组归并为一个有序数组 (arr[left,mid] arr[mid+1, right])
void merge(vector<T>& arr, int left, int right, int mid){
// 额外开辟存储空间
vector<T> left_arr(arr.begin()+left, arr.begin()+mid+1);
vector<T> right_arr(arr.begin()+mid+1, arr.begin()+right+1);
int leftIdx = 0, rightIdx = 0;
for(int i=left; i<=right; i++){
if((left_arr[leftIdx]<=right_arr[rightIdx] && leftIdx<left_arr.size()) || rightIdx>=right_arr.size()){
arr[i] = left_arr[leftIdx];
leftIdx++;
}
else{
arr[i] = right_arr[rightIdx];
rightIdx++;
}
}
}
template<typename T>
void merge_sort(vector<T>& arr, int left, int right){
if(left>=right) return;
int mid = (right-left)/2+left;
merge_sort(arr, left, mid);
merge_sort(arr, mid+1, right);
merge(arr, left, right, mid);
}
6. 快速排序
关键点:利用递归的思想,找到一个参考点(基准),将小于基准的值放置左边,大于基准的值放置在右边。
详细原理见https://www.runoob.com/w3cnote/quick-sort-2.html
template<typename T>
// 写得很妙,值得回味
int quick(vector<T>& arr, int low, int high){
int pivot=arr[low]; // 选择基准点
while(low<high){
// 移动high指针
while(arr[high]>=pivot && high>low)
high--;
arr[low]=arr[high]; // 交换
// 移动low指针
while(arr[low]<=pivot && high>low)
low++;
arr[high]=arr[low]; // 交换
}
arr[low]=pivot; // 最后的空位放上基准值
return low;
}
template<typename T>
void quick_sort(vector<T>& arr, int low, int high){
if(low>=high) return;
int pivot = quick(arr, low, high);
// 经过一次quick arr[low:pivot-1]全小于arr[pivot]
quick_sort(arr, low, pivot-1);
// arr[low:pivot-1]全大于arr[pivot]
quick_sort(arr, pivot+1, high);
}
7. 堆排序
关键点:构建大顶/小顶堆,然后将堆顶元素与末尾元素交换后(类似堆删除操作),再继续将剩余元素堆化。
详细原理见https://www.runoob.com/w3cnote/heap-sort.html
大顶堆
:每个节点的值都大于或等于其子节点的值
此处以大顶堆为例:
template<typename T>
// 构造大顶堆
void max_heapify(vector<T>& arr, int i, int len){
int j = i*2+1;
while(j < len){
// 选择最大的子节点
if(j+1<len && arr[j]<arr[j+1])
j++;
// 如果当前节点比子节点大,结束
if(arr[i]>arr[j])
break;
// 比子节点小,交换后,继续与子节点比较
swap(arr[i], arr[j]);
i = j;
j = 2*i+1;
}
}
template<typename T>
void heap_sort(vector<T>& arr, int len){
// 自底向上构造大顶堆
for(int i=len/2-1; i>=0; i--)
max_heapify(arr, i, len);
// 堆排序
for(int i=0; i<len; i++){
// 将当前堆顶元素移除(最大元素)与数组末尾元素交换
swap(arr[0], arr[len-1-i]);
// 重新构造大顶堆,找到剩余堆中的最大元素
max_heapify(arr, 0, len-1-i);
}
}
8. 计数排序
关键点:构造计数数组,统计出现频次。为了稳定排序:需要将统计数组进行变形。
详细原理见https://www.runoob.com/w3cnote/counting-sort.html 以及参考链接【4】
template<typename T>
vector<T> count_sort(vector<T>& arr, int len){
if(len < 0)
return {0};
// 1. 构建计数数组
int _min, _max, d;
_min=_max=arr[0];
for(int i=1; i<len; i++){
if(arr[i] > _max) _max=arr[i];
if(arr[i] < _min) _min=arr[i];
}
d = _max-_min+1;
vector<T> countArr(d, 0);
// 2. 计数
for(int i=0; i<len; i++){
countArr[arr[i]-_min]++;
}
// 3. 稳定排序: 数组变形
for(int i=1; i<d; i++){
countArr[i] += countArr[i-1];
}
// 4. 得到稳定的排序结果
vector<T> res(len, 0);
for(int i=len-1; i>=0; i--){
res[countArr[arr[i]-_min]-1] = arr[i];
countArr[arr[i]-_min]--;
}
return res;
}
9. 桶排序
关键点:基于计数排序的改进算法

详细原理https://www.runoob.com/w3cnote/bucket-sort.html
template<typename T>
void bucket_sort(vector<T>& arr, int len){
if(len < 0) return;
// 1. 找到最大最小值
int _min, _max, d;
_min=_max=arr[0];
for(int i=1; i<len; i++){
if(arr[i] > _max) _max=arr[i];
if(arr[i] < _min) _min=arr[i];
}
d = _max-_min+1;
// 2. 将元素分到5个桶中
vector< vector<T> > bucket(5);
int step = d/5+1;
for(int i=0; i<len; i++){
int idx = (arr[i]-_min)/step;
bucket[idx].insert(bucket[idx].end(), arr[i]);
}
// 3. 排序每个桶内元素 - 这里采用快速排序,也可采用其他排序方法
for(int i=0; i<5; i++){
sort(bucket[i].begin(), bucket[i].end());
}
// 4. 组合
int idx = 0;
for(int i=0; i<5; i++){
for(int j=0; j<bucket[i].size(); j++){
arr[idx++] = bucket[i][j];
}
}
}
10. 基数排序
关键点:将元素按位数进行切分,每次对每一位进行排序。比较完所有位之后,数组即有序。
详细原理见https://www.runoob.com/w3cnote/radix-sort.html以及链接[5]
计数排序、桶排序、以及基数排序通常适用于整数排序。
分为LSD(Least significant digital)或MSD(Most significant digital)两种算法,一种是从最低位开始,另一种从最高位开始。
这里以LSD算法为例(用到了计数排序中变形数组的思想):
template<typename T>
// 辅助函数:求数组中最大值的位数
int maxbit(vector<T>& arr, int len){
int maxData = arr[0];
for(int i=1; i<len; i++){
if(maxData < arr[i]) maxData = arr[i];
}
int bit = 1;
while(maxData > 9){
bit++;
maxData /= 10;
}
return bit;
}
template<typename T>
void radix_sort(vector<T>& arr, int len){
int Bit = maxbit(arr, len); // 确定最大位数
vector<T> tmp(arr); // 构造临时数组
int radix = 1; // 从最低为开始
for(int i=0; i<Bit; i++){ // 进行Bit次排序
vector<int> count(10, 0); // 计数器 0~9
for(int j=0; j<len; j++){
count[(arr[j]/radix)%10]++; // 找到相应的位数,计数
}
for(int j=1; j<10; j++){ // 转换为变形数组,确保每一次排序均为稳定排序
count[j] += count[j-1];
}
for(int j=len-1; j>=0; j--){ // 从最后一位元素开始放置
int k = (arr[j]/radix)%10;
tmp[count[k]-1] = arr[j];
count[k]--;
}
arr.assign(tmp.begin(), tmp.end()); // 交换临时数组
radix *= 10; // 往前进一位
}
}
参考
[1] 菜鸟教程 https://www.runoob.com/w3cnote/ten-sorting-algorithm.html
[2] LeetCode 101 - A LeetCode Grinding Guide (C++ Version)
[3] 图解排序算法(二)之希尔排序 https://www.cnblogs.com/chengxiao/p/6104371.html
[4] 漫画:什么是计数排序? https://www.cxyxiaowu.com/5437.html
[5] 【算法】排序算法之基数排序
https://zhuanlan.zhihu.com/p/126116878