快速排序算法

快速排序算法

     快速排序由于排序效率在同为O(N*logN)的几种排序方法中效率较高,因此经常被采用,再加上快速排序思想----分治法也确实实用,因此很多软件公司的笔试面试,在了解排序算法之后,我想以我的思路来描述这种排序方法。

引子

    有一个无序的数组 N N N, n i n_{i} ni表示其中的某一个元素。如果先不考虑将整个数组由大到小排序,只是将大于 n i n_{i} ni的数字放在 n i n_{i} ni的右边,小于 n i n_{i} ni的数字放在其左边,该如何实现?
    如果不考虑空间问题的话,很简单,创建一个与 N N N相同大小的数组 N ′ N^{'} N,将大于 n i n_{i} ni的数字从 N ′ N^{'} N右侧开始存放,将小于 n i n_{i} ni的数字从 N ′ N^{'} N的左侧开始存放,等于 n i n_{i} ni的数字最后存放在数组 N ′ N^{'} N的空余位置上,大致代码如下:

//N is an unorderd array
//n is a referance value
vector<int> S(N.size(), 0);
size_t l=0, r=N.size()-1;
for (size_t i=0; i<N.size(); ++i){
	//大于n的数值从最右边开始存放
	if (N[i] > n){
		S[r] = N[i];
		--r;
	}
	//小于n的数值从最左边开始存放
	else if (N[i] < n){
		S[l] = N[i];
		++l;
	}
}
//剩余位置用n补齐
while (l <= r){
	S[l] = n;
}

    那如果只能用 O ( 1 ) O(1) O(1)的空间复杂度呢?最开始的数组是无序的,就好像数字是很随意地坐在一排座位上,上面所述的方法新建立了一个数组,即又搬来一样多的座位,让数字依次以 n i n_{i} ni为参考,分别从最右边(大于 n i n_{i} ni的)和最左边(小于 n i n_{i} ni的)坐起,比较完一轮之后,剩下(等于 n i n_{i} ni的)的坐在中间空余座位上。
    那类似的,如果只能使用 O ( 1 ) O(1) O(1)的额外空间的话,可以先让 n i n_{i} ni跟左边第一个交换位置(为什么交换,后面说明),然后让它站出来,这样最左边位置空了出来,那就可以从最右边开始向左寻找,直到发现一个数值小于 n i n_{i} ni,把它放在最左边位置上。此时右侧的一个位置空出来了,而且这个位置的右边所有数字都不小于 n i n_{i} ni。那就可以从左边第二个位置开始寻找,直到发现一个数值大于 n i n_{i} ni,把它安放在右边空出来的位置上,这样左边又空出来一个,那再从右边开始寻找小于 n i n_{i} ni的数值,以此循环下去,直到左右指针相遇,此时左右指针遍历过的数字都已经重新安排过了,只剩下它们共同所在的位置,我们让 n i n_{i} ni坐过来,这样就完成了数字的划分。
    那为什么要在一开始把 n i n_{i} ni和左边第一个数字交换位置呢?因为是先从右边开始寻找的,找到的第一个小于 n i n_{i} ni的数字需要从最左边开始存放。

void adjustArray(vector<int>& nums, int i = 0) {
	if (i < 0 || i >= nums.size())
        return;
    size_t l = 0, r = nums.size() - 1;
    swap(nums[i], nums[0]);
    int val = nums[0];
    while (l < r) {
        while (l < r && nums[r] >= val)
            --r;
        if (l < r) {
            nums[l] = nums[r];
            ++l;
        }
        while (l < r && nums[l] < val)
            ++l;
        if (l < r) {
            nums[r] = nums[l];
            --r;
        }
    }
    nums[l] = val;
}

    这里会有一个小小的问题,那就是如果存在其它等于 n i n_{i} ni的值,这些值不会被准确划分,因为我们只保证了指定的那个“ n i n_{i} ni”的左边值都不大于它,右边的值都不小于它,对于其他的等于 n i n_{i} ni的值是没有照顾到的。结束时, l l l指针指向一个大于 n i n_{i} ni的数, r r r指向一个不大于 n i n_{i} ni的数,所以r是最终 n i n_{i} ni的位置。

延申

    上面部分实现的是 n i n_{i} ni的左边都不大与 n i n_{i} ni n i n_{i} ni的右边都不小于 n i n_{i} ni,那么此时 n i n_{i} ni所在的位置恰好是排序过后它应该在的位置。那我们可以把这种方法继续应用到 n i n_{i} ni左边的子序列和 n i n_{i} ni右边的子序列中,不断划分,直到每一个子序列只包含一个元素时,整个数组已经排序完成了,这种方法称之为分治,由此快速排序算法的基本思路说明完毕。
    快速排序算法的每一次划分中,都选定了一个参考值 n i n_{i} ni,而且这个值在一开始要和 n 0 n_{0} n0交换位置,那么我们可以在每次划分时直接选择最左边的值作为参考值。

void qsort(vector<int> &nums, int l, int r){
        if (l >= r)
            return;
        int s = l, e = r;
        int n = nums[l];
        while (l < r) {
            while (l < r && nums[r] >= n) {
                --r;
            }
            if (l < r){
                nums[l] = nums[r];
                ++l;
            }
            while (l < r && nums[l] < n) {
                ++l;
            }
            if (l < r) {
                nums[r]=nums[l];
                --r;
            }
        }  
        nums[l] = n;
        qsort(nums, s, l-1);
        qsort(nums, l+1, e);
    }

    如果输入的数组恰好是从小到大排列好的数组的话,那么将进行N次划分,每次划分时双指针遍历N-1次,因此最坏情况下需要 O ( n 2 ) O(n^{2}) O(n2)的时间复杂度。
    还有一种实现方式是每次将数组中间的数值作为参考值,输入恰好是从大到小排列好的数组时,当前划分完全后,将当前排序的范围分成两等份再继续划分,因此是二分进行的,所以总计需要进行 l o g 2 n {log_2}n log2n次划分,每次划分时双指针遍历N次,因此需要 O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n)的时间复杂度。但二分中心恰好是子序列最小值是它的最差情况,同样需要 O ( n 2 ) O(n^{2}) O(n2)的时间复杂度。

void qsort(vector<int> &nums, int l, int r){
        if (l >= r)
            return;
        int s = l, e = r;
        swap(nums[l], nums[(l+r)>>1]);
        int n = nums[l];
        while (l < r) {
            while (l < r && nums[r] >= n) {
                --r;
            }
            if (l < r){
                nums[l] = nums[r];
                ++l;
            }
            while (l < r && nums[l] < n) {
                ++l;
            }
            if (l < r) {
                nums[r]=nums[l];
                --r;
            }
        }  
        nums[l] = n;
        qsort(nums, s, l-1);
        qsort(nums, l+1, e);
    }

    还可以在每次划分时随机选择一个数值作为参考值,因此如果知道数组数值的大概分布形式的话,可以选择更合理的划分方法,好的划分函数能很好的减少耗时期望值。就比如在LeetCode 217: 存在重复元素中,测试用例就有一个很长很长的排序好的数组,使用左端值划分会超时,因此选用中间位置作为参考来划分数组。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值