我们聊聊快排吧...
2016-01-15 18:22
309 查看
最近一直在看《编程珠玑》第二版这一本书,里面的东西真的很实用,以前也看过不少讲解快排的书,但是在编程珠玑上看到的讲解是我见过最好理解,也是最详细的,从效率和空间以及实现等各个方面都做了详细说明,并比较了几种变形的快排的效率,所以在这把我看到的内容写出来记录,留着以后忘了的时候看。
[b]1.1.插入排序[/b]
首先说一下插入排序,这个会在最后的变形快排中用到,插入排序类似于整理扑克牌的方式,假设之前的序列已经有序,当拿到一个新的数字的时候,只要将其插入到之前的合适位置即可,直到所有数据都被处理完,整个数列就有序了,由插入排序的思想可以分析当原序列基本有序时,插入排序交换的次数是非常少的,所以对于基本有序的序列来说采用插入排序是非常高效的,数据类型对程序程序结构的选择是起到很大的决定性的。插入排序的代码如下:
[b]1.2.最简单的快排程序[/b]
还是说一下快排的基本原理吧,快排的思想基于分治法,首先将数组分成两个小部分,使数组的前一部分值都小于某一个哨兵值t,后半部分都大于t,然后再递归的进行快排这两个子数组,直到数组的元素只剩下一个。在最简单的快排中这样的哨兵值设置为字数组中的第一个值x[l].
最简单的快排划分数组部分的伪代码如下:
下面利用图示来进行讲解,这样更直观。
如上图所示,假设这是进行了几次简单快排后的数组的状态,当扫描位置到位置 i 时,如果x[i] < 哨兵值x[l]则需要交换m的下一个位置和位置i的值,m的下一个位置指向的是第一个比哨兵值x[l]大的位置.
当循环终止的时候情况如下:
如上图所示此时整个数组的所有元素都已经划分好了,此时m指向的是最后一个比t小的元素的位置,所以只需要交换一下哨兵位置和m位置的值就得到了数组的有序序列,如下图所示。
下面附上简单快排程序的完整的代码:
[b]1.3.双向划分的快排程序[/b]
双向划分就是设置两个游标分别从数组的左侧,和右侧开始,左侧每次找到比哨兵值t(现在为x[0])大的位置,右侧每次找到比哨兵值t小的位置,如果两个下标没有交叉就交换他们的值,直到,两个游标产生交叉。这种思想可以避免在数组中所有元素都相同时产生平方时间的算法,而是比较差不多n*logn的次数即可,同时可以减少总的比较次数。
双向划分的图示如下:
双向划分的快排程序的代码如下:
[b]1.4.哨兵随机,小数组不处理快排[/b]
当数组已经按升序排好序时,会导致快排是o(n2)时间复杂度的,所以使用哨兵值随机划分,可以避免这种情况,方法是把x[l]和x[l..u]中的一个随机项来进行交换,然后在设置x[l]为划分数组哨兵值,实现函数为swap(x[l],x[rand(l,u)])。
而且当快排进行划分到每个子数组很小时,原来的快排程序花费了大量的时间来排序这小很小的子数组,如果这时用1.1中所介绍的插入排序来排序数组会非常有效,因为当用快排排序到后面的小数组阶段时,数组已经基本有序了,而插入排序对基本有序的数组排序是非常快的,在《编程珠玑》第二版这本书中,作者做了实验,验证出这个小数组的值边界设置为50时,然后再调用插入排序,变形过得快排程序处理最快。
代码如下:
对该快排函数调用时方式如下:
[b]1.5.作者进行了效率对比如下表:[/b]
l
[b] 1.6.线性选择问题(选出数组中第k小的元素)[/b]
线性选择问题可以基于堆排序或者快排做,以前在计算机算法设计与分析那本书上看过这个问题,就放在这里一起记录吧。
解决思路:
由于每次快排将元素分为左右两部分,左边都比哨兵值小,右边都比哨兵值大,所以可以基于这个特性,在每次快排后对哨兵最后交换的位置m进行判断,如果m比k大说明第k小的元素在左边,只需在左边子数组继续边递归找第k小的元素即可,如果m比k小则说明第k小的值在右边,则需要在右边子数组中找第k-m小的元素(由于左侧的m个元素都比k小),直到递归结束后返回x[k-1](因为数组下标从0开始)即可。
基于快排的线性选择代码如下:
该函数的调用如下,kth_select(0,u - 1,k-1);(数组下标从0开始)
[b]1.1.插入排序[/b]
首先说一下插入排序,这个会在最后的变形快排中用到,插入排序类似于整理扑克牌的方式,假设之前的序列已经有序,当拿到一个新的数字的时候,只要将其插入到之前的合适位置即可,直到所有数据都被处理完,整个数列就有序了,由插入排序的思想可以分析当原序列基本有序时,插入排序交换的次数是非常少的,所以对于基本有序的序列来说采用插入排序是非常高效的,数据类型对程序程序结构的选择是起到很大的决定性的。插入排序的代码如下:
insort() { for(i = 1; i < n; i++) for(j = i; j > 0 && x[j - 1] > x[j]; j--) swap(x,j-1,j); }
[b]1.2.最简单的快排程序[/b]
还是说一下快排的基本原理吧,快排的思想基于分治法,首先将数组分成两个小部分,使数组的前一部分值都小于某一个哨兵值t,后半部分都大于t,然后再递归的进行快排这两个子数组,直到数组的元素只剩下一个。在最简单的快排中这样的哨兵值设置为字数组中的第一个值x[l].
最简单的快排划分数组部分的伪代码如下:
m = l for i = [l,u] if x[i] < t swap(x,++m,i);
下面利用图示来进行讲解,这样更直观。
如上图所示,假设这是进行了几次简单快排后的数组的状态,当扫描位置到位置 i 时,如果x[i] < 哨兵值x[l]则需要交换m的下一个位置和位置i的值,m的下一个位置指向的是第一个比哨兵值x[l]大的位置.
当循环终止的时候情况如下:
如上图所示此时整个数组的所有元素都已经划分好了,此时m指向的是最后一个比t小的元素的位置,所以只需要交换一下哨兵位置和m位置的值就得到了数组的有序序列,如下图所示。
下面附上简单快排程序的完整的代码:
void qsort1(int l,int u) { if(l >= u) { return; } int i,m = l; for(i = l + 1; i <= u, i++) { if(x[i] < x[l]) { swap(x,++m,i); } } swap(x,l,m); qsort1(l,m - 1); qsort1(m + 1,u); }
[b]1.3.双向划分的快排程序[/b]
双向划分就是设置两个游标分别从数组的左侧,和右侧开始,左侧每次找到比哨兵值t(现在为x[0])大的位置,右侧每次找到比哨兵值t小的位置,如果两个下标没有交叉就交换他们的值,直到,两个游标产生交叉。这种思想可以避免在数组中所有元素都相同时产生平方时间的算法,而是比较差不多n*logn的次数即可,同时可以减少总的比较次数。
双向划分的图示如下:
双向划分的快排程序的代码如下:
void qsort2(int l,int u) { if(l >= u) { return ; } int t = x[l],i = l,j = u + 1; while(1) { while(i <= u && x[i] < t) {//find first bigger than t position from start i++; } while(x[j] > t) {//find first smaller than t postin from end j--; } if(i > j) { break ; } swap(x,i,j); } swap(x,l,j); qsort2(l,j - 1); qsort2(j + 1,u); }
[b]1.4.哨兵随机,小数组不处理快排[/b]
当数组已经按升序排好序时,会导致快排是o(n2)时间复杂度的,所以使用哨兵值随机划分,可以避免这种情况,方法是把x[l]和x[l..u]中的一个随机项来进行交换,然后在设置x[l]为划分数组哨兵值,实现函数为swap(x[l],x[rand(l,u)])。
而且当快排进行划分到每个子数组很小时,原来的快排程序花费了大量的时间来排序这小很小的子数组,如果这时用1.1中所介绍的插入排序来排序数组会非常有效,因为当用快排排序到后面的小数组阶段时,数组已经基本有序了,而插入排序对基本有序的数组排序是非常快的,在《编程珠玑》第二版这本书中,作者做了实验,验证出这个小数组的值边界设置为50时,然后再调用插入排序,变形过得快排程序处理最快。
代码如下:
#define SAMLL_CUTOFF 50 void qsort3(int l,int u) { if(u - l < SMALL_CUTOFF) {//剩下的小数组的时候结束快排,防止对小数组进行大量排序 return; } swap(x,l,rand(l,u)); int t = x[l],i = l,j = u + 1; while(1) { while(i <= u && x[i] < t) { i++; } while(x[j] < t) { j--; } if(i > j) { break; } swap(x,i,j); } swap(x,l,j); qsort3(l,j - 1); qsort3(j + 1,u); }
对该快排函数调用时方式如下:
qsort3(0,n-1); 2 insort();
[b]1.5.作者进行了效率对比如下表:[/b]
程序 | 时间(纳秒) |
C库函数qsort | 137n logn |
qsort1 | 60 n logn |
qsort2 | 44n logn |
qsort3 | 36n logn |
C++库函数sort | 30n logn |
[b] 1.6.线性选择问题(选出数组中第k小的元素)[/b]
线性选择问题可以基于堆排序或者快排做,以前在计算机算法设计与分析那本书上看过这个问题,就放在这里一起记录吧。
解决思路:
由于每次快排将元素分为左右两部分,左边都比哨兵值小,右边都比哨兵值大,所以可以基于这个特性,在每次快排后对哨兵最后交换的位置m进行判断,如果m比k大说明第k小的元素在左边,只需在左边子数组继续边递归找第k小的元素即可,如果m比k小则说明第k小的值在右边,则需要在右边子数组中找第k-m小的元素(由于左侧的m个元素都比k小),直到递归结束后返回x[k-1](因为数组下标从0开始)即可。
基于快排的线性选择代码如下:
void kth_select(int l,int u,int k) { if(l >= u) { return ; } swap(l,rand(l,u)); int t = x[l],i = l,j = u + 1; while(1) { while(i <= u && x[i] < t) { i++; } while(x[j] > t) { j--; } if(i > j) { break ; } swap(x,i,j); } swap(x,l,j); if(j < k) {//第k个元素在右侧,继续在右侧找第k-j个元素 kth_select(j + 1,u,k - j); } else if(j > k) { kth_select(l,j - 1,k); } }
该函数的调用如下,kth_select(0,u - 1,k-1);(数组下标从0开始)
相关文章推荐
- Node.js_Path模块
- 第六讲 分组的背包问题 HDU 1712ACboy needs your help
- php7.0 + mysql5.7.10 + nginx7.0 web开发环境搭建(CentOS7)
- Valgrind使用简
- PHP超大文件下载,断点续传下载
- C++历史段错误
- MFC 的几个重绘
- hadoop安全机制与kerberos
- Spring-webmvc源码解析之ResourceHttpRequestHandler
- 自行编写数字转换成字符串Myatoi()函数
- 域名记录类型介绍(A记录、MX记录、NS记录等)
- Log中'main', 'system', 'radio', 'events'以及android log分析
- bzoj 1013 [JSOI2008]球形空间产生器sphere(高斯消元)
- nagios根据业务定义多个联系人
- C语言中有string吗?
- 归档(archive)文件(一)
- Unity 出现 ssl peer certificate or ssh remote key
- java 类的继承体系
- 坑你没商量之c#微信回调中核实订单的签名验证
- 公钥和私钥的使用机制