排序算法(二) 快速排序

有三种方法可实现快排:

单循环

方法一,单循环,会将所有与基准数相等元素扔到同一侧,造成递归树倾斜,由于递归开销很大,《算法4》也没有讲这个实现方法,故没有主动实现。现贴别人代码作为参考

	void quickSort(int[] nums,int lo,int hi){
		if(lo >= hi) return;
		int mid = partition(nums,lo,hi);
		quickSort(nums,lo,mid - 1);
		quickSort(nums,mid + 1,hi);
	}
	int partition(int[] nums,int lo,int hi){
		 // 基准值
        int pivot = nums[lo];
        int lt = lo;
        // 循环不变量:
        // all in [lo + 1, lt] < pivot
        // all in [lt + 1, i) >= pivot
        for (int i = lo + 1; i <= hi; i++) {
            if (nums[i] < pivot) {
                lt++;
                exch(nums, i, lt);
            }
        }
        exch(nums, lo, lt);
        return lt;
	}

【图片】

双指针

方法2,双指针,用两个指针遍历序列,分别锁定序列中小于基准数的值大于基准数的值,进行两两交换,直到两个指针相遇后,再将基准数交换到右指针处,这种方法使得与基准数相同的元素等概率的分布在序列两侧,使得递归树基本平衡。

	void quickSort(int[] nums,int lo,int hi){
		if(lo >= hi) return;
		int mid = partition(nums,lo,hi);
        //nums[mid]已经被排定了,只需要继续排序nums[lo,mid-1]和
        //nums[mid+1,hi]的元素
		quickSort(nums,lo,mid - 1);
		quickSort(nums,mid + 1,hi);
	}
	int partition(int[] nums,int lo,int hi){
		int lt = lo + 1, hi = gt;
		 // 基准值
        int pivot = nums[lo];
        int lt = lo;
        // 循环不变量:
        // all in [lo , lt) <= pivot
        // all in (gt , hi] >= pivot
      	while(true){  
			while(lt < hi && nums[lt] < pivot)  
				lt++;  
		 	while(gt > lo && nums[gt] > pivot)  
				gt--;  
		 	if(lt >= gt)   
				break;  
		 	exch(nums,lt,gt);  
            lt++;
            gt--;
	 	}  
		exch(nums,lo,gt);  
		return gt;
	}

【图片】

三向切分

方法3,三向切分,考虑了与基准数相等的元素,将它们挤在在数组的中部,使得左右待排序的子序列长度减少,减少了递归的深度。

而且另一个好处是它不用再写额外的切分函数。

Note:

  • [lo…lt]内值小于pivot
  • [lt…i]内值等于pivot
  • [i…gt] 是尚未切分的部分
  • [gt…hi]内值大于pivot
  • 如果数组内没有重复值,最后遍历完毕应该会有lt == gt
private static void quickSort2(int[] nums, int lo, int hi) {
       if(hi - lo >= MININUM_LEN){
           insertionSort(nums,lo,hi);
           return;
       }
       if(lo >= hi) return;

       int ran = new Random().nextInt(hi - lo + 1) + lo;
       exch(nums,ran,lo);
       int lt = lo,gt = hi;
       int i = lt + 1,pivot = nums[lo];

       while(i <= gt){
           if(nums[i] > pivot)
               exch(nums,i,gt--);
           else if(nums[i] < pivot)
               exch(nums,i++,lt++);
           else
               i++;
       }
       quickSort2(nums,lo,lt-1);
       quickSort2(nums,gt+1,hi);
   }

【图片】

优化

快速排序最好的切分情况是,每次数组都能被对半分。时间复杂度约为 O ( N l o g 2 N ) O( Nlog_2N ) O(Nlog2N)

(个人认为是切分数组时的遍历占N,遍历递归树占了** l o g 2 N log_2N log2N**,相乘就是 N l o g 2 N Nlog_2N Nlog2N)

只从数组首部固定选取基准数在时间上很不稳定,如果每次选到的都是序列中的最值,那么每次切分后的左右序列都会有较大的长度差,导致更多的递归,最极端的情况就是去排序已经反向排好序的数组(待验证),这会使得快排沦为冒泡排序,时间复杂度恶化为O(n²)。因此固定基准数是不可取的,需要优化。

【这里需要用图片比较最坏情况和最坏情况的区别,比较直观】

随机基准数

一种方法是,从序列中随机获取一位数作为基准数,这样能够减少选到最差基准数的概率,但是最坏情况的时间消耗不变,仍旧是O(n²)

	private int partition(int[] nums,int lo,int hi){
		int randomIndex = new Random().nextInt(hi - lo) + lo;
		exch(nums,randomIndex,lo);
		int pivot = nums[lo];
		[快排代码]
	}

三数取中

另外的一种方法是,假定最优基准数在数组的N/2处,将其交换到左端作为pivot,这样也能降低取到最差基准数的概率(至少不是最差的那个),但在leetcode上我并没看到它比随机好到哪里去。。。

   private static int partition(int[] nums,int lo,int hi){
//        三数取中法选基准数
        int mid = lo + (hi - lo) / 2;
//       对调左右端
        if(nums[lo] > nums[hi])
            exch(nums,hi,lo);
//       确保左端比中间大
        if(nums[lo] < nums[mid])
            exch(nums,mid,hi);
//       再确保中间比右端小
        if(nums[mid] > nums[hi])
            exch(nums,mid,hi);
       	
      	int pivot = nums[lo];
        [快排代码]
    }

分析

**时间复杂度:**最好情况是每次切分都能恰好将数组对半分(即找到排定数组中的中间值),此时复杂度为 O ( l o g N ) O(logN) O(logN), 最坏情况是每次切分都只能切到数组的边界端(即找到排定数组的最值),此时每次递归都只会将搜索范围-1,快排沦为冒泡排序,复杂度为 O ( N 2 ) O(N^2) O(N2)

**空间复杂度:**属于原地算法,故为 O ( 1 ) O(1) O(1)

稳定性: 三种实现方法都会改变相等元素的相对次序,故不稳定

初始次序:

​ 移动次数有关:已排好序的数组不需要移动

​ 比较次数无关:不管实现方式是单指针,双指针还是三指针,均要比较N次

​ 时间复杂度有关:时间复杂度最好情况为 O ( l o g N ) O(logN) O(logN) 最坏情况为 O ( N 2 ) O(N^2) O(N2) ,故初始序列有关

​ 排序趟数有关:切分函数的递归深度取决于快排的实现方法和切分数的值

与归并排序的异同

快速排序和归并排序都采用了分治的方法,不同的是快排的处理(切分)是在分解数组前处理的,而归并是在分离之后(合并排序), 此外快排没有合并排序这一环节。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值