快速排序算法思路

快速排序的思想是分治和递归

观察一个有序的数列可以发现一个特点:有序数列中任意一个数,它左边的数一定小于等于它,它右边的数一定大于等于它,比如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也不会动,最后一步交换ij的数相当于没变,于是循环里面就什么操作都没做,然后进入下一次循环,结果还是一样,陷入了死循环。

可以发现出现这个问题的原因是待排序数组里面存在着一些和pivot相等的数,在我们上述的思路下,这些数会阻止i指针和j指针的移动,使得无法跳出循环。那么怎么样解决这个问题呢?我们希望的结果是,就算ij所指的数等于pivot了,他俩也依然能够继续移动直到两个指针碰面或者穿过去,并且还是每次一步一步的慢慢移,慢慢找到碰面的时机,也就是说,即使while里面的条件不成立了,循环操作i ++j --依然能够被执行,于是想到使用do while,先进行i ++j --的操作,然后再去判断while里面的条件,不过这样的话需要在初始确定ij的值的时候先两边各退一步,否则一上来就移动ij会让我们跳过一些数。

于是代码就变成了

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);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值