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

议题:快速排序实现之一(单向遍历)

分析:

  • 算法原理:主要由两部分组成,一部分是递归部分QuickSort,它将调用partition进行划分,并取得划分元素P,然后分别对P之前的部分和P
    之后的部分递归调用QuickSort;另一部分是partition,选取划分元素P(随机选取数组中的一个元素,交换到数组末尾位置),定义两个标记
    值left和right,随着划分的进行,这两个标记值将数组分成三部分,left之左的部分是小于划分元素P的值,left和right之间的部分是大
    于等于划分元素P的值(等于p的值没有必要进行交换),right之右的部分是未划分的部分。运行中right自左向右遍历,left指向最左的一个不小
    于P的值,当right遇见小于P的元素就与left当前索引的值交换,right和left同时前进,否则right直接前进,直到数组末尾,最后将P
    与left当前指向的值交换,并且返回i的值;

  • 弱势:对于已经排序的序列,运行效率相当于插入排序,因为此时的划分极其不平衡。算法受输入序列的顺序影响较大,不能保证某个元素能放到最终位置;

  • 优势:内循环仅仅是比较数组元素和固定值,这种简单性正是快速排序如此高效的原因。处理划分元素恰好为序列中最大值,或者最小值;

  • 性质:算法不稳定(尚未发现使基于数组的快速排序变得稳定的简单办法),任何相等的元素有可能在左右交换的过程中被重排成不同的序列。快速排序中关键点是划分元素的选取;

  • 时间:运行时间为N㏒N;

样例:


 1 int partition_1(int *array, int l, int r) {
2 int temp;
3 /**
4 * 利用rand函数随机获取l和r之间的一个元素作为划分值
5 * 并将其与array[r]进行交换
6 * */
7 srand((int)time(0));
8 int pivot=rand()%(l+(r-l));
9 printf("%d\n",pivot);
10 temp=array[pivot];
11 array[pivot]=array[r];
12 array[pivot]=temp;
13 /**
14 * 单向扫描:
15 * right向右遍历array,当遇到小于pivot的元素,则与
16 * left当前指向的元素进行交换,否则直接跳过,一直到
17 * 达array的最右边
18 * right为主动遍历,left为被动遍历
19 * */
20 int left=l, right=l;
21 while(right<r) {
22 if(array[right]<array[r]) {
23 /**
24 * 如果array[r]是array中最大的元素,则right
25 * 遇到的所有元素都要与left指向的元素进行交换
26 * 如果left与right相等,则交换是不必要的
27 * */
28 if(left!=right) {
29 temp=array[left];
30 array[left]=array[right];
31 array[right]=temp;
32 }
33 left++;right++;
34 } else {
35 /**
36 * 如果array[r]是array中最小的元素,则left会一直
37 * 停留在l处
38 * */
39 right++;
40 }
41 }
42 /**
43 * 最终需要将pivot元素换回其排序最终位置,也就是left当前的位置
44 * */
45 temp=array[left];
46 array[left]=array[r];
47 array[r]=temp;
48
49 return left;
50 }
51
52 void quickSort_1(int *array, int l, int r) {
53 /**
54 * 递归终止条件
55 * */
56 if(l>=r) return;
57 /**
58 * 利用partition方法获取划分元素
59 * */
60 int pivot=partition_1(array, l, r);
61 /**
62 * 划分元素已经到达最终位置,所以不用参与进一步处理
63 * 分别递归处理左右部分的元素
64 * */
65 quickSort_1(array, l, pivot-1);
66 quickSort_1(array, pivot+1, r);
67 }
68
69 int main() {
70 int array[]={2,5,8,2,1,6};
71 quickSort_1(array,0,5);
72 for(int i=0;i<6;i++)
73 printf("%d,",array[i]);
74 return 1;
75 }

议题:快速排序实现之二(双向遍历)

分析:

  • 算法原理:思想与上一种实现相同,只是使用不同的划分策略。使用left和right将数组划分成三部分,left之前的部分为小于等于划分元素P的
    值,right之后的部分为大于划分元素P的值,left和right之间的部分是没有进行划分的区域。外循环使得left自左向右遍历,同时right
    自右向左遍历,在这个过程中当left遇见大于P的值则停止,等待right遇见小于等于P的值又停止之后,交换他们的值,这个循环在left和
    right相遇或者交叉之后停止。最后交换a[r]和left的值,并返回left;


  • 弱势:当序列已经就绪,并每次划分元素选取为最左边或者最右边的值,一次递归划分仅去除一个元素,既是划分元素本身,程序将递归调用N次,而算法也演变为插入排序,比较次数达到(N+1)N/2次;

  • 优势:快速排序满足分治递推式:CN=2CN/2 +
    N,最终化解为CN=NlgN;但此种情况需要划分点在序列的中间位置;

  • 性质:算法不稳定,任何相等的元素有可能在交换的过程中被重排成不同的序列。快速排序中关键点是划分元素的选取。这个实现方式与上一个实现最大的差距就在于对等于划分元素值的处理上,还有就是本实现的遍历方式是两边向中间,而并不是只有一边到另外一边;

  • 时间:当每次划分大约都将序列二分划分,运行时间为N㏒N,平均比较次数为2NlgN;最坏情况下,快速排序使用(N+1)N/2次比较;系统堆栈耗用的大小与logN成比例,退化的情况下雨N成比例;

样例:


 1 int partition_2(int *array, int l, int r) {
2 int temp;
3 /**
4 * 利用rand函数随机获取l和r之间的一个元素作为划分值
5 * 并将其与array[r]进行交换
6 * */
7 srand((int)time(0));
8 int pivot=rand()%(l+(r-l));
9 printf("%d\n",pivot);
10 temp=array[pivot];
11 array[pivot]=array[r];
12 array[pivot]=temp;
13 /**
14 * 双向扫描:
15 * left从array的左边l处开始向右处理,直到r-1
16 * right从array的右边r-1处开始向左处理,直到l
17 * left和right都是主动移动
18 * */
19 int left=l, right=r-1;
20 while(true) {
21 /**
22 * left左边的元素为小于等于array[r]的元素
23 * 并注意array[r]为最大值的情况,left会一直
24 * 移动到r
25 * */
26 while(array[left]<=array[r] && left<r)
27 left++;
28 /**
29 * right右边的元素为大于array[r]的元素
30 * 并注意array[r]为最小值的情况,right会一直
31 * 移动到l-1
32 * 这里仅使用大于的逻辑关系还可以避免当array
33 * 都是相同元素的情况时指针交叉的发生
34 * */
35 while(array[right]>array[r] && right>=l)
36 right--;
37 /**
38 * 有四种序列情况:
39 * 1. 一般情况:left和right在序列中间的某个元素交叉
40 * 2. array[r]是最大值情况:left移动到r,right在r-1
41 * 3. array[r]是最小值情况:left在l,right移动到l-1
42 * 4. array所有元素为同一个值:left移动到r,right在r-1
43 * */
44 if(left>=right)
45 break;
46 /**
47 * 交换元素
48 * */
49 temp=array[left];
50 array[left]=array[right];
51 array[right]=temp;
52
53 left++;right--;
54 }
55 /**
56 * 最终将array[r]的pivot元素与array[left]进行交换
57 * 由于此时的array[right]比array[r]小,所以只能交换
58 * array[left]
59 * */
60 temp=array[left];
61 array[left]=array[r];
62 array[r]=temp;
63 return left;
64
65 }
66
67 void quickSort_2(int *array, int l, int r) {
68 /**
69 * 递归终止条件
70 * */
71 if(l>=r) return;
72 /**
73 * 利用partition方法获取划分元素
74 * */
75 int pivot=partition_2(array, l, r);
76 /**
77 * 划分元素已经到达最终位置,所以不用参与进一步处理
78 * 分别递归处理左右部分的元素
79 * */
80 quickSort_2(array, l, pivot-1);
81 quickSort_2(array, pivot+1, r);
82 }
83
84 int main() {
85 int array[]={2,5,8,2,1,6};
86 quickSort_2(array,0,5);
87 for(int i=0;i<6;i++)
88 printf("%d,",array[i]);
89 return 1;
90 }

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

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

时间: 2024-12-25 12:19:41

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

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

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

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

议题:快速排序实现之五(非递归实现,短序列优先处理,减少递归栈大小) 分析: 算法原理:此算法实现适用于系统栈空间不足够快速排序递归调用的需求,从而使用非递归实现快速排序算法:使用显示下推栈存储快速排序中的每一次划分结果 (将left和right都压入堆栈),并且首先处理划分序列较短的子序列(也就是在得到一次划分的左右部分时,首先将长序列入栈,然后让段序列入栈), 这样可以保证当快速排序退化的线性效率的时候,栈大小仍旧在㏒N范围内.算法策略类似于最小子树优先遍历规则: 弱势:当序列已经就绪,每次

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

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

笔试算法题(08):输出倒数第K个节点

出题:输入一个单向链表,要求输出链表中倒数第K个节点 分析:利用等差指针,指针A先行K步,然后指针B从链表头与A同步前进,当A到达链表尾时B指向的节点就是倒数第K个节点: 解题: 1 struct Node { 2 int v; 3 Node *next; 4 }; 5 Node* FindLastKth(Node *head, int k) { 6 if(head==NULL) { 7 printf("\nhead is NULL\n"); 8 exit(0); 9 } 10 Nod

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

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

笔试算法题(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则以下一个元素作为起始元素重新开始,如果大于

笔试算法题(42):线段树(区间树,Interval Tree)

议题:线段树(Interval Tree) 分析: 线段树是一种二叉搜索树,将一个大区间划分成单元区间,每个单元区间对应一个叶子节点:内部节点对应部分区间,如对于一个内部节点[a, b]而言,其左子节点表示的区间为[a, (a+b)/2],其右子节点表示的区间为[1+(a+b)/2, b]: 对于区间长度为N的线段树,由于其单元节点都是[a, a]的叶子节点,所以其叶子节点数为N,并且整棵树为平衡二叉树,所以总节点数为2N-1,树的深度为log(N)+1: 插入操作:将一条线段[a, b]插入到

笔试算法题(21):将stack内外颠倒 &amp; 判断扑克牌顺子

出题:要求用递归将一个栈结构的元素内外颠倒: 分析: 本题再次说明系统栈是程序员最好的帮手,但递归度较高所以时间复杂度较大,可以使用空间换时间的方法(额外数组保存栈元素,然后逆向压入): 第一层递归(清空栈元素,并使用系统栈保存):[1,2,3,4,5],栈顶元素为1,将1弹出之后,递归处理[2,3,4,5]: 第二层递归(将栈顶元素插入到栈底,同样使用系统栈保存):当[2,3,4,5]已经逆序之后,需要将1插入到栈底,所以将1作为参数传递到递归调用中,之后递归处理2和[3,4,5]: 解题: