快速排序的思想是分治和递归
观察一个有序的数列可以发现一个特点:有序数列中任意一个数,它左边的数一定小于等于它,它右边的数一定大于等于它,比如1, 2, 3, 3, 3, 4, 5
,中间那个3
,前面的三个数小于等于3,后面的数大于等于3。那么利用这个特点,只要我们能让一个数列中任意一个数都满足这样的条件:它前面的数小于等于它,后面的数大于等于它。那么整个数列就是一个有序的数列。
快排基本步骤为:
①从要排序的数中选出一个数作为pivot
(轴),也就是前后两部分的分界点(可以选第一个数或者最后一个数或者正中间的数或者随机选一个数。但有些选法在某些情况下可能会出现问题)
②把整个数列分成两部分:(小于等于pivot的),(大于等于pivot的)
,即遍历一遍整个数列,把小于等于pivot
的数全部放到pivot
的左边,把大于等于pivot
的数全部放到pivot
的右边(数列中会存在一些和pivot
相等的数,这些数分到左边或右边都无所谓,可以只分到某一边也可以两边都分)
③对左右两边再使用快排进行排序,得到:(小于等于pivot且有序),(大于等于pivot且有序)
,这样整列数就有序了
- 例子
举例说明:2 1 5 3 4 6
首先随便选择一个数作为pivot:可以选第一个数pivot = 2
然后把整列数分成两个部分,小于等于2和大于等于2的:(1, 2), (5, 3, 4, 6)
,先对左边排序:
选取pivot = 1
,然后把左边分成两个部分:(1), (2)
,两部分都只有一个数了,左边排序结束。然后再对右边排序:
选取pivot = 5
,分成两个部分(4, 3, 5), (6)
,右边只剩下一个数,只需对左边(4, 3, 5)
排序
选取pivot = 4
,分成两个部分(3, 4),(5)
,右边只剩下一个数,只需对左边排序
选取pivot = 3
,分成两个部分(3), (4)
,两部分都只有一个数,排序完成
最后整理一下就能得到1, 2, 3, 4, 5, 6
整个过程中最麻烦的一部步就是如何用代码把小于等于pivot的数全部放到左边,把大于等于pivot的数全部放到右边
这个操作可以通过两个指针i, j
来完成
- 思路
初始时,i, j
分别在数组q[]
的两边
2
1
5
3
4
6
i
j
\begin{aligned} &2 \qquad 1 \qquad 5 \qquad 3 \qquad 4 \quad &6\\ &i \quad &j \end{aligned}
21534i6j
首先令pivot = q[i]
,也就是pivot=2,然后i
指针开始往右一个一个移动并比较q[i]
和pivot
的大小,如果q[i] < pivot
,就继续往右移动,否则就停下来,这一步是确保i
左侧的数全部小于pivot
(这里可以用一个while
循环来实现)。这里一开始的时候q[i] == pivot
,不满足循环的条件,所以i
原地待命;然后看j
指针,从右往左一个一个移动并比较q[j]
和pivot
的大小,如果q[i] > pivot
,就继续往左移动,否则就停下来,这一步是确保j
右侧的数全部大于pivot
。这里开始q[j]
是6,大于pivot
,所以j
指针左移一位
2
1
5
3
4
6
i
j
\begin{aligned} &2 \qquad 1 \qquad 5 \qquad 3 \quad &4& \qquad 6\\ &i &j \end{aligned}
2153i4j6
q[j]
变成4,还是大于pivot
,再继续左移一位
2
1
5
3
4
6
i
j
\begin{aligned} &2 \qquad 1 \qquad 5 \quad &3& \qquad 4 \qquad 6\\ &i &j \end{aligned}
215i3j46
q[j]
变成3,大于pivot
,继续左移一位
2
1
5
3
4
6
i
j
\begin{aligned} &2 \qquad 1 \quad &5& \qquad 3 \qquad 4 \qquad 6\\ &i &j \end{aligned}
21i5j346
q[j]
变成5,大于pivot
,继续左移一位
2
1
5
3
4
6
i
j
\begin{aligned} &2 \quad &1& \qquad 5 \qquad 3 \qquad 4 \qquad 6\\ &i &j \end{aligned}
2i1j5346
到这一步q[j]
是1,不满足大于pivot
,所以j
指针也原地待命,至此,i
指针和j
指针的第一次循环都结束,此时i
指针所指的数是大于等于pivot
的,它应该放在右边,j
指针所指的数是小于等于pivot
的,它应该放在左边,所以交换一下两个指针上的数就可以让它们各得其所
1
2
5
3
4
6
i
j
\begin{aligned} &1 \quad &2& \qquad 5 \qquad 3 \qquad 4 \qquad 6\\ &i &j \end{aligned}
1i2j5346
这样就实现了i
指针左边的数全部小于pivot
,而i
自身所指的数小于等于pivot
;同理j
指针右边的数全部大于pivot
,而j
自身所指的数大于等于pivot
。
以上思路如果整理成代码,就是
while(i < j) {
while(q[i] < pivot) i ++;//当且仅当i所指的数小于pivot时才继续左移
while(q[j] > pivot) j --;//当且仅当j所指的数大于pivot时才继续右移
if(i < j) swap(q[i], q[j]);//i和j都移动完之后,此时i所指的数是大于等于pivot的,j所指的数是小于等于pivot的,此时理论上应该是i所指的数在右边的,所以交换一下两个数
}
但是请看这样一种情况:1 2 2 2 3
1
2
2
2
3
i
j
\begin{aligned} 1 \qquad &2 \qquad 2 \quad &2& \qquad 3\\ &i &j \end{aligned}
122i2j3
假设pivot = 2
,此时上述代码的外层大循环i<j
是满足条件的,但是内层循环的三个操作中,首先不满足q[i] < pivot
,所以i
不动,同理j
也不会动,最后一步交换i
和j
的数相当于没变,于是循环里面就什么操作都没做,然后进入下一次循环,结果还是一样,陷入了死循环。
可以发现出现这个问题的原因是待排序数组里面存在着一些和pivot
相等的数,在我们上述的思路下,这些数会阻止i
指针和j
指针的移动,使得无法跳出循环。那么怎么样解决这个问题呢?我们希望的结果是,就算i
和j
所指的数等于pivot
了,他俩也依然能够继续移动直到两个指针碰面或者穿过去,并且还是每次一步一步的慢慢移,慢慢找到碰面的时机,也就是说,即使while
里面的条件不成立了,循环操作i ++
和j --
依然能够被执行,于是想到使用do while
,先进行i ++
和j --
的操作,然后再去判断while
里面的条件,不过这样的话需要在初始确定i
和j
的值的时候先两边各退一步,否则一上来就移动i
和j
会让我们跳过一些数。
于是代码就变成了
int i = l - 1, j = r + 1;//l和r是待排部分的左右边界(下标值),先让i = l - 1,这样第一次循环的时候先执行i++,i就会先移到l的位置也就是开头
while (i < j) {
do i ++ ; while (q[i] < pivot);
do j -- ; while (q[j] > pivot);
if (i < j) swap(q[i], q[j]);
}
至此整个数组和i、j
指针的关系可能是这种情况i > j
1
2
2
3
j
i
\begin{aligned} 1 \qquad &2 \quad &2& \qquad 3\\ &j &i& \end{aligned}
12j2i3
或这种情况i == j
1
2
2
2
3
i
/
j
\begin{aligned} 1 \qquad 2 \qquad &2 \qquad 2 \qquad 3\\ &i/j& \end{aligned}
12223i/j
第二步,我们需要利用i、j
指针将数组分成两部分,有几种分法:
①从头到i-1
一部分,从i
到尾一部分
②从头到i
一部分,从i+1
到尾一部分
③从头到j-1
一部分,从j
到尾一部分
④从头到j
一部分,从j+1
到尾一部分
(注意不能从头到j
一部分,从i
到尾一部分,这样上述第二种情况下i、j
共同所指的那个数就被同时分到了两部分)
而根据上面的代码我们知道,i
所指的数永远是大于等于pivot
的,j
所指的数永远是小于等于pivot
的,所以按照之前所说的要把数组分成(小于等于pivot的),(大于等于pivot的)
这样,就只能是上面的第①④两种分法。
但是请注意,如果选用第①种分法,那么在初始确定pivot
值时不能将其赋值为数组的第一个数,因为当数组本身是有序的时候,如1 2
,如果令pivot = 1
,那么i、j
指针移动的结果会是这样:
1
2
i
/
j
\begin{aligned} &1 \qquad 2\\ &i/j \end{aligned}
12i/j
此时是不存在下标为i - 1
的元素的,用第①种分法会报错
同理如果选用第④种分法不能将pivot
赋值为数组最后一个数,因为当数组本事是逆序的时候,会出现和上面一样的问题,如:2 1
,如果令pivot = 1
,那么那么i、j
指针移动的结果会是这样:
2
1
i
/
j
\begin{aligned} 2 \qquad &1\\ &i/j \end{aligned}
21i/j
此时是不存在下标为j + 1
的元素的
那么避开上述的两种情况,我们就能对分成的两部分分别进行快速排序,于是可以得到快排的完整代码
- 代码
快排函数完整代码如下:
void quick_sort(int q[], int l, int r) {
if (l == r) return;//当只剩一个数是此次递归结束
int pivot = q[l], i = l - 1, j = r + 1;
while (i < j) {
do i ++ ; while (q[i] < pivot);
do j -- ; while (q[j] > pivot);
if (i < j) swap(q[i], q[j]);
}
quick_sort(q, l, j);
quick_sort(q, j + 1, r);
}
或者(pivot
换一个初始赋值,后面换一种分法):
void quick_sort(int q[], int l, int r) {
if (l == r) return;
int pivot = q[r], i = l - 1, j = r + 1;
while (i < j) {
do i ++ ; while (q[i] < pivot);
do j -- ; while (q[j] > pivot);
if (i < j) swap(q[i], q[j]);
}
quick_sort(q, l, i - 1);
quick_sort(q, i, r);
}