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;点击跳转&…

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;为语言模型赋予了…

中国科学院大学与美团发布首个交互式驾驶世界模型数据集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;我已经在这个岗位上…

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

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

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

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

IJKPlayer源码分析-整体结构

根据我们的之前的老方法&#xff0c;采用结构化的方式来对IJKPlayer源码做个分析&#xff0c;首先&#xff0c;我们从整体的角度先把IJKPlayer的整体架构和流程讲下&#xff0c;让大家先有个整体的印象。 本地JNI入口 在Android环境下&#xff0c;JVM层载入一个本地so库流程大致…

【C++11】包装器:深入解析与实现技巧

C 包装器&#xff1a;深入解析与实现技巧 个人主页 C专栏 目录 引言包装器的定义与用途C 包装器的常见应用场景实现包装器的技巧使用 RAII 实现资源管理案例分析&#xff1a;智能指针模板包装器的应用包装器与设计模式性能优化更多应用案例总结 引言 C 是一门灵活且强大的语…

vue后台管理系统从0到1搭建(4)各组件的搭建

文章目录 vue后台管理系统从0到1搭建&#xff08;4&#xff09;各组件的搭建Main.vue 组件的初构 vue后台管理系统从0到1搭建&#xff08;4&#xff09;各组件的搭建 Main.vue 组件的初构 根据我们的效果来看&#xff0c;分析一下&#xff0c;我们把左边的区域分为一个组件&am…

Learn OpenGL In Qt之纹理

竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生~ 公众号&#xff1a; C学习与探索 | 个人主页&#xff1a; rainInSunny | 个人专栏&#xff1a; Learn OpenGL In Qt 文章目录 纹理纹理坐标纹理环绕方式纹理采样多级渐远纹理 纹理加载和创建加载纹理创建纹理 应用纹理 纹理 纹理坐标…

【AWS AMI跨境备份】跨境使用 S3 备份和还原 AMI 镜像

文章目录 一、实验场景二、实验目标三、实验架构图四、涉及到AWS服务五、演示操作5.1 创建EC2实例5.2 创建映像5.3 备份AMI至Global S35.4 复制AMI从Global S3至 CN S35.5 还原AMI5.6 测试AMI 六、参考链接 一、实验场景 将 AWS Global区域的EC2实例备份至 AWS CN区域。 备份…

苍穹外卖学习笔记(二十五)

文章目录 Spring Task介绍应用场景&#xff1a; cron表达式例如&#xff1a; 入门案例 订单状态定时处理处理超时订单处理一直配送中的订单OrderMapper WebSocket介绍HTTP协议和WebSocket协议对比应用场景&#xff1a;入门案例1. 使用websocket.html作为WebSocket客户端2. 导入…

前端打印功能(vue +springboot)

后端 后端依赖生成pdf的方法pdf转图片使用(用的打印模版是带参数的 ,参数是aaa)总结 前端页面 效果 后端 依赖 依赖 一个是用模版生成对应的pdf,一个是用来将pdf转成图片需要的 <!--打印的--><dependency><groupId>net.sf.jasperreports</groupId>&l…

LCD补充

LCD补充 目录 LCD补充 tip:随着我们学的越来越多&#xff0c;代码长度越来越长&#xff0c;编译越来越慢&#xff0c;有没有超过内存是我们比较关心的一件事&#xff0c;通过以下方法可以实时看到写的代码的大小 回顾LCD LCD补充功能 -- 1、有关在LCD上显示动图&#xff…

前端使用Canvas实现网页电子签名(撤销、下载)

前言&#xff1a;一般在一些后台的流程资料以及审核的场景中会需要电子签名&#xff0c;介绍一种用canvas实现的电子签名&#xff0c;此案例用的是原生js 效果展示&#xff1a; 一、html和css&#xff1a; <div class"divCla2"><canvas id"myCanvas&q…

数据结构-排序算法

基于交换的排序算法 快速排序&#xff1a; 最优情况 最优情况下&#xff0c;每次找到的参考轴把数据分成均匀的两半&#xff0c;最后应该是一个平衡二叉树状态&#xff1b;二叉树的层数&#xff08;logn&#xff09;即为递归需要进行的次数&#xff0c;并且每轮递归结束时&…

Java语言-抽象类

目录 1.抽象类概念 2.抽象类语法 3.抽象类特性 4.抽象类作用 1.抽象类概念 在面向对象的概念中&#xff0c;所有的对象都是通过类来描绘的&#xff0c;但是反过来&#xff0c;并不是所有的类都是用来描绘对象的&#xff0c; 如果 一个类中没有包含足够的信息来描绘一个具体…