1.快排
(1)霍尔法
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
int GetMidNumi(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
else // a[left] > a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
void QuickSort1(int* a, int left, int right)
{
if (left >= right)
return;
int begin = left, end = right;
//// 随机选key
/*int randi = left + (rand() % (right - left));
Swap(&a[left], &a[randi]);*/
// 三数取中
int midi = GetMidNumi(a, left, right);
if(midi != left)
Swap(&a[midi], &a[left]);
int keyi = left;
while (left < right)
{
// 右边找小
while (left < right && a[right] >= a[keyi])
--right;
// 左边找大
while (left < right && a[left] <= a[keyi])
++left;
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
keyi = left;
// [begin, keyi-1] keyi [keyi+1, end]
// 递归
QuickSort1(a, begin, keyi - 1);
QuickSort1(a, keyi+1, end);
}
(1)为什么这个地方要随机选key 或者说三数取中
如果不三数取中/随机选key
这个地方如果是有序 无论是正序还是逆序 都会导致时间复杂度变成O(N^2)
最可怕的是空间会开很多 深度就是n了 会导致栈溢出!
影响快排性能的是keyi keyi越接近中间的位置 越能二分 越二分越接近满二叉树 越接近满二叉树 深度就越小 效率就越高!
如果不三数取中/随机选key 并且正序或者逆序
正序就是下图左边的模样 逆序就是下图右边模样
通过三数取中我们能优化快排的时间 使其最坏情况 O(n^2)的概率大大降低
(2)为什么 key选left时 right先走 最后相遇的点一定是比key小或者相等的点呢?
这个问题我们要进行分类讨论 直接对相遇情况讨论即可 无需对过程进行讨论
因为过程会发生交换 交换会进行交换调整使其又开始right先走 找小 然后了left找大 就是一个新的循环了!
right先走
1.right找到小的 left没有 找到大的 left遇到right
这个时候right是小的 所以相遇的点是
2.right没有找到小的 直到遇到left 这个时候的left
(1)一步都没走的left的 也就是相遇点和key相同
(2)走过一步及以上的left 也就是相遇点比key小
如果是left先走
1.left找到大的 但是right找不到小的 和left相遇 这个时候的left是没有交换过的left 也就是大的 相遇点就是left且比key要大
2.left没有找到大 和right相遇了 这个时候的right有两种情况
(1)一步都没走的right的 这个时候的1 right是大小还是相等是不确定的
(2)走过一步及以上的right 也就是相遇点比key大
总结
(1)left找大 right找小情况下
left做key right先走 可以确保相遇点比key小
同理 right做key 就要left先走 这样就可以确保 相遇点就比key大
上面两种情况都是派升序 排降序怎么办?
要排降序就变成左边找小 右边找大就可以了!(详见下文)
(2) left找小 right找大情况下
left做key right先走 可以确保相遇点比key大
同理 right做key 就要left先走 这样就可以确保 相遇点就比key小
2.挖坑法
// 挖坑法
void QuickSort2(int* a, int left, int right)
{
if (left >= right)
return;
int begin = left, end = right;
// 三数取中
int midi = GetMidNumi(a, left, right);
if (midi != left)
Swap(&a[midi], &a[left]);
int key = a[left];
int hole = left;
while (left < right)
{
// 右边找小
while (left < right && a[right] >= key)
--right;
a[hole] = a[right];
hole = right;
// 左边找大
while (left < right && a[left] <= key)
++left;
a[hole] = a[left];
hole = left;
}
a[hole] = key;
// [begin, hole-1] hole [hole+1, end]
// 递归
QuickSort2(a, begin, hole - 1);
QuickSort2(a, hole + 1, end);
}
挖坑发和霍尔法 能保证最后结果是一致的 但是无法保证过程结果是一致的!
6 1 2 7 9 3 4 5 10 8
用霍尔法的结果是 3 1 2 5 4 6 9 7 10 8
用挖坑法结果是 5 1 2 4 3 6 9 7 10 8
key的是相同的 但是其他元素位置 未必相同。
3.前后指针法
//前后指针法
void QuickSort3(int* a, int left, int right)
{
if (left >= right)
return;
// 原PartSort3内容开始
// 三数取中逻辑
int midi = left;
if (right - left + 1 >= 3) {
midi = GetMidNumi(a, left, right);
}
if (midi != left) {
Swap(&a[midi], &a[left]);
}
int keyi = left;
int prev = left;
int cur = left + 1;
// 前后指针分区逻辑
while (cur <= right) {
if (a[cur] < a[keyi] && ++prev != cur) {
Swap(&a[cur], &a[prev]);
}
++cur;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
// 原PartSort3内容结束
// 递归调用保持不变
QuickSort3(a, left, keyi - 1);
QuickSort3(a, keyi + 1, right);
}
当然 我们还可以对代码进行优化 比如递归的区间很闲的时候 我们用插入排序就会很方便!
//小区间用插入排序可以优化很多!
// 合并后的函数
void quicksort_combined(int* a, int left, int right) {
if (left >= right)
return;
// 小区间优化--小区间直接使用插入排序
if ((right - left + 1) > 10) {
// 三数取中
int midi = GetMidNumi(a, left, right);
if (midi != left)
Swap(&a[midi], &a[left]);
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur <= right) {
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[cur], &a[prev]);
++cur;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
quicksort_combined(a, left, keyi - 1);
quicksort_combined(a, keyi + 1, right);
}
else {
InsertSort(a + left, right - left + 1);
}
}
(4)快排的非递归实现
// 前后指针法
int PartSort3(int* a, int left, int right)
{
// 三数取中
int midi = GetMidNumi(a, left, right);
if (midi != left)
Swap(&a[midi], &a[left]);
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[cur], &a[prev]);
++cur;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
return keyi;
}
void QuickSortNonR(int* a, int left, int right)
{
stack<int> st;
st.push(right);
st.push( left);
while (!st.empty())
{
int begin = st.top();
st.pop();
int end = st.top();
st.pop();
int keyi = PartSort3(a, begin, end);
// [begin,keyi-1] keyi [keyi+1, end]
if (keyi + 1 < end)
{
st.push(end);
st.push( keyi + 1);
}
if (begin < keyi - 1)
{
st.push( keyi - 1);
st.push( begin);
}
}
}
(5)快排三路划分
快排在遇到一种情况下三数取中也无法做到优化
比如2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 ...
相同元素特别多的时候三数取中就无法做到优化 就有可能出现 递归变成链表这种情况 效率就无法得到优化
于是三路划分出现了
三路划分顾名思义 就是将数组划分成三个区间 小于key 大于key 等于key
//三路划分
int GetMidIndex(int* a, int begin, int end)
{
//int mid = (begin + end) / 2;
int mid = begin + rand() % (end - begin);
if (a[begin] < a[mid])
{
if (a[mid] < a[end])
{
return mid;
}
else if (a[begin] > a[end])
{
return begin;
}
else
{
return end;
}
}
else // a[begin] > a[mid]
{
if (a[mid] > a[end])
{
return mid;
}
else if (a[begin] < a[end])
{
return begin;
}
else
{
return end;
}
}
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
if ((end - begin + 1) < 15)
{
// 小区间用直接插入替代,减少递归调用次数
InsertSort(a + begin, end - begin + 1);
}
else
{
int mid = GetMidIndex(a, begin, end);
Swap(&a[begin], &a[mid]);
int left = begin, right = end;
int key = a[begin];
int cur = begin + 1;
while (cur <= right)
{
if (a[cur] < key)
{
Swap(&a[cur], &a[left]);
cur++;
left++;
}
else if (a[cur] > key)
{
Swap(&a[cur], &a[right]);
--right;
}
else // a[cur] == key
{
cur++;
}
}
// [begin, left-1][left, right][right+1,end]
QuickSort(a, begin, left - 1);
QuickSort(a, right + 1, end);
}
}
要三个指针
left指针 right指针 cur指针
cur>right 就是结束条件
cur指针指向的值和key相同 往后推
比key小的甩到左边
比key大的甩到右边
和key相等的就在中间
2.归并排序
(1)递归实现
//归并排序
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
return;
int mid = (begin + end) / 2;
// [begin, mid] [mid+1,end],子区间递归排序
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
// [begin, mid] [mid+1,end]归并
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail\n");
return;
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
(2)非递归实现
//归并排序非递归写法1
//这种是归并多少 拷贝多少到tmp 再拷贝回来 拷贝多少
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
// 1 2 4 ....
int gap = 1;
while (gap < n)
{
int j = 0;
for (int i = 0; i < n; i += 2 * gap)
{
// 每组的合并数据
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
if (end1 >= n || begin2 >= n)
{
break;
}
// 修正
if (end2 >= n)
{
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
// 归并一组,拷贝一组
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
//这个地方可不可以改成memcpy(a + i, tmp + i, sizeof(int) * (end2 - begin1 + 1))
//不可以 因为在上面的while循环中begin1在变 不是最开始的那个begin1了
}
printf("\n");
//memcpy(a, tmp, sizeof(int) * n);
gap *= 2;
}
free(tmp);
}
//归并排序的循环实现2
//这种是把所有的都拷贝到tmp 再拷贝回来
//void MergeSortNonR(int* a, int n)
//{
// int* tmp = (int*)malloc(sizeof(int) * n);
//
// // 1 2 4 ....
// int gap = 1;
// while (gap < n)
// {
// int j = 0;
// for (int i = 0; i < n; i += 2 * gap)
// {
// // 每组的合并数据
// int begin1 = i, end1 = i + gap - 1;
// int begin2 = i + gap, end2 = i + 2 * gap - 1;
//
// printf("修正前:[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
//
// if (end1 >= n)
// {
// end1 = n - 1;
//
// // 不存在区间
// begin2 = n;
// end2 = n - 1;
// }
// else if (begin2 >= n)
// {
// // 不存在区间
// begin2 = n;
// end2 = n - 1;
// }
// else if(end2 >= n)
// {
// end2 = n - 1;
// }
//
// printf("修正后:[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
//
//
// while (begin1 <= end1 && begin2 <= end2)
// {
// if (a[begin1] <= a[begin2])
// {
// tmp[j++] = a[begin1++];
// }
// else
// {
// tmp[j++] = a[begin2++];
// }
// }
//
// while (begin1 <= end1)
// {
// tmp[j++] = a[begin1++];
// }
//
// while (begin2 <= end2)
// {
// tmp[j++] = a[begin2++];
// }
// }
// printf("\n");
//
// memcpy(a, tmp, sizeof(int) * n);
// gap *= 2;
// }
//
// free(tmp);
//}
循环实现归并要注意
归并排序的循环实现2:
(1) end1 begin2 end2 全部越界
(2) begin2 end2 越界
(3) end2越界
为什么begin1不可能越界 因为begin1是i 如果越界压根就不会进去
处理方法
(1) 不归并了 但是要不要拷贝到tmp? 循环实现2 要拷贝到tmp 为什么?
因为方法2 是最后要把tmp所有值拷贝回原数组 如果不开拷贝 那就是随机值
最后再拷贝会回去 会把原来正确的值给覆盖!
所以这个时候end1要设置成n-1 那么这个时候 begin2和end2怎么设置? 可以设置成begin2 end2都是n-1吗?
答案是不可以 为什么?
因为如果这样 begin1 end1 begin2 end2 两个区间都有 n-1 两个区间归并的时候 n-1这个位置会被归并两次
这个地方本质上 begin2和end2就不用归并了 所有设置成一个不存在的区间就行了 因为这样就不会进入归并
(2)除了end1不用调整 其他本质上和(1)是一样的呀!
(3) 把 begin2 和end2这个区间里面没有超出都是部分进行归并就可以了! 也就是把end2设置成n-1
3.计数排序
//计数排序
// 时间复杂度:O(N+Range)
// 空间复杂度:O(Range)
// 缺陷1:依赖数据范围,适用于范围集中的数组
// 缺陷2:只能用于整形
void CountSort(int* a, int n)
{
int min = a[0], max = a[0];
for (int i = 0; i < n; i++)
{
if (a[i] < min)
{
min = a[i];
}
if (a[i] > max)
{
max = a[i];
}
}
int range = max - min + 1;
int* countA = (int*)malloc(sizeof(int) * range);
memset(countA, 0, sizeof(int) * range);
// 统计次数
for (int i = 0; i < n; i++)
{
countA[a[i] - min]++;
}
// 排序
int k = 0;
for (int j = 0; j < range; j++)
{
while (countA[j]--)
{
a[k++] = j + min;
}
}
}
4.基数排序
//基数排序
#define K 3
#define RADIX 10
queue<int> Q[RADIX];
int GetKey(int value, int k)
{
int key = 0;
while (key >= 0)
{
key = value % 10;
value /= 10;
k--;
}
return key;
}
void Collect(int* ar)
{
int k = 0;
for (int i = 0;i < RADIX;i++)
{
while (!Q[i].empty())
{
ar[k++] = Q[i].front();
Q[i].pop();
}
}
}
void Distribute(int* ar,int left,int right,int k)
{
for (int i = left;i < right;i++)
{
int key = GetKey(*(ar + i),k);
Q[key].push(*(ar + i));
}
}
void RadixSort(int* ar, int left, int right)
{
for (int i = 0;i < K;+i)
{
//分发数据
Distribute(ar, left, right, i);
//回收数据
Collect(ar);
}
}
5.桶排序
void bucketSort(vector<int>& arr) {
vector<vector<int>> buckets(10); // 初始化10个桶,对应0-99的范围划分
// 分配元素到对应的桶中
for (int num : arr) {
int bucketIndex = num / 10; // 计算桶索引,如35→3(30-39),9→0(0-9)
buckets[bucketIndex].push_back(num);
}
// 对每个桶进行排序(这里使用STL的sort,也可替换为插入排序等)
for (auto& bucket : buckets) {
sort(bucket.begin(), bucket.end());
}
// 合并所有桶的元素到原数组
arr.clear();
for (const auto& bucket : buckets) {
arr.insert(arr.end(), bucket.begin(), bucket.end());
}
}
6.排序的稳定性
什么是稳定性呢?
比如说 这串数字 1 23 23 2 53 643 32 52 6 2 53 2
我排完序后 如果能保证 蓝色2还是在红色2前面 就说明这个排序是稳定的
也就是相同元素的相对位置不变!
我们一般不考虑 桶排序 基数排序 计数排序 这三个非比较排序的 稳定性
但是如果硬要考虑
当然稳定性的前提是 可以实现成稳定的 比如像冒泡排序 如果你实现就要它不稳定 也是可以的!