【数据结构】八大排序之快速排序:分而治之的艺术

【数据结构】八大排序之快速排序:分而治之的艺术
在这里插入图片描述

文章目录

快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右 子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

1.hoare版本

在这里插入图片描述

简单来说就是选某个元素为基准值(这里先默认第一个),然后把比基准值小的都放到基准值的左边,比基准值大的都放到基准值的右边

以下图为例

在这里插入图片描述

先以6为基准

然后左边找大,右边找小,之后互换

进行这么一趟后,6左边就都比它小,右边都比它大

然后以6为分界线,再分成两个区间,类似于二叉树

在这里插入图片描述
voidQuickSort(int* a,int left,int right){if(left >= right)return;//区间不存在时直接返回int keyi=left;int begin=left,end=right;while(begin < end){// 从右向左找小于基准的值while(begin < end && a[end]>= a[keyi]){ end--;}// 从左向右找大于基准的值while(begin < end && a[begin]<= a[keyi]){ begin++;}Swap(&a[begin],&a[end]);}// 将基准放到正确位置Swap(&a[keyi],&a[end]);// 递归排序子数组QuickSort(a, left, end -1);QuickSort(a, end +1, right);}}

算法优化

虽然基本快速排序已经很快,但在某些情况下性能会下降,特别是当输入数组已经有序或接近有序时。以下是两种常见的优化策略:

三数取中法

当数组已经有序或接近有序时,选择第一个元素作为基准会导致分区极度不平衡,从而使算法退化为 O(n²) 的时间复杂度。三数取中法通过选择左端、中间和右端三个元素的中值作为基准,可以有效避免这种最坏情况。

三数取中代码如下

intGetMidi(int* a,int left,int right)//左边作key 右边先走{int midi =(left + right)/2;if(a[left]< a[midi]){if(a[midi]< a[right]){//说明midi是中间值return midi;}//走到这说明midi最大,所以剩下两个数中大的就是中间值elseif(a[left]>a[right]){return left;}else//剩下最后一种情况 不必多说{return right;}}else//走到这说明 a[left]>a[midi]{if(a[midi]> a[right]){return midi;}//到这说明midi最小 所以找剩下两个小的elseif(a[left]< a[right]){return left;}else{return right;}}
小区间优化

区间比较小时,递归代价比较大,所以要进行小区间优化

对于递归来说

最后一层递归次数占总体的50%,倒数第二层25% ,所以进行小区间优化可以减少递归次数

在这里插入图片描述
//区间比较小时。进行小区间优化,不再递归分割排序。减少递归次数if((right - left +1)<10){InsertSort(a + left, right - left +1);}

完整代码如下

#include<stdio.h>voidSwap(int* a,int* b){int tmp =*a;*a =*b;*b = tmp;}voidInsertSort(int* a,int n){for(int i =1; i < n; i++){int key = a[i];int j = i -1;while(j >=0&& a[j]> key){ a[j +1]= a[j]; j--;} a[j +1]= key;}}//面试手撕快排 不用写小区间优化和三数取中 后续讲一下思路即可 不然会觉得你写的慢//避免有序情况下效率降低//1.随机数//2。三数取中intGetMidi(int* a,int left,int right)//左边作key 右边先走{int midi =(left + right)/2;if(a[left]< a[midi]){if(a[midi]< a[right]){//说明midi是中间值return midi;}//走到这说明midi最大,所以剩下两个数中大的就是中间值elseif(a[left]>a[right]){return left;}else//剩下最后一种情况 不必多说{return right;}}else//走到这说明 a[left]>a[midi]{if(a[midi]> a[right]){return midi;}//到这说明midi最小 所以找剩下两个小的elseif(a[left]< a[right]){return left;}else{return right;}}}voidQuickSort(int* a,int left,int right){if(left >= right)return;//小区间优化,不再递归分割排序。减少递归次数if((right - left +1)<10){InsertSort(a + left, right - left +1);}else{//三数取中int midi =GetMidi(a, left, right);Swap(&a[left],&a[midi]);int keyi = left;int begin = left +1;// 从基准下一个开始int end = right;while(begin <= end){// 从右向左找小于基准的值while(begin <= end && a[end]>= a[keyi]) end--;// 从左向右找大于基准的值while(begin <= end && a[begin]<= a[keyi]) begin++;// 交换找到的不符合元素if(begin < end)Swap(&a[begin],&a[end]);}// 将基准放到正确位置Swap(&a[keyi],&a[end]);// 递归排序子数组QuickSort(a, left, end -1);QuickSort(a, end +1, right);}}intmain(){int arr[3]={2,1,3};QuickSort(arr,0,2);for(int i =0; i <3; i++){printf("%d ", arr[i]);// 输出:1 2 3}return0;}

为什么要右边先走呢

分析如下图

在这里插入图片描述

算法分析

时间复杂度

O(n log n):一共有logn层递归 每一层都是所有子数组加起来都是n

空间复杂度

  • 最佳情况:O(log n) - 递归调用的栈空间
  • 最坏情况:O(n) - 当分区极度不平衡时

2.前后指针法

前后指针法是快速排序的另一种常见实现方式,通过两个指针prev和cur来遍历数组,将小于基准值的元素移动到前面。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

排序过程

  • 定义两个指针prev和cur,prev指向left,cur指向prev+1。
  • cur指针向右移动,如果a[cur]小于基准值,则prev++并交换a[prev]和a[cur]。
  • 最后将基准值交换到prev位置。

排序一趟的代码如下

int prev = left;int cur = prev+1;while(cur<=right){if(a[cur]<a[keyi]&&++prev!=cur)//cur比基准小就交换Swap(&a[prev],&a[cur])//cur不管交换还是不交换,都要继续走 cur++}Swap(&a[prev],&a[keyi]);return prev;

完整代码

intPartSort2(int* a,int left,int right){// 三数取中int midi =GetMidi(a, left, right);Swap(&a[left],&a[midi]);int keyi = left;int prev = left;int cur = prev+1;while(cur <= right){if(a[cur]< a[keyi]&&++prev != cur)Swap(&a[prev],&a[cur]); cur++;}Swap(&a[prev],&a[keyi]);return prev;}voidQuickSort(int* a,int left,int right){if(left >= right)return;int keyi =PartSort1(a, left, right);// [left, keyi-1] keyi [keyi+1, right]QuickSort(a, left, keyi -1);QuickSort(a, keyi +1, right);}

3.非递归(栈模拟)

递归实现虽然简洁,但在处理大规模数据时可能导致栈溢出。非递归实现通过显式栈来模拟递归过程,避免递归带来的栈开销。

实现思路

  1. 初始化一个栈,将初始左右下标入栈(先右后左)。
  2. 循环取出栈顶的左右下标,进行分区操作。
  3. 将分区后的左右子数组下标入栈,继续处理直到栈为空。
在这里插入图片描述
voidQuickSortNonR(int* a,int left,int right){ ST st;STInit(&st);STPush(&st, right);//先入右 再入左STPush(&st, left);while(!STEmpty(&st)){int begin =STTop(&st);STPop(&st);int end =STTop(&st);STPop(&st);int keyi =PartSort2(a, begin, end);// [begin, keyi-1] keyi [keyi+1, end]if(keyi +1< end){STPush(&st, end);STPush(&st, keyi+1);}if(begin < keyi-1){STPush(&st, keyi-1);STPush(&st, begin);}}STDestroy(&st);}

总结

快速排序是一种高效且应用广泛的排序算法,通过分治策略将问题分解为子问题处理。其性能取决于基准值的选择是否合理,因此在实际应用中常采用三数取中等优化策略来避免最坏情况的发生。

无论是递归还是非递归实现,快速排序都能在平均情况下达到O(n log n)的时间复杂度,使其成为处理大规模数据的理想选择之一

Read more

基于C++11手撸前端Promise

基于C++11手撸前端Promise

文章导航 * 引言 * 前端Promise的应用与优势 * 常见应用场景 * 并发请求 * Promise 解决的问题 * 手写 C++ Promise 实现 * 类结构与成员变量 * 构造函数 * resolve 方法 * reject 方法 * then 方法 * onCatch 方法 * 链式调用 * 使用示例 * `std::promise` 与 `CProimse` 对比 * 1. 基础功能对比 * 2. 实现细节对比 * (1) 状态管理 * (2) 回调注册与执行 * (3) 异步支持 * (4) 链式调用 * 3. 代码示例对比 * (1) `CProimse` 示例 * (2) `std::promise` 示例 * 4.

By Ne0inhk
❿⁄₁₃ ⟦ OSCP ⬖ 研记 ⟧ 密码攻击实践 ➱ 获取并破解Net-NTLMv2哈希(下)

❿⁄₁₃ ⟦ OSCP ⬖ 研记 ⟧ 密码攻击实践 ➱ 获取并破解Net-NTLMv2哈希(下)

郑重声明:本文所涉安全技术仅限用于合法研究与学习目的,严禁任何形式的非法利用。因不当使用所导致的一切法律与经济责任,本人概不负责。任何形式的转载均须明确标注原文出处,且不得用于商业目的。 🔋 点赞 | 能量注入 ❤️ 关注 | 信号锁定 🔔 收藏 | 数据归档 ⭐️ 评论 | 保持连接💬 🌌 立即前往 👉晖度丨安全视界🚀 ▶ 信息收集  ▶ 漏洞检测 ▶ 初始立足点  ▶ 权限提升 ▶ 横向移动 ➢ 密码攻击 ➢  获取并破解Net-NTLMv2哈希(下)🔥🔥🔥 ▶ 报告/分析 ▶ 教训/修复 目录 1.密码破解 1.1 破解Windows哈希实践 1.1.3 捕获Net-NTLMv2哈希实践 1.1.3.3 使用Netcat连接绑定 Shell(kali上) 1.连接流程 2.连接命令

By Ne0inhk
剑指offer第2版:链表系列

剑指offer第2版:链表系列

一、p58-JZ6 从尾到头打印链表(递归/栈) 从尾到头打印链表_牛客题霸_牛客网  解法1、递归,每访问一个节点时,先递归输出它后面的节点,再输出该节点自身,但是这样的话可能导致函数的调用层级很深,从而导致函数调用栈溢出。 class Solution { public: void print(vector<int>&ret,ListNode* head){ if(head==nullptr) return; print(ret,head->next); ret.emplace_back(head->val); } vector<int> printListFromTailToHead(ListNode* head)

By Ne0inhk
Welford 算法 | 优雅地计算海量数据的均值与方差

Welford 算法 | 优雅地计算海量数据的均值与方差

当内存装不下数据时 在机器学习特征工程或数据分析中,我们经常遇到这样的场景:手头有成百上千个独立的特征文件(CSV、Parquet 或 Numpy 格式),总量达到了几百 GB 甚至 TB 级别。现在需要计算这些特征的全局统计量(平均值、方差、标准差)来进行归一化(Standardization)。然而,开发机内存只有 16GB。如果尝试简单的 pandas.read_csv() 或 numpy.concatenate() 把所有数据读入内存,程序会瞬间 OOM(Out of Memory)崩溃。面对据量 >> 内存的场景,我们需要一种流式(Streaming)或增量(Incremental)的计算方案——Welford 算法。 教科书公式的陷阱 在统计学教科书中,

By Ne0inhk