算法是我们学习计算机的基础之一,但时常在我们的日常工作中似乎并不是占有那么大的重要性,但不管怎么样,个人认为,作为一名优秀的程序员,没事的时候看看算法,可以放松情绪,可以提高大脑的灵活性,更何况,很多公司的笔试中算法有很大的比重。其实,就算是一个简单的算法,里面还是有很多讲究的,Jon Bentley在他的《Programming Perls》里面说历史上第一篇二分搜索的论文在1946年就发表了,但是第一个没有错误的二分搜索程序却直到1962年才出现,各种究竟,值得深味。
回到正题,我们来说说快速排序吧,这里我要讲的是比较简单一种快排:先上代码!(c#)
但是不知道大家有没有深究过为什么是这样的,或者说,如果不靠记忆而纯粹的自己推导,能否不经调试一次写出正确的代码。里面好几个地方涉及到边界值的问题,不知道你是否和我有过这样的疑惑,为什么这里是“<”而不是“<=”?为了便于论述,我把问题的探讨以问答的形式展开:(注意,下文中的”暂定算法“值的是上面的实现方式,而暂定结果,指的是下图,我是在11行和12行之间加了一个小方法,现实当前arrInt的情况,然后再进行swap(),方括号中的为行号)
问题1:14行中的,while (i <= j) 可以改成 while (i < j) 吗?
我们看“暂定结果”的第四行,执行完这一句之后i = 6,j=6;按照暂定做法,会继续执行07行的do语句,执行完09、10两个while后,i = 6,j = 4;跳出14行while,进入选择判断,我们发现最终选择的是quick(0, 4) 和quick(6,14)
而如果我们假定可以改为while (i < j),则不会继续执行07行do语句,跳出14行的while语句后,最终选择的是quick(0, 6) 和 quick(6, 14)相比较而言,所以,该假设冗余更多,但是还是会正确排序。
问题2:15行中,if (left < j)可以改成 if (left <= j) 吗?
看“暂定结果”第5行,执行完这一句之后i = 1,j = 1; 按照暂定的做法,会继续执行07行的do语句,执行09、10两个while后,i = 1, j = 0;进入15、16行的判断,此时,按照暂定的办法,left < i 返回false,所以不执行quick(left, j)。但是按照问题2中的修改,则需要执行一次quick(left, j)即quick(0, 0)这一步显然是多余的,也就是说,还是可以正确排序的,只不过冗余更多。
问题3:17行中,if (right > i) 可以改成 if (right >= i) 吗?
同问题2。
问题4:09行中,i < right 改为 i <= right 可否?
通过试验,发现最后排序结果正确,使用swap方法的次数也相同,也就是“<”和“<=”没什么区别,我们来进一步讨论
我们来看while (arrInt[i] < middle && i <= right) i++;这句话,寻找一个i,使得arrInt[i] >= middle或者,i = right + 1,也就是说,我们讨论的重点是,i有没有可能到达right + 1(实验结果是问题4不影响排序,而一旦i = right + 1,如果right = arrInt.Count() - 1,那么,i就涉及到溢出的问题了)。i从左向右查找,j从右向左查找,遇到arrInt[i] = middle 或者 arrInt[j] = middle肯定会结束09行或者10行的while语句
我们分三种情况:(如果有多个值为middle,我们假设是第一个middle,第一个middle的索引是m)
情况1:arrInt[i]、arrInt[j]同时遇到middle,那么,此时,i = j = m(只管第一个middle),swap之后,i = m + 1,j = m - 1;此时执行14行代码,结束,所以没有遇到i = right + 1的问题。
情况2:arrInt[i]先遇到middle,此时,i = m,j = m + k(k是一个未知的大于0的整数),swap后,i = m + 1, j = m + k -1;而arrInt[m + k] = middle,也就是说,当i继续自增i = m + k时候,又会停下来,而此时,j <= m + k -1;这时候,由于i < j,跳出14行,再一次没有遇到i = right + 1问题。
情况3:arrInt[j]先遇到middle,此时,j = m, i = m - k(k是一个未知的大于0的整数),swap后,i = m - k + 1,j = m,同情况2,也不会遇到i = right + 1;所以说,09行代码“i < right”改为“i <= right”对排序没有任何影响。
问题5: 10行,j > left可否 改为 j >= left ?
同问题提4。
问题6:第11行,if (i <= j) 能否改为 if (i < j) ?
通过测试,发现程序进入了死循环,讲到这里,我想到一个笑话:据说,以后的电脑运行速度会远远超过如今的电脑,跑完一个死循环只需要6秒钟
下面我们在仔细分析一下,请看下图,这是改为if (i < j)之后出现的结果,(这还没有结束),此时i = 6, j = 8 ,swap之后,i = j = 7;然后重新执行07行do语句,此时arrInt[7] 正好和middle的值相同,所以通过了09和10行之后i 和j都停在了7,而此时由于修改之后的11行i < j返回一个false,所以没有执行swap,所以没有执行i++和j--;然后再一次返回到07do语句,跑完一个循环,i 和j都卡在了7。。。所以说if (i <= j)不能改为 if (i < j)
问题7:如果将14行的while (i <= j);改成了while (i < j);这个时候第11行,if (i <= j)是不是可以改为 if (i < j)了呢?
我们将快速排序的代码按照假设7来调整之后的下图,
此时left = 0, right = 2, i = 0, j = 2, m = 1,swap()之后,i = j = 1,
通过 09、10行代码后,i和j依然等于1,
由于我们修改后的11行(i < j)返回false,所以不进行swap(),
之后,通过14行代码跳出循环,满足15和17行判断,
进入新的排序quickSort(0, 1) 和quickSort(1, 2),
代码跑到这里,我们已经发现一个问题了,这两个排序都会操作索引为1的数,这显然是不对的,但这不是出错的直接原因,
我们继续来看quickSort(0, 1),left = i = 0, left = j = 1, m = arrInt[(0 + 1) / 2] = 0;(此时arrInt[0] = 0 , arrInt[1] = 1)
通过09、10行之后,i = 0, j = 0,
请注意,此时的11行和14行的判断都已经改成了(i < j)所以,程序会不进行swap() 并跳出大的do循环来到15和17行的判断,
通过这两个判断之后,由于(i < right)返回true,程序会重新执行quickSort(0, 1)。
这就是死循环所在了,所以呢,即便我们同时修改了11和14行的判断,也还是无法正确的进行排序。
问题8:按照问题4,i会在到达边界的时候通过arrInt[i] < middle来停止,那么,是不是可以将09行改为while (arrInt[i] < middle) i++?
记得以前有人说过,存在的即时合理的,其实做软件开发也是,“正常情况”下,之前我的得到的代码之所以在09、10行有i < right和j > left,肯定是有它的原因的,但是我用之前的例子测试之后,确确实实是对排序没有影响。甚至可以妄下结论,可以将09行改为while (arrInt[i] < middle) i++;同理将10行改为while (arrInt[j] > middle) j--;虽然说用测试的例子没有发现问题,但是我觉得我们也不妨搞个小程序验证一下。我把我刚才写的代码贴出来给大家看一下,没有仔细推敲过,我暂时没有测出有异常情况。
现在看来,09行中的i < right的存在的意义很有可能有以下两种
一:给程序员一个双保险,起到定心丸的作用
二:有人被忽悠了
三:可能确实有用,只是本人水平不高,没能发现
问题9:遗漏了一个假设,还是09行的判断中arrInt[i] < middle能否改为 arrInt[i] <= middle?
如果改为arrInt[i] <= middle,那么09行的目的是寻找一个大于middle或者最后一个数字,10行的目的还是寻找一个大于等于middle或者第一个数字(10行不改动)
我们来看下图中运算结果中第15行吧,现在只考虑arrInt[8 : 14] = {4, 4, 5, 4, 7, 5, 6}中间的计算结果我在这里就不具体说明了,最后排序会跑到对arrInt[11 : 14] = {5, 7, 5,6}left = i = 11, right = j = 14, middle = arrInt[(11 + 14) / 2] = 7 ,通过修改后的09行和没有修改的10行之后,i = j = 14,接着执行swap(),i = 15, j = 13, 跳出大循环,来到15和17行判断,由于i < right 返回的是false ,所以不执行quickSort(i, right);我们只执行quickSort(left, j);即quickSort(11, 13);也就是说,arrInt[11 : 14] = {5, 7, 5,6}我们最后只进行quickSort(11, 13) 而把arrInt[14]排除,按照要求{5,7, 5 }应该是任何一个数都是小于或者等于{6}的,这样的显然相违背了。也就是说,i通过自增长从left->right,如果说arrInt[left : right]中没有大于middle的数,这个时候就会出错了。
另外,如果说09行arrInt[i] < middle改为 arrInt[i] <= middle,那么后面的判断i < right就不能改为i <= right了,因为无法保证运行的时候在i = right + 1之前就跳出循环。
问题10:就一个快速排序犯得着这么大动干戈吗?
我觉得不用,如果的平常的工作中不需要总是和算法打交道的话,一般情况下,这个可以作为业余爱好。如果是你还是学生,我觉得不妨一试,好好的跑一边快速排序,如果你上百度百科你会发现快速排序除了上述的这一种还有几种,当然,万变不离其宗,在学习的时候多问自己几个为什么就当是脑筋急转弯,如果你懒了,也可以拿着去问问老师,看看老师的讲解。