跳到主要内容C算法
快速排序详解:三种单趟实现及非递归优化
快速排序算法。涵盖单趟排序的三种实现方式:Hoare 法、挖坑法和前后指针法。介绍了整体排序的递归逻辑及优化 key 值选取的策略,包括随机数选 key 和三数取中法,以解决最坏时间复杂度问题。最后讲解了如何利用栈将递归改为非递归实现,避免栈溢出风险。
山野诗人0 浏览 快速排序的单趟排序 - 算法思想
在开始写代码时,理解算法的思路是不可或缺的一部分。接下来我们来理解一下快速排序的算法思想(以升序排序为主)。
核心思想:在待排序的数组中,选择一个值作为 key 值。key 值的选择会影响快速排序的效率。随后定义两个变量 left 和 right,控制待排序数组的区间。假定待排序数组的第一个元素就是 key 值,此时一定要让 right 变量先动,right 往前动的目的是找比 key 值小的数。之后让 left 变量往后动,left 往前动的目的是找比 key 值大的数。将这两个数进行位置上的交换。这个过程一直持续到 right 和 left 相遇,此时将 key 值一开始所在的位置与相遇位置的值进行互换。最终达到的效果是比 key 值小的数都在 key 值的左边,比 key 值大的数都在 key 值的右边。

基于上述例子进行步骤拆解。

最终效果:

快速排序的单趟排序 - 代码实现
上述思路由 Hoare 提出。该版本代码理解起来有一定难度,书写容易出错,主要是在选最左边的值为 key 时得让 right 先走。为此,后面又出现了挖坑法、以及前后指针法。
Hoare 版本
void QuickSortPart1(int* a, int left, int right) {
int keyi = left;
while (left < right) {
while (right > left && a[right] >= a[keyi]) {
--right;
}
while (left < right && a[left] <= a[keyi]) {
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
keyi = left;
}
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown 转 HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
- HTML 转 Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
细节处理不到位可能导致预期不一样,例如将 a[right] >= a[keyi] 写成 a[right] > a[keyi],或者没有判断 right 和 left 越界导致程序崩溃。
后来,人们根据 Hoare 的思想研发了另一种方式——'挖坑法'。
挖坑法
可以看到上面的动图,当我们在选取的 key 值的时候,就相当于在这个位置上面挖个坑,我们可以用一个变量记录此时坑出现的位置。然后按照 Hoare 大佬的思想,将需要移动数据移动到旧坑,那么此时就会出现新的坑,我们更新记录坑这个位置的变量即可。
void QuickSortPart2(int* a, int left, int right) {
int key = a[left];
int hole = left;
while (left < right) {
while (right > left && 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;
}
前后指针
除了 Hoare 大佬这个思想之外,有人还提出了一种新的思想 —— '用前后指针来实现'。其本质就是将待排序的数组分割成两个子区间,其中一个子区间里面的元素都大于基准值(key 值),另一个子区间里面的元素都小于基准值(key 值)。
可以看到的是:它会分为两种情况:当 key 值大于 cur 指向的值时,先让 prev 往后走一步,紧接着,再让 prev 位置上的值与 cur 位置上的值交换位置。最后,cur 再往后面走一步;当变量 cur 所指向的值大于 key 值时,直接让 cur 往后面走一步即可。
void QuickSortPart3(int* a, int left, int right) {
int n = right - left + 1;
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur < n) {
if (a[keyi] > a[cur]) {
prev++;
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
}
至此,关于快速排序单趟排序的三个版本的代码演示就完成了。
三个版本我该改用哪一个?
这三个版本都能实现同一个功能。如果你实在记不住那么多的话,强烈推荐前后指针这个版本。这个版本也是现在很多人写快速排序算法时会用到的方法。此外,面试中也难免会碰到这个问题,建议大家都要掌握。
快速排序的整体排序
快速排序的整体排序的算法思路
从单趟排序我们就可以知道,单趟排序的目的就是将我们所选的 key 值放到待排序数组中正确的位置上。然后将待排序数组划分成两个区间 [begin, keyi-1] 和 [keyi+1, end]。然后,我们又可以对这两个区间里的值再使用单趟排序的思路,这就是递归。
快速排序的整体排序的代码实现
void QuickSort(int* a, int left, int right) {
if (left >= right) {
return;
}
int begin = left, end = right;
int keyi = left;
while (left < right) {
while (right > left && a[right] >= a[keyi]) {
--right;
}
while (left < right && a[left] <= a[keyi]) {
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
keyi = left;
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
找 key 值的方式
我们为什么要谈论这个问题呢?相信大家在上面已经察觉到了,我们的 key 值目前来说要不就是去待排序数组开头的元素,要不就是待排序数组结尾的元素。这样做会有问题吗?
为什么要提出找 key 值的方式?
有一部分的读者会认为:直接选待排序数组开头的元素,要不就是待排序数组结尾的元素作为 key 值挺好的啊。
可以想一下快速排序的时间复杂度是多少?应该是 O(N*logN)。
但是大家有没有想过这么一个问题:我们用刚才写的快速排序来排一个已经有序(升序、降序)的数组的时间复杂度又是多大呢?
可以看到它的时间复杂度变为了 O(N^2)。这个时间复杂度对于排序算法来说是个大忌。我们有什么办法能够抢救一下这个时间复杂度吗?方法肯定是有的。
可以看到时间复杂度之所以这么高,是因为 keyi 的位置所导致的。总结上面的思想,我们希望看到的是 keyi 值越是取整个待排序数组中较为中间的值时,它的效率是最大的。如果 keyi 所在位置的值是待排序数组的较为中间的值,那么经过一次单趟排序后,其分出的两个子区间各自的元素数量较为均等,这样就越接近 logN 这个量级,而不是 N 这个量级了。
随机数选 key
这个方式我不是很推荐大家使用,不过这个方法现在仍有人在玩。
随机数选 key 的代码很简单就是利用 rand 函数生成一个区间范围内的数,将它与最左边或最右边的值互换。
可能有的读者就会问了,为什么要互换呢,我直接选了就直接用不好吗?这里要说明的一点是,肯定是要交换位置的!原因是承接我们上面代码的思想。
void QuickSort(int* a, int left, int right) {
srand((unsigned int)time(NULL));
if (left >= right) {
return;
}
int begin = left, end = right;
int randi = left + rand() % (right - left + 1);
Swap(&a[randi], &a[left]);
int keyi = left;
while (left < right) {
while (right > left && a[right] >= a[keyi]) {
--right;
}
while (left < right && a[left] <= a[keyi]) {
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
keyi = left;
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
三数取中
选取的三个数字分别位于待排序数组开头位置的元素、中间位置的元素以及结尾位置的元素。
int GetMidNum(int* a, int left, int right) {
int mid = (left + right) / 2;
if (a[left] > a[right]) {
if (a[mid] > a[right]) {
return mid;
} else
{
return right;
}
} else
{
if (a[left] > a[mid]) {
return left;
} else
{
return mid;
}
}
}
void QuickSort(int* a, int left, int right) {
if (left >= right) {
return;
}
int begin = left, end = right;
int midi = GetMidNum(a, left, right);
if (midi != left) {
Swap(&a[midi], &a[left]);
}
int keyi = left;
while (left < right) {
while (right > left && a[right] >= a[keyi]) {
--right;
}
while (left < right && a[left] <= a[keyi]) {
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
keyi = left;
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
这样我们就能很好的解决快速排序时间复杂度为 O(N^2) 的问题了。
快速排序的非递归
我们都知道如果递归深度过深时,会导致栈溢出的情况,从而使整个程序崩溃。所以我们就必须具备一种能力,将递归改为非递归。
- 直接改造成循环(就先斐波那契数列一样);
- 利用栈这个数据结构来实现。
我们在这里用的是第二种方式,如果有对栈这个数据结构还不理解的读者,建议先复习相关概念。
这里我们可以这么想,我们通过单趟排序使得区间划分成了两个部分,然后我们在对这两部分区间再次使用单趟排序的思想。所以这里的关键就在于区间的划分,我们可以使用栈先将我们要后排序的区间先入栈,先排序的区间最后在入栈。
int QuickSortPart(int* a, int left, int right) {
int midi = GetMidNum(a, left, right);
if (midi != left) {
Swap(&a[midi], &a[left]);
}
int keyi = left;
while (left < right) {
while (right > left && a[right] >= a[keyi]) {
--right;
}
while (left < right && a[left] <= a[keyi]) {
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
keyi = left;
return keyi;
}
void QuickSortNonR(int* a, int left, int right) {
Stack st;
STInit(&st);
STPush(&st, right);
STPush(&st, left);
while (!STEmpty(&st)) {
int begin = STTop(&st);
STPop(&st);
int end = STTop(&st);
STPop(&st);
int keyi = QuickSortPart(a, begin, end);
if (keyi + 1 < end)
{
STPush(&st, end);
STPush(&st, keyi + 1);
}
if (keyi - 1 > begin)
{
STPush(&st, keyi - 1);
STPush(&st, begin);
}
}
STDestory(&st);
}