当前位置: 首页 > news >正文

【初阶数据结构篇】冒泡排序和快速排序(中篇)

文章目录

  • 冒泡排序和快速排序
    • 前言
    • 代码位置
    • 冒泡排序
    • 快速排序
      • 递归法实现
        • hoare版本
        • 挖坑法
        • lomuto前后指针
        • 递归法复杂度分析
      • 非递归法实现

冒泡排序和快速排序

前言

本篇以排升序为例

代码位置

gitee

冒泡排序

动图理解
在这里插入图片描述

  • 作为第一个接触的排序算法,冒泡排序想必大家已经很熟悉了
    • 总共n个数据,要排n-1趟
    • 第i(i从0开始取)趟要比较n-1-i次
    • 等差数列求和,最坏时间复杂度为O(n2)
  • 定义exchange变量,当数组已经有序时不进入交换,直接跳出循环
    • 最好时间复杂度为O(n)
  • 空间复杂度O(1)
void BubbleSort(int* arr, int n)
{for (int i = 0; i < n-1; i++){int exchange = 0;for (int j = 0; j < n - i - 1; j++){//升序if (arr[j] < arr[j + 1]){exchange = 1;Swap(&arr[j], &arr[j + 1]);}}if (exchange == 0){break;}}
}
  • 与直接插入排序法相比,比较次数一致,但冒泡排序的交换需要执行三次,而直接插入排序因为使用了tmp临时变量存储要插入的数据,只用执行一次,所以直接插入排序法效率明显更高
  • 与直接选择排序法相比,直接选择排序法无论数组是否有序都要执行到结束条件,不存在最好最坏时间复杂度。而冒泡排序因为使用了exchange变量进行优化,可以在最好时间复杂度上达到线性的结果。所以冒泡排序更胜一筹
  • 虽然但是,实际中还是不会使用冒泡排序,但它的教学意义是我们不能忽视的😂

快速排序

  • 快速排序是Hoare于1962年提出的⼀种⼆叉树结构的交换排序⽅法
  • 其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两⼦序列,左⼦序列中所有元素均⼩于基准值,右⼦序列中所有元素均⼤于基准值,然后最左右⼦序列重复该过程,直到所有元素都排列 在相应位置上为⽌。

递归法实现

快速排序实现主框架:

void QuickSort(int* arr, int left, int right)
{if (left >= right){return;}//[left,right]--->找基准值midint keyi = _QuickSort(arr, left, right);//左子序列:[left,keyi-1]QuickSort(arr, left, keyi - 1);//右子序列:[keyi+1,right]QuickSort(arr, keyi + 1, right);
}
  • 快速排序最重要的就是找基准值:

    • 基准值左边元素都小于它,右边都大于,显然的基准值所在的位置就是所有数据排序好后它应该在的位置上

    • 每次将这个数据(即基准值)放在正确的位置上,然后对其左右序列递归,最后所有数据都被放在了正确的位置上,排序就完成了

将区间中的元素进⾏划分的 _QuickSort ⽅法主要有以下⼏种实现⽅式:

hoare版本

算法思路

  • 假设将序列第一个数作为基准值

  • 定义左右指针

    • left:从左找比基准值大的 ,right:从右找比基准值小的
    • 找到后交换,left++,right–,进入下次循环
  • 跳出循环后交换基准值到正确位置

可以大致写出代码:

int _QuickSort1(int* arr, int left, int right)
{int keyi = left;++left;while (left <right){while (left < right && arr[right] > arr[keyi]){right--;}while (left < right && arr[left] < arr[keyi]){left++;}//right leftif (left < right){Swap(&arr[left++], &arr[right--]);}}Swap(&arr[keyi], &arr[right]);return right;
}

于是这里就抛出几个问题:

  1. 外层循环结束条件是否应该取=?
  2. 内层循环当right或left处数据和基准值相等时是否应该跳出循环?
  3. 最后跳出外层循环我们将基准值交换到正确位置时应该与right还是left处数据交换?

问题1:
在这里插入图片描述

  • 二者相遇时在9的位置,如果不取等,第一次交换完后就跳出循环,此时9和6交换,显然不行

外层循环需要取等,同时在内层循环时相应left和right判断处也要取等,不然left和right相等就死循环了

问题3:

  • 既然跳出循环时是left>right,right处在left扫描过的区域,都是不大于基准值的数据,而left处在right扫描过的区域,都是不小于基准值的数据
  • 显然我们将right处数据和基准值交换,基准值就来到了正确的位置

跳出外层循环应该与right处数据交换,right处数据就是基准值的位置

经过上面两层分析:

改进如下:

int _QuickSort1(int* arr, int left, int right)
{int keyi = left;++left;while (left <= right)//left和right相遇的位置的值比基准值要大{while (left <= right && arr[right] > arr[keyi]){·right--;}//right找到比基准值小/  等于?while (left <= right && arr[left] < arr[keyi]){left++;}//right leftif (left <= right){Swap(&arr[left++], &arr[right--]);}}//right keyi交换Swap(&arr[keyi], &arr[right]);return right;
}

问题2:

  • 假设数组全是相同的数据

在这里插入图片描述

  • 取等于,第一次循环right就和left都在下标为1的位置,此时返回去的基准值就是下标1,左序列只有一个数据,右边序列还有n-2个数据
  • 同样的下次循环的左序列也只有一个数据
  • 像这样一次排一个数据时间复杂度很高

所以不应该取等于,尽量让左右子序列的数据个数平均一些

所以上述改进版本就是最终的hoare排序法


挖坑法

基本思路

创建左右指针。⾸先从右向左找出⽐基准⼩的数据,找到后⽴即放⼊左边坑中,当前位置变为新的"坑",然后从左向右找出⽐基准⼤的数据,找到后⽴即放⼊右边坑中,当前位置变为新的"坑",结束循环后将最开始存储的分界值放⼊当前的"坑"中,返回当前"坑"下标(即分界值下标)

动图解析

在这里插入图片描述

  • 动图演示的很清楚
  • 这里同样有两个问题
    • left和right是否取等?
    • 当right或left处数据与基准值key相等时是否继续循环

问题1:

  • 以上面动图为例,如果取等最后当left和right相遇时left还要++一次,导致hole所在位置偏移,发生错误,所以不取等

问题2:

  • 同hoare版本,如果全是相等数据时每次只会排序一个数据,时间复杂度太高,所以不取等
//挖坑法
int _QuickSort2(int* arr, int left, int right)
{int hole = left;int key = arr[hole];while (left < right){while (left < right && arr[right] > key){--right;}arr[hole] = arr[right];hole = right;while (left < right && arr[left] < key){++left;}arr[hole] = arr[left];hole = left;}arr[hole] = key;return hole;
}

lomuto前后指针

基本思想

创建前后指针,从左往右找⽐基准值⼩的进⾏交换,使得⼩的都排在基准值的左边。

动图解析:

在这里插入图片描述

  • 这种方法比较好理解代码也简单,就不赘述了(就是想不到一点🤣)
//lomuto前后指针法
int _QuickSort(int* arr, int left, int right)
{int prev = left, cur = left + 1;int keyi = left;while (cur <= right){if (arr[cur] < arr[keyi] && ++prev != cur){Swap(&arr[cur], &arr[prev]);}cur++;}Swap(&arr[keyi], &arr[prev]);return prev;
}
  • 这里当cur和keyi数据相同时是否交换?
  • 假设仍然全是重复数据,代入后会发现二者都是一样的,如果不加等号最后prev下标在0;反之prev下标在end。可见其对重复数据无法通过此来进行优化

递归法复杂度分析
  • 时间复杂度:每一层的总时间复杂度都是O(n),因为需要对每一个元素遍历一次。而且在最好的情况下,同样也是有logn层,所以快速排序最好的时间复杂度为O(nlogn)
  • 空间复杂度:二叉树递归最大深度为logn,即O(nlogn)
  • 以上是最好情况,最坏情况则是上面说的一次排序一个数据,时间复杂度O(n2),空间复杂度O(n)。不过现实中基本不会出现这种情况。

注意:在以上找基准值方法中,我们默认都是把基准值定为left所在位置,这种方法当数组接近升序时会导致分割的序列也出现“一边倒”的情况,在高阶数据结构中会讲到如何优化,敬请期待😘


非递归法实现

借助栈这样一种数据结构

有关栈的相关知识,不了解的小伙伴可以看看这篇:
栈的实现方法

  • 栈是先进后出,所以插入先插入right后插入left
  • 找基准值方法使用双指针法最简单
  • 根据基准值划分左右区间
    • 左区间:[begin,keyi-1]
    • 右区间:[keyi+1,end]
  • 循环直到栈为空

在这里插入图片描述

//非递归版本快排
//--借助数据结构--栈
void QuickSortNonR(int* arr, int left, int right)
{ST st;STInit(&st);StackPush(&st, right);StackPush(&st, left);while (!StackEmpty(&st)){//取栈顶元素---取两次int begin = StackTop(&st);StackPop(&st);int end = StackTop(&st);StackPop(&st);//[begin,end]---找基准值int prev = begin;int cur = begin + 1;int keyi = begin;while (cur <= end){if (arr[cur] < arr[keyi] && ++prev != cur){Swap(&arr[cur], &arr[prev]);}cur++;}Swap(&arr[keyi], &arr[prev]);keyi = prev;//根据基准值划分左右区间//左区间:[begin,keyi-1]//右区间:[keyi+1,end]if (keyi + 1 < end){StackPush(&st, end);StackPush(&st, keyi + 1);}if (keyi - 1 > begin){StackPush(&st, keyi - 1);StackPush(&st, begin);}}STDestroy(&st);
}

以上就是冒泡排序和快速排序方法的介绍啦,各位大佬有什么问题欢迎在评论区指正,您的支持是我创作的最大动力!❤️
请添加图片描述

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【深度学习】CosyVoice,论文
  • jetbrain插件市场无法下载插件/idea插件install无效
  • Unity后处理(Post-processing)
  • JAVA的String类的contains方法,Indexof方法不使用KMP算法
  • 顺丰+美团测开面试经验
  • 2024华为OD机试真题-最小矩阵宽度Python-C卷D卷-200分
  • 【MySQL】表的CRUD{insert 替换 查询 运算符 语句执行顺序 排序 limit 分页 truncate截断表 去重 mysql日志 聚合函数}
  • 集合的并发修改异常 31
  • 编程小白如何成为大神?大学新生的最佳入门攻略
  • RK3568笔记四十九:W25Q64驱动开发(硬件SPI1)
  • “八股文”面试题:是招聘程序员的金科玉律?
  • aaaaaaaaaaaaa
  • Day14-Servlet后端验证码的实现
  • WildCard虚拟卡绑定openAI付款方式
  • 从信息论的角度看微博推荐算法
  • gitlab-ci配置详解(一)
  • iOS 颜色设置看我就够了
  • java第三方包学习之lombok
  • mysql 数据库四种事务隔离级别
  • Nginx 通过 Lua + Redis 实现动态封禁 IP
  • React as a UI Runtime(五、列表)
  • select2 取值 遍历 设置默认值
  • SpiderData 2019年2月16日 DApp数据排行榜
  • Swoft 源码剖析 - 代码自动更新机制
  • Webpack4 学习笔记 - 01:webpack的安装和简单配置
  • WordPress 获取当前文章下的所有附件/获取指定ID文章的附件(图片、文件、视频)...
  • 订阅Forge Viewer所有的事件
  • 力扣(LeetCode)22
  • 日剧·日综资源集合(建议收藏)
  • kubernetes资源对象--ingress
  • 数据库巡检项
  • 曜石科技宣布获得千万级天使轮投资,全方面布局电竞产业链 ...
  • 正则表达式-基础知识Review
  • 专访Pony.ai 楼天城:自动驾驶已经走过了“从0到1”,“规模”是行业的分水岭| 自动驾驶这十年 ...
  • #Java第九次作业--输入输出流和文件操作
  • (1)Hilt的基本概念和使用
  • (2)Java 简介
  • (2024,LoRA,全量微调,低秩,强正则化,缓解遗忘,多样性)LoRA 学习更少,遗忘更少
  • (Arcgis)Python编程批量将HDF5文件转换为TIFF格式并应用地理转换和投影信息
  • (二)原生js案例之数码时钟计时
  • (附源码)springboot优课在线教学系统 毕业设计 081251
  • (原創) 如何將struct塞進vector? (C/C++) (STL)
  • (转)memcache、redis缓存
  • (转)重识new
  • ******IT公司面试题汇总+优秀技术博客汇总
  • .bat批处理(七):PC端从手机内复制文件到本地
  • .NET delegate 委托 、 Event 事件,接口回调
  • .NET Standard、.NET Framework 、.NET Core三者的关系与区别?
  • .NET 某和OA办公系统全局绕过漏洞分析
  • .NET 设计一套高性能的弱事件机制
  • .NET实现之(自动更新)
  • .net与java建立WebService再互相调用
  • [18] Opencv_CUDA应用之 基于颜色的对象检测与跟踪
  • [Angularjs]asp.net mvc+angularjs+web api单页应用
  • [autojs]逍遥模拟器和vscode对接