📈十大排序算法C++实现及分析一览
十大排序算法复杂度一览
排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 排序方式 | 稳定性 |
---|---|---|---|---|---|---|
冒泡排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | In-place | 稳定 |
选择排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | In-place | 不稳定 |
插入排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | In-place | 稳定 |
希尔排序 | 不 定 , 约 O ( n 3 2 ) 不定,约O(n^\frac{3}{2}) 不定,约O(n23) | O ( n ) O(n) O(n)- O ( n 2 ) O(n^2) O(n2)之间 | | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | In-place | 不稳定 |
归并排序 | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n ) O(n) O(n) | Out-place | 稳定 |
快速排序 | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n 2 ) O(n^2) O(n2) | O ( l o g n ) O(logn) O(logn) | In-place | 不稳定 |
堆排序 | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( 1 ) O(1) O(1) | In-place | 不稳定 |
计数排序 | O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | O ( k ) O(k) O(k) | Out-place | 稳定 |
桶排序 | O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | Out-place | 稳定 |
基数排序 | O ( n ∗ k ) O(n*k) O(n∗k) | O ( n ∗ k ) O(n*k) O(n∗k) | O ( n ∗ k ) O(n*k) O(n∗k) | O ( n + k ) O(n+k) O(n+k) | Out-place | 稳定 |
三大O(n^2)算法
冒泡
vector<int> bubbleSort(vector<int>& nums){
int n = nums.size();
for(int i = n - 1; i >= 0; i--){
bool isSorted = true;
// nums(i,n]是有序的
for(int j = 1; j <= i; j++){
if(nums[j - 1] > nums[j]){
std::swap(nums[j],nums[j - 1]);
isSorted = false;
}
}
if(isSorted){
break;
}
}
return nums;
}
分析:
时间复杂度:由于经过优化(每趟循环检测有序),最好情况下数组已排好序只需遍历一遍,为 O ( N ) O(N) O(N), 最坏情况下数组完全逆序,每一趟循环让都一个数"冒泡"到末端排定,为 O ( N 2 ) O(N^2) O(N2)
空间复杂度: O ( 1 ) O(1) O(1)
稳定性:试想一个序列5551,冒泡的比较别写成>= 就能稳定。
初始序列:
移动次数有关:最好情况下数组不需要移动,最坏情况下数组每趟循环,第i趟元素移动n-1+i次。
比较次数有关:最好情况下数组比较n-1次,其他情况下总比较次数是每趟循环比较总次数加n-1的总和
时间稳定性有关: 最好 O ( N ) O(N) O(N) , 最坏 O ( N 2 ) O(N^2) O(N2) , 故与初始序列有关
排序趟数有关:最好情况下算法只调用一次内循环。
选择
vector<int> selectionSort(vector<int> &nums){
int n = nums.size();
for(int i = 0; i < n; i++){
int minIdx = i;
//保持 nums[0,i) 有序
for(int j = i + 1; j < n; j++){
if(nums[j] < nums[minIdx])
minIdx = j;
}
std::swap(nums[i],nums[minIdx]);
}
return nums;
}
分析:
时间复杂度: 最好最坏均为 O ( N 2 ) O(N^2) O(N2)
空间复杂度: O ( N ) O(N) O(N)
稳定性:一趟排序之后必有一次交换,交换不管元素是否相等,因此是不稳定的
初始序列:
移动次数无关:不管初始序列有不有序,必有N趟排序,N次交换
比较次数无关:不管初始序列有不有序,必有(N-1)! 次比较
时间稳定性无关: 最好最坏均为 O ( N 2 ) O(N^2) O(N2) --> 时间稳定性无关
排序趟数无关: 不管初始序列有不有序,必有N趟排序(无法通过布尔值检测是否事先排好序,因为可能存在和nums[i] >= nums[i+1,n]的情况)
选择排序的优点:交换次数最小
插入排序
vector<int> insertionSort(vector<int> &nums){
int n = nums.size();
//保持 nums[0,i) 有序
for(int i = 1; i < n; i++){
int j = i;
int tmp = nums[i];
while(j >= 1 && nums[j - 1] > tmp){
nums[j] = nums[j-1];
j--;
}
nums[j] = tmp;
}
return nums;
}
分析:
时间稳定性:最好情况下数组已经有序无需插入,仅需遍历一遍,为 O ( N ) O(N) O(N), 最坏情况下 O ( N 2 ) O(N^2) O(N2)
空间稳定性: O ( 1 ) O(1) O(1)
稳定性:逐后遍历往前插入,不会影响相同值元素次序,故此稳定
初始序列:
移动次数有关:有序数组在算法内不会发生移动
比较次数有关:内循环若满足部分有序可提前跳出
时间稳定性有关:最好情况下 O ( N ) O(N) O(N), 最坏情况下 O ( N 2 ) O(N^2) O(N2) -------> 时间稳定性有关
排序趟数无关:不管数组有不有序,都要进行N趟排序
三大O(nlogn)算法
快速排序
void quickSort(int[] nums,int lo,int hi){
if(lo >= hi) return;
int mid = partition(nums,lo,hi);
//nums[mid]已经被排定了,只需要继续排序nums[lo,mid-1]和
//nums[mid+1,hi]的元素
quickSort(nums,lo,mid - 1);
quickSort(nums,mid + 1,hi);
}
int partition(int[] nums,int lo,int hi){
int lt = lo + 1, hi = gt;
// 基准值
int pivot = nums[lo];
int lt = lo;
// 循环不变量:
// all in [lo , lt) <= pivot
// all in (gt , hi] >= pivot
while(true){
while(lt < hi && nums[lt] < pivot)
lt++;
while(gt > lo && nums[gt] > pivot)
gt--;
if(lt >= gt)
break;
exch(nums,lt,gt);
lt++;
gt--;
}
exch(nums,lo,gt);
return gt;
}
分析:
**时间复杂度:**最好情况是每次切分都能恰好将数组对半分(即找到排定数组中的中间值),此时复杂度为 O ( l o g N ) O(logN) O(logN), 最坏情况是每次切分都只能切到数组的边界端(即找到排定数组的最值),此时每次递归都只会将搜索范围-1,快排沦为冒泡排序,复杂度为 O ( N 2 ) O(N^2) O(N2)
**空间复杂度:**属于原地算法,故为 O ( 1 ) O(1) O(1)
稳定性: 三种实现方法都会改变相等元素的相对次序,故不稳定
初始次序:
移动次数有关:已排好序的数组不需要移动
比较次数无关:不管实现方式是单指针,双指针还是三指针,均要比较N次
时间复杂度有关:时间复杂度最好情况为 O ( l o g N ) O(logN) O(logN) 最坏情况为 O ( N 2 ) O(N^2) O(N2) ,故初始序列有关
排序趟数有关:切分函数的递归深度取决于快排的实现方法和切分数的值
归并排序
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
int n = nums.size();
vector<int> tmp(n);
mergeSort(nums,0,n-1,tmp);
return nums;
}
void mergeSort(vector<int> &nums,int l,int r,vector<int> &tmp){
if(l >= r)
return;
int mid = l + (r - l) / 2;
mergeSort(nums,l,mid,tmp);
mergeSort(nums,mid+1,r,tmp);
//若两个子序列已有序,无需合并
if(nums[mid] <= nums[mid + 1])
return;
mergeTwoArrs(nums,l,mid,r,tmp);
}
void mergeTwoArrs(vector<int> &nums,int l,int mid,int r,vector<int> &tmp){
int n = nums.size();
tmp = vector<int>(nums.begin(),nums.end());
int i = l, j = mid + 1;
for(int k = l; k <= r; k++){
if(i > mid)
nums[k] = tmp[j++];
else if(j > r)
nums[k] = tmp[i++];
//注意别写成<,不然会失去稳定性(要保证次序较前的相等元素先被赋值)
else if(tmp[i] <= tmp[j])
nums[k] = tmp[i++];
else
nums[k] = tmp[j++];
}
}
};
分析:
时间复杂度:和堆排序一样,最好和最坏情况都是 O ( l o g N ) O(logN) O(logN)
空间复杂度:需要用到一个额外数组,故空间复杂度为 O ( N ) O(N) O(N)
稳定性: 如果能保证前一子序列的相同元素先于后一子序列的相同元素被赋值,则该算法就是稳定的
初始次序:
移动次数: 额外数组不管原数组不有序都会赋值,故移动次数与初始次序无关
比较次数: 额外数组不管原数组不有序都会赋值,而一次赋值必定先有一次比较,故比较次数与初始次序无关
时间复杂度: 最好和最坏情况都是 O ( l o g N ) O(logN) O(logN), 故时间复杂度与初始次序无关
排序趟数: 递归深度取决于数组的长度,固定二分,不受初始序列影响,故与初始序列无关(与此相对立的是快排,切分点受初始序列影响)
堆排序
vector<int> HeapSort(vector<int>& nums){
heapify(nums); //顺序排序,建大根堆
int len = nums.size();
//循环不变量: nums[0,i]堆有序, nums(i,len-1]有序
//每次都会把堆顶最大元素排到有序部分前
for(int i = len - 1; i >= 1; ){
std::swap(nums[0],nums[i]);
i--; //减小堆有序区间
sink(nums,0,i);
}
return nums;
}
void heapify(vector<int>& nums){
int len = nums.size();
//每次都从nums[i,len-1]开始逐层下移,i初始量取堆的最后一个非叶子结点
for(int i = (len-1) / 2; i >= 0; i--){
sink(nums,i,len-1);
}
}
// 在nums[pre,end]部分将上方较小元素下沉至建堆位置
void sink(vector<int> &nums,int pre,int end){
while(pre * 2 + 1 <= end){
int next = pre * 2 + 1;
if(next + 1 <= end && nums[next] < nums[next + 1]) //对于大根堆,用较大的孩子与父亲交换
next++;
if(nums[pre] >= nums[next])
break;
std::swap(nums[pre],nums[next]);
pre = next;
}
}
分析
**时间复杂度:**要遍历数组,每个元素都要进行一次下沉操作,下沉操作的处理方式类似二叉树,故时间复杂度为 O ( l o g N ) O(logN) O(logN)
**空间复杂度:**属于原地算法,故为 O ( 1 ) O(1) O(1)
**稳定性:**每次堆顶元素和堆有序末尾元素交换是强制的,若堆顶元素值和堆有序末尾元素值相等,则该算法不稳定。
初始次序:
移动次数有关:取决于所选元素的深度,可能升到顶端后就不会下沉,也可能升到顶端后就会下沉。
比较次数有关:sink函数若出现部分有序就会break, 如果一个元素沉到堆中间就停止下沉的话,比较次数就少了一半。
时间复杂度无关:不管最好情况还是最坏情况时间复杂度都是 O ( l o g N ) O(log N) O(logN),故时间复杂度无关
排序趟数无关:不管你数据有不有序,堆排序建堆都会调用logN
次sink函数,后期交换排序调用N-1次sink函数 (当然如果你加了检测数据是否预习排好序的检测函数那么另行讨论)
三大计数算法
计数排序
class Solution {
public:
// 数据范围 -50000 <= A[i] <= 50000, 故数组要容纳100000个数
int SIZE = 100001;
// 设置偏移量的目的是使得所有值都为正值,防止数组溢出
int OFFSET = 50000;
vector<int> countingSort(vector<int>& nums) {
vector<int> count(SIZE,0);
int n = nums.size();
for(int i = 0; i < n; i++){
count[nums[i] + OFFSET]++;
}
for(int i = 1; i < SIZE; i++){
count[i] += count[i - 1];
}
vector<int> temp(nums.begin(),nums.end());
for(int i = n - 1; i >= 0; i--){
int idx = count[temp[i] + OFFSET] - 1;
count[temp[i] + OFFSET]--;
printf("idx=%d nums[%d]=%d\n",idx,i,temp[i]);
nums[idx] = temp[i];
}
return nums;
}
};
分析
时间复杂度: O ( N + k ) O(N+k) O(N+k) , 其中N为数组的长度,k为哈希表的长度
空间复杂度: O ( N + k ) O(N+k) O(N+k), 需要额外用到一个哈希表和一个额外数组
稳定性:稳定,如果你最后遍历数组是后往前遍历的话。
最后遍历数组时一定要从后往前遍历,因为如果一个相同值在数组中出现了多次,统计这些元素是按从前往后的顺序来叠加计数的,通过前缀和换算以后,值最初的下标位置对应就是最后出现的相同元素,排定一个元素下标就减1,指向这个值在原数组中的前一个相同元素,因此我们也要按计数顺序来做出调整,使得同一个值最后出现的元素进入排定数组最后的位置,如果弄成从前往后遍历,那就是先把最后出现的元素反而先放进排定数组最前的位置了,丧失稳定性。
初始次序:
移动次数无关:和归并一样,排定来源来自于额外的数组,有不有序都要移动,一次排定。
比较次数无关:非比较算法
时间复杂度无关: 最好最坏都是O(N+k)
排序趟数无关:O(N) 算法,有不有序都是一趟过
基数排序
class Solution {
public:
int OFFSET = 50000; // -50000 <= A[i] <= 50000
vector<int> RadixSort(vector<int>& nums) {
int n = nums.size(),maxlen = 0;
for(int i = 0; i < n; i++){
nums[i] += OFFSET; // 加上偏移量,防止负数影响
maxlen = std::max(maxlen,nums[i]); // 取最大元素的位数做循环量
}
vector<int> cnt(10,0),tmp(nums.begin(),nums.end());
int div = 1;
while(div <= maxlen){
for(int i = 0; i < n; i++){
int bit = (nums[i] / div) % 10; //从元素的个位一直取到顶位
cnt[bit]++;
}
for(int i = 1; i < 10; i++){
cnt[i] += cnt[i - 1];
}
for(int i = n - 1; i >= 0; i--){
int bit = (tmp[i] / div) % 10;
int idx = cnt[bit] - 1;
cnt[bit]--;
nums[idx] = tmp[i];
}
// 清空计数器, 更新tmp,进行下一位数的排序
cnt = vector<int>(10,0);
tmp = vector<int>(nums.begin(),nums.end());
div *= 10;
}
for(int i = 0; i < n; i++){
nums[i] -= OFFSET; //排完序别忘了还原原值
}
return nums;
}
int getElemLen(int digit){
int cnt = 0;
while(digit){
digit /= 10;
cnt++;
}
return cnt;
}
};
分析
时间复杂度: O ( N ∗ k ) O(N*k) O(N∗k), 其中N为数组的长度,k为序列中最大值的位数
空间复杂度: O ( N ) O(N) O(N) , 计数器长度固定为10,可以忽略, 主要看额外数组的长度
稳定性: 稳定,理由同计数排序
初始序列:
移动次数无关:同计数排序,排序依靠额外数组一次排定,无需移动
比较次数无关:同计数排序,排序依靠额外数组一次排定,无需比较
时间复杂度无关:最好最坏情况都是 O ( N ∗ k ) O(N*k) O(N∗k)
排序趟数无关:外循环次数取决于最大元素的位数而不是初始序列
桶排序
将原序列按照值域分为多个等长区间的“桶”,将序列各元素分类到对应的桶中并排序,装桶结束后将各桶内元素按顺序逐一“倒回”原数组:
class Solution {
public:
int OFFSET = 50000; // -50000 <= A[i] <= 50000
vector<int> sortArray(vector<int>& nums) {
buckSort(nums);
return nums;
}
void buckSort(vector<int> &nums){
int n = nums.size(),maxi = 0,mini = 0;
for(int i = 0; i < n; i++){
nums[i] += OFFSET; // 加上偏移量,防止负数影响
maxi = std::max(maxi,nums[i]);
mini = std::min(mini,nums[i]);
}
int gap = 2; // 设各桶的区间长,区间设置为左闭右开
int buckAmount = (maxi - mini) / gap + 1; //桶个数
vector<vector<int>> buckets(buckAmount);
//元素入桶,每次更新桶时都需要排序,这里采用插入排序
for(int i = 0; i < n; i++){
int idx = (nums[i] - mini) / gap;
buckets[idx].push_back(nums[i]);
insertionSort(buckets[idx]);
}
int k = 0;
//将排序后的元素逐一出桶
for(int i = 0; i < buckAmount; i++){
int buckLoad = buckets[i].size();
for(int j = 0; j < buckLoad; j++){
nums[k++] = buckets[i][j];
}
}
//恢复数组原值
for(int i = 0; i < n; i++){
nums[i] -= OFFSET;
}
}
void insertionSort(vector<int> &nums){
int n = nums.size();
for(int i = 1; i < n; i++){
int tmp = nums[i];
int j = i;
while(j > 0 && nums[j - 1] > tmp){
nums[j] = nums[j - 1];
j--;
}
nums[j] = tmp;
}
}
};
分析
时间复杂度: 取决于桶的数量k和采用的排序算法。设数组长为n, 最好情况下,各元素被均匀的分到各桶中且都有序,时间复杂度达 O ( N ) O(N) O(N) , 最坏情况下所有元素都挤在一个桶内, 时间复杂度为 O ( N 2 ) O(N^2) O(N2)
本例中采用的排序算法是插入排序,如果采用其他排序算法则需要另行讨论,例如:
排序算法 | 最终复杂度 |
---|---|
插入 | 最好$ O(n/k)$ -> O ( N ) O(N) O(N), 最坏O( (n/k)^2 ) $-> $ O ( N 2 ) O(N^2) O(N2) |
快排 | 最好 O ( ( n / k ) ∗ l o g ( n / k ) ) O( (n/k)*log(n/k) ) O((n/k)∗log(n/k)) -> O ( N l o g N ) O( NlogN) O(NlogN), 最坏 O((n/k)^2) -> O ( N 2 ) O(N^2) O(N2) |
不过在桶的数量合适的情况上,O(NlogN) 约等于 O(N)。
空间复杂度:取决于桶数组的实现方式,本例采用了vector来动态插入数组,类似于链表,空间复杂度为O(n+k), 也就是数组长度+桶数, 如果采用数组固定分配,则空间复杂度为O(nk)
稳定性:装桶的逻辑类似于计数排序,所以过程是稳定的,千万别用非稳定的排序算法来排序桶。
初始次序:
移动次数:无关,排序源来自于桶数组,一次排定,无需移动
比较次数:无关,非比较算法
时间复杂度:取决于排序桶的排序算法
排序趟数:取决于排序桶的排序算法
希尔排序
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
int h = 1, n = nums.size();
// 将数组分为多个间隔为h(或长度h+1)的相交叠的子表
while(h * 3 < n){
h *= 3;
}
// 间隔从 h , h/ 3... 逐渐缩小到1
while(h >= 1){
for(int i = h; i < n; i++){
insertionByGap(nums,h,i);
}
h /= 3;
}
return nums;
}
void insertionByGap(vector<int>& nums, int step,int i){
int j = i;
int tmp = nums[i];
// 逻辑同原版插入排序,其实就是把1换成step
while(j >= step && nums[j - step] > tmp){
nums[j] = nums[j - step];
j -= step;
}
nums[j] = tmp;
}
};
分析
时间复杂度: 至今对希尔排序没有明确的复杂度分析,但认为其平均复杂度一般在 O ( N ) O(N) O(N) - O ( N 2 ) O(N^2) O(N2) 之间,不过最坏情况下复杂度仍旧为 O ( N 2 ) O(N^2) O(N2)
空间复杂度:原地算法,故为 O ( 1 ) O(1) O(1)
稳定性:如果有多个相同值元素被分在不同的子表中,较后的元素被放在较先遍历的子表中,那么排序就会改变相同元素原本的相对次序,显然这种排序是不稳定的。
初始序列:
移动次数:最好情况即已经排好序的情况下,一个元素都不用移动,最坏情况下,最小的元素总在待排序部分末尾,每次都要移动j-1次,故与初始序列有关。
比较次数:有关,如果待排序数在中间,发现前面数组已经有序的话就不用再比较了。
时间复杂度:和插入排序一样,分最好最好情况,故与初始序列有关。
排序趟数:和插入排序一样,发现待排序数及其之前部分已有序就会结束循环,故与初始序列有关。