排序算法--快速排序

前提:

快速排序(Quicksort)是对冒泡排序的一种改进。

快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

hoare版本的快速排序算法分析与实现 :

hoare大佬的算法思想算是一道硬菜,容易咯牙那种,不如说快速排序所有版本都很有东西。

那么hoare大佬的算法思想是这样的:

有图我就喜欢上:

hoare版本的动图:

我们定义的一个基准位key;然后定义两个变量Left和Right,让Right从右向左找比a[key]小的数,让Left从左向右找比a[key]大的数。找到后让Left和Right交换,不断重复上述操作,直到right和left相遇,这时交换a[Left]和a[key](或者a[Right]和a[key]交换也行,毕竟Left和Right处在同一位置);让key的下标更新,这轮操作过后key的左边都比key小,key的右边都比key大。
动图有点快,来个详细步骤:

右边先走没找到小,继续走;左边再走,没找到大,继续走;

右边再走,找到小,停住等待交换;

右边已经找到小,左边再走,找到大;二者可以交换;

交换

右边继续走,找到小,停住;

右边已经找到小,左边走,找到大;二者可以交换;

交换

右边再走,找到小,停住;左边走,与R相遇;a[L]和a[key]交换

交换

更新key的位置,本轮操作完成;


上代码:

我们定义了变量left和right控制遍历;定义变量keyi=begin,确定基准位(基准位有很多选择,一般为了简便选择起始位置)。

然后我们循环遍历序列,让右边先走,没遇到比基准位小的就继续向左走,找到了就停住;左边一样,没找到大继续走,找到就停住;

左右都停住后,就交换。然后继续循环,直到左右相遇,这时外循环结束,交换基准位和left,更新key的位置;

这里需要注意的是,right如果一开始就一直没有找到比a[key]小的数,一直找万一越过了left(这时是a[0])怎么办?这不妥妥的越界?因此我们应该如上图给循环添加限制条件(left<right)防止越界。left也是相同的处理。

上述代码完成后,做到了这个效果:即key的左边都比key小,右边都比key大。

如上图这样,key的左右其实并不有序。我们要做的是快速排序啊?即使是冒泡、直接插入排序排好都是完全有序的,快排难道这么拉吗?

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

所以要做到完全排序我们应该:

完美的递归思想是不是?

完全排序就结束了,我们试着调试一下:

运行了,有问题。

问题就出在递归上,递归到最后只有一个元素或没有元素(见上面简易递归图),数组发生了越界,所以我们需要特殊处理:

当begin>=end时,说明只有一个元素或为空,这时说明序列有序,直接返回,不要再继续。

这样基本的hoare版本的排序就结束了。

hoare版本的快速排序的小优化:

1、三数取中:

三数取中是一种对快速排序的小优化,目的就是为了使基准数更加的小,提高后面的递归的效率;

话不多说,上代码:

三数取中其实挺好理解的,就是取begin和end的中间值,比较三个数,哪个最小让其成为基准值。

2、小区间优化--直接插入排序优化:

我们学习二叉树时,都知道递归的最后一层占据整个递归的50%左右,倒数第二层25%左右,第三层12.5%左右。因此,如果我们能对最后递归次数最多的几层进行优化,效率会有一定的提升。

这就是快速排序的小区间优化的一种:直接插入排序 优化:有不了解直接插入排序的可以看我的博客:排序算法------直接插入排序-CSDN博客

新增的这个判断就是当程序递归到此次,发现要排序的数目<=10,就不要再排序了,直接上直接插入排序(我们知道,直接插入排序对基本有序的序列排序效率是很高的)。

当然,这两种方法都是对直接插入排序的小的优化,甚至可能产生反效果。

直接插入排序的其他思想--挖坑法:

不得不感慨,这个世界总是有大佬不断推动世界推陈出新,挖坑法就是一种针对hoare版本难理解研究出的另一种版本。

挖坑法,顾名思义"挖坑",它的骚操作是这样的:

1、我们将第一个元素仍设置为基准位,将其中的值保存起来,将第一个位置作为坑位。

2、我们依旧设置left和right,让right从右向左走,如果找到比基准位小的数,就将其存到坑位处,那么原right处就形成了新的坑位。然后left向右走找大,找到大就将其放到坑位处,这就又形成新的坑位。不断重复该操作,直到left和right相遇。

3、这时我们保存的原基准位就发挥了作用,将其放到最后的坑位处,仍然达到了基准值左边都比他小,右边都比他大 的效果。

话有点多了,放图:

挖坑法比hoare版本的好理解一点,那么上代码:

这里我们定义Key保存我们选定的初始坑位的值。然后定义变量hole来代表坑的位置。

下面的代码与hoare版本大同小异。当循环结束后,即begin和end相遇,这时坑位就是最后基准值的最终位置。

因为这里我们是封装了这样一个函数来实现挖坑法的步骤,所以要传坑位参数给QuickSort函数来进行递归排序。

直接插入排序的其他思想--前后指针法:

这个世界大佬那么多,好的想法自然不会只有一个,前后指针法,就是另一个快速排序的算法思想。

前后指针法,即使用一前一后两个指针,前指针寻找比基准值小的数,如果找到,后指针先后移一位,再与前指针所指的值做交换,前指针再++;如果遇到比基准值大的数,前指针继续++,后指针不移动。当前指针遍历完成整个序列时,将基准值与后指针指向的数交换。达到左边比基准值小,右边比基准值大的效果。

代码实现:

写个函数封装的一大好处就是可以再利用,哈哈~~

前后指针法,一大重点就是指针一前一后,前指针负责遍历,所以循环的继续条件就是要前指针不越界。然后,交换条件的判断也是一大毒点,要确保前指针指向的值小于基准值的同时,前后指针不能走到一起。这是为了确保我们不会将元素与其自身交换(这没有意义)。

快速排序的时间复杂度:

快速排序的时间复杂度是O(n^logn);

相关推荐
XH华3 小时前
初识C语言之二维数组(下)
c语言·算法
南宫生4 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
不想当程序猿_4 小时前
【蓝桥杯每日一题】求和——前缀和
算法·前缀和·蓝桥杯
落魄君子4 小时前
GA-BP分类-遗传算法(Genetic Algorithm)和反向传播算法(Backpropagation)
算法·分类·数据挖掘
菜鸡中的奋斗鸡→挣扎鸡4 小时前
滑动窗口 + 算法复习
数据结构·算法
Lenyiin4 小时前
第146场双周赛:统计符合条件长度为3的子数组数目、统计异或值为给定值的路径数目、判断网格图能否被切割成块、唯一中间众数子序列 Ⅰ
c++·算法·leetcode·周赛·lenyiin
郭wes代码4 小时前
Cmd命令大全(万字详细版)
python·算法·小程序
scan7245 小时前
LILAC采样算法
人工智能·算法·机器学习
菌菌的快乐生活5 小时前
理解支持向量机
算法·机器学习·支持向量机
大山同学5 小时前
第三章线性判别函数(二)
线性代数·算法·机器学习