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