我自己在学习快排的时候看了不少大佬们的博客,反复对比一直觉得自己写的没问题,直到最后才发现这一点点细微的差别能导致这么大的问题,所以想给初学者们一起分享一下。
下面是一段正确的快排代码
void QuickSort(int first,int last)
{
if (first >= last)
return;
int left = first, right = last, base = arr[left];
while (left < right)
{
while (arr[right] >= base && left < right)
right--;
while (arr[left] <= base && left < right)
left++;
swap(&arr[left], &arr[right]);
}
swap(&arr[first], &arr[left]);
QuickSort(first, left - 1);
QuickSort(left + 1, last);
}
这里以第一个元素做为基准base,并且right的移动先于left
而假如我们进行一点小小的改动,它就寄了,这些是刚接触这个算法的时候很容易犯的错误
一,left和right的移动顺序很重要
对调left和right移动的顺序(或者令最后一个元素为基准base)
while (arr[left] <= base && left < right)
left++;
while (arr[right] >= base && left < right)
right--;
swap(&arr[left], &arr[right]);
为什么?在所有元素还没有排成顺序的情况下,如果left停止移动了,则说明left左侧的元素全部小于等于基准,并且left对应的元不大于基准(废话)。
此时如果right移动到left的位置,则会自动停止,此时不会发生交换(自己跟自己换),而left对应元素是大于基准的,当循环结束后跟首元素(基准)互换,左边就出现问题了。
而如果让right优先移动,则left == right时这个元素是小于基准的(不满足right的条件),因此换到左边是不会出现问题的。当然,把最后一个元素作为基准就需要先移动left了
二,与基准的比较需有'='
可能会有人跟我一样,想着少个'='不就是多换一次而已吗,有什么大不了。
但这就大错特错力(。一旦遇到另外两个基准数,程序就不动了。比如取4 1 2 4 5 4。left碰到一个基准,不动了,right也是不动了,二两者进行交换没有任何意义。于是,寄。
三,在left == right时,基准的交换不能贪小便宜
就是把swap(&arr[first], &arr[left]); 写成swap(&arr[first], &arr[left - 1]); 其实贪不了,但是我一开始以为能贪一手(我是笨比)
如果你已经弄明白了left和right移动的顺序,那可能会觉得这个错误不可思议,因为left == right的点应当移到基准左边来。而如果你也没搞清楚顺序,那这种做法可能会使某些数据输出正确的结果,但这是一种假象,会让人看半天不知道自己错在哪
比如对于:3 1 2 4 5,前一种写法会输出1 2 4 3 5。而后一种(left - 1),成功避免了把大于基准的这个数换到左边,就能输出正确的结果(但还是错的)。让我们来分析一下:left先移动问题无非出现在两者相等的点
第一轮移动left和right便发生了重合,随后3和4换位,然后分成了以下两个区间。基准3不会被分入区间,5单独一个也没有排序的必要
对于前面这个,left和right在数字2处相遇,4和2交换变成2 1 4
最后对于2 1,再进行互换,然后结束
然后如果是用的后一种(left - 1),第一次3和2交换,得到2 1 3,随后2 和1交换,最后一次只有两个元素,所以自己和自己交换,故得出正确结果。
那如果用后一种也能成功排序吗?肯定不是。当遇到凸型数据(左右小,中间大)时,如 3 4 5 4 3,就会发生错误(输出了3 4 4 5 3)。
让我们以3 4 5 4 3为例来看一下:
第一次(分割前)的第一次移动:left移动到第二个位置(数字4),right一直满足条件,也来到这里,然后3和自己互换。
第二次:分割出了(需要处理的)5 4 3,left一直满足条件,来到3,随后4和5互换,然后就出现这个结果了
这个写法的错误跟第一种反了过来,第一种是由于把不该换的数据换到了左边,而这种是没能把右侧该换的数据换过来