【经典排序算法】4-快速排序

1. 快速排序的思想

  • 快速排序其实是对冒泡排序的一种改进,目的是提高排序的效率。
  • 快速排序中最重要的一个词是“基准”,一般取第一个元素作为基准(这个是必须要取第一个元素吗?留个疑问给大家,后面的优化方法中会提到和解释),把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以放到任一边),这个称为分区(partition)操作。
  • 分区操作后可以确定基准最终所在的位置
  • 通过一趟排序,将待排的数列分隔成独立的两部分,一部分中的每个元素都比另一部分中的元素小,然后递归调用上述步骤,分别对这两部分继续上述步骤进行快速排序,最终达到整个序列有序。
  • 注意:快速排序是用分治策略把一个序列分为两个子序列。
  • 注意:快速排序过程中重要的两个步骤,即逆序比较 和 正序比较 交替进行
       正序逆序交替比较的目的就是为了填坑排序

2. 快速排序的算法步骤

这一部分可能比较难懂,可以结合下面的6.3部分所举例子的演示图解进行理解。

  快速排序过程中重要的两个步骤,即逆序比较 和 正序比较 交替进行
  正序逆序交替比较的目的就是为了填坑排序
  具体地,
  (1)假设第一个元素 a[0] 为基准,将其用一个变量 temp 进行暂存,temp = a[0] 。
  (也就是说:先把基准值拿出来“观望”,然后用这个基准进行比较,比基准值小的元素放在左边,比基准值大的元素放在右边。这个基准“观望”的最终目的就是确定基准自己最终在整个数列中的位置。)
  (2)逆序比较:设 i = 0 ,j = n - 1 ,将基准值 a[i] 与最后一个元素 a[j] 进行比较,如果最后一个元素值 a[j] < 基准值a[i] ,就将最后一个元素放在基准的位置 a[j] -> a[j](即:a[i] = a[j] ),即比基准值小的元素左移;如果最后一个元素值 a[j] >= 基准值 a[i] ,逆序往前继续比较,即 j = j - 1 ,直到逆序找到比基准值小的元素,将此元素左移放在基准的位置:a[i] = a[j] 。此时,结束逆序比较的操作。
  (3)正序比较:将基准值与正序的元素进行比较,即进行 i = i + 1 的操作,然后判断 a[i] 与基准值 temp 的大小,直到找到比基准值大的元素。如果 a[i] > 基准值 temp ,则说明这个元素比基准值大,应该后移,将此元素放在步骤(2)中已经左移走的 a[j] 的位置,即 a[j] = a[i] 。(逆序比较 与 正序比较 交替进行的目的就是为了填坑排序。逆序比较时:a[j] < 基准值,就左移;正序比较时:a[i] > 基准值,就将其右移至已经移走的 a[j] 位置)
  (4)持续进行 逆序比较正序比较 的操作,直到 i >= j ,结束第一轮排序。然后将 a[i] = temp,此时,已经确定了基准在整个数列中的位置
  (5)确定了基准在整个数列中的最终位置之后,就可以将数列分成 基准左边的子序列基准右边的子序列 两个部分,而且基准左边的子序列中的所有元素都比基准右边的子序列中的元素小。然后,递归调用上面的步骤(1)~ 步骤(4),分别确定子序列的基准和基准最终的位置。
  (6)递归进行上面的操作,即重复步骤(5),直到排序结束。排序结束的条件是:确定了子序列中的基准所在的最终位置后,子序列中的基准左边和基准右边都只剩下一个元素,此时,排序已经结束。

3. 快速排序的演示图解

以[50, 36, 66, 76, 95, 12, 25, 36]为例,
在这里插入图片描述

4. 快速排序的代码

# coding:utf-8
'''
# @Method:冒泡排序
# @Author: wlhr62
'''
import os
import sys

class Solution:         
    def sortArray(self, nums: List[int]) -> List[int]:
        l = 0
        r = len(nums)-1

        def partition(nums, l, r):
            if l < r:
                i = l
                j = r

                randomNum = random.randint(l, r)
                nums[randomNum], nums[i] = nums[i], nums[randomNum]
                
                temp = nums[i]
                while i < j:
                    while i < j and nums[j] > temp:
                        j -= 1
                    if i < j:
                        nums[i] = nums[j]
                        i += 1
                    while i < j and nums[i] < temp:
                        i += 1
                    if i < j:
                        nums[j] = nums[i]
                        j -= 1
                nums[i] = temp
                partition(nums, l, i-1)
                partition(nums, i+1, r)

        
        partition(nums, l, r)
        return nums

5. 快速排序的算法导论思想

  快排的partition方式不止填坑法这一种,《算法导论 7.1节》中给出的解法是由N.Lomuto提出的。

与归并排序一样,快速排序也使用了分治思想。


对一个典型的子数组A[p…r]进行快速排序的三步分治过程如下:
第一步,分解:
  数组A[p…r]被划分为两个子数组A[p…q-1]和A[q+1…r],使得A[p…q-1]中的每一个元素都小于等于A[q],而A[q]也小于等于A[q+1…r]中的每个元素。
  其中,计算下标q也是划分过程的一部分,划分的结束就是下标q的确定过程。
第二步,解决:
  通过递归调用快速排序,对子数组A[p…q-1]和A[q+1…r]进行排序。
第三步,合并:
  因为子数组都是原址排序的,所以不需要合并操作:数组A[p…r]已经有序。

(1)实现快速排序的程序如下:

QUICKSORT(A, p, r)
    if p < r
        q = PARTITION(A, p, r)
        QUICKSORT(A, p, q-1)
        QUICKSORT(A, q+1, r)

注意:为了排序一个数组A的全部元素,初始调用是QUICKSORT(A, 1, A.length)

(2)数组的划分:
  算法的关键部分是PARTITION过程,它实现了对子数组A[p…r]的原址重排。PARTITION总是选择一个 x=A[r] 作为主元,并围绕它来划分子数组A[p…r]。

PARTITION(A, p, r)
	x = A[r]
	i = p - 1
	for j=p to r-1
		if A[j] <= x
			i = i + 1
			exchange A[i] with A[j]
	exchange A[i+1] with A[r]
	return i + 1

(3)该算法思想的C++代码实现如下:

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        quickSort(nums, 0, nums.size()-1);
        return nums;
    }

    int partition(vector<int>& nums, int l, int r) {
        auto pivot = nums[r];
        auto i=l-1;
        for (auto j=l; j < r; ++j) {
            if (nums[j] <= pivot) {
                swap(nums[++i], nums[j]);
            }
        }
        swap(nums[i+1], nums[r]);
        return i+1;
    }

    int randomized_partition(vector<int>& nums, int l, int r) {
        int k = rand() % ( r-l+1) + l;
        swap(nums[k], nums[r]);
        return partition(nums, l, r);
    }

    void quickSort(vector<int>& nums, int l, int r) {
        if ( l < r) {
            auto pos = randomized_partition(nums, l, r);
            quickSort(nums, l, pos-1);
            quickSort(nums, pos+1, r);
        }
    }

};

6. 快速排序的最早算法思想

  快排最早是由Hoare提出的,最初版本的Partition方式并不同于上述两种Partition策略,该划分算法的伪代码如下所示:

HOARE-PARTITION(A, p, q)
	x = A[p]
	i = p - 1
	j = q + 1
	while True
		repeat
			j = j - 1
		until A[j] <= x
		repeat
			i = i + 1
		until A[i] >= x
		if i < j
			exchange A[i] with A[j]
		else return j

  先前两个版本PARTITION过程中,主元是与他所划分的两个分区分离的,而HOARE版的partition中,主元是存在于分区A[p…j]或A[j+1…r]中的。因为有p<=j<r,所以这一划分总是非平凡的。


C++实现:

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        quickSort(nums, 0, nums.size()-1);
        return nums;
    }

    int partition(vector<int>& nums, int l, int r){
        auto pivot = nums[l];
        int i=l-1, j=r+1;
        while ( true ) {
            do { --j; }
            while ( nums[j] > pivot);
            do { ++i; }
            while ( nums[i] < pivot);
            if ( i < j ) {
                swap(nums[i], nums[j]);
            } else {
                return j;
            }
        }
    }

    int randomized_partition(vector<int>& nums, int l, int r){
        int k = rand() % (r-l+1) + l;
        swap(nums[k], nums[l]);
        return partition(nums, l, r);
    }

    void quickSort(vector<int>& nums, int l, int r){
        if (l < r) {
            int pos = randomized_partition(nums, l, r);
            quickSort(nums, l, pos);
            quickSort(nums, pos+1, r);
        }
    }
};

7. 快速排序的复杂度分析

  • 时间复杂度
    最优时间复杂度:O(nlogn)
    最差时间复杂度:O(n^2)
    平均时间复杂度:O(nlogn)
  • 空间复杂度
    最优空间复杂度:O(logn),每一次都平分数组的情况
    最差空间复杂度:O(n),退化为冒泡排序的情况

8. 快速排序的稳定性分析

快速排序是一个不稳定的排序算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值