10 排序算法:冒泡排序与快速排序(算法原理、算法实现、时间和空间复杂度分析)

目录

1 十大常见的排序算法

1.1 算法的稳定性

2 冒泡排序

2.1 算法原理

2.2 算法实现

2.3 时间空间复杂度分析

2.3.1 时间复杂度分析

2.3.2 空间复杂度分析

3 快速排序

3.1 算法原理

3.1.1 排序思想

3.1.2 递归过程

3.2 示例

3.2.1 示例 1

3.2.2 示例 2

3.2.3 示例 3

3.3 算法实现

3.4 时间空间复杂度分析

3.4.1 时间复杂度分析

3.4.2 空间复杂度分析


1 十大常见的排序算法

        排序算法众多,实现方式各异,其时间复杂度、空间复杂度和稳定性各不相同,如下图所示:

1.1 算法的稳定性

  • 稳定排序算法:如果一个排序算法在排序前后,相等的元素之间的相对顺序保持不变,那么这个算法是稳定的
  • 不稳定排序算法:如果一个排序算法在排序前后,相等的元素之间的相对顺序可能发生改变,那么这个算法是不稳定的

        假设有一个学生记录列表,每个记录包含学生的姓名和成绩:

[("Alice", 85), ("Bob", 90), ("Charlie", 85), ("David", 90)]

按成绩升序排序:

  • 稳定排序算法

    • 排序结果:[("Alice", 85), ("Charlie", 85), ("Bob", 90), ("David", 90)]
    • 相同成绩的学生(85 分的 Alice 和 Charlie,90 分的 Bob 和 David)保持了原来的顺序
  • 不稳定排序算法

    • 排序结果:[("Charlie", 85), ("Alice", 85), ("David", 90), ("Bob", 90)]
    • 相同成绩的学生的顺序可能发生变化。

2 冒泡排序

2.1 算法原理

        冒泡排序是一种直观且易于理解的排序算法,其基本思想是通过不断地交换相邻的、顺序错误的元素,逐步将较大的元素(如果是升序排列的话)像水泡一样 “浮” 到序列的末尾。具体步骤如下:

        1. 比较相邻元素:从数组的第一个元素开始,逐一比较相邻的两个元素。

        2. 交换位置:如果前一个元素大于后一个元素(对于升序排序而言),则交换这两个元素的位置。如果是降序排序,则当发现前一个元素小于后一个元素时进行交换。

        3. 一趟遍历完成:经过一次完整的遍历后,数组中最大的元素(升序时)或最小的元素(降序时)会被移动到数组的最后一个位置。

        4. 重复遍历与交换:在排除了已排序的最后一个元素后,再次从头开始重复第 1 步和第 2 步的操作,直到整个数组中的所有元素都被正确地排序。

        5. 循环遍历直至完全排序:整个过程会不断重复,每完成一次遍历就使得更多的元素处于正确的位置,直到没有更多的交换发生,即整个数组被完全排序为止。

        通过上述步骤,冒泡排序能够有效地对一组数字进行排序,尽管它的效率在大规模数据集上不如更高级的排序算法,如快速排序或归并排序,但对于小规模数据集来说,冒泡排序仍然是一个很好的选择

        如下图所示:

2.2 算法实现

#include <stdio.h>// 冒泡排序函数
void bubbleSort(int arr[], int size)
{// 外层循环控制遍历次数(n-1)次for (int i = 0; i < size - 1; i++){// 内层循环进行相邻元素的比较和交换 (n-1-i)次for (int j = 0; j < size - i - 1; j++){// 如果前一个元素大于后一个元素,则交换if (arr[j] > arr[j + 1]){// 交换 arr[j] 和 arr[j+1]int temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;}}}
}int main()
{// 定义一个待排序的数组int arr[] = {3, 1, 5, 4, 2};int size = sizeof(arr) / sizeof(arr[0]); // 计算数组长度// 输出原始数组printf("原始数组: ");for (int i = 0; i < size; i++){printf("%d ", arr[i]);}printf("\n");// 调用冒泡排序函数bubbleSort(arr, size); // 传递数组名即传递数组首元素的地址// 输出排序后的数组printf("排序后的数组: ");for (int i = 0; i < size; i++){printf("%d ", arr[i]);}printf("\n");return 0;
}

        输出结果如下所示: 

        下面是优化后的冒泡排序:

#include <stdio.h>// 冒泡排序函数
void bubbleSort(int arr[], int size)
{// 外层循环控制遍历次数 (n-1)次for (int i = 0; i < size - 1; i++){// 设置标志位,用于检测本轮是否有元素交换int swapped = 0;// 内层循环进行相邻元素的比较和交换 (n-1-i)次for (int j = 0; j < size - i - 1; j++){// 如果前一个元素大于后一个元素,则交换if (arr[j] > arr[j + 1]){// 交换 arr[j] 和 arr[j+1]int temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;// 标记发生了交换swapped = 1;}}// 如果没有发生任何交换,说明数组已经有序,提前退出,避免不必要的遍历。if (!swapped){break;}}
}

优化点解释:

        设置标志位 swapped

  • 在每次内层循环开始时,设置一个标志位 swapped 为 0。
  • 如果在内层循环中发生了元素交换,将 swapped 设置为 1。
  • 内层循环结束后,检查 swapped 是否仍为 0。如果是,则说明数组已经有序,可以提前退出外层循环,避免不必要的遍历

注意:

        即使外层循环 i < size - 1 不小心写成了 i < size,内层循环 j < size - i - 1 不小心写成了 j < size - 1,最终的结果也不会错。这是因为在这些情况下,虽然会进行一些多余的比较和交换,但不会影响最终的排序结果。

  1. 外层循环 i < size

    • 正确的外层循环条件是 i < size - 1,这样可以确保在最后一趟遍历时,只剩下最后一个元素,而这个元素已经是有序的。
    • 如果写成 i < size,外层循环会多进行一次遍历,即进行 size 次遍历。
    • 在第 size 次遍历时,内层循环 j < size - 1 会比较所有相邻的元素,但由于数组已经排序完成,不会有元素交换发生。因此,这次遍历是多余的,但不会影响最终的排序结果。
  2. 内层循环 j < size - 1

    • 正确的内层循环条件是 j < size - i - 1,这样可以确保每次遍历只比较未排序部分的元素。
    • 如果写成 j < size - 1,内层循环会比较所有相邻的元素,包括已经排序好的部分。
    • 由于已经排序好的部分不会再发生交换,这些多余的比较不会影响最终的排序结果。

        尽管这些错误不会影响最终的排序结果,但会导致不必要的比较和交换,从而降低算法的效率。因此,建议使用正确的循环条件以提高性能。

2.3 时间空间复杂度分析

2.3.1 时间复杂度分析

  • 最坏情况(Worst Case)

    • 当输入数组是完全逆序的时,冒泡排序需要进行最多的比较和交换操作
    • 每次内层循环都需要进行 n−1 次比较和最多 n−1 次交换
    • 因此,总的时间复杂度为 O(n^2)
  • 最好情况(Best Case)

    • 当输入数组已经是有序的时,冒泡排序只需要进行一次完整的遍历,而且不会进行任何交换
    • 使用优化后的冒泡排序(带有标志位 swapped),可以在第一次遍历后就提前结束。
    • 因此,总的时间复杂度为 O(n)
  • 平均情况(Average Case)

    • 对于随机分布的数组,冒泡排序的时间复杂度通常也是 O(n^2)
    • 这是因为在大多数情况下,数组不会完全有序,也不会完全逆序,但仍然需要多次比较和交换。

2.3.2 空间复杂度分析

  • 原地排序
    • 冒泡排序是一种原地排序算法,即它只需要常数级的额外存储空间。
    • 主要的额外空间用于临时变量(如交换元素时使用的 temp 变量)。
    • 因此,空间复杂度为 O(1)

3 快速排序

3.1 算法原理

        快速排序(Quick Sort)是由图灵奖获得者 Tony Hoare 发明的一种高效排序算法,被列为 20 世纪十大算法之。快速排序以其卓越的性能和简洁的实现方式,在实际应用中非常广泛,尤其是在处理大规模数据时表现尤为出色。快速排序的时间复杂度为 O(n log n)通常比其他同样具有 O(n log ⁡n) 时间复杂度的排序算法更快

3.1.1 排序思想

        快速排序的核心思想是 “分治法”,即通过递归的方式将大问题分解成小问题来解决。具体步骤如下:

        1. 选择基准(Pivot)从数列中挑选一个元素作为 “基准”(pivot)。选择基准的方法有多种,常见的包括选择第一个元素、最后一个元素、中间元素或随机选择一个元素。

        2. 分区(Partition)重新排列数列,使得所有比基准值小的元素都摆放在基准的前面,所有比基准值大的元素都摆放在基准的后面。相同的元素可以任意放置。在这个分区操作结束之后,基准元素会处于数列的中间位置,这个位置称为基准的最终位置。

        3. 递归排序递归地对基准左侧的子数列和右侧的子数列进行快速排序。递归的最底层情况是子数列的大小为零或一,这时子数列已经自然排序好了。

3.1.2 递归过程

  • 递归的最底层当子数列的大小为零或一时,递归结束。这是因为单个元素或空数列自然是有序的。

  • 递归的迭代在每次递归调用中,快速排序都会将一个元素放到其最终位置。因此,随着递归的进行,越来越多的元素会被正确排序,最终整个数列将变得有序。

3.2 示例

3.2.1 示例 1

        假设有一个数列 [3, 6, 8, 10, 1, 2, 1],我们选择第一个元素 3 作为基准:

1. 分区

  • 重新排列数列,使得所有比 3 小的元素在左边,所有比 3 大的元素在右边。结果可能是 [1, 2, 1, 3, 10, 6, 8]

2. 递归排序

  • 对基准左侧的子数列 [1, 2, 1] 进行快速排序。
  • 对基准右侧的子数列 [10, 6, 8] 进行快速排序。

3. 递归结束

  • 当子数列的大小为零或一时,递归结束。

        通过上述步骤,快速排序能够高效地对数列进行排序。尽管快速排序在最坏情况下(例如数列已经有序或完全逆序)的时间复杂度退化为 O(n^2),但在实际应用中,通过合理选择基准和优化分区操作,快速排序通常能保持其优秀的性能。

3.2.2 示例 2

3.2.3 示例 3

        第 1 轮操作:

        第 2 轮操作:

3.3 算法实现

#include <stdio.h>// 子排序函数,实现快速排序的核心逻辑
void subSort(int arr[], int start, int end)
{if (start < end){// 选择基准元素,这里选择数组的第一个元素int base = arr[start];int low = start;int high = end + 1;// 分区操作while (1){// 从左向右查找大于基准的元素while (low < end && arr[++low] <= base);// 从右向左查找小于基准的元素while (high > start && arr[--high] >= base);// 如果找到了需要交换的元素if (low < high){// 交换两个位置的元素int temp = arr[low];arr[low] = arr[high];arr[high] = temp;}else{// 分区结束,跳出循环break;}}// 交换基准元素和 high 位置的元素int temp1 = arr[start];arr[start] = arr[high];arr[high] = temp1;// 递归调用子排序函数,对基准左侧和右侧的子数组进行排序subSort(arr, start, high - 1);subSort(arr, high + 1, end);}
}// 快速排序主函数
void quickSort(int arr[], int size)
{// 调用子排序函数,从数组的第一个元素到最后一个元素subSort(arr, 0, size - 1);
}// 打印数组
void print(int arr[], int size)
{for (int i = 0; i < size; i++){printf("%d  ", arr[i]);}printf("\n");
}int main()
{// 定义一个待排序的数组int arr[] = {9, -16, 40, 23, -30, -49, 25, 21, 30};int length = sizeof(arr) / sizeof(int); // 计算数组长度// 输出排序前的数组printf("排序前的数组: ");print(arr, length);// 调用快速排序函数quickSort(arr, length);// 输出排序后的数组printf("排序后的数组: ");print(arr, length);return 0;
}

        输出结果如下所示:

        另一种实现方法:

#include <stdio.h>// 函数声明
void quickSort(int arr[], int low, int high);
int partition(int arr[], int low, int high);// 主函数
int main()
{int arr[] = {10, 7, 8, 9, 1, 5, -13, -20, -19, -50};int n = sizeof(arr) / sizeof(arr[0]);printf("原始数组: \n");for (int i = 0; i < n; i++){printf("%d ", arr[i]);}// 调用快速排序函数quickSort(arr, 0, n - 1);printf("\n排序后的数组: \n");for (int i = 0; i < n; i++){printf("%d ", arr[i]);}return 0;
}// 快速排序函数
void quickSort(int arr[], int low, int high)
{if (low < high){// pi 是分区操作后的基准元素索引int pi = partition(arr, low, high);// 分别对基准元素左边和右边的子数组进行递归排序quickSort(arr, low, pi - 1);  // 排序左子数组quickSort(arr, pi + 1, high); // 排序右子数组}
}// 分区函数
int partition(int arr[], int low, int high)
{int pivot = arr[high]; // 选择最后一个元素作为基准int i = (low - 1);     // i 指向较小元素的索引for (int j = low; j <= high - 1; j++){// 如果当前元素小于或等于基准if (arr[j] <= pivot){i++; // 增加较小元素的索引// 交换 arr[i] 和 arr[j]int temp = arr[i];arr[i] = arr[j];arr[j] = temp;}}// 交换 arr[i+1] 和 arr[high] (或 pivot)int temp = arr[i + 1];arr[i + 1] = arr[high];arr[high] = temp;return (i + 1); // 返回基准元素的最终位置
}

        输出结果如下所示:

3.4 时间空间复杂度分析

3.4.1 时间复杂度分析

  • 最坏情况(Worst Case)

    • 当输入数组已经有序或完全逆序时,每次划分只能将数组分成一个元素和剩下的元素,导致递归深度为 n。
    • 每次划分需要 O(n) 的时间,因此总的时间复杂度为 O(n^2)
  • 最好情况(Best Case)

    • 当每次划分都能将数组均匀分成两个相等的部分时,递归深度为 log n。
    • 每次划分需要 O(n) 的时间,因此总的时间复杂度为 O(n log n)
  • 平均情况(Average Case)

    • 对于随机分布的数组,快速排序的平均时间复杂度也是 O(n log n)。
    • 这是因为在大多数情况下,数组的划分接近均匀。

3.4.2 空间复杂度分析

  • 递归栈空间

    • 快速排序是一个递归算法,递归调用的深度决定了所需的空间。
    • 在最坏情况下,递归深度为 n,因此空间复杂度为 O(n)。
    • 在最好和平均情况下,递归深度为 log⁡ n,因此空间复杂度为 O(log n)
  • 辅助空间

    • 快速排序是原地排序算法,除了递归栈空间外,只需要常数级的额外存储空间。
    • 因此,辅助空间复杂度为 O(1)。

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

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

相关文章

设计一个支持自动化测试执行的测试框架

设计一个支持自动化测试执行的测试框架 在现代软件开发中,自动化测试是确保软件质量的重要手段。一个良好的测试框架不仅可以提高测试效率,还能帮助开发团队快速发现和修复缺陷。本文将介绍如何设计一个支持自动化测试执行的测试框架,涵盖框架的基本结构、核心功能以及实现…

JAVA 中系统相关的类

System 类 代表的是当前 Java 程序运行的平台&#xff08;操作系统&#xff09;&#xff0c;该类被关键字 final 修饰&#xff0c;即该类不能够派生子类&#xff0c;同时该类的构造器被关键字 private 修饰&#xff0c;因此不能够创建 System 类型的实例对象。 System 类中定…

【数据采集工具】Sqoop从入门到面试学习总结

国科大学习生活&#xff08;期末复习资料、课程大作业解析、大厂实习经验心得等&#xff09;: 文章专栏&#xff08;点击跳转&#xff09; 大数据开发学习文档&#xff08;分布式文件系统的实现&#xff0c;大数据生态圈学习文档等&#xff09;: 文章专栏&#xff08;点击跳转&…

【DNF mysql8.0安装】DNF安装MySQL服务器教程

在基于Red Hat的Linux发行版中&#xff0c;如CentOS或Fedora&#xff0c;DNF&#xff08;Dandified Yum&#xff09;是包管理器&#xff0c;用于安装、更新和卸载软件包。以下是使用DNF安装MySQL服务器&#xff08;也称为MySQL Community Server&#xff09;的步骤&#xff1a;…

网易博客旧文----Xtreme ToolkitPro 的CommandBarsDesigner编辑生成的界面如何使用

Xtreme ToolkitPro 的CommandBarsDesigner编辑生成的界面如何使用 2013-03-04 17:22:42| 分类&#xff1a; MFC | 标签&#xff1a; |举报 |字号大中小 订阅 著名的界面开发商Codejock Software开发的MFC界面控件和BCGsoft公司开发的BCGControlBarPro界面有得一拼。 codejock…

SpringBoot整合Freemarker(一)

Freemarker和jsp一样是一个视图的引擎模板&#xff0c;其实所有的模板引擎的工作原理都是类似的&#xff0c;如下图&#xff1a; 接下来就具体讲解一下Freemarker的用法&#xff0c;参考手册&#xff1a;模板 数据模型 输出 - FreeMarker 中文官方参考手册 SpringBoot默认就…

Agentic RAG(基于智能体的检索增强生成)是检索增强生成(Retrieval-Augmented Generation,RAG)技术的一种高级形式

Agentic RAG&#xff08;基于智能体的检索增强生成&#xff09;是检索增强生成&#xff08;Retrieval-Augmented Generation&#xff0c;RAG&#xff09;技术的一种高级形式&#xff0c;它通过引入人工智能代理&#xff08;Agent&#xff09;的概念&#xff0c;为语言模型赋予了…

本地项目上传Github+Gitee上传特定分支到Linux服务器(自用)

一、本地项目上传Github 上传整个项目到一个全新的repositories 或者 上传一个文件到一个特定的分支 步骤 1: 在本地创建 Git 仓库 在项目的根目录下运行以下命令&#xff0c;初始化本地 Git 仓库&#xff1a; git init步骤 2: 添加文件到 Git 仓库 使用以下命令将所有文件…

vite server正则表达式

vite server支持正则表达式&#xff0c;这样可以在测试时将一些请求模拟转发到本地后端服务的端口。且不会出现跨域的问题。 例如下面的配置&#xff0c;解决了3个问题&#xff1a; 1&#xff09;API请求URI地址转发到本地后端服务 2&#xff09;文件资源路径转发到本地后端服…

微前端架构及其解决方案对比

微前端架构及其解决方案对比 微前端架构是一种通过将大型前端应用拆分为多个独立的、可单独部署的小型应用的设计模式。随着这种模式的流行&#xff0c;诞生了多种微前端实现方案&#xff0c;每个方案都有其独特的特点和适用场景。以下是常见的微前端解决方案及其优缺点对比&a…

Socket 网络编程与 C# 中的应用实例

一、引言 在当今数字化时代&#xff0c;网络编程成为构建各种分布式应用和实现设备间通信的关键技术。Socket 网络编程作为一种底层且强大的网络通信方式&#xff0c;为开发者提供了直接操控网络连接和数据传输的手段。从即时通讯应用到网络游戏服务器&#xff0c;从文件传输工…

智能EDA小白从0开始 —— DAY24 Pulsic

Pulsic公司简介及其革命性EDA工具Animate Preview深度解析 一、公司概览与背景 Pulsic&#xff0c;一家深耕于电子设计自动化&#xff08;EDA&#xff09;领域的精密设计自动化公司&#xff0c;自2001年1月在英国布里斯托尔成立以来&#xff0c;便致力于解决先进节点设计所面…

一些简单的汇编指令

硬件架构和汇编指令 一、硬件架构 ARM芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing)&#xff0c; 它所用的指令比较简单&#xff0c;有如下特点: ① 对内存只有读、写指令 ② 对于数据的运算是在CPU内部实现 ③ 使用RISC指令的CPU复杂度小一点&#xff0…

中国科学院大学与美团发布首个交互式驾驶世界模型数据集DrivingDojo:推进交互式与知识丰富的驾驶世界模型

中国科学院大学与美团发布首个交互式驾驶世界模型数据集DrivingDojo&#xff1a;推进交互式与知识丰富的驾驶世界模型 Abstract 驾驶世界模型因其对复杂物理动态的建模能力而受到越来越多的关注。然而&#xff0c;由于现有驾驶数据集中的视频多样性有限&#xff0c;其卓越的建…

简述RESTFul风格的API接口

目录 传统的风格API REST风格 谓词规范 URL命令规范 避免多级URL 幂等 CURD的接口设计 REST响应 响应成功返回的状态码 重定向 错误代码 客户端 服务器 RESTful的返回格式 返回格式 从上一篇文章我们已经初步知道了怎么在VS中创建一个webapi项目。这篇文章来探讨一…

外包干了2个月,技术明显退步

回望过去&#xff0c;我是一名普通的本科生&#xff0c;于2019年通过校招有幸加入了南京某知名软件公司。那时的我&#xff0c;满怀着对未来的憧憬和热情&#xff0c;投入到了功能测试的岗位中。日复一日&#xff0c;年复一年&#xff0c;转眼间&#xff0c;我已经在这个岗位上…

Git协作场景

1、日常需求开发 在日常开发中&#xff0c;研发人员通常需要从 develop 分支创建 feature 分支进行功能开发&#xff0c;并在开发完成后将其合并回 develop。常规的操作流程如下&#xff1a; 创建 feature 分支&#xff1a; 研发人员从 develop 分支创建一个新的 feature 分支…

优化SpringBoot接口:异步处理提升系统吞吐量策略

作者介绍&#xff1a;✌️大厂全栈码农|毕设实战开发&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。 推荐订阅精彩专栏 &#x1f447;&#x1f3fb; 避免错过下次更新 Springboot项目精选实战案例 更多项目&#xff1a;CSDN主页YAML墨韵 学如逆水行舟&#xff0c…

牵手App红娘来助力,打造线上交友“好管家”

线上交友以其便捷性、广泛性和互动性等特点&#xff0c;正逐渐成为单身男女寻找恋爱伴侣的重要渠道。相较于传统相亲模式&#xff0c;线上交友不仅打破了时间和空间的限制&#xff0c;更以其丰富的互动功能和个性化的匹配算法&#xff0c;为用户提供了前所未有的交友体验。在这…

Python数据分析-航空公司客户满意度分析

一、研究背景 随着航空业的快速发展&#xff0c;航空公司之间的竞争愈发激烈。航空公司不再仅仅依靠价格、航班时间等基本要素来吸引客户&#xff0c;而更多地关注如何提升客户体验与满意度。乘客的飞行体验和满意度不仅影响了他们的忠诚度&#xff0c;也对航空公司在市场中的…