您的位置:首页 > 职场人生

快速排序的两种写法(站在巨人的肩膀上加深理解)

2017-09-11 15:42 651 查看

前言

这是面试总结,好多要求手写快排,堆排序,思路简单,但是手写还是有一定难度的,所以总结下。由于本人太渣,不足之处请各位不吝赐教,在下方留言。

在网上搜索了下,发现有两种写法很受欢迎,且两种方法实现思路还是有点区别的,了解过区别之后,对快排应该可以更加熟悉。无论哪种写法,本质没变:

分治

将所有比temp大的移动到右边,比temp小的移动到左边。

分治,分而治之,在递归中用的尤其多,这篇博文就不会讲解这个问题了,不然文章就太长了。

首先提出一个问题,快排一定要从右往左排吗?难道不能从左往右排吗?这个问题可以自己先想想,后面会给出答案。

这里快排的讲解按照默认的从小往大排,选取当前治理的数组序列中第一个位置。

两种写法中,最经典的,就是下面这种

1 挖坑填数

经典讲解在这里

这是我自己临时手撸的,没有验证,主要是把自己的思想记录下来,真正代码可以看上面这篇博客(一下讲解中,temp代表选取的比较标准)

public void quick_sort(int l,int r){
if(l>=r)
return ;
//算法核心:以temp为基础,比temp小的全部移动到左边,比temp大的全部移动到右边
int i = l,j=r,temp=a[l];
while(i<j){
while(i<j&&a[j]>=temp){//从右往左找
j--;
}
if(i<j){//说明找到了比temp小的值,把这个值填到i的坑
a[i++] = a[j];
}
//a[j]给a[i]了,i的坑j来填,那么j的坑也要有人填,所以从i往右找到第一个比temp大的值,把这个值填到j的坑
while(i<j&&a[i]<=temp){
i++;
}
if(i<j){
a[j--] = a[i];
}
}
//i==j,所有坑填完了,该考虑temp这个数填哪个坑了。
a[i]=temp;
quick_sort(l,i-1);
quick_sort(i+1,r);
}


很多算法代码的思路过程可能现在记得很清楚,但是长时间不使用,我们很快便忘记了。所以记住这个算法的这个写法的一些特性,是回忆代码,加深理解,以及运用的关键。因此我们敲完后需要多想想。

这个排序法,每次排完序后,数组中都有两个位置的数是相同的,在一次循环完成之前,看不到temp的值,当一次循环全部完了之后,才把i == j这个坑填上temp。注意,一次循环后,所有元素相对于temp的位置就不会再变了,也就是说,此时比temp小的,最终结果仍然在temp的左边,比temp大的仍然在temp右边(因为这里递归左右两边不会相互影响。)

这里提出个问题,如果从左往右开始找,会怎么样?

如果从左开始找,找到第一个比temp大的值得时候,填谁的坑?temp的坑吗?i的坑吗?j的坑吗?都不行。

这里我们可以记住,从左找,还是从右找,是以你的选取标准(这里我们默认的选取标准是l,也就是数组的第一个元素)来的,你选取第一个位置作为标准,那么不管你从大到小排序,还是从小到大排序,都需要从右开始往左找。原因很简单:从左开始找,找到的第一个比temp大(小)的数会先和0位置交换,再和后面交换,即数组两边都会出现比比temp大(小)的数。

例如

4,2,6,3,1,10,12,0

如果从左开始找,在6的位置找到了比temp大的,占了4的位置,变成

6,2,6,3,1,10,12,0

那么从右往左找的时候,找到比temp小的放到6位置,变为

6,2,0,3,1,10,12,0

明显看出不对。

如果你选取的标准是最后一个位置,那么就要从左边开始找。

理解了

while(i < j && a[j] > temp)
j--;


中a[j] > temp的作用了吧?保证了j右边都数都比temp大,这样从左边找到比temp大的数时,才能放心放到j这里来,同理,保证了i左边的数都比temp小………)

这个a[j]>temp就是快排中的数能够交换的保证。

参考这个博客:快排为什么从右往左排

2 直接替换 不占坑

这个算法的详解见

连接戳这里

void quicksort(int left,int right)
{
int i,j,t,temp;
if(left>right)
return;
temp=a[left]; //temp中存的就是基准数
i=left;
j=right;
while(i!=j)
{
//顺序很重要,要先从右边开始找
while(a[j]>=temp && i<j)
j--;
//再找左边的
while(a[i]<=temp && i<j)
i++;
//交换两个数在数组中的位置
if(i<j)
{
t=a[i];
a[i]=a[j];
a[j]=t;
}
}
//最终将基准数归位
a[left]=a[i];
a[i]=temp;

quicksort(left,i-1);//继续处理左边的,这里是一个递归的过程
quicksort(i+1,right);//继续处理右边的 ,这里是一个递归的过程
}


这个算法就没有占坑,而是直接一次性找到比他大和比他小的两个位置,两个位置直接替换。这是另一种写法,这里主要说一下刚开始看这个
4000
形式的快排算法会出现的疑问。

如上面代码,最后一定是把temp和i==j这个位置替换。如果i==j的位置的数比temp大呢?那么也会替换吗?如果替换,那就不满足所有左边的数都比temp小了。

仔细思考后发现,不存在这种情况: 在i==j的位置的数大于temp。看看作者在文中说的:

//顺序很重要,要先从右边开始找

如果从左边开始找会怎么样?

首先需要明白,找坑的时候(交换之前),让i 停留的位置,一定是比temp大的,让j 停留的位置,一定是比temp小的。

如果从左边开始找,i最后会停在比temp大的值的位置,这时候再找j,会发现i==j停留在比temp大的位置,最总会把会将temp和a[i]替换,也就是比temp大的放在了temp左边。

如果从右边开始找,j会停在比temp小的值的位置,这时候在i==j的时候,仍然可以保证temp和比他小的值交换。

综上,从右往左找很重要(这个从右开始的原因就和上面不一样了)。

以上这两种算法均可,个人认为第二种更好理解和实现。

应用

这里说一下快速排序算法的应用(排序就不说了),他的核心思想是把比temp小的数都往左移动,比temp大的数都往右移动,也就是说,假设temp的位置是i,那么temp一定是第(i+1)小的数,第(n-i)个大的数(数组从0位置开始存储),这个就是快排的一个经典应用,面试中经常出现——求出数组中第k个最大的数,或者求出数组的前k个大的数

int k;
public int quick_sort(int l,int r){
if(l>=r)
return -1;
//算法核心:以temp为基础,比temp小的全部移动到左边,比temp大的全部移动到右边
int i = l,j=r,temp=a[l];
while(i<j){
while(i<j&&a[j]>=temp){//从右往左找
j--;
}
if(i<j){//说明找到了比temp小的值,把这个值填到i的坑
a[i++] = a[j];
}
//a[j]给a[i]了,i的坑j来填,那么j的坑也要有人填,所以从i往右找到第一个比temp大的值,把这个值填到j的坑
while(i<j&&a[i]<=temp){
i++;
}
if(i<j){
a[j--] = a[i];
}
}
//i==j,所有坑填完了,该考虑temp这个数填哪个坑了。
a[i]=temp;
int ans=-1;
if(i==k)
return a[i];
else if(i>k)//这个数一定在左边
ans = quick_sort(l,i-1);
else//这个数一定在右边
ans = quick_sort(i+1,r);
return ans;
}


应用的应用(为了面试方便,放在一起)

再来说一下这个求出数组的前k个大的数,这个题还有一个解法,就是堆排,堆排序我个人认为比快排难一点,因为涉及到一种类似递归的思想,建树,删树,插入,不断替换等,完全理解起来比快排难度大点。

思路:针对此题来说,可以维护一个大小为k的最小堆,然后拿后面的点与最小堆的根节点来进行对比,如果大于根节点,就把根节点的值替换,并调整为最小堆。

记住,前k个大的数,维护最小堆,每次比较如果比根节点大,那就替换,再调整为最小堆(原因:我们的目标是找到前k个最大的值,如果维护最大堆,你怎么知道子节点能不能替换?最小堆的根结点root一定是堆中最小的,如果新数比root大,说明当前最小堆不是前k个最大值组成的最小堆)。同理,前k个小的数,维护最大堆。这个最小堆的根节点一定是第k个最大的数。

最后

再来谈一下辅助空间和稳定性的问题。这里留个空间吧,后面有时间再补上。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息