看起来满是bug的程序,居然是对的?比冒泡算法还简单的排序算法

关注+星标公众,不错过精彩内容

2b802d926545956be33e79dc341923f1.gif

来源 | 量子位

程序bug也能负负得正吗?

世界之大,无奇不有,还真可以。

5c90a31498db6ab948dc2c5358212170.png

比如程序员们再熟悉不过的排序算法,通过两个“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论坛上突然火了起来,引来大批程序员围观。

3ad997d1dd754371a0d0488be9f80ce2.png

乍一看这段代码,你的反应会是什么?会不会觉得这个程序员水平太差了,连基本的冒泡算法都写不好:

不等号方向错了,第二层循环指数j的范围也弄错了。

总之,这段代码“绝对不可能正确”。

382ba8f37947c050452a7bcc940164f1.gif

冒泡算法

但如果你真的运行一下会发现,结果还真的是按照升序排列的。

我们再来看一下正确的冒泡算法代码是怎样的:

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:

首先假设A[k](k介于1~i之间)满足A[k]>A[i+1]最小的一个数,那么A[k−1]≤A[i+1](k≠1)。

如果A[i+1]≥A[i],那么这样的k不存在,我们就令k=i+1。

考虑以下三种情况:

1、1 ≤ j ≤ k−1

由于A[i+1]>A[j],没有任何元素交换发生。

2、 k ≤ j ≤ i (如果k=i+1,则不存在此步骤)

由于A[j]>A[i+1],所以每次比较后都会有元素交换发生。

我们使用A[ ]和A′[ ]来表示交换前和交换后的元素,所以

A′[i+1] = A[k],A′[k]=A[i+1]

经过一系列交换,最大元素最终被放到了A[i+1] 位置上,原来的A[i+1]变成了最大元素,A[k]被插入了大小介于原来A[k]和A[k-1]之间的元素。

3、i+1 ≤ j ≤ n

由于最大元素已经交换到前i+1个元素中,此过程也没有任何元素交换。

最后,Pₙ就是升序排序算法执行完以后的结果。

由于内外两组循环没有任何范围差别,因此这可以说是“最简单”的排序算法了。

从代码上来看,它很像冒泡算法,但从证明过程中可以看出,这实际上是一种插入算法

ff30cd235c1b443a120aa3cf282eed75.gif

插入算法

算法复杂度

显然,该算法总会进行次比较,接下来计算算法的交换次数。

可以证明交换其次最多为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²)。

网友:这个算法我以前见过

比最容易理解的冒泡算法还要简单,这个排序算法在Hacker News上很快引起了网友的围观。

不少人觉得它“很眼熟”。

有位网友表示,自己曾在奥林匹克数学竞赛中看到一个同学用了一种非常奇怪的排序算法,它可以运行但是效率很低,更像是一种插入排序。

如果我没记错的话,他用的就是这种算法。

3073baa6b23cfa636a89a2ed500ae63f.png

事实上,关于这种算法的讨论已久,从2014年开始就不断有人发帖,这次作者将论文上传到arXiv后又引起了广泛热议。

2125e960cf47c242dbd35bc0c4571188.png

甚至还有乌龙事件发生。

有位网友扫了一眼论文就以为这个算法和自己10年前提出的一样。

留言网友的算法:

8168defb094f28c1d04428dcfc7502ec.png

乍一看两种算法的代码确实很像,原理上的确有些相似。

都是看起来像冒泡排序,但其实更贴近选择排序。

不过很快有人指出真相:这种算法中 j=i+1 to n,并且是当 A[i] > A[j] 时交换。

而作者提出的算法中 j=1 to n,A[i] < A[j] 时交换。

两种算法相比,网友此前提出的更容易被理解为什么可以运行。

166d77a6ddffc5f0ae616a7f1df75f9e.png

当然也有歪楼的,有人就调侃自己刚学编程时写过这个算法。

我百分百确定,在我刚开始学编程、并想要找到最短的排序方法时就写过它。

ba9b1a10a1fccac0591f405fe3b14528.png

bca6c7ebbaf351057404ac60e9a2aa0f.png

不过说到实际应用上,这种算法需要的计算时间太长了。

有人就认为,这种算法此前被发现过很多次,但是那些人根本没打算用它。

8c8725e2549eea04d0e466dd94d21709.png

也有人提出:这种排序没有睡眠排序简单。

4eb816206a32564c56283da9136beea6.png

睡眠排序就是构造n个线程,让线程和排序的n个数对应。

例如对于[4,2,3,5,9]这样一组数字,就创建5个线程,每个线程睡眠4s,2s,3s,5s,9s。这些线程睡醒之后,就把自己对应的数报出来即可。这样等所有线程都醒来,排序就结束了。

但和作者提出的算法一样,睡眠排序由于多线程的问题,在真正实现上也有困难

此外,这位网友也表示自己看到过这种算法:

我确定我此前看到过这种算法,它没有名字吗?

很快就有人提议说——

如果它没有名字的话,我建议称之为“面试排序”。

07900c525b9b5d4cb9b1af2ef2b3265f.png

参考链接:
[1]https://news.ycombinator.com/item?id=28758106
[2]https://arxiv.org/abs/2110.01111

声明:本文素材来源网络,版权归原作者所有。如涉及作品版权问题,请与我联系删除。

------------ END ------------

fb653785a5c1c781a83c706b3d6065e3.gif

●嵌入式专栏精选教程

●精选汇总 | ST工具、下载编程工具

●精选汇总 | 嵌入式软件设计与开发

●精选汇总 | STM32、MCU、单片机

迎关注我的公众号回复“加群”按规则加入技术交流群,回复“1024”查看更多内容。

欢迎关注我的视频号:

bce37e6920b9588fd06da6d61a666d7c.png

点击“阅读原文”查看更多分享,欢迎点分享、收藏、点赞、在看。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值