快排初步理解

简而言之,快排的思想就是快速定位元素位置

    如果数组类的所有数字都是有序的,那么挑选任意一个元素,它的所有前项的个数是确定的,所有后项的个数也是确定的。那么有人会说了,1,2,3,4,5,5,5,6 这样的数组,就5这个元素出现了3次,在乱序的状态下可能有1,2,3,5,4,5,6,5或者1,5,3,5,2,4,6,5等等的情况,怎么知道哪一个5前面有几个元素,换句话说,对于每一次出现的5,它的位置是位于三个有序的5中的哪一个位置?这个就牵涉到数组排序的稳定和非稳定问题了,而快速排序是非稳定的,具体后面会分析到。

    快排的过程,实现的时候步骤就是把大于选定基准元素的数放在基准元素之后,而小于基准的元素放在元素的前面。比较直观 的实现方式就是使用数组元素的移动。先选出基准,然后每选择一个基准之外的数就和基准值比较,小于就插入到基准值前面,大于的话就插入到基准值的后面,但是这种实现方式会引发大量的元素位移,数据量大的时候步骤很多,增加了算法的时间复杂度。

    网络上的算法实现的基准选择都是一上来选择数组的第一个元素,但是这样并不好理解。其实基准的选择不重要,我们可以选择任意位置的元素作为基准,只要循环操作过程中保证每一个元素都选择到。

    为便于理解,我们选择数组中的中间位置元素作为基准,循环过程中如果左方的元素值大于基准那么,把元素放到“基准元素的右边”,右方元素的位置如果小于基准值就放到“基准值的左方”。为什么要加“”呢,这个左右方其实并不是通常意义上的左右。

如数组:

11,2,54,3,1,2,19,45
选择靠中间位置的元素,8/2选择4位置的元素,就是1了,我们用一个变量mid_val=1来保存它。

然后定义两个游标,left和right,left从0位置往后移动,而right从数组的最后一个位置7往前移。

    先从left开始,left为0,选取元素11和基准1比较,11>1,所以11应放到1的“右方”,但是如果直接插入到1的右方就会引发元素移动位置,为避免之前说过的时间复杂度的问题,这里把11直接放到1的位置。反正1有变量mid_val保存记录着,不担心1丢失,这就是为什么对“基准元素的右边”和"基准值的左方”加引号的原因了。
 

    现在左方找到一个大于基准的值,并且把这个值放在了右边,那么这个位置的值11就没有意义了

11,2,54,3,11,2,19,45

    这之后就轮到right了,选right所在的值45,mid_val=1,   45>1,所以45的位置不用动,right接着向前移动,19>1,接着向前移动...

    right向前的过程中在和left=0相遇之前没有比mid_val=1还要大的元素,所以,right会一直向前直到和left相遇在left=0位置,那么此时有left==right,也就是left在第一次和基准比较之后就一直没有移动,那么它所在的位置的元素11之前又“放在”了数组的右边,所以left停留的位置的值是无意义的,可以直接把mid_val放在left此时的位置


1,2,54,3,11,2,19,45

     left和right相遇意味着针对基准进行的数组遍历完成,基准做选取的元素位置确定。比较重要的一点是当用游标走过的元素被“扔到”另一个方向的时候,也就是另一个方向的某个位置被重新赋值了,那么游标所在的位置元素变得没有意义,它被用来后续放置另一方向上不满足条件的值,因为在一边不满足条件的值必然是另一边上的。

    可以继续选择4位置元素的值11,过程如下


mid_val=11,left=0,right=7;


left<mid_val;left++;


2,54,3,11,2,19,45


left>mid_val;mid_val位置元素被覆盖


2,54,3,54,2,19,45
然后比较right,right所在位置元素right>11,right--;直到走到2所在位置,right<mid_val,left所在位置用2覆盖


2,2,3,54,2,19,45
然后left++,直到走到54位置,left>mid_val


2,2,3,54,2,19,45
right位置用left覆盖


2,2,3,54,54,19,45
然后right--和left相遇,用户mid_val覆盖该位置的值


2,2,3,11,54,19,45
11的位置确定。

    过程中,当左方数据遇到大于基准值的时候,说明这个元素应位于右方,所以覆盖掉右方的元素,然后右方游标移动。如果右方的元素值小于基准那么说明这个元素应在左方,覆盖左方的值,然后操作左方游标移动。

    元素赋值覆盖时不能覆盖当前方向未做比较的值,而是应该覆盖已经抽取比较完成的位置的值。首次覆盖比较特殊覆盖的是基准值,而后续覆盖的是之前非己方方向的,且已经经过比较的值(无意义的值)。不论是抽取的是基准还是游标所在的位置的值,只要其所在的方位需要变化,那么变化完成后,在变化之前其所在的位置就变得没有意义,值只起到占位的作用,可以用来放置应该存在于其方位的任何元素值。

    现在应该比较能够理解一开始选取数组第一个元素位置作为基准的情况了,当抽取完成基准之后,基准位置值就没有意义了,可以用它来存放大于或者小于基准值所在方位的任何元素。数组遍历过程中在左边找大的放到右边占位的地方,在右边找小的放到左边占位的地方。

    最后就是递归了,当一个元素的位置确定后,把数组相当于切成两段,在对这两段数组做快排,递归执行下去,直到全部的元素的位置确定。

算法实现如下:

C语言

void quickSort(int nums[],int start,int end)
{
    if(start >= end)
        return;//如果左边的位置超过了或者等于了右方的位置那么返回
    int left=start,right=end;
    int mid_val=nums[left];
    while(left<right){//左方游标不能超过右方游标
        while(left <= right){
            if(nums[right]<=mid_val){//如果右方存在小于基准的值
                nums[left] = nums[right];//覆盖左方的占位
                break;//覆盖操作完成后,暂时跳出右方的比较,去执行下一个左方比较的循环
            }
            else//右方的值是大于基准的
                right--;//右方游标前移
        }
        while(left < right){
            if(nums[left] > mid_val){//如果左边存在比基准大的值
                nums[right] = nums[left];//覆盖右边的占位
                break;//暂时跳出循环,外层循环会转到右方比较的循环
            }
            else//左方的值是小于基准的
                left++;//左方游标后移
        }
    }
    
    nums[left] = mid_val;//循环都退说明不满足left<right条件,也就是左右游标相遇,将基准值赋给最后的占位
    quickSort(nums,start,left-1);//对已确定元素的左方数组递归做快排
    quickSort(nums,left+1,end);//对已确定元素的右方数组继续递归做快排
}

    回到之前的关于快排非稳定排序的问题上,对于如数组1,2,3,5,4,5,6,5的情况,如果选择基准为第二个5,那么当左边的游标走到第一个5的时候,5和5是非小于的状态,按照<=就覆盖的做法,第一个5被覆盖到第二个5上,然后右边的游标又是一个5,右方大于才覆盖,非大于情况下继续前移,直到4,覆盖左方的值变成1,2,3,4,4,5,6,5,然后left右移动和right相遇,left=rght,响应位置赋值为基准5,变成1,2,3,4,5,5,6,5,4之后的第一个5是选择的基准的值,它原来是第二个5,现在变成了第一个5,顺序被打乱了,所以是非稳定的排序。

    值得注意的是针对和基准值相等的情况,不能左边和右边比较都同时存在=的情况,如>=和<=同时存在,这不能满足“非此即彼”的原则,即左方大的应在右方和右方小的应在左方,而不是左方大于或等于的在右方和右方小于或等于的在左方,相等的时候会造成左右混乱。等于的情况只能在一方处理。

    好了,絮絮叨叨的显得比较啰嗦,但是个人认为还算是比较细致,可以给理解快排的童鞋一些帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值