算法:快速排序

引言

想象一下,当你打开手机通讯录寻找一个朋友时,当你在电商网站按价格筛选商品时,或者当搜索引擎在毫秒间为你呈现最相关的结果时------排序,这个看似简单的操作,正在悄无声息地支撑着现代数字生活的每一次高效交互。

排序算法是计算机科学中最基础、最核心的领域之一,其重要性远超表面所见。

在众多排序算法中,快速排序占据着独特的位置。自1960年由英国计算机科学家托尼·霍尔发明以来,它以其优雅的"分而治之"策略和卓越的平均性能,成为实际应用中最广泛使用的排序算法。从C语言的qsort()到Java的Arrays.sort(),从Python的sorted()到浏览器JavaScript引擎,快速排序的身影无处不在。

这是排序专题的第一篇内容,我们将以托尼·霍尔发明的快速排序作为开篇。

一、快排原理

整体逻辑

  • 快速排序用的是分而治之的思想,以从小到大的顺序为例,我们要做以下工作:
    • 1、在整个区间中选取一个基准key。
    • 2、把基准放在它该在的位置上,把比它小的数放左边,比它大的数放右边。
    • 3、对这个基准左边的区间和右边的区间再分别进行快速排序。
  • 如上,我们可以先写个大框架:
c 复制代码
void QuickSort(int* a, int n) 
{
	if (有序了)
		return;
	int key;            //选一个基准
	_QuickSort();       //假设这是单趟排序的逻辑
	QuickSort(左区间);
	QuickSort(右区间);
}

单趟逻辑

  • 在单趟排序中,我们一般把基准值放在第一个,这样方便代码实现
  • 之后,我们定义一个left和一个right,分别从两头遍历数组。
  • right先走,找到比key小的值就停下;left再走,找比key大的值,两个都停下后,交换left和right的值。
  • 一直重复此过程,直到left和right相遇,此时相遇位置的值比key小,且相遇位置是key应该在的位置,交换相遇位置与key的值
  • 此时,key在它该在的位置上,比它小的数都在它左边,比它大的数都在它右边,单趟排序完成。
  • 最后再对key的左右区间递归,递归完成后排序结束。
为什么相遇位置的值比key小?

相遇位置有两种情况

  • 1、left遇到right:我们是让right先走,如果是left遇到right,说明是right先停下,而right停下的条件是找到比key小的值,left与right相遇后位置的值就是比key小的值。
  • 2、right遇到left:上一趟left找到比key大的值之后与上一趟的right找到的比key小的值交换,所以上一趟的left存的是比key小的值。而right遇到left,说明right没有遇到比key小的值,直接与上一趟的left相遇,此时相遇位置也是比key小的值。

二、代码实现

c 复制代码
void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
int partion(int a[], int left, int right)
{
	int key = a[left];
	int i = left, j = right;
	while(i<j) 
	{
		while ( i < j && a[j] >= key)//右边找小
			j--;
		while (i < j && a[i] <= key)//左边找大
			i++;
		if(i<j)
			Swap(&a[i], &a[j]);//找到交换(若i=j,则意味着没有两个都找到就相遇了)
	}
		Swap(&a[left], &a[i]);//交换相遇位置与key的值
	return i;
}
//快速排序
void QuickSort(int a[], int left, int right)
{
	if (left >= right)
		return;

	int mid = partion(a, left, right);//记录key的值

	//对左右区间递归
	QuickSort(a, left, mid-1);
	QuickSort(a,mid+1,right);

}

三、快排优化

  • 根据上面的代码,我们知道:快排的Hoare法是用递归实现的,那么如果递归深度太深,就容易造成栈溢出。
    那么我们看这样一种场景:假如对一堆正序的n个数据排序,我们先选择第一个数为key,此时第一个数已经在它的位置上了,因为是正序的数据。
    然后我们划分左右区间递归,左区间没有,右区间是除了key的n-1个数。
  • 这时候再进行第二层排序,左区间依旧没有,右区间是除了key的n-2个数。
    以此推类,我们需要n层栈帧才能完成这次排序,效率极低。
  • 为了解决快排的一系列问题,我们可以做出很多优化:
1. 随机选 key
  • 在排序区间内随机选取一个元素作为 key,能有效避免数据分布带来的最坏情况,但随机数生成本身有一定开销。
cpp 复制代码
int GetRandomKey(int left, int right) {
    srand(time(NULL));  // 需确保只调用一次
    return rand() % (right - left + 1) + left;
}

int PartSort(int* a, int left, int right) {
    int keyi = GetRandomKey(left, right);
    Swap(&a[keyi], &a[left]);  // 将 key 换到最左边,保持原有分区逻辑
    // 后续分区逻辑不变 ...
}
2. 三数取中法(最推荐)
  • 取区间 left, right 的第一个元素、中间元素、最后一个元素,比较三者大小,选择值居中的元素作为 key。这种方法能有效避免极端情况,且无需额外开销。
cpp 复制代码
int GetMidIndex(int* a, int left, int right) {
    int mid = (left + right) / 2;
    if (a[left] < a[mid]) {
        if (a[mid] < a[right])
            return mid;      // left < mid < right
        else if (a[left] < a[right])
            return right;    // left < right < mid
        else
            return left;     // right < left < mid
    } else {  // a[left] >= a[mid]
        if (a[left] < a[right])
            return left;     // mid < left < right
        else if (a[mid] < a[right])
            return right;    // mid < right < left
        else
            return mid;      // right < mid < left
    }
}

int PartSort(int* a, int left, int right) {
    int midi = GetMidIndex(a, left, right);
    Swap(&a[midi], &a[left]);  // 将中位数换到最左边
    // 后续分区逻辑不变 ...
}

取三个数: 3, 9, 8

排序后: 3, 8, 9

中间值: 8 ← 选作 key(比 3 更接近中位数)

3. 小区间优化(递归降级)
  • 当子区间规模小于某个阈值时,不再使用递归,而是直接调用插入排序。插入排序在小数据量下常数小、效率高,同时减少了递归深度。
c 复制代码
void QuickSort(int* a, int left, int right) {
    // 小区间优化:数据量小于 10 时改用插入排序
    if (right - left + 1 < 10) {
        InsertSort(a + left, right - left + 1);
        return;
    }
    // 继续快排逻辑...
}
//阈值选择建议: 一般取 10 ~ 20 之间
//太大会浪费快排的优势,太小则优化效果不明显。
4.三路划分(处理大量重复数据)

当数组中有大量重复元素时,传统的左右分区方法会将所有等于 key 的元素集中在同一侧,导致分区不均衡。三路划分将数组分为三部分:

left, less → 小于 key

less+1, great-1 → 等于 key

great, right →大于 key

c 复制代码
void QuickSortThreeWay(int* a, int left, int right) {
    if (left >= right) return;
    
    int key = a[left];
    int less = left;
    int great = right;
    int i = left + 1;
    
    while (i <= great) {
        if (a[i] < key) {
            Swap(&a[less], &a[i]);
            less++;
            i++;
        } else if (a[i] > key) {
            Swap(&a[i], &a[great]);
            great--;
        } else {
            i++;
        }
    }
    // 递归处理小于 key 和大于 key 的部分
    QuickSortThreeWay(a, left, less - 1);
    QuickSortThreeWay(a, great + 1, right);
}
  • 适用场景: 数组中大量重复值(如成绩统计、性别分类等),可大幅提升效率。

四、非递归快排

  • 我们能否模拟压栈与出栈,来实现一个不需要递归的快速排序呢?
  • 当然可以,具体做法是:使用显式栈模拟递归过程,将需要处理的区间压入栈中,循环取出处理。
c 复制代码
void QuickSortNonR(int* a, int left, int right) {
    Stack st;
    StackInit(&st);
    StackPush(&st, right);
    StackPush(&st, left);   // 先压左,后压右,取时先右后左

    while (!StackEmpty(&st)) {
        int begin = StackTop(&st);
        StackPop(&st);
        int end = StackTop(&st);
        StackPop(&st);

        int keyi = PartSort(a, begin, end);
        // [begin, keyi-1] 和 [keyi+1, end] 入栈	
        if (keyi + 1 < end) {
            StackPush(&st, end);
            StackPush(&st, keyi + 1);
        }
        if (begin < keyi - 1) {
            StackPush(&st, keyi - 1);
            StackPush(&st, begin);
        }
    }
    StackDestroy(&st);
}
相关推荐
geovindu35 分钟前
java: Backtracking Algorithm
java·开发语言·windows·后端·算法·回溯算法
晚风叙码1 小时前
C++ vector底层模拟实现与迭代器失效深度剖析
c++·算法
To_OC9 小时前
LC 51 N 皇后:我以为难的是回溯,结果栽在了对角线下标
javascript·算法·leetcode
2401_8414956410 小时前
【操作系统】进程同步与互斥实验报告
c++·算法·操作系统·进程·并发·同步·互斥
爱刷碗的苏泓舒14 小时前
PPP-AR 中的参考星选取:数学原理、评价指标与切换处理
算法·gnss·模糊度固定·ppp-ar·星间单差·参考星·卫星端偏差
烬羽16 小时前
递归老写崩?一个"退回"公式,把回溯题变成填空题
javascript·深度学习·算法
闪电悠米17 小时前
力扣hot100-41.缺失的第一个正数-原地哈希详解
数据结构·算法·哈希算法
星空露珠17 小时前
28种颜色对应名称,
开发语言·数据库·算法·游戏·lua
QXWZ_IA19 小时前
桥梁数字孪生怎么落地?
人工智能·科技·算法·智能硬件·政务