算法导论 9.1-1 求第二小元素

最后更新于:2022-04-01 07:35:22

# 一、题目描述 证明:在最坏情况下,利用n+ceil(lgn)-2次比较,即可得到n个元素中的第2小元素。(提示:同时找最小元素) # 二、常规方法 使用两次for循环,分别将数组从前往后扫描。 第一扫次扫描过程中,不断记录和更新当前情况下的最小元素。 第二次和扫描过程中,不断记录和更新当前情况下的第二小元素。 伪代码如下: ~~~ Int min = array[0], minId = 0; FOR i in [1, n), i++ DO IF (array[i] < min) DO Min = array[i]; minId = i; DONE DONE Int secId = ( minId = 0 ? 1 : 0 ), sec = array[secId]; FOR i in [secId+1, n), i++ DO IF (i = minId) CONTINUE IF ( array[i] < sec ) DO Sec = array[i]; secId = i; DONE DONE ~~~ # 三、常规方法的比较次数 第一个循环过程中,每次循环内部的比较次数为1次,循环次数即扫描的元素个数为n-1,因此比较第一次循环过程的次数是n-1。 第二个循环过程中,每次循环内部的比较次数也中1次,循环次数即扫描的元素个数为n-2(去去掉了最小元素),因此比较第二次循环过程的次数是n-2。 常规方法的比较次数为(n-1)+(n-2),即2n-3次。 # 四、方法改进一 在第一次循环求最小元素的过程时,对元素内容一无所知,因此n-1次的比较是必要的,难有优化空间。 在第二次循环求第二小元素的过程时,对元素的内容不再是一无所知。如果充分利用第一次循环所得到的信息,也许可以省掉第二次循环中不必要的比较。 本次优化的重点在第二次循环。 # 五、第一次循环的可用信息 原理: 如果 a > b 且 b > c,那么不需要经过a和c的比较就可以知道 a > c。 信息1: 假设数组中的元素为a, b, c, d, e, f, g … 。 当扫描到元素d时,最小元素为c,可知 a > c, b > c, d > c。 当扫描到元素e时,最小元素变为了e,可知 e > c。 那么 a > c > e, b > c > e, d > c > e, a,b,d不可能是第二小元素或最小元素 c是第二小元素,e是最小元素 信息2: 接着上文继续扫描,当扫描到元素e时,最小元素为e,结论如上。 当扫描到元素g时,最小元素仍为e,可知f > e, g > e。 那么a > c > e, b > c > e, d > c > e, f > e, g > e a, b, d不可能是第二小元素或最小元素 c, f, g有可能是第二小元素,e是最小元素 # 六、根据第一遍的可用信息作第二次循环 假设已经作过第一次扫描,找到了最小元素,为e,现在要充分利用第一次扫描的信息作第二次扫描。 这一次,我们并不是对除了e以外的所有元素扫描,而是只选择其中一部分。选择的依据来自于上一节的内容。 我们大胆猜测,第二小元素的候选值是那些在第一次扫描过程中与e做过直接比较的元素。 证明如下: 1. 在第一次循环的过程中,第二小元素一定与最后元素e直接比较过。 证明: 第一种情况,在数组顺序中,第二小元素在e之前出现 在出现之前,第二小元素一直都是被当作当前状态的最小元素,遇到e之后,因为与e比较失败,最小元素才变成了e。 第二种情况,在数组顺序中,第二小元素在e之后出现 e会与后面的每一个元素直接比较。 2. 没有与最小元素e直接比较过的元素一定不会是第二小元素 证明过程与1相反,略。 # 七、方法改进一的伪代码 第一次扫描的比较没有改变,只是每次比较的过程记录下来,以便第二次扫描的时候知道某个元素曾经和谁做过比较。 ~~~ Int min = array[0], minId = 0; FOR i in [1, n), i++ DO IF (array[i] < min) DO Min = array[i]; minId = i; QUEUE[i].PUSH(min); ELSE QUEUE[minId].push(array[i]); DONE DONE ~~~ 第二次扫描时,仅取出最小元素所对应的队列元素做比较 ~~~ Int secId = QUEUE[minId].ELEMET(0) FOR i in [1, QUEUE[minId].size), i++ DO IF (QUEUE[minId].ELEMET(i) < sec ) DO Sec = QUEUE[minId].ELEMET(i); DONE DONE ~~~ # 八、方法改进一的比较次数 第一次扫描的次数仍然为n-1,主要差别在于第二次扫描。 第二次扫描也是FOR循环,每次循环内部的比较次数是1次,总的比较次数取决于 队列中元素的个数。假设个数为m,那么比较次数为m-1。 为了计算m,我画了这样一个图。叶子结点为数组中的元素。没有标字母的节点表示其它两个孩子结点的比较结果。画出这样的图,任假设一个结点为最小元素,都能很清晰地看出其队列中元素的个数。 ![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-02_56b02bd4114f7.jpg "") 我们取两种最极端的情况: 假如最小元素为A,那么其队列中的元素为B, C, D,3个元素 假如最小元素为D,那么其队列中的元素为A, B, C中的最小值,1个元素。 结论,方法改进一的比较次数最多为2n-3,最少为n-1,取决于最小元素所在位置。 # 九、方法改进二 虽然方法改进一相比于常规方法是有了改善,但最多比较次数仍然需要2n-3次,未达到题目所要求。这次我们方找最小元素的过程入手。 上文已经说过,求最元素的过程到少要经过n-1次比较,这一点是难以改变的。但我们仍可以通过调整第一次遍历方式,使留给第二次遍历更多有用的信息。尽量让每个元素与其它元素的比较次数平均化,这样,当任意一个元素成为最小元素时,它的队列的元素个数都不会太多。仍然以上文的图为例: ![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-02_56b02bd430783.jpg "") 方法改进一的第一次遍历可以画成这一样的图,数组中的每一元素是这个二叉树的叶子结点。假如某一个元素是最小元素,那么它的队列的元素即该叶子结点的高度。只要将树平均化,如下图所示,保证每一个叶子的高度都不会太高,那么每个元素对应的队列都就不会太大。 ![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-02_56b02bd44ed02.jpg "") # 十、方法改进二的比较次数 方法改进二的伪代码比较复杂,请大家直接阅读源代码。 对于第一次遍历,比较次数仍为n-1。 对于第二次遍历,比较次数取决于第一次遍历转化生成的二叉树的高度h。 将二叉树平均化以后,树的高度为h= ceil[lgn]。 结论,方法改进二的比较次数最多为n+ ceil[lgn]-2。 # 十一、代码 产品代码 [https://code.csdn.net/mishifangxiangdefeng/exerciseforalgorithmsecond/tree/master/src/chapter9/Exercise9_1_1.cpp](https://code.csdn.net/mishifangxiangdefeng/exerciseforalgorithmsecond/tree/master/src/chapter9/Exercise9_1_1.cpp) 测试代码 [https://code.csdn.net/mishifangxiangdefeng/exerciseforalgorithmsecond/tree/master/tst/chapter9/Exercise9_1_1Test.cpp](https://code.csdn.net/mishifangxiangdefeng/exerciseforalgorithmsecond/tree/master/tst/chapter9/Exercise9_1_1Test.cpp)
';