👦个人主页:Weraphael
✍🏻作者简介:目前正在学习c++和算法
✈️专栏:数据结构
🐋 希望大家多多支持,咱一起进步!😁
如果文章有啥瑕疵
希望大佬指点一二
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍
目录
- 一、hoare版本(左右指针法)
-
-
- [1.1 算法思想](#1.1 算法思想)
- [1.2 hoare版本代码实现](#1.2 hoare版本代码实现)
- [1.3 hoare版本性能分析](#1.3 hoare版本性能分析)
-
- [二、 基准值选取随机值(优化)](#二、 基准值选取随机值(优化))
- 三、三数取中(优化)
- 四、小区间优化
- 五、三路划分(优化)
- 六、快速排序之挖坑法
-
-
- [6.1 算法思路](#6.1 算法思路)
- [6.2 代码实现](#6.2 代码实现)
-
- 七、前后指针法(最推荐的方法)
-
-
- [7.1 算法思路](#7.1 算法思路)
- [7.2 代码实现](#7.2 代码实现)
-
一、hoare版本(左右指针法)
1.1 算法思想
【思想】
-
任取待排序元素序列中的某元素作为 基准值
key
。一般是第一个元素和最后一个元素。 -
【✨重点】 将待排序集合 分割成两子序列。使得左子序列中所有元素均小于
key
,右子序列中所有元素均大于key
。
做法:定义两个变量i
和j
分别指向开头和最后一个元素。请务必记住此结论:如果选取的基准值是第一个元素,要让j
先动,反之让i
先动。(假设选取的基准值为第一个元素并且要求序列为升序)
若j
遇到小于等于 key
的数,则停下,然后i
开始走,直到i
遇到一个大于key
的数时,将i
和j
的内容交换,如果区间内还有元素,则重复以上操作。最后你会发现:i
和j
最后一定会相遇(可以参考下面动图),此时将相遇点的内容与key
交换即可
- 最后左右子序列重复该过程,直到所有元素都排列在相应位置上为止。(递归)
以下是一趟排序的动图演示
在往期博客中,我写了一篇快排算法模板 ,有兴趣的可以来看看:点击跳转
1.2 hoare版本代码实现
bash
#include <stdio.h>
void Swap(int* x, int* y)
{
int t = *x;
*x = *y;
*y = t;
}
// hoare版本
void quick_sort(int a[], int l, int r)
{
// 如果区间只有一个元素或者没有元素,就没必要排序了
if (l >= r) return;
// 1. 选取一个基准值(以选取第一个元素为例)
int key = a[l];
// 2. 定义i和j,i从左向右走,j从右向左走。
int i = l, j = r;
while (i < j)
{
// 注意:若选择第一个元素作为基准值,则需要j先走;反之让i先走
while (i < j && a[j] >= key) // 找小
{
j--;
}
while (i < j && a[i] <= key) // 找大
{
i++;
}
// 交换
if (i < j)
Swap(&a[i], &a[j]);
}
// 循环结束后,i和j一定会相遇,和基准值交换
Swap(&a[l], &a[i]);
// 3.递归
quick_sort(a, l, i - 1);
quick_sort(a, i + 1, r);
}
【程序结果】
注意:这里会有一个越界 + 死循环问题 + 我犯的错误
- 在
while (i < j && a[j] >= key)
循环中,如果不加i < j
,那么假设序列已经是升序了,那么就会越界;while (i < j && a[i] <= key)
也同理
- 并且如果只写
a[i] < key
,将序列中出现数据冗余,就会陷入死循环 - 最后还有一个问题,就是本人初学时犯的(忽略了一个小的知识点qwq)。
与基准值交换Swap(&a[l], &a[i])
,不能写成Swap(&key, &a[i])
。因为key
是一个局部变量,只是存储序列a[l]
,虽然交换了,但是序列第一个元素并没有改变。我也是通过调试发现的hh
1.3 hoare版本性能分析
- 时间复杂度
快速排序其实是二叉树结构的交换排序方法
递归的高度是logN
,而单趟排序基本都要遍历一遍序列,大约有N
个数,因此时间复杂度是NlogN
接下来可以和堆排序以及希尔排序来比较一下,它们三者的时间复杂度的量级都是NlogN
我们发现,当数据个数为一百万的时候,快速排序还是非常快的。不愧叫快排
那么快排最坏的情况是什么?
最坏的情况即包括逆序,也包括有序 。其时间复杂度是O(N^2^)
如果数据量大的话,那么栈一定会爆。那如果是这样,快排还叫快排吗?
先说结论:快排的时间复杂度是:O(NlogN)
那么如何解决这个问题呢?通过分析发现,有序和无序就是因为基准值选取的不好。
因此,有人提出了优化基准值可以选取随机值或者三数取中
二、 基准值选取随机值(优化)
做法:使用rand函数随机选取区间中的下标rand() % (r - l)
,但是这样远远不够,因为递归的时候,左区间会随之改变。因此正确下标取法rand() % (r - l) + l
bash
void quick_sort(int a[], int l, int r)
{
if (l >= r) return;
// 随机选key
// 区间下标范围
int randIdx = (rand() % (r - l)) + l;
Swap(&a[l], &a[randIdx]);
// 以下都和hoare版本一样
int key = a[l];
int i = l, j = r;
while (i < j)
{
while (i < j && a[j] >= key)
{
j--;
}
while (i < j && a[i] <= key)
{
i++;
}
Swap(&a[i], &a[j]);
}
Swap(&a[l], &a[i]);
quick_sort(a, l, i - 1);
quick_sort(a, i + 1, r);
}
【程序结果】
三、三数取中(优化)
- 三数取中是指:第一个元素、最后一个元素和中间元素,选出不是最小也不是最大的那一个(找的是下标)
bash
int GetMinNum(int a[], int l, int r)
{
int mid = (l + r) >> 1;
// 选出不是最大也不是最小的
// 两两比较
if (a[l] < a[mid])
{
if (a[mid] < a[r])
{
return mid;
}
else if (a[r] < a[l])
{
return l;
}
else
{
return r;
}
}
else // a[l] >= a[mid]
{
if (a[l] < a[r])
{
return l;
}
else if (a[r] < a[mid])
{
return mid;
}
else
{
return r;
}
}
}
void quick_sort(int a[], int l, int r)
{
if (l >= r)
return;
// 三数取中
int mid = GetMinNum(a, l, r);
Swap(&a[mid], &a[l]);
// 以下和hoare版本一样
int key = a[l];
int i = l, j = r;
while (i < j)
{
while (i < j && a[j] >= key)
{
j--;
}
while (i < j && a[i] <= key)
{
i++;
}
Swap(&a[i], &a[j]);
}
Swap(&a[l], &a[i]);
quick_sort(a, l, i - 1);
quick_sort(a, i + 1, r);
}
【程序结果】
四、小区间优化
由于快速排序是基于分治的思想。其实就是二叉树结构的交换排序方法。而我们知道,二叉树最后一层的结点个数是占整个结点个数的一半。并且快排递归到最后每一个都是小区间,但是每一个小区间都需要使用多次递归。这样的消耗确实挺大。
因此我们可以对小区间进行优化 ,让小区间不要使用递归了,直接使用插入排序来进行优化。因为小区间以及很接近有序了。使用插入排序最佳。当然区间不可以太大,因为我们要考虑小区间直接插入的效率高于递归的效率
bash
#include <iostream>
using namespace std;
void Swap(int *x, int *y)
{
int t = *x;
*x = *y;
*y = t;
}
int GetMinNum(int a[], int l, int r)
{
int mid = (l + r) >> 1;
// 选出不是最大也不是最小的
// 两两比较
if (a[l] < a[mid])
{
if (a[mid] < a[r])
{
return mid;
}
else if (a[r] < a[l])
{
return l;
}
else
{
return r;
}
}
else // a[l] >= a[mid]
{
if (a[l] < a[r])
{
return l;
}
else if (a[r] < a[mid])
{
return mid;
}
else
{
return r;
}
}
}
void InsertSort(int a[], int n)
{
for (int i = 1; i < n; i++)
{
int end = i - 1;
int tmp = a[i];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
void QuickSort(int a[], int l, int r)
{
if (l >= r)
return;
// 如果区间个数超过10,就使用递归
if ((l - r + 1) > 10)
{
// 三数取中
int mid = GetMinNum(a, l, r);
Swap(&a[mid], &a[l]);
// 以下和hoare版本一样
int key = a[l];
int i = l, j = r;
while (i < j)
{
while (i < j && a[j] >= key)
{
j--;
}
while (i < j && a[i] <= key)
{
i++;
}
Swap(&a[i], &a[j]);
}
Swap(&a[l], &a[i]);
QuickSort(a, l, i - 1);
QuickSort(a, i + 1, r);
}
else
{
InsertSort(a + l, r - l + 1);
}
}
int main()
{
int a[] = {9, 8, 7, 6, 5, 4, 3, 2, 1};
int n = sizeof(a) / sizeof(a[0]);
QuickSort(a, 0, n - 1);
for (int i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
return 0;
}
五、三路划分(优化)
这个优化是为了解决大量元素重复的问题,这个博主还未学到。暂且先放放hh
六、快速排序之挖坑法
6.1 算法思路
【算法思路】
- 选取一个基准值并用一个变量保存(这个基值一般是第一个元素或者是最后一个元素),然后序列中基准值这个位置相当于是一个坑等待下一个元素填入
- 如果选取的基准值是第一个元素,老样子
j
要从右边向左开始找小,如果找到小,就将j
指向的元素填入到坑中,而此时j
这个位置是一个坑等待填入;接下来就是i
从左向右找大,如果找到了大,就将i
指向的元素填入到坑中,同理的,i
这个位置是一个坑等待填入 - 最后
i
和j
相遇,并且一起站着一个坑位hole
,然后就把基准值key
填入即可 - 递归重复区间
[l, hole - 1]
和区间[hole + 1, r]
本质上来说,填坑法和hoare
版本类似,相比其更加容易理解
6.2 代码实现
c++
void QuickSort5(int a[], int l, int r)
{
if (l >= r) return;
int x = a[l];
// 如果选择左端点为基准值
// 那么坑位一开始是以基准值为下标
int hole = l;
int i = l, j = r;
while (i < j)
{
while (i < j && a[j] >= x) // 找小
{
j--;
}
// 循环结束后,来到此处说明找到小了
// 将小的填入上一个坑位
// 再更新坑位
a[hole] = a[j];
hole = j;
while (i < j && a[i] <= x)
{
i++;
}
// 和上同理
a[hole] = a[i];
hole = i;
}
// 最后i和j相遇一定会同站一个坑位
// 将基准值填入坑位即可
a[hole] = x;
// 递归
QuickSort5(a, l, hole - 1);
QuickSort5(a, hole + 1, r);
}
七、前后指针法(最推荐的方法)
7.1 算法思路
- 选出一个基准值
key
,一般是最左边或是最右边的。 - 起始时,
prev
指针指向序列开头,cur
指针指向prev
的下一个位置。 - 若
cur
指向的内容小于key
,prev
先向后移动一位,然后交换prev
和cur
指针指向的内容,然后cur
指针继续向后遍历 - 若
cur
指向的内容大于key
,则cur
指针直接向后遍历。因此可以得出结论,cur
本质就是在找小,然后让小的往前靠 - 若
cur
超出序列,此时将key
和prev
指针指向的内容交换即可。
经过一次单趟排序,最终也能使得key
左边的数据全部都小于key
,key
右边的数据全部都大于key
。
最后再重复以上操作,直至序列只有一个数据或者序列没有数据时。(递归区间[l, prev - 1]
和[prev + 1, r]
)
7.2 代码实现
bash
void quick_sort(int a[], int l, int r)
{
if (l >= r)
return;
// 1. 选出一个key
int key = a[l];
// 2. 起始时,prev指针指向序列开头,cur指针指向prev+1。
int prev = l, cur = prev + 1;
while (cur <= r)
{
// 3. 若cur指向的内容小于key
// 则prev先向后移动一位,
// 然后交换prev和cur指针指向的内容,
// 然后cur指针++(可以归到第四点)
if (a[cur] < key)
{
++prev;
Swap(&a[prev], &a[cur]);
}
// 4. 若cur指向的内容大于key,则cur指针直接++
++cur;
}
// 若cur到达r + 1位置,此时将key和prev指针指向的内容交换即可。
Swap(&a[l], &a[prev]);
// 递归
quick_sort(a, l, prev - 1);
quick_sort(a, prev + 1, r);
}