排序复杂度汇总
方法 | 平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
插入排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
快速排序 | O(nlogn) | O(nlogn) | O(n^2) | O(logn)-O(n) | 不稳定 |
希尔排序 | O(nlogn)-O(n^2) | O(n^1.3) | O(n^2) | O(1) | 不稳定 |
一、平均时间复杂度为O(n^2)的算法
1. 冒泡排序
void BubbleSort(vector<int> &data) {
// 1 辅助变量
bool flag; //若某一次循环不需要交换说明已经完全有序,提前结束
int tmp;
// 2 正式排序
for (int i=1; i<data.size(); ++i) {
flag = true;
for (int j=0; j<data.size() - i; ++j) {
if (data[j] > data[j+1]) {
tmp = data[j];
data[j] = data[j+1];
data[j+1] = tmp;
flag = false;
}
}
if (flag) break;
}
}
时间复杂度:平均时间复杂度O(n^2), 最好时间复杂度O(n), 最坏时间复杂度O(n^2)
是否原地算法:额外空间复杂度为O(1),是原地算法
是否稳定排序:是,因为只有交换会影响位置,而相等时不交换因此是稳定的
2. 插入排序
思路:每次从后往前找当前元素的位置,同时移动数据,找到位置后将当前元素放到合适的位置,注意数据保存
void InsertionSort(vector<int> &data) {
for (int i=1; i<data.size(); ++i) {
int j = i - 1;
int curr = data[i]; // 保存当前元素
// 从后往前找当前元素的正确位置,顺便向后移动数据
for (; j>=0; --j) {
if (data[j] > curr) {
data[j+1] = data[j];
}
else {
break;
}
}
data[j+1] = curr;
}
}
时间复杂度:平均时间复杂度O(n^2), 最好时间复杂度O(n), 最坏时间复杂度O(n^2)
是否原地算法:额外空间复杂度为O(1),是原地算法
是否稳定排序:是
3. 选择排序
void SelectionSort(vector<int> &data) {
for (int i=0; i<data.size(); ++i) {
int min_idx = i;
for (int j=i+1; j<data.size(); ++j) {
if (data[j] < data[min_idx]) min_idx = j;
}
if (min_idx != i) {
int tmp = data[min_idx];
data[min_idx] = data[i];
data[i] = tmp;
}
}
}
时间复杂度:平均时间复杂度O(n^2), 最好时间复杂度O(n^2), 最坏时间复杂度O(n^2)
是否原地算法:额外空间复杂度为O(1),是原地算法
是否稳定排序:每次都取最小交换,可能改变相同元素的相对位置,不是稳定的
二、平均时间复杂度为O(nlogn)的算法
1.归并排序
参考归并排序思路
void Merge(vector<int> &data, int start1, int end1, int start2, int end2) {
vector<int> sorted_data;
int i = start1;
int j = start2;
// 1.两个分段都有数据
while (i <= end1 && j <= end2) {
int tmp = data[i] < data[j] ? data[i++] : data[j++];
sorted_data.push_back(tmp);
}
// 2.第一个分段还有数据
while (i <= end1) {
sorted_data.push_back(data[i++]);
}
// 3.第二个人分段还有数据
while (j <= end2) {
sorted_data.push_back(data[j++]);
}
// 4.拷贝数据
for (int k=0; k<sorted_data.size(); ++k) {
data[start1+k] = sorted_data[k];
}
}
void MergeSortRecursion(vector<int> &data, int start, int end) {
// 1.终止条件
if (start >= end) return;
// 2.划分
int mid = (start + end) / 2;
MergeSortRecursion(data, start, mid);
MergeSortRecursion(data, mid+1, end);
// 3.合并
Merge(data, start, mid, mid+1, end);
};
void MergeSort(vector<int> &data) {
MergeSortRecursion(data, 0, data.size()-1);
}
时间复杂度:平均时间复杂度O(nlogn), 最好时间复杂度O(nlogn), 最坏时间复杂度O(nlogn)
是否原地算法:额外空间复杂度为O(n),不是原地算法
是否稳定排序:是否稳定主要看Merge函数,当前一个分段的数据和后一个分段数据相等时,先将前一个分段数据放入结果中就是稳定的,也就是
int tmp = data[i] <= data[j] ? data[i++] : data[j++];
sorted_data.push_back(tmp);
引申:归并排序利用的是分支思想,利用这种方法可以解决逆序度求解问题, 参考 分治算法介绍
2.快速排序
2.1 自己的实现
思路参考5.快速排序
void QuickSortRecursion(vector<int> &data, int start, int end) {
// 1.终止条件
if (start >= end) return;
// 2.当前处理
int left = start;
int right = end;
int backup = data[left];
bool direction = false;
while (left < right) {
// 从左往右
if (direction) {
if (data[left] <= backup) {
++left;
}
else {
data[right] = data[left];
--right;
direction = false;
}
}
// 从右往左
else {
if (data[right] > backup) {
--right;
}
else {
data[left] = data[right];
++left;
direction = true;
}
}
}
data[left] = backup;
// 3.划分递归
QuickSortRecursion(data, start, left-1);
QuickSortRecursion(data, left+1, end);
}
void QuickSort(vector<int> &data) {
QuickSortRecursion(data, 0, data.size()-1);
}
时间复杂度:平均时间复杂度O(nlogn), 最好时间复杂度O(nlogn), 最坏时间复杂度O(n^2)
是否原地算法:额外空间复杂度为O(1),是原地算法,但是递归栈占用空间,空间复杂度O(logn)-O(n)
是否稳定排序:由于每次和哨兵元素比较后可能往两个方向移动,因此快速排序不是稳定算法
2.2 更好的实现
思路:将递归和划分分开,划分使用交换,实现更加巧妙简洁
void swap(vector<int> &data, int i, int j) {
int tmp = data[i];
data[i] = data[j];
data[j] = tmp;
}
/**
* @brief 划分数组并使得最终i之前元素都小于data[i],i之后元素都大于等于data[i],划分元素为data[r]
* @param data 数组
* @param l 左边起始位置
* @param r 右边结束位置
* @return 划分点位置
*/
int partition(vector<int> &data, int l, int r) {
int i = l, j = l; // i标记大数,j遍历数组
for (; j<r; ++j) {
if (data[j] < data[r]) {
if (i != j) {
swap(data, i, j);
}
++i;
}
}
swap(data, i, r);
return i;
}
/**
* @brief 快排递归过程
* @param data 数据
* @param l 左边起点位置
* @param r 右边结束位置
*/
void _quick_sort(vector<int> &data, int l, int r) {
// 1.终止条件
if (l >= r) return;
// 2.划分后继续划分
int pivot = partition(data, l, r);
_quick_sort(data, l, pivot-1);
_quick_sort(data, pivot+1, r);
}
/**
* @brief 快速排序
* @param data - 原始数据
*/
void quick_sort(vector<int> &data) {
_quick_sort(data, 0, data.size()-1);
}
2.3 利用快排思想的问题
class Solution {
private:
// 划分,使得原区间分成三部分,左区间 哨兵 右区间,左区间的元素都小于等于哨兵,右区间的元素都大于哨兵
// 返回哨兵最终位置
int partition(vector<int>& nums, int left, int right) {
bool direction = false;
int backup = nums[left];
while (left < right) {
if (direction) {
if (nums[left] <= backup) {
++left;
}
else {
nums[right--] = nums[left];
direction = false;
}
}
else {
if (nums[right] > backup) {
--right;
}
else {
nums[left++] = nums[right];
direction = true;
}
}
}
nums[left] = backup;
return left;
}
void recursion(vector<int>& nums, int left, int right, int k) {
// 1. 划分
int pivot = partition(nums, left, right);
// 2. 终止或者选取子区间
if (nums.size() - pivot == k) return;
else if (nums.size() - pivot > k) {
recursion(nums, pivot+1, right, k);
}
else {
recursion(nums, left, pivot-1, k);
}
}
public:
int findKthLargest(vector<int>& nums, int k) {
recursion(nums, 0, nums.size()-1, k);
return nums[nums.size()-k];
}
};
时间复杂度O(n),空间复杂度O(1)
2.4 三色旗问题
思路:先划分0和非0,在划分1和2,每次划分类似于快排的partition,知识判断条件变了,快排是选择最后一个元素,而这一题是指定target,因此最后这道题也不用交换,循环包含最后一个元素
class Solution {
private:
int partition(vector<int> &nums, int start, int target) {
int i = start;
for (int j = start; j < nums.size(); ++j) {
if (nums[j] == target) {
if (i != j) {
swap(nums[i], nums[j]);
}
++i;
}
}
return i; //下一个划分起点
}
public:
void sortColors(vector<int>& nums) {
// 1.划分0和大于0
int pivot = partition(nums, 0, 0);
// 2.划分1和2
partition(nums, pivot, 1);
}
};
时间复杂度O(n),而且是一趟扫描
空间复杂度O(1)