[导读]↓推荐关注↓转自:量子位 公众号(QbitAI)程序bug也能负负得正吗?还真可以。比如程序员们再熟悉不过的排序算法,通过两个“bug”居然能歪打正着,实在令人匪夷所思。请看这位程序员写的数组升序排序代码:for i = 1 to n do for j = 1 to n do ...
↓推荐关注↓
转自:量子位 公众号( QbitAI )
程序 bug 也能负负得正吗?还真可以。比如程序员们再熟悉不过的排序算法,通过两个“bug”居然能歪打正着,实在令人匪夷所思。请看这位程序员写的数组升序排序代码:for i = 1 to n do for j = 1 to n do if A[i] < A[j] then swap A[i] and A[j] 最近这串代码在 Hacker News 论坛上突然火了起来,引来大批程序员围观。乍一看这段代码,你的反应会是什么?会不会觉得这个程序员水平太差了,连基本的冒泡算法都写不好:
不等号方向错了,第二层循环指数 j 的范围也弄错了。
总之,这段代码“绝对不可能正确”。冒泡算法但如果你真的运行一下会发现,结果还真的是按照升序排列的。我们再来看一下正确的冒泡算法代码是怎样的:for i = 1 to n do for j = i 1 to n do if A[i] > A[j] then swap A[i] and A[j] 后者不同之处是 j = i 1 且 A[i] > A[j],两段程序大相径庭。然而我要告诉你一个不可思议的事实,其实第一串代码是对的,而且可以严格证明。那么它是如何实现正确排序的?
为何能歪打正着
仔细一想,其实很容易理解。因为该算法比冒泡排序多一半交换操作,正好可以将降序编程升序。不过,作者还是给出了严格的证明。我们定义 Pᵢ 是经过 i 次(1 ≤ i ≤ n)外循环后得到的数组。如果算法正确,那么前 i 项已经是升序排列,即 A[1] ≤ A[2] ≤ . . . ≤ A[i]。证明该算法正确,实际上就是证明 Pₙ 对于任何 n 都成立。根据数学归纳法,我们只要证明 P₁ 成立,假设 Pᵢ 成立,接着再证明 Pi 1 也成立,命题即可得证。P₁ 显然是正确的,而且这一步和普通的冒泡算法降序没有区别,经过第 1 次外循环,A[1] 就是整个数组的最大元素。接着我们假设 Pᵢ 成立,然后证明 Pi 1 成立。我们先定义一个序数 k:
显然,该算法总会进行 n² 次比较,接下来计算算法的交换次数。可以证明交换其次最多为 I 2(n-1),最少为 n-1。其中 I 为初始数字的逆序数,最大为 n(n-1)/2因此整个算法的复杂度为 O(n²)。从证明过程中可以看出,除了 i=1 的循环以外,其余循环里 j=i-1 之后的部分完全无效,因此可以将这部分省略,得到简化后的算法。for i = 2 to n do for j = 1 to i − 1 do if A[i] < A[j] then swap A[i] and A[j] 该算法减少了比较和交换次数,不过算法复杂度依然是 O(n²)。
不过说到实际应用上,这种算法需要的计算时间太长了。有人就认为,这种算法此前被发现过很多次,但是那些人根本没打算用它。也有人提出:这种排序没有睡眠排序简单。睡眠排序就是构造 n 个线程,让线程和排序的 n 个数对应。例如对于 [4,2,3,5,9] 这样一组数字,就创建 5 个线程,每个线程睡眠 4s,2s,3s,5s,9s。这些线程睡醒之后,就把自己对应的数报出来即可。这样等所有线程都醒来,排序就结束了。但和作者提出的算法一样,睡眠排序由于多线程的问题,在真正实现上也有困难。此外,这位网友也表示自己看到过这种算法: