十问快速排序

算法是我们学习计算机的基础之一,但时常在我们的日常工作中似乎并不是占有那么大的重要性,但不管怎么样,个人认为,作为一名优秀的程序员,没事的时候看看算法,可以放松情绪,可以提高大脑的灵活性,更何况,很多公司的笔试中算法有很大的比重。其实,就算是一个简单的算法,里面还是有很多讲究的,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:就一个快速排序犯得着这么大动干戈吗?

我觉得不用,如果的平常的工作中不需要总是和算法打交道的话,一般情况下,这个可以作为业余爱好。如果是你还是学生,我觉得不妨一试,好好的跑一边快速排序,如果你上百度百科你会发现快速排序除了上述的这一种还有几种,当然,万变不离其宗,在学习的时候多问自己几个为什么就当是脑筋急转弯,如果你懒了,也可以拿着去问问老师,看看老师的讲解。

<think>嗯,用户的是,当有1000个元素需要找出最大的个时,哪种排序方法最优,选项是快速排序、堆排序、二路归并排序和插入排序。首先,我得回忆一下这些排序算法的特点,以及它们在处理这类题时的优劣。 首先,用户的题其实是Top K题,也就是从大量数据中找出前K个最大的元素。这时候,通常不需要对整个数据集进行完全排序,只需要找到最大的K个元素。这种情况下,不同的算法可能有不同的效率。 先想快速排序快速排序的平均时间复杂度是O(n log n),最坏情况下是O(n²),但实际应用中通常很快。不过,如果只是找最大的个元素,是否需要用快速排序对整个数组排序呢?其实不需要,因为快速排序的划分过程可以帮助找到前K大的元素。比如,可以用快速选择算法,找到一个基准点,将数组分为两部分,如果基准点的位置刚好是第K大的位置,那么右边的元素就是最大的K个。这种方法的时间复杂度平均是O(n),最坏情况是O(n²),但可能比完全排序快。不过用户可能的是排序方法,所以可能需要考虑是否适用。 接下来是堆排序。堆排序的时间复杂度是O(n log n),但这里有个优化点。如果只需要前K个元素,可以使用一个大小为K的最小堆。具体来说,遍历数组,维护一个包含当前最大的K个元素的堆。每次新元素如果比堆顶大,就替换堆顶并调整堆。这样时间复杂度是O(n log K),对于K=10,这个log K的值很小,所以整体效率很高。而堆排序本身需要构建堆然后依次取出最大元素,但这里可能不需要完全堆排序,而是部分堆操作,可能更高效。 然后是二路归并排序,时间复杂度是O(n log n),稳定但需要额外的空间。归并排序无论数据如何分布都需要分割和合并,对于Top K题来说,可能没有优势,因为需要完全排序后才能取出前个,这会浪费时间和空间。所以可能不是最优选择。 插入排序的时间复杂度是O(n²),在数据量大的情况下效率非常低,1000个元素的话,插入排序显然不合适,应该排除。 所以综合来看,堆排序的方法,或者更准确地说,使用堆结构来维护前K个元素,可能是最优的。不过用户给出的选项是堆排序,而不是堆结构的选择,可能需要确认。如果严格按照选项中的排序方法来说,堆排序的时间复杂度是O(n log n),而快速排序的平均也是O(n log n),但实际可能更快,不过对于Top K题,快速选择更适合,但用户是否将快速选择视为快速排序的一部分? 这时候需要明确用户的题是否需要使用完整的排序算法,还是可以使用部分排序或选择算法。用户的题可能倾向于使用排序方法中的最优者,但可能没有意识到Top K题的最优解不是完整排序,而是部分选择。比如,标准答案应该是使用堆的方法,即维护一个大小为K的最小堆,时间复杂度O(n log K),而如果是使用堆排序的话,可能时间复杂度更高。但选项中可能没有这个选项,用户给出的选项是堆排序,所以需要看在这四个选项中哪个更好。 另外,快速排序的快速选择算法可以在平均O(n)时间内找到第K大的元素,然后划分出最大的K个元素,这比堆排序的O(n log n)可能更快,尤其是当K远小于n的时候,比如这里n=1000,K=10。所以快速选择可能更优,但用户是否将其视为快速排序的一种应用? 在这种情况下,用户可能希望知道在四个选项中,哪种排序算法作为整体排序方法时最优,但实际处理Top K题时,最优方法可能不是完全排序,而是部分选择。所以可能需要指出,虽然堆排序作为完整排序的时间复杂度是O(n log n),但使用堆结构来维护Top K的时间复杂度是O(n log K),而快速选择的平均时间复杂度是O(n)。但选项中是否有对应的算法? 用户给出的选项是快速排序、堆排序、二路归并排序、插入排序。所以需要在这四个中选最优的。如果必须使用完整的排序算法,那么堆排序和快速排序都是O(n log n),而归并排序同样,插入排序是O(n²)。因此在这四个中,堆排序可能不是最优的,但题目可能希望选择堆排序,因为可以用堆结构来优化Top K题,而其他排序方法需要完全排序。 或者,用户可能想知道在这四个排序算法中,哪个在处理这个题时最快。例如,快速排序在实际中通常比堆排序和归并排序更快,因为常数因子小,所以尽管都是O(n log n),快速排序可能更快。而归并排序需要额外的空间,而插入排序在大数据量下显然慢。因此,如果必须完全排序的话,快速排序可能最优,但如果是Top K题,更优的是使用堆结构或者快速选择,但选项中没有这些。 这时候可能需要指出,虽然题目中的选项是四个排序方法,但实际最优解并非完全排序,而是使用堆或者快速选择。但用户可能希望知道在四个选项中哪个最好,或者需要解释两者的区别。 例如,在四个选项中,如果必须用排序算法,那么快速排序可能是最快的,但如果是解决Top K题,正确的方法应该是使用堆结构,即维护一个大小为K的最小堆,时间复杂度O(n log K),或者使用快速选择找到第K大的元素,然后取出前K个,平均O(n)。但这两个方法可能不在选项中。 所以,用户的选项中可能希望得到的答案是堆排序,因为堆结构在处理Top K题时更高效,但堆排序本身是排序整个数组,而使用堆结构来维护Top K并不等同于堆排序,所以这里可能存在混淆。 因此,在回答时需要澄清,如果必须从四个排序算法中选择,快速排序可能更快,但正确的解决方法是使用堆结构(不是堆排序)或者快速选择。但根据用户提供的选项,可能应该推荐堆排序,因为其与堆结构有关,但需要明确说明。 或者,可能存在误解,用户可能认为堆排序是处理Top K的最优方法,但实际并非如此,正确的做法是使用最小堆来维护Top K,时间复杂度更低。因此,可能需要指出正确的做法是使用堆的方法,而不是堆排序,但选项中可能没有,所以需要比较四个选项中的哪一个更好。 另外,当n=1000,K=10时,部分排序方法可能更优。例如,C++中的std::partial_sort就是使用堆的方法,时间复杂度是O(n log K),而如果使用快速选择,然后对前K个元素排序,时间复杂度是O(n + K log K)。对于K=10,这非常高效。 但用户的题是关于四个排序算法的选择,所以可能需要解释,在这些排序算法中,如果必须使用完整排序的话,快速排序可能最快,但实际最优解是部分排序或选择算法,如堆或快速选择,而选项中可能没有这些,所以需要根据选项进行判断。 总结:用户的题可能希望了解在四个排序算法中选择哪个来处理Top K题,但正确的答案可能不在选项中,而是使用堆结构或快速选择。但根据选项,可能选择堆排序,因为堆结构相关,但需要明确说明两者的区别,指出正确的做法是维护一个大小为K的堆,而不是进行完整的堆排序。或者,如果必须使用完整的排序算法,那么快速排序可能更快,但效率不如专门处理Top K的方法。</think>### 一、题分析 需从 **1000个元素中找出最大的10个**,属于 **Top-K 题**(K=10)。 关键要求:**无需完全排序所有元素**,只需定位最大的10个,因此需选择 **时间和空间效率最优** 的方法。 --- ### 二、算法对比与选择 | 方法 | 时间复杂度 | 适用性分析 | |--------------|------------------|---------------------------------| | **快速排序** | 平均 $O(n \log n)$,最差 $O(n^2)$ | 需完全排序,冗余操作多,效率低 | | **堆排序** | 稳定 $O(n \log n)$ | 可优化为 **部分堆构建**,更高效 | | **二路归并** | 稳定 $O(n \log n)$ | 需完全排序,额外空间 $O(n)$ | | **插入排序** | $O(n^2)$ | 仅适合小规模数据,1000元素极慢 | --- ### 三、最优方案:堆排序的改进策略 **核心思路**:利用 **最小堆(Min-Heap)** 维护当前最大的10个元素,仅需一次遍历: 1. 初始化一个大小为10的最小堆; 2. 遍历1000个元素: - 若元素 > 堆顶(当前第10大),替换堆顶并调整堆; - 否则跳过; 3. 最终堆中元素即为最大的10个。 **时间复杂度**: $$ O(n \log K) = 1000 \times \log 10 \approx 1000 \times 3.3 = 3300 \text{次操作} $$ 远低于完全排序的 $O(n \log n) \approx 1000 \times 10 = 10,000$ 次操作。 --- ### 四、其他方法的局限性 1. **快速排序**: - 完全排序浪费资源,且最坏情况 $O(n^2)$ 不可控; - 若用 **快速选择(QuickSelect)** 找第10大元素,理论平均 $O(n)$,但需额外处理前10元素排序。 2. **归并排序**: - 必须完全排序,空间复杂度高; - 无针对Top-K的优化空间。 3. **插入排序**: - 时间复杂度 $O(n^2)$,1000元素需约 $500,000$ 次操作,效率极低。 --- ### 五、结论 **最优方法**:基于堆排序思想改进的 **最小堆策略**(时间复杂度 $O(n \log K)$)。 **次优选择**:若必须使用原排序算法,优先选 **堆排序**(稳定 $O(n \log n)$),其次是快速排序(平均性能好,但风险高)。 **代码示例(Python)**: ```python import heapq def find_top_k(nums, k): min_heap = [] for num in nums: if len(min_heap) < k: heapq.heappush(min_heap, num) else: if num > min_heap[0]: heapq.heappop(min_heap) heapq.heappush(min_heap, num) return min_heap # 示例:1000个随机数中找最大的10个 nums = [ ... ] # 1000个元素 top_10 = find_top_k(nums, 10) ``` --- ### 六、扩展思考 - **海量数据场景**(如10亿元素):需结合 **分治+堆**,分块处理后再合并结果; - **动态数据流**:持续用最小堆维护Top-K,时间复杂度稳定为 $O(n \log K)$。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值