比较
std::stable_sort - cppreference.com
- 二者使用时都需要引入
<algorithm> 头文件,
stable_sort是基于归并排序的,是一种稳定排序。比较适合对比较复杂的数据进行排序,比如一些订单数据,,已经按照订单号排好了,现在想对交易日期再进行排序,就可以使用归并排序,排序后元素之间相对位置没有发生变化。
sort是基于快排实现的,是一种不稳排序,理想情况下时间复杂度 O(NlogN),空间复杂度O(logN),极端情况下时间复杂度O(N^2),空间复杂度O(N)
- 二者都可以自定义comp函数,如果结果为true,则排序为args1, args2,否则args2,args1。所以,
- 升序 return args1<args2;
- 降序 return args1>args2
基于二叉树的实现
归并排序
总结:先把左半边数组排好,再把右半边数组排好,再把两个数组合并
这个过程可以在逻辑上抽象成一个二叉树,节点是数组区间,叶子结点的值就是数组元素。和二叉树的后续遍历是一样,
class Merge {
// 用于辅助合并有序数组
// 不在merge中new辅助数组,避免在递归中频繁分配内存和释放内存带来的性能问题
private:
vector<int> tmp;
public:
void sort(vector<int> *nums) {
tmp.clear();
// 排序整个数组(原地修改)
sort(nums, 0, nums->size() - 1);
}
private:
void sort(vector<int> *nums, int low, int high) {
// 单个元素不排序
if (low == high) {
return;
}
int mid = (high - low) / 2 + low;
sort(nums, low, mid);
sort(nums, mid+1, high);
merge(nums, low, mid, high);
}
void merge(vector<int> *nums, int low, int mid, int high) {
// 先把num[low, high] 复制到tmp中
// 以便合并后结果能直接存入nums
for (int i=low; i<=high; i++) {
tmp[i] = (*nums)[i];
}
// 数组双指针技巧,合并两个有序数组
int i = low;
int j = mid + 1;
for (int idx=low; idx<=high; idx++) {
if (i == mid+1) {
(*nums)[idx] = tmp[j++];
} else if (j == high+1) {
(*nums)[idx] = tmp[i++];
} else if (tmp[i] < tmp[j]) {
(*nums)[idx] = tmp[i++];
} else {
(*nums)[idx] = tmp[j++];
}
}
}
}
执行的次数是二叉树节点的个数,每次执行的复杂度就是每个节点代表的子数组的长度,所以总时间复杂度就是整棵树中数组元素的个数。整个树高度是logN,一层元素个数是N,所以总的时间复杂度是O(NlogN)
快速排序
总结:快速排序是先将一个元素排好序(左边元素都小于它,右边元素都大于它),再将剩下的元素排好序(递归左边的也排好,右边的也排好)。
这个过程抽象出来就是一个二叉搜索树的构建过程。为避免极端情况,可以先用洗牌算法将数组乱序,或者随机选择元素作为分界点。
class Quick {
public:
void sort(vector<int> *nums) {
// 避免极端情况,先随机打乱
shuffle(nums);
// 排序整个数组,原地修改
sort(nums, 0, nums.size()-1);
}
private:
void sort(vector<int> *nums, int low, int high) {
if (low >= high) {
return;
}
int p = partition(nums, low, high);
sort(nums, low, p);
sort(nums, p+1, high);
}
private:
void partition(vector<int> *nums, int low, int high) {
int pivot = (*nums)[low];
int i = low + 1;
int j = high;
while (i <= j) {
while(i<high && (*nums)[i]<=pivot) {
i++;
} // 结束时 nums[i] > pivot
while(j>low && (*nums)[j]>pivot) {
j--;
} // 结束时,nums[j]<=pivot
if (i>=j) break;
swap(nums, i, j);
}
// 将pivot放在合适的位置
swap(nums, low, j);
return j;
}
// 洗牌算法
private:
void shuffle(vector<int> *nums) {
int n = nums.size()-1;
for (int i = 0; i<n; i++) {
int r = (rand() % (n-i))+ i + 1;
swap(nums, i, r);
}
}
private:
void swap(vector<int> *nums, int i, int j) {
int tmp = (*nums)[i];
(*nums)[i] = (*nums)[j];
(*nums)[j] = tmp;
}
}
partiton 执行次数就是二叉树及诶蛋的个数,每次执行的复杂度就是每个节点代表的子数组nums[low, high]的长度,所以总的时间复杂度就是整棵树中数组元素的个数
假设数组元素个数为N,那么二叉树每一层元素个数之和就是O(N);分界点分布均匀情况下,树的层数是O(logN),所以理想时间复杂度O(NlogN)
由于快排米有使用任何辅助数组,所以空间复杂度就是递归的深度,即树高O(logN)