从零实现数据结构:一文搞定所有排序!(下集)

1.快速排序

思路框架:

在有了前面冒泡选择插入希尔排序之后,人们就在想能不能再快一点,我们知道排序算法说人话就是把大的往后放小的往前放,问题就在于如何更快的把大的挪到数组队尾小的挪到数组前面。这里我们先总结一下上集前面几种排序算法,冒泡排序一轮单趟排序相当于只完成了把一个最大的挪到后面;选择排序可以做到一轮单趟把一个最小的往前放一个最大的往后放;插入排序是从队头开始重新建立一个有序数组,一轮单趟完成了插入一个元素;希尔排序是一轮完成了一个组的排序,在前面排序的基础上完成了优化,大的元素只需要两到三步就可以跳到队伍的后端,小的元素也是同理,因此比前面那些排序时间复杂度要快出一个量级。

在有了前面这些排序的基础上,我们开始想单趟排序能不能一步到位,也不要像希尔排序那样分两三步,直接定义一个基准线,找到比key大的和比key小的同时交换,这样既完成了一次性既把大的往后放了也把小的往前面挪了,又只需要交换这一步就完成了元素挪动,这也是交换排序种类的魅力之一。接下来基于这个思想,我们介绍三种常见版本的写法

版本一:发明者hoare版本

我们先如上图定义最左边的元素为key也就是基准线(图中的红色),定义一个左一个右两个变量来同时往中间走,right找比基准线小的left找比基准线大的,找到之后就交换left和right,重复此动作知道left和right相遇停止。最后再交换key和left,完成一轮单趟排序。

下面我们一步一步从最基础的想法开始,跳过经典的坑逐渐丰富修正细节完成实现:

void PartSort(int* a, int left, int right)
{int key = left;while (left < right){while (a[right] > a[key]){--right;}while (a[left] < a[key]){++left;}swap(&a[left], &a[right]);}swap(&a[left], &a[key]);
}

按照上面的思想我们完成了第一步最基础的实现,但是这里有一个潜在的问题,也就是死循环和等号问题。如果我们给出如下数组:

[6 1 6 7 9 6 10 8]

当left和right同时走到中间两个6的时候,就会出现死循环走不下去的情况,所以我们要加上等于号

修改后版本如下:

void PartSort(int* a, int left, int right)
{int key = left;while (left < right){while (a[right] >= a[key]){--right;}while (a[left] <= a[key]){++left;}swap(&a[left], &a[right]);}swap(&a[left], &a[key]);
}

这里又出现了新的潜在问题,那就是由于key值的选择越界问题,例如如果我们修改一下刚刚的数组例子,令第一个元素等于0,也就是key=0

[0 1 6 7 9 6 10 8]

这个时候right从右边一直走会一直找不到比0小的数,从而走出了数组导致越界访问的问题,所以我们还要增加一个控制越界的判断条件。

修改版本如下:

void PartSort(int* a, int left, int right)
{int key = left;while (left < right){while (left<right && a[right] >= a[key]){--right;}while (left<right && a[left] <= a[key]){++left;}swap(&a[left], &a[right]);}swap(&a[left], &a[key]);
}

这里我们需要注意判断顺序,一定是先判断是否越界再去访问,c语言对于越界访问是一种类似于抽查的行为,不一定能够检测的到,但是一旦我们这里换成vector类的话,检测就变得很严格,无论是访问还是干啥都能检测到。至此我们的单趟排序就已经完成了,下面我们思考两个问题:

1.为什么两个指针相遇的位子的数一定比key小

这里其实我们在问的是最后和left和key交换这一动作的合理性,我们分两种情况来讨论这个问题:

情况一:如果最后是right在移动和left相遇了,这里又分为两种情况

1.left一次没有动过,right直接从数组尾部一直找到left,这个时候left的位置就是key的位置,所以满足left位置一定比key小

2.前面已经经过一些轮次的交换了,这时候right再往左找比key小的时候遇到left,由于前面已经交换过了,所以left所在的位置已经是前面轮次right找到的比key小的数交换过来了,所以也满足left这个位置一定比key小

情况二:如果最后是left在移动和left相遇了

由于是先走的right,而right已经停下来了,所以肯定满足比key小,这个时候和left相遇也满足一定比key小

2.左右两边哪个先动有关系吗

根据上面的逻辑我们不难看出下面的规律:

1.如果左边作为key,那就要右边先走,保障了相遇位置的值比key小

2.如果右边作为key,那就要左边先走,保障了相遇位置的值比key大

至此我们完成了单趟排序的所有细节问题,我们来考虑整体的排序之前先想想单趟排序完成了啥:

一次单趟排序所完成的任务是:

1.key就已经排好了,就在它现在的位子以后都不用动了

2.如果左区间有序,右区间有序那整个数组就是有序了(递归分治思想)

所以我们根据这个思路给出整体排序的实现:

void QuickSort(int* a, int begin, int end)//这里传入区间的开始和结束的位置,方便递归
{if (begin >= end)//两种情况1.只有一个值2.区间不存在return;int key = PartSort(a, begin, end);QuickSort(a, begin, key - 1);QuickSort(a, key + 1, end);
}

下面引用一下网上的动图演示:
在这里插入图片描述

版本二:挖坑法

在完成了初始hoare版本的实现以后,不难看出这种实现坑有点多,后来又出现一种相对理解实现简单一点的挖坑法。我们先给出引用的动图,大家看图就能理解了,就不再作过多文字阐述:

在这里插入图片描述

下面先给出实现:

int PartSort2(int* a, int left, int right)
{int key = a[left];int hole = left;while (left < right){while (left<right && a[right] >= key){--right;}a[hole] = a[right];hole = right;while (left<right && a[left] <= key){++left;}a[hole] = a[left];hole = left;}a[hole] = key;return hole;
}

需要注意的是这种挖坑法和第一种hoare版本的单趟排序结果可能不同,但其本质都是交换前后元素,时间复杂度是一样的。由于基本逻辑和前面基本相似所以不做过多解释。

版本三:前后指针法

在这里插入图片描述

这种方法本质上有别于前面两种,也是相对来说不好理解的一种方式,我们来具体解释一下。

这里cur指针是无论什么情况都往前走一格,用来找到比key小的数,找到之后和prev交换,如果cur和prev重合则无事发生,只有当cur走过了比key大的数的时候,再遇到比key小的数的时候发生交换,此时他们之间的数都是比key大的数,这个时候的交换就相当于把大的数一路翻滚到后面

前后指针法与前后交换法的区别:

特性前后指针法前后交换法
扫描方式单向(左到右)双向(两端向中间)
基准值位置通常选择最后一个元素通常选择第一个元素
交换方式只交换小于基准值的元素与大于基准值的元素直接交换找到的不符合基准值的两个元素
操作次数O(n)O(n)
适用场景通常适用于快速分区且实现简洁常用于理解双向扫描,适合手动模拟操作
稳定性不稳定不稳定
int PartSort3(int* a, int left, int right)
{int prev = left;int cur = left+1;int key = left;while (left <= right){if (a[cur] < a[key] && ++prev != cur){++prev;swap(&a[prev], &a[cur]);}cur++;}swap(&a[prev], &a[key]);key = prev;return key;
}

非递归栈版本

例如还是这个例子,在我们实现非递归版本之前先搞清楚递归版本的顺序,再模拟成栈的顺序即可。下面是递归的区间顺序:区间 0-9->区间 0-6->区间 8-9->区间 0-3->区间 5-6->区间 0-2->区间 0-1。因此模拟栈的思路就是每次从栈里面拿出来栈顶的一个区间,然后再让该区间的左右区间入栈。下面给出实现:

void QuickSortIterative(int* a, int n) {std::stack<std::pair<int, int>> stack;stack.push({0, n - 1}); // 初始区间while (!stack.empty()) {int left = stack.top().first;int right = stack.top().second;stack.pop();if (left < right) {int pivotIndex = Partition(a, left, right);// 将右区间压入栈中stack.push({pivotIndex + 1, right});// 将左区间压入栈中stack.push({left, pivotIndex - 1});}}
}
1. 初始化栈
  • 创建一个 std::stack 来模拟递归过程。
  • 初始时,将整个数组的区间 [0, n-1] 压入栈中。栈中存储的每个区间用一对整数表示,其中第一个整数是区间的起始索引 left,第二个整数是区间的结束索引 right
2. 循环处理栈中的区间
  • 进入 while 循环,只要栈不为空,就表示还有未排序的区间需要处理。

    • 弹出栈顶区间

      • 使用 stack.top() 获取栈顶元素,分别取出当前区间的起始和结束位置 leftright
      • stack.pop() 将这个区间从栈中移除,因为我们即将处理它。
    • 检查区间是否有效

      • 检查 left < right,确保当前区间至少包含两个元素。如果 left >= right,则说明这个区间已经排序好(一个元素或空区间),不需要进一步处理。
    • 将子区间压入栈

      • 将左右两个子区间压入栈中,继续处理。注意先将右子区间 [pivotIndex + 1, right] 压入栈,再将左子区间 [left, pivotIndex - 1] 压入栈。这样栈顶的元素总是左子区间,确保以“先处理左侧,再处理右侧”的顺序执行,和递归版本一致。

极端情况与优化版本:

其实我们现在这种版本的快排在绝大多数情况下是比其他排序算法要快的,因此各大语言的sort算法也基本是按照快排来写的,但是唯一的缺陷就是怕遇到极端情况,例如数组在接近有序的情况下就显的非常慢甚至不如插入排序。在有序情况下就会出现如下情况,每次选key选是最小(最大)的数,就造成了每层只能排好一个数O(N),需要排N层,很明显这是一个等差数列,通过公式得出最坏的情况时间复杂度是

因此我们给出解决方案有两种:1随机数取key        2三数取中作为key

随机数只需要添加一行即可:

int pivotIndex = left + rand() % (right - left + 1);

三数取中这里我们添加一个取中函数:

int MedianOfThree(int* a, int left, int right) {int mid = left + (right - left) / 2;// 对 left, mid, right 三个元素排序,选择中间值作为基准if (a[left] > a[mid]) std::swap(a[left], a[mid]);if (a[left] > a[right]) std::swap(a[left], a[right]);if (a[mid] > a[right]) std::swap(a[mid], a[right]);// 将中间值移到最右边,作为基准std::swap(a[mid], a[right]);return a[right];
}

小区间插入优化:

我们还可以做进一步优化,我们知道二叉树最后一层叶子结点占了整个树的结点个数大部分,这里递归调用也是如此,因此会造成递归深度太深的问题,因此为了解决这个问题我们可以设置一个阈值,如果小于该阈值的时候直接调用插入排序即可,插入排序在接近有序的情况下还是很好用的。

  if (right - left <= threshold) 
{InsertionSort(a, left, right);continue;
}

至此快速排序的所有内容结束!

2.归并排序

还是先给出引用的动图方便理解:

在这里插入图片描述

可以看到归并排序的思想也是分治,想要整体有序,先让左区间右区间有序,以此递归下去。和快排不同的是,快排是自顶向下的,而归并是自下而上的,先分到不能再分为止然后再一层一层往上合并。下面给出具体的实现:

void PartMergeSort(int* a, int begin, int end, int* tmp)
{if (begin >= end)return;int mid = (begin + end) / 2;PartMergeSort(a, begin, mid, tmp);PartMergeSort(a, mid + 1, end, tmp);int beginl = begin, endl = mid;//左区间int beginr = mid + 1, endr = end;//右区间//合并两个有序数组int index = begin;while (beginl <= endl && beginr <= endr){if (a[beginl] < a[beginr]){tmp[index++] = a[beginl++];}else{tmp[index++] = a[beginr++];}}//解决某一个已经结束的情况,处理剩下的数据//由于不知道哪个先结束所以只能两个都判断一遍while (beginl <= endl)tmp[index++] = a[beginl++];while (beginr <= endr)tmp[index++] = a[beginr++];//最后拷贝回原数组memmove(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}void MergeSort(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);PartMergeSort(a, 0, n - 1, tmp);free(tmp);
}

需要注意的是,这里由于我们要申请临时的拷贝空间,所以定义了一个子函数。

至此常见的排序算法结束力,感谢您看到这里!

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

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

相关文章

NVR录像机汇聚管理EasyNVR多品牌NVR管理工具/设备视频报警功能详解

在科技日新月异的今天&#xff0c;视频监控系统作为现代社会的“第三只眼”&#xff0c;正以前所未有的方式深刻影响着我们的生活与社会结构。从公共场所的安全监控到个人生活的记录分享&#xff0c;视频监控系统以其独特的视角和功能&#xff0c;为社会带来了诸多好处&#xf…

【Linux】————磁盘与文件系统

作者主页&#xff1a; 作者主页 本篇博客专栏&#xff1a;Linux 创作时间 &#xff1a;2024年10月17日 一、磁盘的物理结构 磁盘的物理结构如图所示&#xff1a; 其中具体的物理存储结构如下&#xff1a; 磁盘中存储的基本单位为扇区&#xff0c;一个扇区的大小一般为512字…

Python 自动化运维:Python基础知识

Python 自动化运维&#xff1a;Python基础知识 目录 &#x1f4ca; Python 基础复习 数据类型、控制结构与常用函数面向对象编程&#xff08;OOP&#xff09;与类的使用函数式编程概念与 lambda 表达式异常处理与日志记录的基本实践 1. &#x1f4ca; Python 基础复习 数据…

二十二、Python基础语法(模块)

模块(module)&#xff1a;在python中&#xff0c;每个代码文件就是一个模块&#xff0c;在模块中定义的变量、函数、类别人都可以直接使用&#xff0c;如果想要使用别人写好的模块&#xff0c;就必须先导入别人的模块&#xff0c;模块名须满足标识符规则&#xff08;由字母、数…

SwiftUI(三)- 渐变、实心形状和视图背景

引言 在现代的应用的UI设计中&#xff0c;渐变和形状背景为界面带来了丰富的层次与视觉效果&#xff0c;而SwiftUI提供了一系列简单且强大的API&#xff0c;可以轻松实现这些效果。在这篇文章中&#xff0c;我们将介绍SwiftUI中的渐变、实心形状和视图背景的基础用法&#xff…

【论文阅读】Learning persistent homology of3D point clouds

摘要 motivation&#xff1a;PD计算过程非常耗时&#xff0c;严重限制了TDA的应用 本文提出了一种端到端的神经网络模型TopologyNet&#xff0c;用于直接从3D点云数据中拟合拓扑表示。TopologyNet显著减少了生成拓扑表示的计算时间&#xff0c;并在实际实例中保持了较小的近似…

Python4

4. 更多控制流工具 除了刚介绍的 while 语句&#xff0c;Python 还用了一些别的。我们将在本章中遇到它们。 4.1. if 语句 if elif else if x<0: x 0 print(Negative changed to zero) elif x0: print( zero) else: print(More) 4.2. for 语句 Pyth…

2024.7最新子比主题zibll7.9.2开心版源码+授权教程

授权教程&#xff1a; 1.进入宝塔搭建一个站点 绑定 api.zibll.com 域名 并上传 index.php 文件 2.设置伪静态 3.开启SSL证书&#xff0c;找一个能用的域名证书&#xff0c;将密钥(KEY)和证书(PEM格式)复制进去即可 4.在宝塔文件地址栏中输入 /etc 找到 hosts文件并打开&a…

【Docker】docker | 部署nginx

一、概述 记录下nginx的部署流程&#xff1b;将conf配置文件映射到宿主机 前提依赖&#xff1a;自行准备nginx的镜像包 二、步骤 1、运行、无映射 docker run --name nginx -p 80:80 -d nginx:1.18.0-alpine 80&#xff1a;80&#xff0c;前面是宿主机端口&#xff1b;如果冲…

uniapp:上拉加载更多、下拉刷新、页面滚动到指定位置

提醒 本文实例是使用uniapp进行开发演示的。 一、需求场景 在开发商品&#xff08;SKU&#xff09;列表页面时&#xff0c;通常有三个需求&#xff1a; 页面下拉刷新&#xff0c;第一页展示最新数据&#xff1b;上拉加载更多数据&#xff1b;列表页面可以滚动到指定位置&#x…

Liunx权限概念及权限管理

目录 一&#xff1a;shell命令以及运行原理 二&#xff1a;Linux权限的概念 三&#xff1a;Linux的权限管理 3.1文件访问者的分类 3.2文件类型和访问权限&#xff08;事物属性&#xff09; 3.3文件权限的表达方式&#xff1a; 3.4文件访问权限的相关设置方法 四&…

前沿技术与未来发展第一节:C++与机器学习

第六章&#xff1a;前沿技术与未来发展 第一节&#xff1a;C与机器学习 1. C在机器学习中的应用场景 C在机器学习中的应用优势主要体现在高效的内存管理、强大的计算能力和接近底层硬件的灵活性等方面。以下是 C 在机器学习领域的几个主要应用场景&#xff1a; 1.1 深度学习…

Vue3 学习笔记(七)Vue3 语法-计算属性 computed详解

#1024程序员节|征文# 1、计算属性 computed 在 Vue.js 中&#xff0c;计算属性&#xff08;computed properties&#xff09;是一种特殊的响应式属性&#xff0c;它们根据依赖的响应式数据自动更新。计算属性非常适合用于当你需要根据现有数据派生出一些状态时。 (1)、基本用法…

IntelliJ IDEA 查看类class的结构Structure轮廓outline窗口, 快捷键是Alt+7

IntelliJ IDEA 查看类class的结构Structure轮廓outline窗口, 快捷键是Alt7 idea的结构Structure窗口相当于Eclipse的outline 快捷键是: Alt7 或者点击左上角主菜单面包屑,打开主菜单 然后菜单找到-视图&#xff08;View&#xff09;→ 工具窗口&#xff08;Tool Windows&…

基于大数据 Python+Vue 酒店爬取可视化系统(源码+LW+部署讲解+数据库+ppt)

&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 会持续一直更新下去 有问必答 一键收藏关注不迷路 源码获取&#xff1a;https://pan.baidu.com/s/1aRpOv3f2sdtVYOogQjb8jg?pwdjf1d 提取码: jf1d &#…

FineReport 分栏报表

将报表中的数据根据所需要的展示的样式将数据进行分栏展示列分栏 报表中数据是横向扩展的,超过一页的数据会显示在下一页,而每页下面会有很大的一片空白区域,不美观且浪费纸张。希望在一页中第一行扩展满后自动到下一行继续扩展 1、新建数据集 SELECT * FROM 公司股票2、内…

前端代码分享--爱心

给对象写的&#xff0c;顺便源码给大家分享一下 就是简单的htmlcssjs&#xff0c;不复杂 xin1.html <!DOCTYPE html> <html lang"zh-CN"> <head> <meta charset"UTF-8"> <title>写你自己的</title> <lin…

深入解析机器学习算法

深入解析机器学习算法 机器学习已经成为当今技术进步的核心推动力量&#xff0c;推动了众多行业的创新。其背后依赖的是各种各样的算法&#xff0c;帮助计算机通过从数据中学习来完成任务。这篇文章将对常见的几类机器学习算法进行深入探讨&#xff0c;帮助你理解其工作原理、…

攻防世界的新手web题解

攻防世界引导模式 1、disabled_button 好&#xff0c;给了一个按钮&#xff0c;第一道题目就不会做 看的wp<input disabled class"btn btn-default" style"height:50px;width:200px;" type"submit" value"flag" name"auth&q…

qt 滚动条 美化

qt QScrollBar 滚动条分为竖直与水平滚动条&#xff0c;两者设置上类似&#xff0c;但也有一些不同&#xff0c;下面主要讲述美化及注意事项。 一、竖直滚动条 竖直滚动条分为7个部分&#xff1a; sub-line、 up-arrow 、sub-page、 hanle、 add-line、 dow-arrow、 add-pag…