用递归解决冒泡排序问题

冒泡排序是种很简单的排序方式. 如果用循环方式, 通常就是两层循环.

由于两层循环都是与元素个数 N 线性相关, 所以可以简单估算出它的时间复杂度是 O(N2), 通常而言, 这是较糟糕的复杂度.

当然, 这也几乎是所有简单方式的宿命, 想简单就别想效率高!

前面篇章说到递归也是一种循环, 所以普通循环能解决的问题, 用递归也能解决. 我们来看看怎么写出它来.

定义好接口先

首先啰嗦几句写代码习惯问题:

要有服务意识, 要有 API 意识.
先把接口定义好, 与具体的实现分开. 另外则作一些基本的判断, 内部实现就不需要再作判断了, 也能简化内部实现:

// 对外接口
public static void sort(int[] array) {if (array != null && array.length > 1) {bubbleSort(array);}
}// 具体内部实现
private static void bubbleSort(int[] array) {// TODO
}

想更加通用就用 Comparable 接口, 不过这些不是这里的重点, 这里就简单用 array 了.

主体结构

前面说到, 递归分成 基本情形(base case)递归情形(recursive case), 所以, 不管三七二十一, 先把主体结构写出来:

private static void bubbleSort(int[] array) {// TODOif (baseCase) {return;} else {// 1. do something maybe// ...// 2. 递归调用// bubbleSort(xxx);}
}

基本情形(base case)

这个简单, 当只有一个元素时, 自然就无需排序了, 所以:

if (array.length == 1) {return;
}

显然, base case 没做任何事, 因此只要把代码条件反转一下, 就可以省略掉这个 base case 了.

递归情形(recursive case)

如何找出递归模式呢? 我们从一个具体的事例考虑:

假如有四个元素: 6 9 8 4, 假设向右边冒泡, 那么从左向右两两比较并根据情况决定是否交换:

第一次比较 6 和 9, 6 比 9 小, 无需交换, 继续;

第二次比较 9 和 8, 9 比 8 大, 所以交换它们得到: 6 8 9 4;

第三次比较 9 和 4, 9 比 4 大, 交换, 得到: 6 8 4 9.

那么, 经过一轮冒泡之后, 最大的元素 9 已经冒泡到了最右边上, 然后下一步就可以只考虑剩下的三个元素了, 而这正好可视作一个新的待排序的数组.

先不考察冒泡的细节, 用一个抽象的 bubble 表示它, 那么可以归纳出如下递归模式:

bubbleSort 一个元素为 N 的数组就是:

  1. 进行一轮冒泡(bubble),
  2. 然后对左边的 N-1 个元素(子数组)递归地进行 bubbleSort.

如下图示:

冒泡排序 递归方式

据此, 写出以下代码(省略了 base case):

private static void bubbleSort(int[] array) {// TODO pseudo codeif (array.length > 1) {bubble(array);bubbleSort(array.subArray(0, array.length - 1));}
}

你可能会想, array 并没有 subArray 方法呀, 另外应该是在原数组上排序.

没错, 这里也注明了这是伪代码(pseudo code), 此刻只需要思考抽象的逻辑是否有问题即可, 诸如是否会发生无限递归呀等情况.

敲定细节

代码中还是伪代码的形式, 显然我们是要在原数组上排序, 这使得我们必须增加一个显式的参数.

如果 array 有更加对象化的方法来能直观表示 subArray(并非指复制的那种, 而是一种指向新位置的引用), 那么增加参数就不是必要的了.

目前我们无法摆脱 array 的结构限制去思考. (这其实暴露了 java 在表达能力上的缺陷或者说 array 的设计上存在一些问题, 使得我们只能笨拙地去表达我们想要的意思)

根据前面的归纳, 定为 N-1 来表示子数组 subArray:

bubbleSort(n – 1, array);

注: 你可能忍不住会把这里的 n 想像成 array 的 index. 没错, 它最终是要映射到 index 上, 但此刻你只要抽象地从概念上把握它即可, 这里的 n 就是指 n 个元素, 它不是什么 index.

方法要形成递归, 所以定义处也修改, 增加一参数 n:

private static void bubbleSort(int n, int[] array)

公共接口里调用时也因此要作改动, 需要显式传入 N, 对于初始情况, N 也即是 array.length:

bubbleSort(array.length, array);

另外有一点要特别注意, 因为不是传递 subArray, array.length 始终不变, 所以 if 语句中的判断已经不正确:

必须由 if(array.length > 1) 改为 if(n > 1)

现在考察 bubble, 显然 N 跟 array 都要往里面传. 现在直接将 bubble 方法考虑成递归的, 以减少中间环节. 那么, 还要再增加一个参数 bn, 表示有几个元素需要 bubble.

让 IDE 生成方法签名, 最终结果如下:

// 对外接口
public static void sort(int[] array) {if (array != null && array.length > 1) {bubbleSort(array.length, array);}
}// 具体内部实现
private static void bubbleSort(int n, int[] array) {if (n > 1) {bubble(n, n, array);bubbleSort(n - 1, array);}
}private static void bubble(int bn, int n, int[] array) {// TODO
}

注: bubble(n, n, array)

第一个 n 表示有 n 个元素需要冒泡;

第二个 n 表示有 n 个元素需要排序.

其实你也能感觉到, 这完全就是面向过程式的编程.

至此, 主体结构已经 OK 了. 现在考虑冒泡 bubble 的实现问题.

冒泡子过程

冒泡同样可视作一个递归的过程. 按着一样的思路, 先是一个大体的框架:

private static void bubble(int bn, int n, int[] array) {if (baseCase) {return;} else {// 1. do something// ...// 2. 递归调用// bubble(xx, n, array);}
}

基本情形(base case)

显然, bn==1 时表示只有一个元素, 就无需冒泡了, 直接返回:

if (bn == 1) {return;
}

显然, 这一 case 也是可以省略的.

递归情形(recursive case)

其实就是一个从左到右不断比较与交换的过程. 根据前面的例子, 同样可以归纳出它的递归模式如下:

bubble一个元素为 N 的数组就是:

  1. 对第一个元素(以及它后续的那个元素)进行一次比较和交换(compareAndSwitch),
  2. 然后对余下的 N-1 个元素递归地进行 bubble.

比较与交换同样的先用一个抽象的过程表示, 据此, 得出代码如下(同样省略了 base case):

private static void bubble(int bn, int n, int[] array) {if (bn > 1) {compareAndSwitch(bn, n, array);bubble(bn - 1, n, array);}
}private static void compareAndSwitch(int bn, int n, int[] array) {// TODO
}

你会发现这里的 bubble 与前面的 bubbleSort 在形式上是非常像的.

有些人可能会在 bn 或者 n 的实际含义上纠结, 总是忍不住把它们往 array 的 index 上去想, 这实际上还是没能摆脱往具体实现层面上去思考的惯性. 我们应该抽象地去思考, 不要老惦记着 array, 其实关键就在于要表达清楚我们的意思, 这样就行了.

我们最终肯定要处理 index 的问题, 但目前还不是时候, 你应该坚信 index 肯定能用 bn 及 n 表达出来, 你只需往下传递它们就是了.

比较与交换

现在终于要处理让我们有点头痛的 index 问题了.

我们只需考察最简单的两个元素第一次比较的情况, 此时 n=2, bn=2, 我们要比较 array[0], array[1], 显然 0=n-bn, 也即是要比较 array[n-bn] 和 array[n-bn+1].

最终结果如下:

private static void compareAndSwitch(int bn, int n, int[] array) {int index = n - bn;if (array[index] > array[index + 1]) {int temp = array[index];array[index] = array[index + 1];array[index + 1] = temp;}
}

你可能有点怀疑, 这样就正确了吗? 我们可以实际测试一下即可, 通常八九不离十.

改进

如果一轮 bubble 没有发生任何交换, 意味着数组已经是有序的了, 可以提前 break. 可以引入一个 boolean 来达到这一点, 这里就不演示了. 最终的代码如下:

// 对外接口
public static void sort(int[] array) {if (array != null && array.length > 1) {bubbleSort(array.length, array);}
}// 具体内部实现
private static void bubbleSort(int n, int[] array) {if (n > 1) {bubble(n, n, array);bubbleSort(n - 1, array);}
}// 冒泡
private static void bubble(int bn, int n, int[] array) {if (bn > 1) {compareAndSwitch(bn, n, array);bubble(bn - 1, n, array);}
}// 比较与交换
private static void compareAndSwitch(int bn, int n, int[] array) {int index = n - bn;if (array[index] > array[index + 1]) {int temp = array[index];array[index] = array[index + 1];array[index + 1] = temp;}
}

总结

其实写递归程序的套路经常是比较固定的, 大的方面就是一个 if else, 递归部分可以放 if, 也可以放在 else, 就看 if 条件是如何判断了.

然后就是找出递归模式, 把条件填充上去. 暂时不明朗的地方, 可以先用一个抽象来表达, 然后逐步地把细节完善.

对递归程序而言, 通常一旦正确了, 它就是正确的.

当然, 这好像是一句废话. 为什么这么说呢? 要知道正确的方式通常只有一种, 而错误的方式则有千万种.

俄国有位师太(叫托尔斯泰), 在他的<<安娜·卡列妮娜>>中曾经曰过: "幸福的家庭总是相似的, 不幸的家庭各有各的不幸. "

‘All happy families are alike; each unhappy family is unhappy in its own way.’–Leo Tolstoy Anna Karenina

所以, 这里真正想表达的意思是, 递归式通常更为精练, 没有过多枝枝蔓蔓的东西. 它的环节比较少, 过程式的东西较少, 写法通常也很固定, 因而减少了你犯错的机会. 你只要找准了终止条件和递归模式, 它通常就自然而然的正确了.

另外则是要强调抽象的思考问题, 不要过早地陷于具体实现及细节的纠缠中, 而是要一步步地最终使得一切水落石出.

当然, 就这个例子而言, 比起普通循环而言, 并没有什么优势.

递归反而可能显得有点拖沓, 这里不过是想说明普通循环能解决的问题也能用递归来解决;

另一方面, 如果还不习惯于递归式的表达, 恐怕写起来比普通循环更费劲;

此外, 除非是简单小量的排序, 否则性能也是个问题.

在下一篇章中, 将探讨一个更加复杂的例子, 也即是前面提到的求换零钱种数的问题, 那时才能体现出递归的优势来.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1473631.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

基于Java技术的人事管理系统

你好&#xff0c;我是专注于计算机科学领域的小野。如果你对人事管理系统感兴趣或有相关需求&#xff0c;欢迎私信交流。 开发语言&#xff1a; Java 数据库&#xff1a; MySQL 技术&#xff1a; B/S模式、Java技术、SpringBoot 工具&#xff1a; Eclipse、MySQL、浏览…

【C++:类的基础认识和this指针】

C的类与C语言的struct结构体有啥区别&#xff1f; 默认的访问限定符不同 类的简要 关键字&#xff1a;class{}里面是类的主体&#xff0c;特别注意&#xff1a;{}后面的&#xff1b;不可以省略类中的变量叫做成员变量&#xff0c;类中的函数叫做成员函数类中访问有三种访问权限…

HTML5使用<blockquote>标签:段落缩进

使用<blockquote>标签可以实现页面文字的段落缩进。这一标签也是每使用一次&#xff0c;段落就缩进一次&#xff0c;并且可以嵌套使用&#xff0c;以达到不同的缩进效果。语法如下&#xff1a; <blockquote>文字</blockquote> 【实例】使用<blockquote&…

谷粒商城----通过缓存和分布式锁获取数据。

高并发下缓存失效的问题 高并发下缓存失效的问题--缓存穿透 指查询一个一定不存在的数据&#xff0c;由于缓存是不命中&#xff0c;将去查询数据库&#xff0c;但是数据库也无此记录&#xff0c;我们没有将这次查询的不写入缓存&#xff0c;这将导致这个不存在的数据每次请求…

工厂模式之简单工厂模式

文章目录 工厂模式工厂模式分为工厂模式的角色简单工厂模式案例代码定义一个父类&#xff0c;三个子类定义简单工厂客户端使用输出结果 工厂模式 工厂模式属于创造型的模式&#xff0c;用于创建对象。 工厂模式分为 简单工厂模式&#xff1a;定义一个简单工厂类&#xff0c;根…

VuePress 的更多配置

现在&#xff0c;读者应该对 VuePress、主题和插件等有了基本的认识&#xff0c;除了插件&#xff0c;VuePress 自身也有很多有用的配置&#xff0c;这里简单说明下。 ‍ ‍ VuePress 的介绍 在介绍了 VuePress 的基本使用、主题和插件的概念之后&#xff0c;我们再来看看官…

【MySQL】1.初识MySQL

初识MySQL 一.MySQL 安装1.卸载已有的 MySQL2.获取官方 yum 源3.安装 MySQL4.登录 MySQL5.配置 my.cnf 二.MySQL 数据库基础1.MySQL 是什么&#xff1f;2.服务器&#xff0c;数据库和表3.mysqld 的层状结构4.SQL 语句分类 一.MySQL 安装 1.卸载已有的 MySQL //查询是否有相关…

vue事件参数

事件参数 事件参数可以获取event对象和通过事件传递数据 获取event对象 <template> <buttonclick"addCount">点击</button><p>count is: {{ count }}</p><p>{{ coutent_e }}</p> </template> <script>expor…

昇腾910B部署Qwen2-7B-Instruct进行流式输出【pytorch框架】NPU推理

目录 前情提要torch_npu框架mindsport框架mindnlp框架 下载模型国外国内 环境设置代码适配&#xff08;非流式&#xff09;MainBranch结果展示 代码适配&#xff08;流式&#xff09; 前情提要 torch_npu框架 官方未适配 mindsport框架 官方未适配 mindnlp框架 官方适配…

25.【C语言】循环结构之for 上

1.基本使用 类比while 在while循环中&#xff0c;有三个不可或缺的部分&#xff1a;初始化&#xff0c;判断部分&#xff0c;调整部分 int i 0;//初始化 while (i < 10)//判断部分 {……i;//调整部分 }三个部分太分散&#xff0c;用for循环可集为一体&#xff0c;简洁 …

如何使用uer做多分类任务

如何使用uer做多分类任务 语料集下载 找到这里点击即可 里面是这有json文件的 因此我们对此要做一些处理&#xff0c;将其转为tsv格式 # -*- coding: utf-8 -*- import json import csv import chardet# 检测文件编码 def detect_encoding(file_path):with open(file_path,…

使用flask的web网页部署介绍

使用flask的web网页部署介绍 文章目录 前言一、网页介绍二、数据库设计介绍总结 前言 flaskbootstrapjquerymysql搭建三叶青在线识别网站&#xff0c;使用nginxgunicorn将网站部署在腾讯云上&#xff0c;配置SSL证书。网站地址&#xff1a;https://www.whtuu.cn 三叶青图像识…

Android增量更新----java版

一、背景 开发过程中&#xff0c;随着apk包越来越大&#xff0c;全量更新会使得耗时&#xff0c;同时浪费流量&#xff0c;为了节省时间&#xff0c;使用增量更新解决。网上很多文章都不是很清楚&#xff0c;没有手把手教学&#xff0c;使得很多初学者&#xff0c;摸不着头脑&a…

爬虫笔记20——票星球抢票脚本的实现

以下内容仅供交流学习使用&#xff01;&#xff01;&#xff01; 思路分析 前面的爬虫笔记一步一步走过来我们的技术水平也有了较大的提升了&#xff0c;现在我们来进行一下票星球抢票实战项目&#xff0c;实现票星球的自动抢票。 我们打开票星球的移动端页面&#xff0c;分…

KDTree 简单原理与实现

介绍 K-D树是一种二叉树的数据结构&#xff0c;其中每个节点代表一个k维点&#xff0c;可用于组织K维空间中的点&#xff0c;其中K通常是一个非常大的数字。二叉树结构允许对多维空间中的点进行非常有效的搜索&#xff0c;包括最近邻搜索和范围搜索&#xff0c;树中的每个非叶…

Newport太阳光模拟器MSOL-UV-X使用说明手侧

Newport太阳光模拟器MSOL-UV-X使用说明手侧

死锁-活锁与活锁的预防、死锁与死锁的预防和检测(处理死锁的方式:事务等待图)

一、引言 1、死锁是因采用封锁技术实现并发控制而产生的一种运行事务被阻塞或等待的现象 2、如果利用严格两阶段封锁协议来解决我们前面提到的“更新丢失”这种数据不一致问题&#xff0c;非串行调度中的事务T1首先获得数据对象X上的读锁并开始执行&#xff0c;随后事务T2也获…

算法库应用--Brute - Force算法串匹配(顺序串)

学习贺利坚老师关于B-F算法的算法库 数据结构例程——串的模式匹配&#xff08;Brute-Force算法&#xff09;_sqstring s, t; strassign(s,"ababcabcacbabcaccab");-CSDN博客 本人规则解析博客 串的匹配 (Brute - Force 算法)_brute force算法-CSDN博客\ 版本更新日志…

在5G/6G应用中实现高性能放大器的建模挑战

来源&#xff1a;Modelling Challenges for Enabling High Performance Amplifiers in 5G/6G Applications {第28届“集成电路和系统的混合设计”(Mixed Design of Integrated Circuits and Systems)国际会议论文集&#xff0c;2021年6月24日至26日&#xff0c;波兰洛迪} 本文讨…

跟着峰哥学java 第四天 商品分类 前后端显示

1.后端 1.1mybatis-plus分页查询配置 在商品热卖数据中&#xff0c;只让其显示八条数据 将要使用分页 也就是service.page方法 此时需要配置 mp拦截器 Configuration public class MybatisPlusConfig {Beanpublic PaginationInterceptor paginationInterceptor() {return …