大家好,我是雨乐。

今天在搜论文的时候,偶然发现一篇文章,名为<Is this the simplest (and most surprising) sorting algorithm ever?>,看了里面的内容,蛮有意思,所以今天借助此文,分享给大家。

算法

下面我看下伪代码实现,在证明该排序算法正确性之前,我们暂且将其命名为ICan’tBelieveItCanSort。

ICan’tBelieveItCanSort(A[1..n]) {
  for i = 1 to n do
    for j = 1 to n do
      if A[i] < A[j] then
        swap(A[i], A[j])
}

看了上面代码的第一反应是什么?会不会跟我一样,觉得

这不就是一个错误的冒泡排序算法么,只是把第二行把范围写错,第三行交换的条件写反了罢了。

下面是冒泡排序的伪代码:

BubbleSort(A[1..n]) {
  for i = 1 to n do
    for j = i + 1 to n do
      if (A[i] > A[j]) then
        swap(A[i], A[j]);
}

为了后续描述方便,我将该算法统一称之为"新算法"。

从上面两个伪代码的实现来看,新算法ICan’tBelieveItCanSort和传统的冒泡排序算法BubbleSort的区别如下:

  • 在新算法中,内循环为 for j = 1 to n do

    而在传统的冒泡算法中,内循环为 for j = i + 1 to n do

  • 在新算法中,交换的条件为 if A[i] < A[j] then

    而在传统的冒泡排序算法中,交换条件为 if A[i] > A[j] then

好了,我们言归正传,重新转回到新算法,为了方便大家阅读,再此重新贴一次新算法的伪代码:

ICan’tBelieveItCanSort(A[1..n]) {
  for i = 1 to n do
    for j = 1 to n do
      if A[i] < A[j] then
        swap(A[i], A[j])
}

先不论算法的正确与否,因为在A[i] < A[j]时候才进行交换,所以上述代码给我们的第一印象就是 按照降序排列。但实际上,通过代码运行结果来分析,其确实是升序排列。

下面给出证明过程。

证明

下面将通过数学归纳法来证明此算法的正确性。

假设Pᵢ是经过 i 次(1 ≤ i ≤ n)外循环后得到的数组,那么前i项已经是升序排列,即 A[1] ≤ A[2] ≤ . . . ≤ A[i]。

要证明该算法正确,只需要证明P对于任何[i + 1..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 ≤ j ≤ k−1 此时,由于A[1..i]是递增有序,且A[K]是满足A[k] > A[i+1] 最小的一个数,所以A[j] < A[i +1],没有任何元素交换发生。

  • 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]之间的元素。

  • i+1 ≤ j ≤ n

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

经过上面一系列条件,最终,P就是升序排序算法执行完以后的结果。

由于内外循环完全一样,所以此算法可以说是最简单的排序算法了。

优化

从上面的证明过程中,我们可以发现,除了 i=1 的循环以外,其余循环里 j=i-1 之后的部分完全无效,因此可以将这部分省略,得到简化后的算法。

ICan’tBelieveItCanSort(A[1..n]) {
  for i = 2 to n do
    for j = 1 to i - 1 do
      if A[i] < A[j] then
        swap(A[i], A[j])
}

对比

但从代码来看,新算法像是冒泡算法的变种,但是从上面证明过程来看,新算法实际上是一种插入算法。

下面为新算法的模拟图:

新算法

下面为冒泡算法的模拟图:

冒泡算法

实现

代码实现比较简单,如下:

#include 
#include 
#include 

void SimplestSort(std::vector &v) {
  for (int i = 0; i < v.size(); ++i) {
    for (int j = 0; j < v.size(); ++j) {
      if (v[i] < v[j]) {
 std::swap(v[i], v[j]);
      }
    }
  }
}

int main() {
  std::vector v = {9, 8, 1, 3,2, 5, 4, 7, 6};
  SimplestSort(v);

  for (auto item : v) {
    std::cout << item << std::endl;
  }

  return 0;
}

输出结果:

1
2
3
4
5
6
7
8
9

更简单的算法?

看完这篇论文,突然想起之前有个更简单且容易理解的算法,我们暂且称之为休眠算法。

思想:

构造n个线程,它们和这n个数一一对应。初始化后,线程们开始睡眠,等到对应的数那么多个时间单位后各自醒来,然后输出它对应的数。这样最小的数对应的线程最早醒来,这个数最早被输出。等所有线程都醒来,排序就结束了。

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

算法思路很简单,但是存在一个问题,创建的线程数依赖于需要排序的数组的元素个数,因此这个算法暂且只能算是一个思路吧。

结语

这个算法不一定是史上最简单的排序算法,但却是最神奇的排序算法。神奇之处在于 大于号和小于号颠倒了却得到了正确的结果。

其实,我们完全可以用另外一个简单的思路来理解这个算法,那就是冒泡两次,第一次非递增排序,第二次非递减排序,算是负负得正,得到了正确的结果吧。

由于"最简单"算法的时间复杂度过高,其仅仅算是一种实现思路,也算是开拓一下思路,实际使用的时候,还是建议使用 十大经典排序算法。

今天的文章就到这里,下期见。

史上最简单的排序算法?看起来却满是bug的更多相关文章

  1. 史上最简单,一步集成侧滑(删除)菜单,高仿QQ、IOS。

    重要的话 开头说,not for the RecyclerView or ListView, for the Any ViewGroup. 本控件不依赖任何父布局,不是针对 RecyclerView. ...

  2. [分享] 史上最简单的封装教程,五分钟学会封装系统(以封装Windows 7为例)

    [分享] 史上最简单的封装教程,五分钟学会封装系统(以封装Windows 7为例) 踏雁寻花 发表于 2015-8-23 23:31:28 https://www.itsk.com/thread-35 ...

  3. (转) 史上最简单的 SpringCloud 教程 | 第一篇: 服务的注册与发现(Eureka)

    一.spring cloud简介 spring cloud 为开发人员提供了快速构建分布式系统的一些工具,包括配置管理.服务发现.断路器.路由.微代理.事件总线.全局锁.决策竞选.分布式会话等等.它运 ...

  4. 史上最简单的SpringCloud教程 | 第十篇: 高可用的服务注册中心(Finchley版本)

    转载请标明出处: 原文首发于 https://www.fangzhipeng.com/springcloud/2018/08/30/sc-f10-eureka/ 本文出自方志朋的博客 文章 史上最简单 ...

  5. 史上最简单的 SpringCloud 教程 | 第一篇: 服务的注册与发现Eureka(Finchley版本)

    转载请标明出处: 原文首发于:https://www.fangzhipeng.com/springcloud/2018/08/30/sc-f1-eureka/ 本文出自方志朋的博客 一.spring ...

  6. 史上最简单的 SpringCloud 教程

    史上最简单的 SpringCloud 教程 | 第一篇: 服务的注册与发现(Eureka)史上最简单的SpringCloud教程 | 第二篇: 服务消费者(rest+ribbon)史上最简单的Spri ...

  7. 史上最简单的 SpringCloud 教程 | 终章

    https://blog.csdn.net/forezp/article/details/70148833转载请标明出处:http://blog.csdn.net/forezp/article/det ...

  8. 史上最简单的 GitHub 教程

    史上最简单的 GitHub 教程 温馨提示:本系列博文已经同步到 GitHub,如有需要的话,欢迎大家到「github-tutorial」进行Star和Fork操作! 1 简介 GitHub 是一个面 ...

  9. 史上最简单的 MySQL 教程(十五)「列属性 之 自动增长」

    自动增长 自动增长:auto_increment,当对应的字段,不给值,或者是默认值,或者是null的时候,就会自动的被系统触发,系统会从当前字段中取已有的最大值再进行+1操作,得到新的字段值. 自增 ...

随机推荐

  1. python二级 第七套

    第一部分 基本操作 第一题 1.format()  故名思意  就是格式化什么东西.所以你就是将你 需要格式化的东西 放在里面就行了  .   format(s)  对s 有要求 就是 int(s) ...

  2. MFC读写.txt文件时进度条显示实时进度

    整体实现方式:先获得文件长度,然后用每次读取的长度,计算出完成的百分比,用百分比的值设置进度条. 一.MFC进度条 Progress Control 相关函数 1. create() --创建Prog ...

  3. Winform 控件命名规范

    前言 最近 Winform 项目做得比较多,控件命名规范上常用的能记住,但是有些总要查,写个记录吧.方便以后自己用,大家也可以参考. 标准控件 序号 控件类型简写 控件类型 1 btn Button ...

  4. 【vue】使用 Video.js 播放视频

    目录 安装 引入 使用 参考文档 环境: vue 2.0+ element ui (这里的代码用了elmentui的按钮样式,可以不用elment ui的样式) 安装 在项目中安装 video.js. ...

  5. FastAPI(59)- 详解使用 OAuth2PasswordBearer + JWT 认证

    JWT JSON Web Tokens 它是一个将 JSON 对象编码为密集且没有空格的长字符串的标准 使用 JWT token 和安全密码 hash 使应用程序真正安全 JWT 小栗子 eyJhbG ...

  6. Beta实际开发与初始计划的比较

    零.说明 本篇博客为Beta阶段开始十天后,实际开发工作与初始计划的比较 截止至本篇博客发布为止,团队所有成员已完成计网考试,将在本周日进行充分的接口测试 一.比较 1.与初始计划对比 初始计划 实际 ...

  7. oo第四单元及期末总结

    一.第四单元作业架构总结 第一次UML作业: 在分析各指令所需要的信息后建立了类(class),操作(operation),属性(Attribute)这几个类用来存储分析后的结果,而接口在本次作业中与 ...

  8. [源码解析] Pytorch 如何实现后向传播 (3)---- 引擎动态逻辑

    [源码解析] Pytorch 如何实现后向传播 (3)---- 引擎动态逻辑 目录 [源码解析] Pytorch 如何实现后向传播 (3)---- 引擎动态逻辑 0x00 摘要 0x01 前文回顾 0 ...

  9. Java RMI学习与解读(二)

    Java RMI学习与解读(二) 写在前面 接上篇文章,这篇主要是跟着看下整个RMI过程中的源码并对其做简单的分析 RMI源码分析 还是先回顾下RMI流程: 创建远程对象接口(RemoteInterf ...

  10. Python 调用上级目录的文件

    程序结构如下: – src |-- mod1.py |-- lib | |-- mod2.py |-- sub | |-- test.py 具体代码如下: 在test.py里调用mod1 mod2 i ...