算法-排序算法:快速排序(QuickSort )【O(nlogn)】【单路:随机化无法避免元素全相等时退化为O(n^2)】、【双路+随机化:元素全相等时退化概率极低】【三路:元素全相等时为O(n)】

快速排序(QuickSort ),平均来说比归并排序快一些;

  • 单路快排:+“随机化”也无法解决元素全相等时退化为 O ( n 2 ) O(n^2) O(n2)
  • 双路快排:+“随机化”可解决元素全相等时的退化问题
  • 三路快排(在有大量重复元素的数组中,比双路快排好,其他情况不如双路快排)

在这里插入图片描述

快速排序是通常比其他基于比较的排序算法 效率更高,效率最好体现在大部分情况下都能达到O(nlogn)的时间复杂度。

快排的算法实现利用到了分治法(Divide and Conquer)和递归(recursive),以某个特定值为基准(pivot)将一个List分成两个子List处理。

快速排序(Quick Sort)的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

在这里插入图片描述

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

  • 从数列中挑出一个元素,称为 “基准”(pivot);
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

快速排序-时间复杂度

  • 最优时间复杂度:O(nlogn)
  • 最坏时间复杂度:O(n2)
  • 稳定性:不稳定

一、单路快排(使用双指针)

1、版本01:单路快排-用数组第一个元素作基准pivot

使用数组的第一个元素作为基准pivot;

在这里插入图片描述

在这里插入图片描述
在Partition步骤中,使用快慢指针进行分区:

在这里插入图片描述
在这里插入图片描述
其中:

  • nums[fast] > nums[pivot] \text{nums[fast] > nums[pivot]} nums[fast] > nums[pivot] 时:
    在这里插入图片描述
  • nums[fast] < nums[pivot] \text{nums[fast] < nums[pivot]} nums[fast] < nums[pivot] 时:
    在这里插入图片描述
class Solution:
    def sort(self, nums, left, right):
        if left >= right:
            return
        p = self.partition(nums, left, right)  # 将pivot值放在其应该在的位置,并且返回其应该在的位置
        self.sort(nums, left, p - 1)
        self.sort(nums, p + 1, right)

    # 单路快排【此轮交换只为值nums[pivot]找到其应该在的位置,并且将小于nums[pivot]的值放在其左边,大于nums[pivot]的值放在其右边】
    def partition(self, nums, left, right):
        pivot = left  # 取”用于排序的基准值“为第一个元素

        slow = left  # slow用于标记 nums[pivot] 值应该在的位置
        fast = left + 1  # fast用于遍历数组中的元素,逐个与 nums[pivot] 比较大小

        # 保证 [left + 1 : slow]区间的所有元素小于pivot,[slow + 1 : fast - 1]区间的所有元素大于pivot,判断 nums[fast] 属于哪个区间,将其放入该区间
        while fast <= right:
            if nums[fast] > nums[pivot]:
                fast += 1
            elif nums[fast] < nums[pivot]:  # 如果遇到有一个元素小于基准值,则将用于标记pivot值应该在的位置slow向右移动一位
                slow += 1
                nums[slow], nums[fast] = nums[fast], nums[slow]
                fast += 1

        nums[slow], nums[pivot] = nums[pivot], nums[slow]  # 将值 nums[pivot] 放在其真正应该在的位置 slow 上
        return slow


solution = Solution()
nums = [7, 1, 4, 2, 8, 3, 6, 5]
solution.sort(nums, 0, len(nums) - 1)
print("nums = ", nums)
class Solution:
    def sort(self, nums, left, right):
        if left >= right:
            return
        p = self.partition(nums, left, right)  # 将pivot值放在其应该在的位置,并且返回其应该在的位置
        self.sort(nums, left, p - 1)
        self.sort(nums, p + 1, right)

    # 单路快排【此轮交换只为值nums[pivot]找到其应该在的位置,并且将小于nums[pivot]的值放在其左边,大于nums[pivot]的值放在其右边】
    def partition(self, nums, left, right):
        pivot = left  # 取”用于排序的基准值“为第一个元素

        slow = left  # slow用于标记 nums[pivot] 值应该在的位置
        fast = left + 1  # fast用于遍历数组中的元素,逐个与 nums[pivot] 比较大小

        while fast <= right:
            if nums[fast] < nums[pivot]:  # 如果遇到有一个元素小于基准值,则将用于标记pivot值应该在的位置slow向右移动一位
                slow += 1
                nums[slow], nums[fast] = nums[fast], nums[slow]
            fast += 1

        nums[slow], nums[pivot] = nums[pivot], nums[slow]  # 将值 nums[pivot] 放在其真正应该在的位置 slow 上
        return slow


solution = Solution()
nums = [7, 1, 4, 2, 8, 3, 6, 5]
solution.sort(nums, 0, len(nums) - 1)
print("nums = ", nums)

单路快排-用数组第一个元素作基准pivot的缺点:

  • 如果当前 “数组已经完全有序”,性能退化为 O ( n 2 ) O(n^2) O(n2) 最坏情况
  • 如果 “当前数组元素完全相同”,性能退化为 O ( n 2 ) O(n^2) O(n2) 最坏情况

在这里插入图片描述

2、版本02:单路快排-随机化初始索引位置【解决“有序数组”性能退化为 O ( n 2 ) O(n^2) O(n2)的问题】

随机化初始索引位置,解决对于有序数组,性能退化为 O ( n 2 ) O(n^2) O(n2)的问题但是,如果当前数组元素全部相等,仍然会出现时间复杂度为 O ( n 2 ) O(n^2) O(n2)的最坏情况

在当前无序区中选取划分的基准关键字(pivot)是决定算法性能的关键,通过优化pivot选取算法可以提高快速排序算法的效率。

用一个随机函数产生一个取位于 left 和 right 之间的随机数 k(left ≤k≤right ),用 arr[k] 作为基准,这相当于强迫 arr[left,…,right ]中的记录是随机分布的。用此方法所得到的快速排序一般称为随机的快速排序

partition方法中添加以下代码即可:

        rand = random.randint(left, right)  # 随机化基准值 pivot
        nums[left], nums[rand] = nums[rand], nums[left]
import random


class Solution:
    def sort(self, nums, left, right):
        if left >= right:
            return
        p = self.partition(nums, left, right)  # 将pivot值放在其应该在的位置,并且返回其应该在的位置
        self.sort(nums, left, p - 1)
        self.sort(nums, p + 1, right)

    # 单路快排【此轮交换只为值nums[pivot]找到其应该在的位置,并且将小于nums[pivot]的值放在其左边,大于nums[pivot]的值放在其右边】
    def partition(self, nums, left, right):
        rand = random.randint(left, right)  # 随机化基准值 pivot
        nums[left], nums[rand] = nums[rand], nums[left]

        pivot = left  # 取”用于排序的基准值“为第一个元素

        slow = left  # slow用于标记 nums[pivot] 值应该在的位置
        fast = left + 1  # fast用于遍历数组中的元素,逐个与 nums[pivot] 比较大小

        while fast <= right:
            if nums[fast] < nums[pivot]:  # 如果遇到有一个元素小于基准值,则将用于标记pivot值应该在的位置slow向右移动一位
                slow += 1
                nums[slow], nums[fast] = nums[fast], nums[slow]
            fast += 1

        nums[slow], nums[pivot] = nums[pivot], nums[slow]  # 将值 nums[pivot] 放在其真正应该在的位置 slow 上
        return slow


solution = Solution()
nums = [7, 1, 4, 2, 8, 3, 6, 5]
solution.sort(nums, 0, len(nums) - 1)
print("nums = ", nums)

二、二路快排(使用双指针)

在这里插入图片描述

1、过程分析

1.1 场景

对 6 1 2 7 9 3 4 5 10 8 这 10 个数进行排序

1.2 思路

先找一个基准数(一个用来参照的数),为了方便,我们选最左边的 6,希望将 >6 的放到 6 的右边,<6 的放到 6 左边。
如:3 1 2 5 4 6 9 7 10 8
先假设需要将 6 挪到的位置为 k,k 左边的数 <6,右边的数 >6

  1. 我们先从初始数列“6 1 2 7 9 3 4 5 10 8 ”的两端开始“探测 ”,先从右边往左找一个 <6 的数,再从左往右找一个 >6 的数,然后交换。我们用变量 i 和变量 j 指向序列的最左边和最右边。刚开始时最左边 i=0 指向 6,最右边 j=9 指向 8 ;
    在这里插入图片描述
  2. 现在设置的基准数是最左边的数,所以序列先右往左移动(j–),当找到一个 <6 的数(5)就停下来。
    接着序列从左往右移动(i++),直到找到一个 >6 的数又停下来(7);
  3. 两者交换,结果:6 1 2 5 9 3 4 7 10 8;
    在这里插入图片描述
  4. j 的位置继续向左移动(友情提示:每次都必须先从 j 的位置出发),发现 4 满足要求,接着 i++ 发现 9 满足要求,交换后的结果:6 1 2 5 4 3 9 7 10 8;
    在这里插入图片描述
  5. 目前 j 指向的值为 9,i 指向的值为 4,j-- 发现 3 符合要求,接着 i++ 发现 i=j,说明这一轮移动结束啦。现在将基准数 6 和 3 进行交换,结果:3 1 2 5 4 6 9 7 10 8;现在 6 左边的数都是 <6 的,而右边的数都是 >6 的,但游戏还没结束

在这里插入图片描述

  1. 我们将 6 左边的数拿出来先:3 1 2 5 4,这次以 3 为基准数进行调整,使得 3 左边的数<3,右边的数 >3,根据之前的模拟,这次的结果:2 1 3 5 4
  2. 再将 2 1 抠出来重新整理,得到的结果: 1 2
  3. 剩下右边的序列:9 7 10 8 也是这样来搞,最终的结果: 1 2 3 4 5 6 7 8 9 10 (具体看下图)
    在这里插入图片描述
    快速排序的每一轮处理其实就是将这一轮的基准数归位,当所有的基准数归位,排序就结束啦

2、二路快排版本

2.1 版本01:二路快速排序(取left作为pivot)

在这里插入图片描述

在这里插入图片描述

对于 “所有元素都一样的数组”,二路快排也可以将所有元素分为2部分,而不是一边为空,一边为n-1个元素,从而避免性能的恶化

在这里插入图片描述
C++版本:

#include <iostream>

void swap(int &a, int &b) {
    int temp = a;
    a = b;
    b = temp;
}

int partition(int arr[], int left, int right) {
    int povit = left;
    int i = left + 1;
    int j = right;
    while (true) {
        while (i <= j && arr[i] < arr[povit]) {
            i++;
        }
        while (i <= j && arr[j] > arr[povit]) {
            j--;
        }
        if (i >= j) {
            break;
        }
        swap(arr[i], arr[j]);
    }
    swap(arr[povit], arr[j]);
    return j;
}


void sort_quick(int arr[], int left, int right) {
    if (left >= right) {
        return;
    }
    int p = partition(arr, left, right);
    sort_quick(arr, left, p - 1);
    sort_quick(arr, p + 1, right);
}

int main() {
    int a[] = {8, 4, 10, 5, 3, 7, 6, 9, 2, 1};
    int len = sizeof(a) / sizeof(int);
    std::cout << "len = " << len << std::endl;
    sort_quick(a, 0, len - 1);
    for (int i = 0; i < 10; i++) {
        std::cout << a[i] << std::endl;
    }
}

python版本:

class Solution:
    def sort(self, arr: List[int], left: int, right: int):
        if left >= right:
            return
        p = self.partition(arr, left, right)
        self.sort(arr, left, p - 1)
        self.sort(arr, p + 1, right)

    # 二路快排
    def partition(self, arr, left, right):
        pivot = left
        i = left + 1
        j = right

        while True:
            while i <= j and arr[i] < arr[pivot]:
                i += 1
            while i <= j and arr[j] > arr[pivot]:
                j -= 1
            if i >= j:
                break
            arr[i], arr[j] = arr[j], arr[i]

        arr[left], arr[j] = arr[j], arr[left]
        return j


solution = Solution()
arr = [7, 1, 4, 2, 8, 3, 6, 5]
solution.sort(arr, 0, len(arr) - 1)
print("arr = ", arr)

2.2 版本02:二路快速排序(随机化pivot)

C++版本:
partition方法中添加以下代码即可:

    int rand_idx = rand() % (right - left + 1) + left; // 随机取left,right之间的idx
    swap(arr[left], arr[rand_idx]);
#include <iostream>

void swap(int &a, int &b) {
    int temp = a;
    a = b;
    b = temp;
}

int partition(int arr[], int left, int right) {
    int rand_idx = rand() % (right - left + 1) + left; // 随机取left,right之间的idx
    swap(arr[left], arr[rand_idx]);

    int povit = left;
    int i = left + 1;
    int j = right;
    while (true) {
        while (i <= j && arr[i] < arr[povit]) {
            i++;
        }
        while (i <= j && arr[j] > arr[povit]) {
            j--;
        }
        if (i >= j) {
            break;
        }
        swap(arr[i], arr[j]);
    }
    swap(arr[povit], arr[j]);
    return j;
}


void sort_quick(int arr[], int left, int right) {
    if (left >= right) {
        return;
    }
    int p = partition(arr, left, right);
    sort_quick(arr, left, p - 1);
    sort_quick(arr, p + 1, right);
}

int main() {
    int a[] = {8, 4, 10, 5, 3, 7, 6, 9, 2, 1};
    int len = sizeof(a) / sizeof(int);
    std::cout << "len = " << len << std::endl;
    sort_quick(a, 0, len - 1);
    for (int i = 0; i < 10; i++) {
        std::cout << a[i] << std::endl;
    }
}

python版本:
partition方法中添加以下代码即可:

        rand = random.randint(left, right)  # 随机化基准值 pivot
        nums[left], nums[rand] = nums[rand], nums[left]
import random
from typing import List


class Solution:
    def sort(self, arr: List[int], left: int, right: int):
        if left >= right:
            return
        p = self.partition(arr, left, right)
        self.sort(arr, left, p - 1)
        self.sort(arr, p + 1, right)

    # 二路快排
    def partition(self, arr, left, right):
        rand = random.randint(left, right)
        arr[left], arr[rand] = arr[rand], arr[left]
        
        pivot = left
        i = left + 1
        j = right

        while True:
            while i <= j and arr[i] < arr[pivot]:
                i += 1
            while i <= j and arr[j] > arr[pivot]:
                j -= 1
            if i >= j:
                break
            arr[i], arr[j] = arr[j], arr[i]

        arr[left], arr[j] = arr[j], arr[left]
        return j


solution = Solution()
arr = [7, 1, 4, 2, 8, 3, 6, 5]
solution.sort(arr, 0, len(arr) - 1)
print("arr = ", arr)

三、三路快排(使用三指针)

在这里插入图片描述

在这里插入图片描述

三路快排:

  • 当所有元素都相等时,三路排序时间复杂度会优化为 O ( n ) O(n) O(n)
  • 如果数据中包含大量重复元素的话,使用三路排序会比二路排序好一些。
  • 对于“完全随机” “完全有序”的数组三路排序不如二路排序

其中:

  • nums[i] == nums[pivot] \text{nums[i] == nums[pivot]} nums[i] == nums[pivot] 时:
    在这里插入图片描述

  • nums[i] < nums[pivot] \text{nums[i] < nums[pivot]} nums[i] < nums[pivot] 时:
    在这里插入图片描述

  • nums[i] > nums[pivot] \text{nums[i] > nums[pivot]} nums[i] > nums[pivot] 时:
    在这里插入图片描述

import random


class Solution:
    def sort(self, nums, left, right):
        if left >= right:
            return

        lt, gt = self.partition(nums, left, right)

        self.sort(nums, left, lt - 1)
        self.sort(nums, gt, right)

    # 三路快排
    def partition(self, nums, left, right):
        rand = random.randint(left, right)  # 随机化基准值 pivot
        nums[left], nums[rand] = nums[rand], nums[left]

        pivot = left

        lt = left  # 循环不变量:nums[left+1, lt] < v
        gt = right + 1  # 循环不变量:nums[gt, right] > v
        i = left + 1  # 循环不变量:nums[lt + 1, i - 1] ==v

        while i < gt:
            if nums[i] < nums[pivot]:
                lt += 1  # 扩充 nums[left+1, lt] 区间
                nums[lt], nums[i] = nums[i], nums[lt]
                i += 1  # 扩充 nums[lt + 1, i - 1] 区间
            elif nums[i] > nums[pivot]:
                gt -= 1  # 扩充 nums[gt, right] 区间
                nums[i], nums[gt] = nums[gt], nums[i]
            else:  # nums[i] == nums[pivot]
                i += 1  # 扩充 nums[lt + 1, i - 1] 区间
        nums[left], nums[lt] = nums[lt], nums[left]  # 此时:nums[left, lt - 1] < v; nums[lt, gt - 1] ==v; nums[gt, right] > v
        return lt, gt


solution = Solution()
nums = [7, 1, 4, 4, 4, 2, 8, 3, 6, 5]
solution.sort(nums, 0, len(nums) - 1)
print("nums = ", nums)



参考资料:
快速排序(动画示例)
在线动画演示插入/选择/冒泡/归并/希尔/快速排序算法过程工具

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值