数据结构与算法 | 快速排序:Hoare法, 挖坑法,双指针法,非递归, 优化
前两章讲解了排序中的选择排序和插入排序,这次就来讲一讲交换排序中的快速排序。
快速排序时间复杂度:平均 O(nlogn) 最坏 O(n2)
快速排序,顾名思义,它的排序的效率是非常高的,在数据十分杂乱的时候他的效率甚至能远远超过希尔排序和堆排序。而这个排序的核心思想就是以交换为核心的划分
还是以前的方法,首先讲解单趟的思路。
快速排序单趟的思路,就是先选定某个枢轴,以这个枢轴为基准,展开划分,划分的结果就是,这个枢轴的左边的数据全都比它小,而右边的数据全都比它大。而划分后枢轴的位置也就是它的最终位置,这样就完成了枢轴的归位
一般常用的划分方法有三种:Hoare法,挖坑法,双指针法,下面就来一一讲解
单趟排序—划分
Hoare法
Hoare法的思路非常简单, 就是交换比枢轴大的值和比枢轴小的值
从两端开始,先从前面找到一个比枢轴大的数据,再从尾部找到一个比枢轴小的位置,然后将小的放在前面,大的后面,直到前后走到同一个位置,说明这个位置的前面都是比他小的,后面都是比它大的,所以再将枢轴和这个位置交换,就可以保证枢轴归位
int Hoare(int* arr, int begin, int end)
{int pivot = arr[end];//保存枢轴数据,用于对比int pivot_index = end;//保存枢轴下标while (begin < end){while (begin < end && arr[begin] <= pivot){++begin;}//从前往后找到比枢轴大的数据while (begin < end && arr[end] >= pivot){--end;}//从后往前找到比枢轴小的数据Swap(&arr[begin], &arr[end]);//交换两个数据的位置}Swap(&arr[begin], &arr[pivot_index]);//枢轴归位return begin;
}
例如
测试数据
int arr[10] = { 46, 74, 53, 14, 26, 36, 86, 65, 27, 34 };
这就是一趟的思路,最后当两个指针走到一起时,该位置与枢轴交换,枢轴归位,左边比他大,右边比他小,单趟划分结束。
这就是Hoare划分的思路
挖坑法
挖坑法的思路的核心就是挖坑填数
首先在枢轴的位置挖一个坑, 然后从前往后找到一个比枢轴大的数据, 填进这个坑中, 再将那个数据的位置也挖一个坑, 然后从后往前找一个比枢轴小的值,填进刚刚的坑中,再将这个位置挖坑。然后循环,直到前后指针指向同一个位置,这时这就是最后一个坑,将最开始挖走的枢轴填进坑中,就完成了划分。
总结下来就是不停的将数据填进上一个数据的坑中,然后再将该数据的位置挖一个坑
int DigHole(int* arr, int begin, int end)
{int pivot = arr[end];//保留枢轴值,进行对比和填坑while (begin < end){while (begin < end && arr[begin] <= pivot){++begin;}arr[end] = arr[begin];//将比枢轴大的数据填入坑中while (begin < end && arr[end] >= pivot){--end;}arr[begin] = arr[end];//将比枢轴小的数据填入坑中}arr[begin] = pivot;//将枢轴填入坑中,归位return begin;
}
测试数据
int arr[10] = { 46, 74, 53, 14, 26, 36, 86, 65, 27, 34 };
这就是一趟的排序过程, 还是十分容易理解的
双指针法
这个方法比起前两个方法来说就显得有点难理解, 但是代码也是这三种划分方法中最简洁的
前后指针法的核心思路就是将大的数据推到后面
前指针指向大的数据,后指针指向小的数据,不停交换将大的数据推向后面, 小的数据放在前面。
当遇到cur比枢轴大的数据时, prev停下来, 只让cur走,直到cur走到小于枢轴的数据时,prev前进,如果两者不同,则交换,因为此时prev指向的是原来cur指向的大的数据,而cur是小的数据,两者交换,即可将大的数据推向后面
//双指针法
int PrevCurMethod(int* arr, int begin, int end)
{int cur = begin, prev = begin - 1;int pivot = arr[end];while (cur < end){//当cur指向的数大于枢轴时,就只有cur前进//当cur指向的数据小于枢轴时,prev才前进,如果与cur位置不同则交换if (arr[cur] < pivot && ++prev != cur){Swap(&arr[cur], &arr[prev]);}++cur;}++prev;Swap(&arr[cur], &arr[prev]);return prev;
}
测试数据
int arr[10] = { 46, 74, 53, 14, 26, 36, 86, 65, 27, 34 };
因为前几个数都比34小,所以直到cur走到14时才找到比枢轴小的值,这时prev前进一步走到46,两者不同,交换
这时26也比34小,prev前进,两者交换
cur再继续前进,走到27,prev再前进,两者交换
此时34走到枢轴位置,跳出循环, prev再走一步,这时再让枢轴和prev交换,这就是枢轴的最终位置
这就是枢轴的最终位置,划分结束
多趟排序
递归
理解了单趟,下面就该进行多趟的排序。因为上面说过,单趟的核心就是划分,每次将数据划分为两个区间,然后再将区间再度划分。
如图:
是不是看起来有点类似树的结构,每次通过划分将枢轴左右边划分为两个子序列,再对子序列划分,知道所有枢轴归位,数据有序,这就是多趟的思路。
在我们实现树形操作的时候通常都会使用递归来实现,因为这样更加代码更加精简且思路简单。
void QuickSort(int* arr, int begin, int end)
{//当begin和end处于同一位置时说明全部子序列都划分结束if (begin < end){int pivot = PrevCurMethod(arr, begin, end);//上面任意一种划分都可以QuickSort(arr, begin, pivot - 1);QuickSort(arr, pivot + 1, end);//分别划分枢轴左右端的区间//[begin][pivot - 1]和[pivot + 1][end]}
}
非递归
因为递归会不断在栈上创建新的栈帧,递归越深开辟的栈帧也越多,所以当数据过多时就会产生栈溢出的情况,这时如果我们利用非递归来实现,就不会有这样的问题,因为我们借助其他数据结构或者方法来进动态开辟,所以就会利用堆的空间,堆的空间比栈要大很多,所以很难有溢出的情况。
当然非递归也可以实现,但是会操作会相对来说更加复杂。
我们需要用到栈来模拟递归,我们通过将每次划分的左右区间入栈,然后通过区间来进行划分,而不是将数据入栈。
栈的实现之前有写过
https://blog.csdn.net/qq_35423154/article/details/104327861
首先放入其实的左右区间,然后将左右区间出栈,通过前面的几种方法划分后再将划分后的区间入栈,不断操作,直到栈为空,说明所有区间划分完成
void noRecursive(int* arr, int begin, int end)
{Stack s;StackInit(&s);StackPush(&s, begin);StackPush(&s, end);while (!StackEmpty(&s)){//因为入栈时先入左子树再入右子树,所以先出应该是右子树再出左子树int right = StackTop(&s);StackPop(&s);int left = StackTop(&s);StackPop(&s);int povit = PrevCurMethod(arr, left, right);//划分区间[left][povit - 1] [povit + 1][right]if (left < povit){StackPush(&s, left);StackPush(&s, povit - 1);}if (right > povit){StackPush(&s, povit + 1);StackPush(&s, right);}}StackDestroy(&s);
}
优化
优化枢轴:三数取中法
我们现在实现的快速排序会遇到一种情况,就是如果数据本身有序,这时快速排序的时间复杂度就会非常高,有时甚至比不上较为低级的交换排序冒泡排序, 这时一种非常大的问题,原因就出在了我们默认选择固定的枢轴,所以解决的方法也很简单,优化枢轴的选择即可。
而我们用到的方法也很简单,通过比较begin , mid ,end三个数据,选择大小适中的数据作为枢轴即可。
//三数取中,优化枢轴, 保证中间值在end的位置
void chancePovit(int* arr, int begin, int end)
{int mid = ((end - begin) >> 1) + begin;//取中间数,之所以不直接相加除二是因为数据过大时可能整型溢出,同时除号效率也没有右移效率高。if (arr[begin] > arr[end]){Swap(&arr[begin], &arr[end]);}//当首位比末尾低时两者交换,较小的放在前面if (arr[mid] < arr[begin]){Swap(&arr[mid], &arr[begin]);}//将中间和首位交换,将较小的放在前面if (arr[end] > arr[mid]){Swap(&arr[end], &arr[mid]);}//将末尾和中间交换,将中间数据放在最后,也就是我们选择的枢轴位置end
}
只需要在选择枢轴前执行即可。
优化递归:尾递归优化
虽然讲了非递归实现,但是那种方法还是有点复杂,所以我们还有一种优化递归的方法,借助迭代来减少一轮的递归,减少对栈的消耗。
我们将if改为while,然后在递归划分左区间时, 将begin改为右区间的首部,这样进入下一次循环的时候就会变为划分[pivot + 1][end],也就是我们原本的右区间,这样我们就将原本的一部分递归改为了循环,大大的减少了对栈的消耗
void QuickSort_Plus(int* arr, int begin, int end)
{//这里将if改为whilewhile (begin < end){int pivot = PrevCurMethod(arr, begin, end);QuickSort(arr, begin, pivot - 1);begin = pivot + 1;//将begin的值改为右区间的首部, 这时到进入下一次循环的时候就等价于原来的递归右区间}
}
到这里,快速排序的几种划分和多趟思路就全部讲完了,其实快速排序并没有想象中的难,了解了具体思路后实现起来还是挺简单的。
下面给出所有代码
#include"Stack.h"//交换
void Swap(int* a, int* b)
{int temp = *a;*a = *b;*b = temp;
}//三数取中,优化枢轴, 保证中间值在end的位置
void chancePovit(int* arr, int begin, int end)
{int mid = ((end - begin) >> 1) + begin;if (arr[begin] > arr[end]){Swap(&arr[begin], &arr[end]);}if (arr[mid] < arr[begin]){Swap(&arr[mid], &arr[begin]);}if (arr[end] > arr[mid]){Swap(&arr[end], &arr[mid]);}
}// Hoare法
int Hoare(int* arr, int begin, int end)
{chancePovit(arr, begin, end);int pivot = arr[end];int pivot_index = end;while (begin < end){while (begin < end && arr[begin] <= pivot){++begin;}while (begin < end && arr[end] >= pivot){--end;}Swap(&arr[begin], &arr[end]);}Swap(&arr[begin], &arr[pivot_index]);return begin;
}//挖坑法
int DigHole(int* arr, int begin, int end)
{chancePovit(arr, begin, end);int pivot = arr[end];while (begin < end){while (begin < end && arr[begin] <= pivot){++begin;}arr[end] = arr[begin];while (begin < end && arr[end] >= pivot){--end;}arr[begin] = arr[end];}arr[begin] = pivot;return begin;
}//双指针法
int PrevCurMethod(int* arr, int begin, int end)
{int cur = begin, prev = begin - 1;chancePovit(arr, begin, end);int pivot = arr[end];while (cur < end){if (arr[cur] < pivot && ++prev != cur){Swap(&arr[cur], &arr[prev]);}++cur;}++prev;Swap(&arr[cur], &arr[prev]);return prev;
}//递归多趟
void QuickSort(int* arr, int begin, int end)
{if (begin < end){int pivot = PrevCurMethod(arr, begin, end);QuickSort(arr, begin, pivot - 1);QuickSort(arr, pivot + 1, end);}
}//优化递归
void QuickSort_Plus(int* arr, int begin, int end)
{while (begin < end){int pivot = PrevCurMethod(arr, begin, end);QuickSort(arr, begin, pivot - 1);begin = pivot + 1;}
}//非递归
void noRecursive(int* arr, int begin, int end)
{Stack s;StackInit(&s);StackPush(&s, begin);StackPush(&s, end);while (!StackEmpty(&s)){//因为入栈时先入左子树再入右子树,所以先出的应该是右子树再到左子树int right = StackTop(&s);StackPop(&s);int left = StackTop(&s);StackPop(&s);int povit = DigHole(arr, left, right);//划分区间[left][povit - 1] [povit + 1][right]if (left < povit){StackPush(&s, left);StackPush(&s, povit - 1);}if (right > povit){StackPush(&s, povit + 1);StackPush(&s, right);}}StackDestroy(&s);
}
数据结构与算法 | 快速排序:Hoare法, 挖坑法,双指针法,非递归, 优化相关推荐
- 【Java数据结构与算法】第十七章 二分查找(非递归)和分治算法(汉诺塔)
第十七章 二分查找(非递归)和分治算法(汉诺塔) 文章目录 第十七章 二分查找(非递归)和分治算法(汉诺塔) 一.二分查找 1.思路 2.代码实现 二.分治算法(汉诺塔) 1.概述 2.汉诺塔 一.二 ...
- 数据结构 快速排序的三种实现 (hoare版本 挖坑法 前后指针版本)与非递归实现
快速排序 hoare版本 挖坑法 前后指针版本 非递归实现快速排序 快速排序:快速排序算法通过多次比较和交换来实现排序 基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另 ...
- 数据结构与算法(Python)– 回溯法(Backtracking algorithm)
数据结构与算法(Python)– 回溯法(Backtracking algorithm) 1.回溯法 回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条 ...
- 常见数据结构和算法实现(排序/查找/数组/链表/栈/队列/树/递归/海量数据处理/图/位图/Java版数据结构)
常见数据结构和算法实现(排序/查找/数组/链表/栈/队列/树/递归/海量数据处理/图/位图/Java版数据结构) 数据结构和算法作为程序员的基本功,一定得稳扎稳打的学习,我们常见的框架底层就是各类数据 ...
- 链表反转的四种方法(栈、头插法、三指针法、递归法)
单链表反转或转置的四种方法 链表的反转实质上是反转链表上的内容: 若链表存储的数据是:1->2->3->4->5; 那么反转后则是:5->4->3->2-&g ...
- 快速排序常见3种方法(hoare、挖坑法、前后指针法)以及改进。
快速排序 快速排序的思路: 通过一趟快速排序:找到基准值正确的索引位置,将序列划分为2部分,左侧序列小于基准值,右侧序列大于基准值.然后再对左右两侧的序列分别进行递归处理,最终左右两侧的序列均为有序序 ...
- java 数据结构与算法 ——快速排序法
快速排序法: 顾名思议,快速排序法是实践中的一种快速的排序算法,在c++或对java基本类型的排序中特别有用.它的平均运行时间是0(N log N).该算法之所以特别快,主要是由于非常精练和高度优化的 ...
- java快排原理_Java数据结构与算法——快速排序
声明:码字不易,转载请注明出处,欢迎文章下方讨论交流. 前言:Java数据结构与算法专题会不定时更新,欢迎各位读者监督.本篇文章介绍排序算法中最常用也是面试中最容易考到的排序算法--快排,包括快排的思 ...
- 救生艇(Java算法每日一题)(双指针法)
问: 第 i 个人的体重为 people[i],每艘船可以承载的最大重量为 limit. 每艘船最多可同时载两人,但条件是这些人的重量之和最多为 limit. 返回载到每一个人所需的最小船数.(保证每 ...
最新文章
- ASP.NET MVC Model元数据(五)
- ISAPI_Rewrite3.1相关知识
- mysql 按照指定字段拼接_mysql 根据某个字段将多条记录的某个字段拼接成一个字段...
- Linux命令学习手册-grep命令
- docker 修改容器的主机名
- Windows7 支付宝证书安装方法
- Android 在onCreate()方法中获取控件宽高值为0解决方案
- 全球破300万!小米11系列高端市场地位稳了
- 数据分析,怎么做才算到位?
- Access入门之索引查询
- 【历史上的今天】1 月 23 日:现代集成电路雏形;JDK 1.0 发布;数学大师诞生
- TensorFlow的Dataset的padded_batch使用
- 筛选后系列填充_Excel2013里筛选后复制粘贴制作成绩表方法大剖析,3分钟搞定...
- 哈啰出行高质量故障复盘法:“3+5+3”(附模板)
- 【JavaSE】IO流(下)
- 【android】调用系统app打开word文档遇到的问题
- 字符串搜索算法之Sunday
- html5 原生插件,前端必备插件之纯原生JS的瀑布流插件Macy.js
- 使用SVN的commit上传如何全选文件
- android+mdm+解决方案,Android平台下的MDM (Mobile Device Management)解决方案