▷ 快速排序:原理、实现与应用详解

⌹ 365bet线上 ⏱️ 2026-06-23 21:38:21 👤 admin 👁️‍🗨️ 5267 ❤️ 132
快速排序:原理、实现与应用详解

目录

一、对本次内容的简要介绍(应用等)

二、对于原理及实现步骤的简要说明

算法原理

代码模板实现(C++)

关键要点说明

三、对于常见问题的剖析

边界条件与指针移动

基准值选择与递归调用的对应关系

比较条件的要求

时间复杂度分析

空间复杂度分析

四、例题的实现

题目一:基础排序应用

题目二:快速选择算法

五、内容的优势与劣势等等

快速排序的优势

快速排序的劣势

优化策略

适用场景

与其他排序算法的对比

一、对本次内容的简要介绍(应用等)

算法概述:快速排序(Quicksort)是由英国计算机科学家 Tony Hoare 于 1960 年提出的一种高效的排序算法,采用“分治”(Divide and Conquer)策略,被誉为“二十世纪十大算法”之一。

核心思想:通过一趟排序将待排序列分割成独立的两部分,其中一部分的所有元素均比另一部分的元素小,然后对这两部分分别进行快速排序,最终使整个序列有序。

实际应用:

系统库函数:C 语言的 qsort()、C++ 的 std::sort()(许多实现采用快速排序的变体)、Java 的 Arrays.sort()(对原始类型使用双轴快速排序)等标准库中的排序函数均以快速排序为基础。

数据库系统:在数据库查询优化、索引构建等场景中,快速排序因其较高的平均效率而被广泛应用。

大数据处理:在需要处理海量数据的场景(如数据分析、机器学习数据预处理)中,快速排序是常用的内存排序算法之一。

编程竞赛:因其实现简洁、效率较高,快速排序是算法竞赛中最常用的排序算法之一。

学习价值:理解快速排序不仅有助于掌握分治思想,还能深入理解递归、指针操作、边界处理等编程核心概念,是算法学习的经典案例。

二、对于原理及实现步骤的简要说明

算法原理

快速排序的核心思想是“分而治之”。它通过一次“划分”操作,将待排序的序列分割成两个独立的部分,使得其中一部分的所有元素都比另一部分的所有元素小(或大)。然后,再递归地对这两部分进行相同的操作,直至整个序列有序。

算法步骤详解:

选择基准值:从当前待排序的子数组 a[l...r] 中选取一个元素作为“基准”。基准的选择策略多样,常见的有选择中间元素、第一个元素、最后一个元素或随机元素。

划分操作:重新排列子数组,将所有小于基准值的元素移到基准的左边,所有大于基准值的元素移到基准的右边。在此过程中,基准值最终会位于其排序后应处的正确位置。这是算法的关键步骤。

递归排序:递归地将上述过程应用于基准值左、右两侧新形成的子数组。

基准情形:当子数组的长度小于等于1时(即 l >= r),递归终止,因为单个元素或空数组天然有序。

2. 代码模板实现(C++)

点击查看代码

void q_sort(int a[], int l, int r) {

// 递归终止条件:子数组为空或只有一个元素

if (l >= r) return;

// 1. 选择基准值(此处选择中间位置的元素)

int mid = a[(l + r) / 2];

int j = l - 1; // 左指针起始位置

int i = r + 1; // 右指针起始位置

// 2. 划分操作

while (j < i) {

// 左指针向右移动,直到找到大于等于基准值的元素

do j++; while (a[j] < mid);

// 右指针向左移动,直到找到小于等于基准值的元素

do i--; while (a[i] > mid);

// 如果左右指针尚未交错或相遇,则交换它们指向的元素

if (j < i)

swap(a[i], a[j]);

}

// 3. 递归排序

// 注意:此处的递归边界 i 与基准值 mid 的选择方式严格对应

q_sort(a, l, i);

q_sort(a, i + 1, r);

}

关键要点说明

基准值选择:通常选择中间元素 q[(l+r)/2],避免在已排序数组上出现最坏情况。

指针移动:使用 do-while 循环确保指针至少移动一次,避免死循环。

分区平衡:分区后基准值不一定在正中间,但能保证左半部分 ≤ 基准值,右半部分 ≥ 基准值。

递归终止:当区间长度 ≤ 1 时停止递归。

三、对于常见问题的剖析*

边界条件与指针移动

问题描述:为什么分区循环中不能使用 while(q[i] < x) i++; 而要用 do i++; while(q[i] < x);?

原因分析:

do-while 保证指针至少移动一次,避免在相等元素时指针停滞导致死循环。

考虑极端情况:如果所有元素都等于基准值,使用 while 循环会导致指针不移动,循环无法终止。

do-while 确保每次迭代指针都会前进,最终相遇退出循环。

2. 基准值选择与递归调用的对应关系

问题描述:为什么基准值选择与递归调用方式必须严格对应?

对应规则:

| 基准值选择 | 递归调用方式 | 原因说明 |

| x = q[l] 或 x = q[(l+r)/2](不取右边界) |quick_sort(q, l, i); quick_sort(q, i+1, r); | 指针 i 最终停在 ≤ x 的区域,保证了划分的正确性 |

| x = q[r] 或 x = q[(l+r+1)/2](不取左边界) |quick_sort(q, l, j-1); quick_sort(q, j, r); | 指针 j最终停在 ≥ x 的区域,保证了划分的正确性 |

当数组为 [1, 2] 时:

j 从左边开始,1 == 1 停下

i 从右边开始,2 > 1 往左走,1 == 1 停下

此时 j = 0, i = 0,递归调用 quick_sort(q, 0, 0) 和 quick_sort(q, 0, 1),导致无限递归。

记忆口诀:递归调用时使用的指针(i 或 j)对应的那一侧不能取边界值。

本质原因:由于比较条件严格不取等,指针会在等于基准值的元素处停下。如果基准值取自左边界,那么左指针 j 的初始位置就是基准值所在位置,它在第一轮比较中就会停下。若此时递归边界使用 j,就会导致一个子数组与原始数组完全相同,陷入无限递归。因此,必须使用从另一端开始移动的指针 i 来划分。

比较条件的要求

问题描述:为什么比较条件中不能包含等号?

原因分析:

while(a[j] < mid) 和 while(a[i] > mid) 中严格使用 < 和 >,而非 <= 和 >=,是为了避免在特定边界情况下产生死循环或越界访问。

考虑一个仅有两个元素 [1, 2] 的子数组。若允许 a[j] <= mid,当 mid = 1 时,左指针 j 会越过 1 指向 2,而右指针 i 也可能因此无限向左移动,最终导致错误的划分或无限循环。

严谨解释:比较条件中不使用等号(<= 或 >=)是为了确保指针在遇到与基准值相等的元素时能可靠地停止。若使用等号,当数组中存在大量重复元素时,指针可能径直穿过整个区间,导致分区失效(所有元素都被分到一边),递归深度变为O(n),从而引发栈溢出。例如,对数组 [2, 2, 2, 2] 进行排序,若使用 while(a[j] <= mid),左指针 j 将一路移动到数组末尾,无法形成有效分割。

4. 时间复杂度分析

最佳情况:每次分区都能将数组均匀分成两半,时间复杂度为 O(n log n)。

最坏情况:每次分区都极度不平衡(如数组已排序且选择边界作为基准),时间复杂度退化为 O(n²)。

平均情况:通过随机选择基准值或选择中位数,平均时间复杂度为 O(n log n)。

5. 空间复杂度分析

递归调用栈的深度决定了空间复杂度。

最佳情况下深度为 O(log n),最坏情况下深度为 O(n)。

可以通过尾递归优化减少栈空间使用。

四、例题的实现

题目一:基础排序应用

题目描述:给定一个长度为 n 的整数数列,使用快速排序对其进行升序排序。

输入格式:

第一行包含整数 n

第二行包含 n 个整数(范围 1∼10⁹)

输出格式:排序后的数列

数据范围:1 ≤ n ≤ 100000

输入样例:

5

3 1 2 4 5

输出样例:

1 2 3 4 5

代码实现:

点击查看代码

#include

using namespace std;

const int MAX = 1e6;

void q_sort(int a[],int l,int r);

int main(){

int n;

int num[MAX];

cin >> n;

for(int i=0;i < n ;i++)

cin >> num[i];

q_sort(num,0,n-1);

for(int i=0;i

cout << num[i] << " ";

return 0;

}

void q_sort(int a[],int l,int r){

if(l >= r) return;

int mid = a[(l+r)/2];

int j = l-1;int i = r+1;

while(j < i){

do j ++ ;while(a[j] < mid);

do i -- ;while(a[i] >mid);

if(j < i)

swap(a[i],a[j]);

}

q_sort(a,l,i);//写j-1/j时 mid = r或a[(l+r+1)/2]不取左边界 反之亦然只能mid = l或a[(l+r)/2]

q_sort(a,i+1,r);

}

题目二:快速选择算法

题目描述:给定一个长度为 n 的整数数列和一个整数 k,使用快速选择算法找出数列从小到大排序后的第 k 个数。

输入格式:

第一行包含两个整数 n 和 k

第二行包含 n 个整数(范围 1∼10⁹)

输出格式:第 k 小的数

数据范围:1 ≤ n ≤ 100000,1 ≤ k ≤ n

输入样例:

5 3

2 4 1 5 3

输出样例:

3

代码实现:

点击查看代码

#include

using namespace std;

void q_sort(int a[],int l,int r);

const int MAX = 1e5+1;

int main(){

int num[MAX];

int n;

cin >> n;

int k;

cin >> k;

for(int i =0;i

cin >> num[i];

q_sort(num,0,n-1);

/*for(int i =0;i

cout << num[i];

cout << endl;*/

cout << num[k-1];

return 0;

}

void q_sort(int a[],int l,int r){

if(l >= r)

return ;

int mid = a[(l+r)/2];

int j = l-1;

int i = r+1;

while(j < i)

{

do j++;while(a[j] < mid);

do i--;while(a[i] > mid);

if( j < i)

swap(a[i],a[j]);

}

q_sort(a,l,i);

q_sort(a,i+1,l);

}

五、内容的优势与劣势等等

快速排序的优势

平均性能优异:在平均情况下,快速排序的时间复杂度为 O(n log n),且常数因子较小,实际运行效率高于其他 O(n log n) 算法。

原地排序:只需要 O(log n) 的额外栈空间(递归调用),空间效率高。

缓存友好:由于快速排序主要通过相邻元素比较和交换进行操作,对 CPU 缓存命中率较高。

适应性强:对于部分有序的数据,通过合理选择基准值,仍能保持较好性能。

可分治并行:快速排序的递归结构天然适合并行化处理,可以充分利用多核处理器。

快速排序的劣势

最坏情况性能差:当输入数组已经有序或逆序,且选择边界作为基准值时,时间复杂度退化为 O(n²)。

不稳定排序:快速排序是不稳定的排序算法,相等元素的相对位置可能改变。

递归深度问题:在最坏情况下递归深度可达 O(n),可能导致栈溢出。

基准值选择敏感:性能高度依赖于基准值的选择策略,不当选择会导致性能下降。

优化策略

三数取中法:选取左端、中间、右端三个元素的中值作为基准值,避免最坏情况。

随机化快速排序:随机选择基准值,使得算法在概率上避免最坏情况。

小数组切换:当子数组长度小于某个阈值(如 10)时,切换到插入排序。

三路快速排序:处理大量重复元素时,将数组分为小于、等于、大于基准值三部分。

尾递归优化:减少递归调用栈的深度。

适用场景

推荐使用:

数据量较大且对稳定性无要求

内存空间有限

数据随机分布,无大量重复元素

不推荐使用:

需要稳定排序的场景

数据已基本有序

对最坏情况时间有严格限制

与其他排序算法的对比

算法 平均时间复杂度 最坏时间复杂度 空间复杂度 稳定性 适用场景

快速排序 O(n log n) O(n²) O(log n) 不稳定 通用排序,内存受限

归并排序 O(n log n) O(n log n) O(n) 稳定 需要稳定性,外部排序

堆排序 O(n log n) O(n log n) O(1) 不稳定 实时系统,内存受限

插入排序 O(n²) O(n²) O(1) 稳定 小规模数据,基本有序数据

◈ 相关文章

苹果id怎么输入格式
⌹ 365提款成功但是不到账

▷ 苹果id怎么输入格式

⏱️ 01-28 👁️‍🗨️ 7407
新买路由器如何快速安装?保姆级教程来了!
⌹ 365bet现场滚球

▷ 新买路由器如何快速安装?保姆级教程来了!

⏱️ 02-21 👁️‍🗨️ 2089
闰的意思,闰的解释,闰的拼音,闰的部首,闰的笔顺
⌹ 365bet线上

▷ 闰的意思,闰的解释,闰的拼音,闰的部首,闰的笔顺

⏱️ 07-03 👁️‍🗨️ 8335