🔥前言:快速排序——分治艺术的极致演绎
在算法的璀璨星河中,快速排序(Quick Sort)犹如一颗耀眼的超新星✨。由计算机科学泰斗Tony Hoare于1959年提出的这个算法,用其优雅的分治策略和惊人的实践效率,常年霸占着受欢迎排序算法"的宝座👑。
想象一下:你面前有一堆杂乱无章的扑克牌🃏。快速排序就像一位经验丰富的荷官,每次都能精准地选出一张"王牌"(pivot),眨眼间就把整副牌分成"比王牌小"和"比王牌大"的两堆。这种"分而治之"的魔法,正是快速排序的灵魂所在!
让我们开始这段快速排序的奇妙之旅吧!🚀
(小贴士:据说Hoare爵士当年发明这个算法时,是为了更高效地排序俄语词典...看来解决实际问题永远是创新的最佳动力呢😉)
快速排序
快速排序是一种基于分治思想的高效排序算法,由英国计算机科学家Tony Hoare于1959年提出。它以其优雅的实现方式和卓越的平均性能(O(n log n))成为最常用的排序算法之一,被广泛应用于各类编程语言的标准库中(如C++的std::sort
、Java的Arrays.sort
)。
✨ 核心思想:分而治之
快速排序的核心可以概括为三个步骤:
- 选基准(Pivot):从数组中选取一个元素作为基准值
- 分区(Partition):将数组重新排列,所有比基准小的元素放在左边,比基准大的放在右边
- 递归:对左右两个子数组重复上述过程
具体如何排序呢?最好使用三路分区策略
排序策略:三路分区快速排序
根据基准值,将数组进行三分
- 比基准值大的值,统一放在基准值的左边
- 和基准值一样大的值,统一放在中间
- 比基准值小的值,统一放在基准值的右边
当然,具体怎么分,还是需要看具体场景如何使用
如何进行对三分呢?
我们可以使用三指针遍历数组
- left指针维护左半区域
- right指针维护右半区域
- i指针来遍历数组
当i遇到比基准值大的元素,停下脚步,与right指向的数组元素进行交换
交换后
- i不变,因为从right区域换过来的元素也是没有被判断的
- right--,寻找下一个没有被判断的元素
当i遇到比基准值小的元素,停下脚步,与left指向的数组元素进行交换
交换后
- left++,寻找下一个没有被判断的元素
- i++,寻找下一个没有被判断的元素,因为从left区域换过来的元素一定是判断过的
当i遇到等于基准值的元素,直接i++,代表该元素不用被移动
当i>right,代表此间事了,但此刻数组并没有完全有序
例如:
- 初始数组:[5, 3, 8, 4,4, 2, 7, 1, 10] 基准值(pivot)=4
- 经过一次排序后,数组变成[3,2,1,4,4,7,8,10,5]
- 此时left指向1,right指向7
因为你只是将数组进行对三分,只是相对于此时的基准值有序
因此,需要对左半区域和右半区域重复上述操作
- 左半区域——[0,left]
- 右半区域——[right,0]
什么时候算排序结束?
- 直到左半区域的左边界>=右边界,代表此时左半区域只有一个,甚至没有元素
- 直到右半区域的左边界>=右边界,代表此时右半区域只有一个,甚至没有元素
代码实现:
void qsort(vector<int>&nums,int left ,int right){
if(left >= right ) return ;
//开始排序
int pos = rand()%(right -left + 1 ) + left;//随机选择避免最坏情况
int key = nums[pos];
int new_left = left-1;// 初始化小于区右边界
int new_right = right+1;//初始化大于区左边界
for(int i = left;i<new_right;){
if(nums[i] == key) i++;
else if(nums[i] < key){
swap(nums[++new_left],nums[i++]);
}
else if(nums[i] > key){
swap(nums[--new_right],nums[i]);
}
}
qsort(nums,left,new_left);
qsort(nums,new_right,right);
}
快速排序解决问题
例题:排序数组
思路:
直接快速排序即可
代码:
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
srand(time(nullptr));
qsort(nums,0,nums.size()-1);
return nums;
}
void qsort(vector<int>&nums,int left ,int right){
if(left >= right ) return ;
//开始排序
int pos = rand()%(right -left + 1 ) + left;
int key = nums[pos];
int new_left = left-1;
int new_right = right+1;
for(int i = left;i<new_right;){
if(nums[i] == key) i++;
else if(nums[i] < key){
swap(nums[++new_left],nums[i++]);
}
else if(nums[i] > key){
swap(nums[--new_right],nums[i]);
}
}
qsort(nums,left,new_left);
qsort(nums,new_right,right);
}
};
例题:寻找数组中的第K大元素
思路:
要得到数组当中第k大的元素
我们需要的不是快速排序,而是快速选择
快速选择也是在快速排序的基础上寻找结果
每次快速排序会将规定数组分为三个区域,小、中、大
- 大区域的数是数组当中最大的,令大区域的数据个数为c
- 中区域的数是数组当中始终,且每个数相等,令小区域的数据个数为b
- 小区域的数是数组当中最小的,令中区域的数据个数为c
如果此时c>=k
- 那么我们要寻找的元素一定在大区域中,此时进入大区域继续寻找即可
如果此时b+c>=k
- 那么我们要寻找的元素一定在中区域中,中区域的值是相同的,就是基准值
- 直接return key;
如果此时b+c+a>=k
- 那么我们要寻找的元素一定在小区域中,此时进入小区域继续寻找
- 而一旦进入小区域,我们寻找的值将不再是第k大的值,而是第k-c-b大的值
代码实现:
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
srand(time(nullptr));
return qsort(nums,0,nums.size()-1,k);
}
int qsort(vector<int>& nums,int left,int right,int num){
if(left>=right) return nums[left];
int pos = rand()%(right-left+1)+left;
int key = nums[pos];
int new_left = left - 1;
int new_right = right + 1;
int i = left;
while(i<new_right){
if(nums[i]==key){
i++;
}else if(nums[i]>key){
swap(nums[i],nums[--new_right]);
}else if(nums[i]<key){
swap(nums[i++],nums[++new_left]);
}
}
int len_r = right-new_right+1;
int len_m = new_right-1-new_left-1+1;
if(len_r>=num) return qsort(nums,new_right,right,num);
else if(len_r+len_m>=num) return key;
return qsort(nums,left,new_left,num-len_m-len_r);
}
};
🎉 结语:快速排序——让数据"快"乐起来的魔法!
恭喜你!现在你已经掌握了快速排序的终极奥义——"分而治之,随机应变"的算法哲学!🎯
快速排序就像一位效率狂魔的整理大师,面对一堆杂乱的数据,它总能以闪电般的速度(O(n log n))把它们安排得明明白白。当然,偶尔遇到"杠精"数据(比如完全逆序的数组),它也会小小地卡顿一下(O(n²)),但别担心,随机选基准就像给算法吃了颗"定定惊丸",让最坏情况变成稀有事件!
记住:
- "三路分区"是处理重复元素的终极武器,比传统二分法更懂"去重"的痛!
- 递归不是洪水猛兽,只要基准选得好,栈溢出?不存在的!
- 稳定性? 快速排序表示:"我快就够了,稳定交给归并排序吧!" 😎
最后,送给大家一句算法界的至理名言:
"人生就像快速排序,选对基准(方向),才能高效前进!"
下次当你看到std::sort
在毫秒间搞定百万数据时,记得微微一笑——"这波我在博客里学过!" 🚀
(P.S. 如果你的朋友还在用冒泡排序,请把这篇文章甩给他,并附言:"兄弟,该升级算法了!")
Happy Coding! 🎨💻