笔试算法题(55):快速排序实现之非递归实现,最小k值选择(non-recursive version, Minimal Kth Selection of Quick Sort)

议题:快速排序实现之五(非递归实现,短序列优先处理,减少递归栈大小)

分析:

  • 算法原理:此算法实现适用于系统栈空间不足够快速排序递归调用的需求,从而使用非递归实现快速排序算法;使用显示下推栈存储快速排序中的每一次划分结果
    (将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

笔试算法题(55):快速排序实现之非递归实现,最小k值选择(non-recursive version, Minimal Kth Selection of Quick Sort)的相关文章

笔试算法题(54):快速排序实现之三路划分, 三元中值法和插入排序处理小子文件

议题:快速排序算法实现之三(三路划分遍历,解决与划分元素相等元素的问题) 分析: 算法原理:使用三路划分策略对数组进行划分(也就是荷兰国旗问题,dutch national flag problem).这个实现是对实现二的改进,它添加处理等于划分元素的值的逻辑,将所有等于划分元素的值集中在一起,并且以后都不会再对他们进行划分. 本算法中使用四个标示值进行操作.使用left和right同时向中间遍历时,当left遇见等于划分元素时,就与iflag指向的值进行交换 (iflag指向的当前值到最左端表

笔试算法题(54):快速排序实现之单向扫描、双向扫描(single-direction scanning, bidirectional scanning of Quick Sort)

议题:快速排序实现之一(单向遍历) 分析: 算法原理:主要由两部分组成,一部分是递归部分QuickSort,它将调用partition进行划分,并取得划分元素P,然后分别对P之前的部分和P 之后的部分递归调用QuickSort:另一部分是partition,选取划分元素P(随机选取数组中的一个元素,交换到数组末尾位置),定义两个标记 值left和right,随着划分的进行,这两个标记值将数组分成三部分,left之左的部分是小于划分元素P的值,left和right之间的部分是大 于等于划分元素P的

笔试算法题(07):还原后序遍历数组 &amp; 半翻转英文句段

出题:输入一个整数数组,判断该数组是否符合一个二元查找树的后序遍历(给定整数数组,判定其是否满足某二元查找树的后序遍历): 分析:利用后序遍历对应到二元查找树的性质(序列最后一个元素必定是根节点,从左向右第一个比根节点大的元素开始直到根节点之前的所有元素必定在右子树,之前的所有元素必定在左子树): 解题: 1 bool PostOrderCheck(int *array, int i, int j) { 2 /** 3 * 如快速排序一样,解决小子文件 4 * */ 5 if(j-i+1 ==

笔试算法题(41):线索二叉树(Threaded Binary Tree)

出题:线索二叉树(Threaded Binary Tree) 分析: 为除第一个节点外的每个节点添加一个指向其前驱节点的指针,为除最后一个节点外的每个节点添加一个指向其后续节点的指针,通过这些额外的指针可以某种遍历方式对二叉树进行遍历,而加了这些额外指针的二叉树就是线索二叉树: 对于含有N个节点的二叉树而言,一共有2N个指针,但除了根节点的其他节点都有来自其父节点的指针,所以耗用了N-1个指针,则最终剩下2N-(N- 1)=N+1个空指针:线索二叉树就是利用这些空指针存储具有某种遍历顺序的前驱和

笔试算法题(53):四种基本排序方法的性能特征(Selection,Insertion,Bubble,Shell)

四种基本算法概述: 基本排序:选择,插入,冒泡,希尔.上述算法适用于小规模文件和特殊文件的排序,并不适合大规模随机排序的文件.前三种算法的执行时间与N2成正比,希尔算法的执行时间与N3/2(或更快)成正比: 前三种算法在平均,最坏情况下都是N2,而且都不需要额外的内存:所以尽管他们的运行时间只相差常数倍,但运行方式不同: 对于已经就序的序列而言,插入排序和冒泡排序的运行时间都是O(N),但是选择排序的时间仍旧是O(N^2): 因为Insertion和Bubble都是相邻项间的比较交换,所以不会出

笔试算法题(37):二叉树的层序遍历 &amp; 最长递增的数字串

出题:要求层序遍历二叉树,从上到下的层次,每一层访问顺序为从左到右,并将节点一次编号,输出如下:如果只要求打印指定的level的节点,应该如何实现. a b  c d  e  f  g h  i  分析: 原始的层序遍历类似于BFS,打印当前访问的节点curNode的序列号,并将其直接子节点放入队列queue中,然后从queue中取出下一个节点,直 到队列为空:此方法仅能按照层序打印所有节点,并不能区分每一层节点的数量:如果需要区分当前层次的节点,和当前层次节点的子节点,可以使用两个队列 que

笔试算法题(50):简介 - 广度优先 &amp; 深度优先 &amp; 最小生成树算法

广度优先搜索&深度优先搜索(Breadth First Search & Depth First Search) BFS优缺点: 同一层的所有节点都会加入队列,所以耗用大量空间: 仅能非递归实现: 相比DFS较快,空间换时间: 适合广度大的图: 空间复杂度:邻接矩阵O(N^2):邻接表O(N+E): 时间复杂度:O(V+E): DFS优缺点: 无论是系统栈还是用户栈保存的节点数都只是树的深度,所以空间耗用小: 有递归和非递归实现: 由于有大量栈操作(特别是递归实现时候的系统调用),执行速度

笔试算法题(24):找出出现次数超过一半的元素 &amp; 二叉树最近公共父节点

出题:数组中有一个数字出现的次数超过了数组长度的一半,请找出这个数字: 分析: 解法1:首先对数组进行排序,时间复杂度为O(NlogN),由于有一个数字出现次数超过了数组的一半,所以如果二分数组的话,划分元素肯定就是这个数字: 解法2:首先创建1/2数组大小的Hash Table(哈希表可以替代排序时间,由于一个数字出现超过了数组的一半,所以不同元素个数肯定不大于数组的一半),空间复杂度O(N),顺序扫描映射数 组元素到Hash Table中并计数,最后顺序扫描Hash Table,计数超过数组

笔试算法题(06):最大连续子数组和 &amp; 二叉树路径和值

出题:预先输入一个整型数组,数组中有正数也有负数:数组中连续一个或者多个整数组成一个子数组,每个子数组有一个和:求所有子数组中和的最大值,要求时间复杂度O(n): 分析: 时间复杂度为线性表明只允许一遍扫描,当然如果最终的最大值为0表明所有元素都是负数,可以用线性时间O(N)查找最大的元素.具体算法策略请见代码和注释: 子数组的起始元素肯定是非负数,如果添加的元素为正数则记录最大和值并且继续添加:如果添加的元素为负数,则判断新的和是否大于0,如果小于0则以下一个元素作为起始元素重新开始,如果大于