用递归解决冒泡排序问题

news2024/11/19 22:35:01

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

由于两层循环都是与元素个数 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) {
    // TODO
    if (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 code
    if (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.coloradmin.cn/o/1896258.html

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

相关文章

【LabView学习篇 - 1】:初始LabView

文章目录 初始LabView前面板和程序框图前面板&#xff08;Front Panel&#xff09;程序框图&#xff08;Block Diagram&#xff09;交互和工作流程 练手小案例&#xff1a;LabView中实现加法操作 初始LabView LabVIEW&#xff08;Laboratory Virtual Instrument Engineering W…

学习笔记——动态路由——OSPF(基础配置)

九、OSPF基础配置 1、OSPF基础配置 <Huawei>sys [Huawei]sys AR1 [AR1]un in en //取消配置回馈信息 [AR1]int g0/0/0 [AR1-GigabitEthernet0/0/0]ip add 10.1.12.1 24 //给ar1路由接口0配置IP地址 # 配置OSPF [AR1-GigabitEthernet0/0/0]ospf 1 router…

Java中的日期时间类详解(Date、DateFormat、Calendar)

1. Date类 1.1 概述 java.util.Date类表示特定的瞬间&#xff0c;精确到毫秒。Date类的构造函数可以把毫秒值转成日期对象。 继续查阅Date类的描述&#xff0c;发现Date拥有多个构造函数&#xff0c;只是部分已经过时&#xff0c;我们重点看以下两个构造函数 1.2 Date类构造…

三界-欢迎来到Web3D+GIS学习天地!

三界-欢迎来到Web3DGIS学习天地&#xff01; 地址&#xff1a;threelab.cn ** 坚持封装自己的引擎已经有三年了&#xff0c;每天都是加班熬夜开发功能&#xff0c;做东西。 虽然这段时间内&#xff0c;我一直在业余时间坚持开发&#xff0c;但实际投入的开发时间并不长&#…

iOS 练习项目 Landmarks (五):UISwitch 切换展示数据

iOS 练习项目 Landmarks &#xff08;五&#xff09;&#xff1a;UISwitch 切换展示数据 iOS 练习项目 Landmarks &#xff08;五&#xff09;&#xff1a;UISwitch 切换展示数据引入 Lookin优化项目结构纯代码方式重写主界面设置详情页的返回按钮的文本Switch切换TableView展示…

通江银耳销售管理系统-计算机毕业设计源码15998

摘要 随着人们健康意识的增强&#xff0c;银耳这种传统的中药食材备受关注。而通江银耳是四川省通江县特产&#xff0c;中国国家地理标志产品。四川省通江县是银耳的发源地&#xff0c;中国银耳之乡&#xff0c;通江银耳因主产于此而得名&#xff0c;以其独到的质厚、肉嫩、易炖…

【linux高级IO(一)】理解五种IO模型

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:Linux从入门到精通⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学更多操作系统知识   &#x1f51d;&#x1f51d; Linux高级IO 1. 前言2. 重谈对…

Chrome插件分享-Stylus

简介 Stylus 是一个调整网页外观的用户样式管理器。它可以让您轻松为许多热门网站安装主题和皮肤。 这是 chrome 应用商店对Stylus插件的介绍&#xff0c;通俗一点讲&#xff0c;就是可以根据不同网站来定制网页的主题和皮肤&#xff0c;甚至可以去广告。 还有一个重点是&#…

高薪程序员必修课-java中 类加载器以及JVM类加载机制

前言 在Java中&#xff0c;类加载器&#xff08;ClassLoader&#xff09;是Java虚拟机&#xff08;JVM&#xff09;的一个重要组成部分&#xff0c;负责将.class文件加载到内存中并生成对应的Class对象。类加载器的主要任务是动态加载Java类&#xff0c;即在程序运行时根据需要…

刚办理的手机号被停用,你可能遇到这些问题了!

很多朋友都会遇到手机号被停用的情况&#xff0c;那么你知道你的手机号为什么会被停用吗&#xff1f;接下来&#xff0c;关于手机号被停用的问题&#xff0c;跟着小编一块来了解一下吧。 ​停机的两种形态&#xff1a; 1、第一个是局方停机&#xff0c;即语音、短信和流量都不…

Amazon SageMaker 机器学习之旅的助推器

一、前言 在当今的数字化时代&#xff0c;人工智能和机器学习已经成为推动社会进步的重要引擎。亚马逊云科技在 2023 re:Invent 全球大会上&#xff0c;宣布推出五项 Amazon SageMaker 新功能&#xff1a; Amazon SageMaker HyperPod 通过为大规模分布式训练提供专用的基础架构…

香橙派AIpro开发板评测:部署yolov5模型实现图像和视频中物体的识别

OrangePi AIpro 作为业界首款基于昇腾深度研发的AI开发板&#xff0c;自发布以来就引起了我的极大关注。其配备的8/20TOPS澎湃算力&#xff0c;堪称目前开发板市场中的顶尖性能&#xff0c;实在令人垂涎三尺。如此强大的板子&#xff0c;当然要亲自体验一番。今天非常荣幸地拿到…

Kubernetes基于helm安装 harbor

Kubernetes基于helm安装 harbor 之前harbor的安装都是借助docker完成一键安装部署&#xff0c;安装完成之后harbor组件均运行到一台机器上面&#xff0c;本文实践harbor在k8s环境中的部署。 准备工作 根据harbor官方要求&#xff1a; Kubernetes cluster 1.20Helm v3.2.0 …

kkFileView一款好用开源免费的文件在线预览项目

在这个数字化时代&#xff0c;我们每天都要跟各种文件打交道&#xff0c;但很多时候&#xff0c;文件预览却成了个头疼的问题&#xff0c;很多时候我们都希望能够在不下载文件的情况下&#xff0c;快速查看文件内容。 今天开源君就来分享一款文件在线预览项目 - kkFileView&am…

编译rust程序,并让它依赖低版本的GLIBC库

目录 方法一&#xff1a;在较低版本的linux系统里面编译更新centos源安装 gcc 方法二&#xff1a;静态编译 在linux环境下编译rust程序&#xff0c;编译好的程序会依赖你当前系统的GLIBC库&#xff0c;也就是说你的程序无法在使用更低版本GLIBC库的linux系统中运行。 查看当前系…

Java案例:完成用户登录

一案例要求&#xff1a; 二代码实现&#xff1a; Ⅰ package 重修;import java.util.Random; import java.util.Scanner;public class first {public static void main(String[] args) {javabean s1new javabean("张世杰","5201314");Scanner scnew Scan…

【Linux】Linux常用指令合集精讲,一篇让你彻底掌握(万字真言)

文章目录 一、文件与目录操作1.1 ls - 列出目录内容1.2 cd - 切换目录1.3 pwd - 显示当前目录1.4 mkdir - 创建目录1.5 rmdir - 删除空目录1.6 rm - 删除文件或目录1.7 cp - 复制文件或目录1.8 mv - 移动或重命名文件或目录1.9 touch - 创建空文件或更新文件时间戳 二、文件内容…

日期选取限制日期范围antdesign vue

限制选取的日期范围 效果图 <a-date-pickerv-model"dateTime"format"YYYY-MM-DD":disabled-date"disabledDate"valueFormat"YYYY-MM-DD"placeholder"请选择日期"allowClear />methods:{//回放日期选取范围限制&…

nginx 搭理禅道

1.安装nginx。 2.安装禅道。 3.nginx 配置文件 location /zentao/ { proxy_pass http://192.168.100.66/zentao/;proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_set_header X-F…