前两章讲解了排序中的选择排序和插入排序,这次就来讲一讲交换排序中的快速排序。

快速排序时间复杂度:平均 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法, 挖坑法,双指针法,非递归, 优化相关推荐

  1. 【Java数据结构与算法】第十七章 二分查找(非递归)和分治算法(汉诺塔)

    第十七章 二分查找(非递归)和分治算法(汉诺塔) 文章目录 第十七章 二分查找(非递归)和分治算法(汉诺塔) 一.二分查找 1.思路 2.代码实现 二.分治算法(汉诺塔) 1.概述 2.汉诺塔 一.二 ...

  2. 数据结构 快速排序的三种实现 (hoare版本 挖坑法 前后指针版本)与非递归实现

    快速排序 hoare版本 挖坑法 前后指针版本 非递归实现快速排序 快速排序:快速排序算法通过多次比较和交换来实现排序 基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另 ...

  3. 数据结构与算法(Python)– 回溯法(Backtracking algorithm)

    数据结构与算法(Python)– 回溯法(Backtracking algorithm) 1.回溯法 回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条 ...

  4. 常见数据结构和算法实现(排序/查找/数组/链表/栈/队列/树/递归/海量数据处理/图/位图/Java版数据结构)

    常见数据结构和算法实现(排序/查找/数组/链表/栈/队列/树/递归/海量数据处理/图/位图/Java版数据结构) 数据结构和算法作为程序员的基本功,一定得稳扎稳打的学习,我们常见的框架底层就是各类数据 ...

  5. 链表反转的四种方法(栈、头插法、三指针法、递归法)

    单链表反转或转置的四种方法 链表的反转实质上是反转链表上的内容: 若链表存储的数据是:1->2->3->4->5; 那么反转后则是:5->4->3->2-&g ...

  6. 快速排序常见3种方法(hoare、挖坑法、前后指针法)以及改进。

    快速排序 快速排序的思路: 通过一趟快速排序:找到基准值正确的索引位置,将序列划分为2部分,左侧序列小于基准值,右侧序列大于基准值.然后再对左右两侧的序列分别进行递归处理,最终左右两侧的序列均为有序序 ...

  7. java 数据结构与算法 ——快速排序法

    快速排序法: 顾名思议,快速排序法是实践中的一种快速的排序算法,在c++或对java基本类型的排序中特别有用.它的平均运行时间是0(N log N).该算法之所以特别快,主要是由于非常精练和高度优化的 ...

  8. java快排原理_Java数据结构与算法——快速排序

    声明:码字不易,转载请注明出处,欢迎文章下方讨论交流. 前言:Java数据结构与算法专题会不定时更新,欢迎各位读者监督.本篇文章介绍排序算法中最常用也是面试中最容易考到的排序算法--快排,包括快排的思 ...

  9. 救生艇(Java算法每日一题)(双指针法)

    问: 第 i 个人的体重为 people[i],每艘船可以承载的最大重量为 limit. 每艘船最多可同时载两人,但条件是这些人的重量之和最多为 limit. 返回载到每一个人所需的最小船数.(保证每 ...

最新文章

  1. ASP.NET MVC Model元数据(五)
  2. ISAPI_Rewrite3.1相关知识
  3. mysql 按照指定字段拼接_mysql 根据某个字段将多条记录的某个字段拼接成一个字段...
  4. Linux命令学习手册-grep命令
  5. docker 修改容器的主机名
  6. Windows7 支付宝证书安装方法
  7. Android 在onCreate()方法中获取控件宽高值为0解决方案
  8. 全球破300万!小米11系列高端市场地位稳了
  9. 数据分析,怎么做才算到位?
  10. Access入门之索引查询
  11. 【历史上的今天】1 月 23 日:现代集成电路雏形;JDK 1.0 发布;数学大师诞生
  12. TensorFlow的Dataset的padded_batch使用
  13. 筛选后系列填充_Excel2013里筛选后复制粘贴制作成绩表方法大剖析,3分钟搞定...
  14. 哈啰出行高质量故障复盘法:“3+5+3”(附模板)
  15. 【JavaSE】IO流(下)
  16. 【android】调用系统app打开word文档遇到的问题
  17. 字符串搜索算法之Sunday
  18. html5 原生插件,前端必备插件之纯原生JS的瀑布流插件Macy.js
  19. 使用SVN的commit上传如何全选文件
  20. android+mdm+解决方案,Android平台下的MDM (Mobile Device Management)解决方案

热门文章

  1. 服务容错和Hystrix
  2. C语言烧写C51单片机的线,51单片机烧写程序过程以及详细说明【图文】
  3. mysql 秒杀 隔离级别_MySQL 四种隔离级别详解,看完吊打面试官
  4. JVM-垃圾收集器与内存分配策略
  5. python如何读入dat数据_python二进制dat数据怎么转成txt文本
  6. c语言 错误 无效的控制谓词,PAT 1025反转链表的代码实现及错误分析(C语言)
  7. 正确率 精度 召回率 错误率
  8. Allegro PCB 如何测量距离?比如走线之间的距离
  9. 同步异步阻塞非阻塞杂记
  10. NSUserDefaults数据保存使用