议题:快速排序实现之五(非递归实现,短序列优先处理,减少递归栈大小)
分析:
- 算法原理:此算法实现适用于系统栈空间不足够快速排序递归调用的需求,从而使用非递归实现快速排序算法;使用显示下推栈存储快速排序中的每一次划分结果
(将left和right都压入堆栈),并且首先处理划分序列较短的子序列(也就是在得到一次划分的左右部分时,首先将长序列入栈,然后让段序列入栈),
这样可以保证当快速排序退化的线性效率的时候,栈大小仍旧在㏒N范围内。算法策略类似于最小子树优先遍历规则; - 弱势:当序列已经就绪,每次划分元素选取为最左边或者最右边的值,一次递归划分仅去除一个元素,既是划分元素本身,而算法也演变为插入排序;
- 优势:栈大小的最大值与㏒N成比例,但退化情况下增长到N成比例;此实现可以防止因为系统栈空间不足够线性耗用的时候,可以利用显示的堆栈替代;
- 性质:算法不稳定,任何相等的元素有可能在交换的过程中被重排成不同的序列。快速排序中关键点是划分元素的选取。这个实现方式与上一个实现最大的差距就在于对等于划分元素值的处理上;
- 时间:由于每一次都将较小子序列优先放入栈中,所以保证每一项都小于它下面一项的1/2,当排序N个元素时,可保证栈大小的最大值为㏒N;
样例:
1 struct MyStack {
2 void push(int n);
3 int pop();
4 bool isEmpty();
5 };
6
7 void partition_5(int *array, int l, int r);
8
9 void quickSort_5(int *array, int l, int r) {
10 /**
11 * 使用显示下推推展替代系统递归栈
12 * */
13 MyStack* stack=new MyStack();
14 /**
15 * 初始化堆栈
16 * */
17 stack->push(l);stack->push(r);
18
19 int left, right, pivot;
20 while(!stack->isEmpty()) {
21 /**
22 * 从堆栈中获取序列段的端点
23 * */
24 right=stack->pop();left=stack->pop();
25
26 pivot=partition_5(array, left, right);
27 /**
28 * 如果当前序列段交叉,则跳过
29 * */
30 if(left>=right) continue;
31 /**
32 * 为了防止快速排序退化成线性处理,堆栈空间耗用
33 * 也为线性,此处首先处理较小的序列,也就是将较
34 * 长的子序列首先入栈,较小的子序列延后入栈;这样
35 * 可以优先处理最小子序列,从而防止退化成线性空间
36 * */
37 if((pivot-left)<(right-pivot)) {
38 stack->push(pivot+1);stack->push(right);
39 stack->push(left);stack->push(pivot-1);
40 } else {
41 stack->push(left);stack->push(pivot-1);
42 stack->push(pivot+1);stack->push(right);
43 }
44 }
45 }
46
47 int main() {
48 int array[]={2,5,8,2,1,6};
49 quickSort_5(array,0,5);
50 for(int i=0;i<6;i++)
51 printf("%d,",array[i]);
52 return 1;
53 }
议题:选择指定排序位置的元素(利用快速排序递归划分,求数值集合中第K小的元素)
分析:
- 算法原理:为了获取序列中第K小的元素,一种办法是首先对序列进行排序,然后直接找到第k个元素,但是对于获取第K小的元素而言,全序列排序是不需要的;
所以通过使用快速划分来获得某一个划分值,有K-1个元素位于它的左边,则称这个划分元素为这个序列中第K小的值,这样的策略可以在接近线性的运算时间中
完成。与排序相关但又不需要完全进行排序的一个重要应用就是求一系列数值中排在第几位的值(第K小的值); - 基于快速排序的选择总体上为线性运行时间O(N)。使用Selection解决第K小问题,需要的时间与Nk成比例(也就是先寻找第一小,然后第二,直到第K小);
样例:
1 int quickSelect_1(int *array, int l, int r, int k) {
2 /**
3 * 如果l和r交叉,则说明r-l+1小于k,没有第K小的元素
4 * 返回-1表示失败;
5 * */
6 if(l>r) return -1;
7 /**
8 * 获取划分元素
9 * */
10 int pivot=partition(array, l, r);
11 /**
12 * 注意k的大小是针对序列的排序值
13 * 而l,r和pivot的大小是在array中的索引值,
14 * 所以k需要经过转化才能与pivot进行比较
15 * */
16 if(pivot>k+l-1)
17 return quickSelect_1(array, l, pivot-1, k);
18 else if(pivot<k+l-1)
19 return quickSelect_1(array, pivot+1, r, k-(pivot-l+1));
20 else
21 return array[pivot];
22 }
23
24 int quickSelect_2(int *array, int l, int r, int k) {
25 int ltemp=l, rtemp=r;
26 int pivot;
27 while(ltemp<rtemp) {
28 pivot=partition(array, ltemp, rtemp);
29 if(pivot>k+l-1) {
30 rtemp=pivot-1;
31 } else if(pivot<k+l-1) {
32 ltemp=pivot+1;
33 k=k-(pivot-l+1);
34 } else
35 return array[pivot];
36 }
37 return -1;
38 }
39
40 int main() {
41 int array[]={2,5,8,2,1,6};
42
43 printf("%d\n",quickSelect_1(array,0,5,4));
44 return 1;
45 }
对快速排序算法的性能总结
- 可以使用下面几种策略提升性能:随机选取划分元素,三元素中值确定划分元素,小子文件非递归排序;
- 快速排序算法的性能取决于递归部分(㏒N)和划分部分(N),尽管快速排序中前面的操作会给后续的操作提供一定的排序信息从而加速后续的操作速度,但是性能仍然极大地取决于原序列的已排序程度;
- 如果原始序列已经为正序序列,并且划分元素为数组中的最左值或最右值(实际情况中极有可能选择最右边的元素作为划分元素),这样的划分会使得极端不平等的划分,并且导致最终的性能接近于插入排序(N2);
- 所以应该保证划分的均衡性,使用随机数组(可以将原始序列进行随机化)和随机选择划分元素(就像是上面几种实现那样)就能够在整体上保证划分的随机性,从而保证均衡划分,性能接近二分排序(N㏒N);
笔试算法题(55):快速排序实现之非递归实现,最小k值选择(non-recursive version, Minimal Kth
Selection of Quick Sort)
时间: 2024-10-05 03:48:53