只会用 Go 写 O(N²) 的冒泡排序算法?来看看优化后最好情况下的 O(N) 算法吧

简介: 本文首先对冒泡排序进行简单的介绍,然后通过图片演示冒泡排序的思路。普通冒泡排序算法一共要遍历 n - 1 轮,由测试用例 [4 2 1 3 5] 的结果可以推断出 如果在一轮遍历中,没有进行元素交换位置的操作,那么此时数组的里所有元素都处于正确位置。 根据这个结论,对算法进行优化,优化后的算法,最好的情况下时间复杂度为 O(N)。

耐心和持久胜过激烈和狂热。

哈喽大家好,我是陈明勇,本文分享的内容是使用 Go 实现冒泡排序算法。如果本文对你有帮助,不妨点个赞,如果你是 Go 语言初学者,不妨点个关注,一起成长一起进步,如果本文有错误的地方,欢迎指出!

冒泡排序

冒泡排序是交换排序中最简单的一种算法。 算法思路:

  • 遍历数组,相邻的两个元素进行比较,以升序为例,如果前面的元素大于后面的元素,则将它们的位置进行交换
  • 第一轮遍历结束之后,最大的元素会处于所遍历范围的最后一个位置,然后继续下一轮遍历
  • 每轮都会固定一个元素,直到所有元素都被固定,因此会执行 n - 1轮,n 为元素的个数,也就是数组(切片)的长度。为什么会是 n - 1 而不是 n,因为到了第 n 轮,只剩下最后一个元素没有被固定,没有元素可以和它进行比较了,因此第 n 轮可以忽略。

图片演示

冒泡排序.png

  • 第一轮遍历 [4, 2, 1, 3]
  • i = 0 时,比较第 i 个元素 4 与第 i + 1 个元素 2 的大小,因为 nums[i] > num[i+1],也就是 4 > 2,因此交换它们的位置。
  • i = 1 时,4 > 1,互换位置。
  • i = 2 时,4 > 3,互换位置。最大值 4 被交换到最后一个位置,此时所有元素都参与比较过了,结束第一轮遍历,执行下一轮遍历。
  • 第二轮遍历 [2, 1, 3, 4]
  • i = 0 时,2 > 1,互换位置。
  • i = 1 时,2 < 3,不做交换。次大值 3 被交换到 4 的左边,此时所有元素都参与比较过了,结束第二轮遍历,执行下一轮遍历。
  • 第三轮遍历 [1, 2, 3, 4]
  • i = 0 时,1 < 2,不做交换。此时所有元素都参与比较过了,结束第三轮遍历,
  • 执行了 n - 1 轮遍历,n 为数组的长度,n - 1个元素被交换到正确的位置,第 n 轮遍历时,只剩最后一个元素,因此不用继续进行。

普通的冒泡排序算法

import "fmt"
func main() {
    nums := [4]int{4, 2, 1, 3}
    fmt.Println("原数组:", nums)
    fmt.Println("--------------------------------")
    NormalBubbleSort(nums)
}
func NormalBubbleSort(nums [4]int) {
    for i := 0; i < len(nums)-1; i++ {
        for j := 0; j < len(nums)-i-1; j++ {
            if nums[j] > nums[j+1] {
                    nums[j], nums[j+1] = nums[j+1], nums[j]
            }
        }
        fmt.Printf("第 %d 轮遍历后的数组:%v\n", i+1, nums)
    }
    fmt.Println("--------------------------------")
    fmt.Println("排序后的数组:", nums)
}
复制代码

执行结果:

原数组: [4 2 1 3]
--------------------------------
第 1 轮遍历后的数组:[2 1 3 4]
第 2 轮遍历后的数组:[1 2 3 4]
第 3 轮遍历后的数组:[1 2 3 4]
--------------------------------
排序后的数组: [1 2 3 4]
复制代码
  • 值得注意的一个地方是第二层循环的条件 j < len(nums)-i-1,为什么会减去 i,因为每轮遍历结束之后,都会有一个元素被固定到后面,因此再进行下一轮的时候,那个元素无须再进行比较。
  • 算法遍历次数为 n -1,每次遍历时元素比较的次数依次为 n - 1、n - 2、n - 3、···、3、2、1,将所有次数求和 = 1 + 2 + 3 + ··· + n - 2 + n - 1= n - 1 * (n - 1 + 1) / 2 = (n² - 1) / 2,因此时间复杂度为 O(n²)。

优化算法

上述例子中,对数组 [4,2,1,3] 进行排序,我们来看看对数组 [4,2,1,3,5] 进行排序,打印数组排序的变化过程中:

原数组: [4 2 1 3 5]
--------------------------------
第 1 轮遍历后的数组:[2 1 3 4 5]
第 2 轮遍历后的数组:[1 2 3 4 5]
第 3 轮遍历后的数组:[1 2 3 4 5]
第 4 轮遍历后的数组:[1 2 3 4 5]
--------------------------------
排序后的数组: [1 2 3 4 5]
复制代码

不难看出,第三轮与第四轮遍历过程中,都没有进行元素交换位置的操作,对此我们可以推出一个结论,如果在一轮遍历中,没有进行元素交换位置的操作,那么此时数组的里所有元素都处于正确位置。 根据这个结论,我们可以对算法进行优化:

import "fmt"
func main() {
    nums := [5]int{4, 2, 1, 3, 5}
    fmt.Println("原数组:", nums)
    fmt.Println("--------------------------------")
    BestBubbleSort(nums)
}
func BestBubbleSort(nums [5]int) {
    isSwapped := true
    for isSwapped {
        isSwapped = false
        for i := 0; i < len(nums)-1; i++ {
            if nums[i] > nums[i+1] {
                nums[i], nums[i+1] = nums[i+1], nums[i]
                isSwapped = true
            }
        }
        fmt.Println("遍历后的数组:", nums)
    }
    fmt.Println("--------------------------------")
    fmt.Println("排序后的数组:", nums)
}
复制代码

执行结果:

原数组: 
--------------------------------
遍历后的数组: [2 1 3 4 5]
遍历后的数组: [1 2 3 4 5]
遍历后的数组: [1 2 3 4 5]
--------------------------------
排序后的数组: [1 2 3 4 5]
复制代码
  • 定义交换的标记变量 isSwapper,作为第一层循环的条件,每轮遍历开始之后,将标记变量  isSwapper 赋值为 false,如果在比较的过程中发生元素交换,则将标记变量 isSwapper 赋值为 true。直到 isSwapperfalse 时,数组的里所有元素都处于正确的位置,此时可以结束遍历了。
  • 根据执行结果可知,相比普通的算法,优化后的算法少了一轮遍历,这只是在数组元素少的情况下,如果在数组元素多的情况下,对比结果会更明显。
  • 如果数组为 [5,1,2,3,4],那么算法只会遍历一轮,就能得到正确的排序结果。因此优化后的算法,最好的情况下时间复杂度为 O(N),最坏的情况下仍为 O(N²)。

小结

本文首先对冒泡排序进行简单的介绍,然后通过图片演示冒泡排序的思路。普通冒泡排序算法一共要遍历 n - 1 轮,由测试用例 [4 2 1 3 5] 的结果可以推断出 如果在一轮遍历中,没有进行元素交换位置的操作,那么此时数组的里所有元素都处于正确位置。 根据这个结论,对算法进行优化,优化后的算法,最好的情况下时间复杂度为 O(N)。

目录
相关文章
|
9天前
|
算法 关系型数据库 MySQL
分布式唯一ID生成:深入理解Snowflake算法在Go中的实现
在分布式系统中,确保每个节点生成的 ID 唯一且高效至关重要。Snowflake 算法由 Twitter 开发,通过 64 位 long 型数字生成全局唯一 ID,包括 1 位标识位、41 位时间戳、10 位机器 ID 和 12 位序列号。该算法具备全局唯一性、递增性、高可用性和高性能,适用于高并发场景,如电商促销时的大量订单生成。本文介绍了使用 Go 语言的 `bwmarrin/snowflake` 和 `sony/sonyflake` 库实现 Snowflake 算法的方法。
21 1
分布式唯一ID生成:深入理解Snowflake算法在Go中的实现
|
14天前
|
算法 调度
基于遗传模拟退火混合优化算法的车间作业最优调度matlab仿真,输出甘特图
车间作业调度问题(JSSP)通过遗传算法(GA)和模拟退火算法(SA)优化多个作业在并行工作中心上的加工顺序和时间,以最小化总完成时间和机器闲置时间。MATLAB2022a版本运行测试,展示了有效性和可行性。核心程序采用作业列表表示法,结合遗传操作和模拟退火过程,提高算法性能。
|
15天前
|
人工智能 算法 大数据
Linux内核中的调度算法演变:从O(1)到CFS的优化之旅###
本文深入探讨了Linux操作系统内核中进程调度算法的发展历程,聚焦于O(1)调度器向完全公平调度器(CFS)的转变。不同于传统摘要对研究背景、方法、结果和结论的概述,本文创新性地采用“技术演进时间线”的形式,简明扼要地勾勒出这一转变背后的关键技术里程碑,旨在为读者提供一个清晰的历史脉络,引领其深入了解Linux调度机制的革新之路。 ###
|
25天前
|
人工智能 算法 数据安全/隐私保护
基于遗传优化的SVD水印嵌入提取算法matlab仿真
该算法基于遗传优化的SVD水印嵌入与提取技术,通过遗传算法优化水印嵌入参数,提高水印的鲁棒性和隐蔽性。在MATLAB2022a环境下测试,展示了优化前后的性能对比及不同干扰下的水印提取效果。核心程序实现了SVD分解、遗传算法流程及其参数优化,有效提升了水印技术的应用价值。
|
26天前
|
搜索推荐
冒泡排序算法
【10月更文挑战第19天】冒泡排序是一种基础的排序算法,虽然在实际应用中可能不是最优的选择,但对于理解排序算法的基本原理和过程具有重要意义。
|
24天前
|
存储 缓存 算法
优化轮询算法以提高资源分配的效率
【10月更文挑战第13天】通过以上这些优化措施,可以在一定程度上提高轮询算法的资源分配效率,使其更好地适应不同的应用场景和需求。但需要注意的是,优化策略的选择和实施需要根据具体情况进行详细的分析和评估,以确保优化效果的最大化。
|
25天前
|
并行计算 算法 IDE
【灵码助力Cuda算法分析】分析共享内存的矩阵乘法优化
本文介绍了如何利用通义灵码在Visual Studio 2022中对基于CUDA的共享内存矩阵乘法优化代码进行深入分析。文章从整体程序结构入手,逐步深入到线程调度、矩阵分块、循环展开等关键细节,最后通过带入具体值的方式进一步解析复杂循环逻辑,展示了通义灵码在辅助理解和优化CUDA编程中的强大功能。
|
26天前
|
存储 缓存 算法
前端算法:优化与实战技巧的深度探索
【10月更文挑战第21天】前端算法:优化与实战技巧的深度探索
21 1
|
28天前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
5天前
|
算法 数据安全/隐私保护 索引
OFDM系统PAPR算法的MATLAB仿真,对比SLM,PTS以及CAF,对比不同傅里叶变换长度
本项目展示了在MATLAB 2022a环境下,通过选择映射(SLM)与相位截断星座图(PTS)技术有效降低OFDM系统中PAPR的算法实现。包括无水印的算法运行效果预览、核心程序及详尽的中文注释,附带操作步骤视频,适合研究与教学使用。
下一篇
无影云桌面