以长度为n=20的数组nums=[1,3,5,7,2,6,4,8,9,2,8,7,6,0,3,5,9,4,1,0]为例。要求:升序。
相关概念:
- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
- 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
- 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
- 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
1. 冒泡排序
算法描述
两层遍历。
- 外层遍历:每一次循环确认未排序元素中最大元素的最终位置。共遍历n-1个元素。
- 内层遍历:每一次循环,比较相邻元素的大小。若前者比后者大,则交换,否则不交换。遍历次数为未排序元素的个数-1。
代码实现
void bubble_sort(vector<int> &nums, int n) {
for(int i = 0; i < n - 1; ++i) {
for(int j = 0; j < n - i - 1; ++j) {
if(nums[j] > nums[j+1]) {
int temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
}
}
}
}
复杂度分析
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
- 稳定性:稳定
2. 选择排序
算法描述
两层遍历。
- 外层遍历:每一次循环确认未排序元素中最小元素的最终位置。共遍历n-1个元素。
- 内层遍历:每一次循环,寻找未排序元素中的最小值,将它与未排序的第一个元素交换位置。遍历次数为未排序元素的个数-1。
代码实现
void selection_sort(vector<int> &nums, int n) {
int k;
for(int i = 0; i < n - 1; ++i) {
k = i;
for(int j = i + 1; j < n; ++j) {
if(nums[j] < nums[k])
k = j;
}
if(k != i) {
int temp = nums[i];
nums[i] = nums[k];
nums[k] = temp;
}
}
}
复杂度分析
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
3. 插入排序
算法描述
基本思想:无序序列插入到有序序列中,数组的第一个元素默认有序。
两层遍历。
- 外层遍历:每一次循环,将未排序的第一个元素插入到有序序列的正确位置。共遍历n-1个元素。
- 内层遍历:每一次循环,待插入元素依次从有序序列的最后一个元素开始进行比较。如果待插入元素更小,被比较元素向后挪一个位置。如果待插入元素更大,则直接插入到被比较元素的后一个位置。遍历次数看情况。
代码实现
void insertion_sort(vector<int> &nums, int n) {
for(int i = 1; i < n; ++i) {
int cur = nums[i];
int j;
for(j = i-1; j >= 0 && cur < nums[j]; --j) {
nums[j+1] = nums[j];
}
nums[j+1] = cur;
}
}
复杂度分析
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
- 稳定性:稳定
4. 希尔排序
算法描述
实质上是分组插入排序。事先定义一个增量数组,用来分组。
三层遍历:
- 外层遍历:遍历增量数组。一般采取[n/2, n/2/2, …, 1]
- 中间遍历:遍历各个分组。
- 内层遍历:组内实现插入排序。
代码实现
void shell_sort(vector<int> &nums, int n) {
for(int gap = n / 2; gap > 0; gap /= 2) {
for(int i = gap; i < n; ++i) {
int cur = nums[i];
int j;
for(j = i - gap; j >= 0 && cur < nums[j]; j -= gap) {
nums[j+gap] = nums[j];
}
nums[j+gap] = cur;
}
}
}
复杂度分析
- 时间复杂度:O(n^(1.3-2))
- 空间复杂度:O(1)
- 稳定性:不稳定
5. 归并排序
算法描述
分治思想。
递归算法。
- 把长度为n的数组分成两个长度n/2的子数组;
- 对两个子数组分别采用归并排序;
- 直到两个子数组长度分别为1(默认看作是有序数组);
- 将两个子数组合并成一个有序数组,直到最后合并成一个完整的有序数组。(两个有序数组合并成一个大的有序数组,之间题目里做到过,利用双指针)
代码实现
void merge_sort(vector<int> &nums, int l, int r, vector<int> &temp) {
// 边界条件
if(l + 1 >= r) return;
// divide
int m = l + (r - l) / 2;
merge_sort(nums, l, m, temp);
merge_sort(nums, m, r, temp);
// conquer
int i = l, j = m, k = l;
while(i < m || j < r) {
if(j >= r || (i < m && nums[i] <= nums[j])) {
// 后一个区间排完了,把前一个区间全部排一下
// 或者,两者都没排完的情况下,满足前一个区间当前元素更小
temp[k++] = nums[i++];
} else {
temp[k++] = nums[j++];
}
}
for(i = l; i < r; ++i) {
nums[i] = temp[i];
}
}
复杂度分析
- 时间复杂度:O(nlog_2n)
- 空间复杂度:O(n)
- 稳定性:稳定
6. 快速排序
算法描述
分治思想。
递归算法。
- 从数组中选出一个元素作为基准(pivot);
- 把数组中所有小于基准的放在基准前面(前半区),大于基准的放在基准后面(后半区);
- 重复上述操作,递归遍历两个半区,直到有序。
代码实现
void quick_sort(vector<int> &nums, int l, int r) {
if (l + 1 >= r) {
return;
}
int first = l, last = r - 1, key = nums[first];
while (first < last){
while(first < last && nums[last] >= key) {
--last;
}
nums[first] = nums[last];
while (first < last && nums[first] <= key) {
++first;
}
nums[last] = nums[first];
}
nums[first] = key;
quick_sort(nums, l, first);
quick_sort(nums, first + 1, r);
}
复杂度分析
- 时间复杂度:O(nlog2n)
- 空间复杂度:O(nlog2n)
- 稳定性:不稳定
堆排序
算法描述
基本思想:保证每一个堆的根节点大于(或小于)其左右叶子结点。
一般升序采用大顶堆,降序采用小顶堆。
-
构造初始堆
对于一个无序序列,从最后一个非叶子结点(nums.size()/2 - 1)开始,从左至右,从下至上进行调整,保证每个堆符合条件,最后就能得到一个大顶堆。
-
堆排序
两次遍历。
外层遍历:确认待排序数组中的最大元素。将堆顶元素和待排序末尾元素进行交换,使得待排序中的末尾元素是最大的。
内层遍历:每一次交换,都需要调整堆。
代码实现
void HeapAdjust(vector<int> &nums, int start, int end) {
int head = start; // 当前结点的下标
int l = head * 2 + 1; // 当前结点的左孩子
int r = head * 2 + 2; // 当前结点的右孩子
if(l < end && nums[l] > nums[head]) {
head = l;
}
if(r < end && nums[r] > nums[head]) {
head = r;
}
if(head != i) {
int temp = nums[i];
nums[i] = nums[head];
nums[head] = temp;
// 递归
HeadAdjust(nums, head, end);
}
}
void heap_sort(vector<int> &nums, int n) {
// 构建大顶堆
for(int i = n / 2 - 1; i >=0; --i) {
HeapAdjust(nums, i, n);
}
// 堆排序
for(int i = n - 1; i > 0; --i) {
int temp = nums[i];
nums[i] = nums[0];
nums[0] = temp;
HeapAdjust(nums, 0, i)
}
}
复杂度分析
- 时间复杂度:O(nlog2n)
- 空间复杂度:O(1)
- 稳定性:不稳定
计数排序
算法描述
利用数组下标确定元素的正确位置
三次遍历。
- 第一次遍历,找到数组的最大元素和最小元素;
- 第二次遍历,统计数组各个元素个数;
- 第三次遍历,根据统计数组,得到正确的有序数组。
代码实现
void count_sort(vector<int> &nums, int n) {
// 找到数组的最大最小元素
int min = nums[0], max = nums[0];
for(int i = 1; i < n; ++i) {
if(nums[i] < min) min = nums[i];
if(nums[i] > max) max = nums[i];
}
// 统计数组
int l = max - min + 1;
int *count = new int[l]();
for(int i = 0; i < n; ++i) {
++count[nums[i]-min];
}
// 数组填充
int cur = 0;
for(int i = 0; i < max; ++i) {
for(int j = count[i]; j > 0; --j) {
nums[cur++] = min + i;
}
}
}
复杂度分析
- 时间复杂度:
- 空间复杂度:
- 稳定性:
桶排序
算法描述
可以看作是计数排序的扩展版本,计数排序的count数组,每个下标桶存储相同元素,而桶排序每个下标桶存储一定范围的元素。
- 根据映射函数(自定),把数组里的元素划分到各个桶;
- 各个桶自排;
- 依次取出桶中元素,完成最后的正确排序。
代码实现
void bucket_sort(vector<int> &num, int n) {
// 找到数组的最大最小元素
int min = nums[0], max = nums[0];
for(int i = 1; i < n; ++i) {
if(nums[i] < min) min = nums[i];
if(nums[i] > max) max = nums[i];
}
// 统计数组
// 此处运用的映射规则:每个下标存储相同元素(同计数排序)
int l = max - min + 1;
int *count = new int[l]();
for(int i = 0; i < n; ++i) {
++count[nums[i]-min];
}
// 数组填充
int cur = 0;
for(int i = 0; i < max; ++i) {
for(int j = count[i]; j > 0; --j) {
nums[cur++] = min + i;
}
}
}
复杂度分析
- 时间复杂度:O(n+k) k表示桶的个数
- 空间复杂度:O(n+k)
- 稳定性:稳定
基数排序
算法描述
排序 + 收集
先按低位排序,然后收集;再按高位排序,然后收集;以此类推直到最高位。
代码实现
void radix_sort(vector<int> &nums, int n) {
// 先得到数组中的最大值的位数
int max = nums[0];
for(int i = 1; i < n; ++i) {
if(max < nums[i])
max = nums[i];
}
int count = 0;
while(max > 0) {
max /= 10;
++count;
}
// 排序 + 收集
vector<vector <int>> temp(10);
for(int i = 0, mod = 10, dev = 1; i < count; ++i, dev *= 10, mod *= 10) {
for(int j = 0; j < n; ++j) {
int loca = nums[j] % mod / dev; // 获得相应位数上的数字
temp[loca].push_back(nums[j]);
}
int cur = 0;
for(int j = 0; j < 10; ++j) {
for(int k = 0; k < temp[j].size(); ++k) {
nums[cur++] = temp[j][k];
}
}
// 清空temp
temp.clear();
}
}
复杂度分析
- 时间复杂度:O(n*k)
- 空间复杂度:O(n+k)
- 稳定性:稳定
参考: