快速排序
作者:互联网
1. 快速排序——分治
# 算法原理:
在给定序列找到一个点x使得x左边区间数都小于x,右边区间数都大于x
# 步骤:
- 确定分界点
- 随机,可以是第一个数
- 调整区间
- 使左边都小于分界点,右边都大于分界点
- 递归处理左右两段
- 递归停止的条件
if(l >= r) return;
即区间里没有数或只有1个数就直接返回
- 递归停止的条件
# 如何实现x左边都小于等于x右边都大于等于x?
方法一:暴力法
- 开辟两个数组
a[]
和b[]
- 对序列
q[l~r]
:小于x的放a[]
大于x放b[]
if q[i] <= x
thenq[i]-->a[]
if q[i] > x
thenq[i] -->b[]
- 把
a[],b[]
依次放回q[]
a[],b[]-->q[]
方法二:双指针
- 当
i
指针指向的数小于x,指针后移- 当
q[i]<x
,i++
- 当
- 直到
i
指向的数大于x,开始判断j
指向的数- 当
q[i]>x
, 判断q[j]
- 当
- 当
j
指针指向的数大于x,指针前移- 当
q[j]>x
,j--
- 当
- 直到
j
指向的数小于x,交换i,j
指向的数- 当
q[j]<x
,swap(q[i], q[j])
- 当
- 重复上述过程,直到
i>j
最终j
指针一定在i
指针前面,i
指向x
因为循环结束的条件是i<j
,然后每次循环至少执行1次i++
和j--
,因此在退出循环时,必定满足j + 1 = i
,即j
在i
的前面。后文有解析这个问题,点击跳转
i
左边的数永远<=x
j
右边的数永远>=x
当i, j
两个指针穿过后,左边的数为<=
x,右边的数位>=x
,从而分成2个区间。
# 快排模板
void quick_sort(int q[], int l, int r)
{
if(l >= r) return;
int i = l - 1, j = r + 1, x = q[l + r >> 1];
while(i < j)
{
do i++; while(q[i] < x);
do j--; while(q[j] > x);
if(i < j) swap(q[i], q[j]);
}
quick_sort(q, l, j);
quick_sort(q, j + 1, r);
}
# 需要斟酌的细节(边界问题)
细节一:指针移动的判断不带等号
考虑一个边界问题,为什么移动i
和j
指针的条件是q[i] < x
和q[j] > x
,而不是q[i] <= x
和q[j] >= x
?
原因如下:
- 如果选取的
x
是数组里最大的数,序列中所有的数都满足q[i] <= x
,会导致i
会一直++
发生越界都不会停下来。 - 如果选取的
x
是数组里最小的数,同理q[j] >= x
恒成立,j
会一直--
发生越界。
这也是造成快排不稳定的原因,排序算法是否稳定,与时间效率是否稳定无关。稳定是指若源序列中两个值相同的数,排序后这两个数的先后次序不会发生改变。
而快排中当边界点存在重复的数会交换位置。因此快排不稳定。
解决不稳定的方法:把序列中的数改成二元数,Ai
改成 <Ai, i>
,从而使所有的数都不相同。
细节二:使用do-while在判断前先移动指针
考虑一个边界问题,为什么不能让i = l
和j = r
然后使用while循环代替do-while循环?
探讨一下写成如下形式会有什么问题
while(q[i] < x) i++;
while(q[j] > x) j--;
若数组中存在重复的数字,某一轮可能存在i
和j
都指向重复的数字,并且分界点x
也是这个数字,上述两个while语句的判断就会结束循环,此时q[i] = q[j] = x
,交换i
和j
指向这个局面仍然不会改变,因此下一轮会重复这个过程,陷入死循环。
因此,要确保每轮下来两个指针都至少会移动一步,保证上一次交换的结果不会再次判断。
思路是,在判断while条件时,先移动指针。用do-while
最容易实现这个思想,也可以用while
实现:
do i++; while(q[i] < x);
do j--; while(q[j] > x);
//等价于
while(q[++i] < x);
while(q[--j] > x);
同时为了保证每轮下来边界l
和r
都能被判断到,因此初始化要i = l - 1, j = r + 1
使指针在数组两端之外。
细节三:区间左半边使[l, j]而不是[l, i]
考虑一个边界问题,q[i]
和 q[j]
在 i == j - 1
时停下来做交换的场景,交换完成之后i
和j
会各自前进(i ++, j --
)一步,形成i > j
(即i == j + 1
)的不合法局面。
这个局面的原因:由于i
指针移动时,其走过的数都满足< x
,j
移动时其走过的数都满足> x
。i, j
相遇时,其再继续移动就会穿过对方,进入对方走过的数,因此会停下来等待交换。此时i, j
位置就发生了交换, j + 1 = i
。并且i, j
指向的数不用再交换了,因为前后的数都已经判断过了。
此时,从l
到j
的数都满足q[l, j] <= x
。为什么不是< x
,因为i
走过的数如果需要和j
交换,换过来的数可能是= x
的。
所以,在这个局面下,满足性质<= x
的区间是[l, j]
而不是[l, i]
,因此划分的两个区间是[l, j]
、[j + 1, r]
。
细节四:关于x的取的位置
考虑一个边界问题,x
取q[l], q[r], q[l+r>>1], q[l+r+1>>1]
会有什么不同的结果?区别在于上取整还是下取整。
当按照q[l+r+1>>1]
,递归区间[l, j], [j+1, r]
时,会有以下边界问题:
最终i = j = 2
,跟前面分析的不一样,最后结果也是进入死循环无限递归。
这个细节等二分的细节研究完再补充。
2022.04.10
标签:do,指向,--,++,while,排序,快速,指针 来源: https://www.cnblogs.com/Ethan-Code/p/16600622.html