【数据结构】图解八大排序(下)

文章目录

  • 一、前言
  • 二、快速排序
    • 1. hoare 版
    • 2. 挖坑法
    • 3. 前后指针法
    • 4. 快排的非递归实现
    • 5. 时空复杂度分析
  • 三、归并排序
    • 1. 递归实现
    • 2. 非递归实现
  • 四、计数排序

一、前言

在上一篇文章中,我们已经学习了五种排序算法,还没看过的小伙伴可以去看一下:传送门
今天要讲的是八大排序中剩下的三种,这三种排序算法用的是非常多的,需要好好掌握。
下面介绍的排序算法均以排升序为例。

二、快速排序

快排的思想是分治,就是选定一个基准值,使这个值的左边都小于等于它,右边都大于等于它,然后递归处理左右子区间。

因此快排的关键就在于如何选定基准值和如何实现划分左右区间。
关于前者,我们可以选左端点,也可以选右端点,也可以选中点,还可以随机选。我们不妨先以左端点为基准值,来继续下面的分析。
关于后者,其实有很多种方法可以实现划分,这里只讲几种比较经典的。

1. hoare 版

hoare是快排的发明者,我们先来学习他的划分方法。
给几分钟认真观察下面的动图。
在这里插入图片描述
以排升序为例,我们要使左边都小于等于key,右边都大于等于key,就可以利用左右双指针来维护这段区间,右指针找小,左指针找大,找到后交换,再继续找,直至左右指针相遇为止。最后把key与相遇处的值交换即可。

先来解释下为什么选左端点为基准值的时候,要右指针先走。
先说结论,这样做是为了保证最后左右指针相遇处的值比key小,从而保证key与相遇位置的值交换后,左边都小于key,右边都大于key。同理如果以右端点为基准值,就要左指针先走。
再来解释下为什么会这样。左右指针相遇无非两种情况,要么左遇右,要么右遇左。
如果左遇右,因为右指针先走,所以右指针先停。由于右指针是找小,所以右指针停的位置的值比key小,所以相遇处的值就比key小。
如果右遇左,也就是说右指针还没停,因为右指针是先走的,一种情况是左指针根本没动过,还停留在左端点;另一种情况是左指针动过,但是上一轮左右指针指向的值交换后,左指针位置的值就比key小了。所以上述两种情况都能保证key与左右指针相遇处的值交换后,左边都小于key,右边都大于key

来看看代码怎么写。

void QuickSort1(int a[], int l, int r)
{if (l >= r) return;int keyi = l;//左端为key,右端先走int i = l, j = r;//保存一下左右指针的初始位置while (l < r){while (l < r && a[r] >= a[keyi]) r--;//右边找小while (l < r && a[l] <= a[keyi]) l++;//左边找大swap(&a[l], &a[r]);}swap(&a[keyi], &a[l]);keyi = l;QuickSort1(a, i, keyi - 1);QuickSort1(a, keyi + 1, j);
}

第一趟结束后,因为key的左边都小于等于它,右边都大于等于它,所以key已经在最终排好序的位置上了。故只需递归[l, keyi - 1][keyi + 1, r]即可。

2. 挖坑法

这种方法比较好理解,直接上动图和代码。
在这里插入图片描述

void QuickSort2(int a[], int l, int r)
{if (l >= r) return;int key = a[l];int hole = l;int i = l, j = r;while (l < r){while (l < r && a[r] >= key) r--;a[hole] = a[r];hole = r;while (l < r && a[l] <= key) l++;a[hole] = a[l];hole = l;}a[hole] = key;QuickSort2(a, i, hole - 1);QuickSort2(a, hole + 1, j);
}

3. 前后指针法

在这里插入图片描述

void QuickSort3(int a[], int l, int r)
{if (l >= r) return;int keyi = l;int prev = l, cur = l + 1;while (cur <= r){if (a[cur] < a[keyi])swap(&a[++prev], &a[cur]);cur++;}swap(&a[prev], &a[keyi]);QuickSort3(a, l, prev - 1);QuickSort3(a, prev + 1, r);
}

以上三种方法效率并无差异,只是实现划分的思想不同,挑一种自己好理解的掌握就行。个人比较喜欢 hoare 版的快排。

4. 快排的非递归实现

众所周知,函数栈帧是建立在栈空间上的,而栈空间的大小是有限的,如果递归过深可能会导致栈溢出。因此算法的非递归实现具有很重要的意义。

我们可以利用数据结构中的栈来模拟递归的过程。

void QuickSortNonR(int a[], int l, int r)
{ST st;STInit(&st);STPush(&st, r);STPush(&st, l);while (!STEmpty(&st)){l = STTop(&st);STPop(&st);r = STTop(&st);STPop(&st);//这里是hoare版的划分思想int keyi = l;int i = l, j = r;while (i < j){while (i < j && a[j] >= a[keyi]) j--;while (i < j && a[i] <= a[keyi]) i++;swap(&a[i], &a[j]);}swap(&a[keyi], &a[i]);keyi = i;//如果区间长度==2//它的两个子区间长度就分别为1和0,故不用继续递归if (r - keyi > 2)//r - (keyi + 1) + 1{STPush(&st, r);STPush(&st, keyi + 1);}if (keyi - l > 2)//(keyi - 1) - l + 1{STPush(&st, keyi - 1);STPush(&st, l);}}STDestroy(&st);
}

由于 C 语言的特性,我们只能自己实现栈这个数据结构,具体可以看我这篇文章:【数据结构】栈和队列

另外,快排的非递归还可以用队列实现,有兴趣的可以自己尝试。
提示:栈模拟的是dfs,队列模拟的是bfs

5. 时空复杂度分析

快排的平均时间复杂度是O(NlogN),数组有序的情况下会退化成O(N^2)
空间复杂度是O(logN)

快排是不稳定的排序算法。

三、归并排序

归并排序的思想是分治,就是先递归到最后一层,此时每个子区间都只有一个数,即每个子区间都有序,每两个子区间再合并成一个有序区间,最后扫尾。

在这里插入图片描述

完成这个过程需要一个辅助数组,另外要开辟logN层栈帧,故空间复杂度为O(N)
在这里插入图片描述

归并排序的时间复杂度是O(NlogN)
归并排序是稳定的排序算法。

来看看代码怎么写。

1. 递归实现

void _MergeSort(int a[], int l, int r, int* tmp)
{if (l >= r) return;int mid = l + r >> 1;_MergeSort(a, l, mid, tmp);_MergeSort(a, mid + 1, r, tmp);int i = l, j = mid + 1, k = l;while (i <= mid && j <= r){if (a[i] <= a[j]) tmp[k++] = a[i++];else tmp[k++] = a[j++];}while (i <= mid) tmp[k++] = a[i++];while (j <= r) tmp[k++] = a[j++];for (int i = l; i <= r; i++) a[i] = tmp[i];
}void MergeSort(int a[], int n)
{int* tmp = (int*)malloc(sizeof(int) * n);_MergeSort(a, 0, n - 1, tmp);free(tmp);
}

2. 非递归实现

void MergeSortNonR(int a[], int n)
{int* tmp = (int*)malloc(sizeof(int) * n);int gap = 1;while (gap < n){int j = 0;for (int i = 0; i < n; i += 2 * gap){// 每组的合并数据int begin1 = i, end1 = i + gap - 1;int begin2 = i + gap, end2 = i + 2 * gap - 1;if (end1 >= n || begin2 >= n){break;}// 修正if (end2 >= n){end2 = n - 1;}while (begin1 <= end1 && begin2 <= end2){if (a[begin1] <= a[begin2]){tmp[j++] = a[begin1++];}else{tmp[j++] = a[begin2++];}}while (begin1 <= end1){tmp[j++] = a[begin1++];}while (begin2 <= end2){tmp[j++] = a[begin2++];}// 归并一组,拷贝一组memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));}gap *= 2;}free(tmp);
}

归并排序的缺点在于O(N)的空间复杂度和拷贝数组的消耗。
因此归并排序的思想更多的是用于外部排序。

四、计数排序

计数排序的思想是相对映射。

直接来看代码怎么实现。

void CountSort(int a[], int n)
{int min = 0, max = 0;for (int i = 0; i < n; i++){if (a[i] < min) min = a[i];if (a[i] > max) max = a[i];}int range = max - min + 1;int* count = (int*)calloc(range, sizeof(int));for (int i = 0; i < n; i++){int idx = a[i] - min;count[idx]++;}for (int i = 0, j = 0; i < range; i++){while (count[i]--){a[j++] = i + min;}}free(count);
}

时间复杂度 O(N + range)
空间复杂度 O(range)

缺陷:
1.只适合用于数据范围较为集中的情况。
如果range过大,时间复杂度和空间复杂度都会很大,时空效率都很一般。
但是在range较小的情况下,时间复杂度可以做到线性,空间复杂度可以做到常数级。
2.只能用于整型数据的排序。
因为计数排序是用计数数组的下标来相对映射原数组每个元素的值,而数组下标只能是整型。

总之,计数排序在排序范围较集中的整型数据时,效率非常好,大幅优于其他排序算法。

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

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

相关文章

Redis数据结构 — Listpack

目录 listpack 结构 listpack 节点结构 quicklist 虽然通过控制 quicklistNode 结构里的压缩列表的大小或者元素个数&#xff0c;来减少连锁更新带来的性能影响&#xff0c;但是并没有完全解决连锁更新的问题。 于是&#xff0c;Redis 在 5.0 新设计一个数据结构叫 listpack…

(学习笔记)TCP基础知识

什么是TCP? TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。 面向连接&#xff1a;一定是[一对一]才能连接&#xff0c;不能像UDP协议可以一个主机同时向多个主机发送消息&#xff0c;也就是一对多是无法做到的&#xff1b;可靠的&#xff1a;无论网络链路中出现了…

mongodb集群搭建

下载地址&#xff1a; https://www.mongodb.com/try/download/community下载mongodb-linux-x86_64-rhel70-5.0.18 搭建集群 tar -zxvf mongodb-linux-x86_64-rhel70-5.0.18.tgz mkdir -p data/dp cd mongodb-linux-x86_64-rhel70-5.0.18 mkdir -p data/db mkdir log mkdir c…

AIGC之文本内容生成概述(下)——Transformer

在上一篇文章中&#xff0c;我们一口气介绍了LSTM、Word2Vec、GloVe、ELMo等四种模型的技术发展&#xff0c;以及每种模型的优缺点与应用场景&#xff0c;全文超过一万字&#xff0c;显得冗长且繁杂&#xff0c;在下文部分我们将分开介绍Transformer、BERT、GPT1/GPT2/GPT3/Cha…

Java:输入与输出

目录 输入输出args 输入Scanner 输入格式化输出文件输入与输出 输入输出 args 输入 利用main函数中的参数args&#xff0c;当然也可以起别的名字。其他语言也是一样的。输入时空格分隔。 args的作用&#xff1a;在程序启动时可以用来指定外部参数 Scanner 输入 需要import j…

JDK、JRE、JVM之间的关系是什么?

目录 JVM、JRE、JDK的关系&#xff1f; JDK、JRE、JVM都是什么&#xff1f; JVM JRE JDK JVM、JRE、JDK的关系&#xff1f; 三者包含关系&#xff1a; JDK>JRE>JVM JDK、JRE、JVM都是什么&#xff1f; jdk&#xff1a;是用于java开发的最小环境 包括&#xff1a;ja…

如何助力企业DCMM贯标落地,这里有答案

DCMM作为国家第一个数据管理领域标准&#xff0c;是企业落实数据驱动战略、实现数字化转型的重要抓手。从行业实践来看&#xff0c;国内多个行业开始在全面拥抱DCMM模型&#xff0c;根据模型开展数据管理评估和能力提升工作。 01 什么是DCMM DCMM是国家标准《GB/T36073-2018 数…

Word文档突然无法打开?如何修复损坏文档?

在工作学习中&#xff0c;通常会遇到这种情况&#xff0c;我们正在编辑Word文件&#xff0c;电脑忽然断电关机&#xff0c;或者死机需要重启。当电脑重启以后&#xff0c;辛辛苦苦编辑很久的Word文件却忽然打不开了&#xff01;一直提示文件错误&#xff0c;如何解决Word无法打…

postgresql 内核源码分析 表锁relation lock的使用,session lock会话锁的应用场景,操作表不再困难

​专栏内容&#xff1a; postgresql内核源码分析 手写数据库toadb 并发编程 个人主页&#xff1a;我的主页 座右铭&#xff1a;天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物. 表锁介绍 当表打开&#xff0c;或者操作表时&#xff0c;都需要…

Go速成-常量

1.常量的定义 Go语言常量&#xff0c;定义的值是不能进修修改的&#xff0c;定义常量就是const&#xff0c;常量定义建议全部大写 const PI float32 3.1415946 //显式定义 const (x int 16ys "abc"z)fmt.Print(x,y,s,z) 在定义常量的时候如果没有声明值&#xff…

Gradle和Aritifactory,实现上传Apk并发送消息到钉钉

Gradle和Aritifactory 本文链接&#xff1a;https://blog.csdn.net/feather_wch/article/details/131746580 文章目录 Gradle和AritifactoryGradle基本介绍Gradle插件开发流程本地仓库artifactory搭建添加仓库使用本地仓库gradle插件仓库引入 插件buildSrc开发步骤xxxPluginPg…

五、DQL-2.基本查询

一、数据准备 1、删除表employee&#xff1a; drop table employee; 2、创建表emp&#xff1a; 3、添加数据&#xff1a; 4、查看表数据&#xff1a; 【代码】 -- 查询数据--------------------------------------------------------- drop table emp;-- 数据准备-----------…

linux之Ubuntu系列 find 、 ln 、 tar、apt 指令 软链接和硬链接 snap

查找文件 find 命令 功能非常强大&#xff0c;通常用来在 特定的目录下 搜索 符合条件的文件 find [path] -name “.txt” 记得要加 “ ” 支持通配符 &#xff0c;正则表达式 包括子目录 ls 不包括 子目录 如果省略路径&#xff0c;表示 在当前路径下&#xff0c;搜索 软链接…

LabVIEW开发航空电子设备嵌入式诊断半物理仿真系统

LabVIEW开发航空电子设备嵌入式诊断半物理仿真系统 航电集成系统是现代战争飞机的重要组成部分&#xff0c;包括惯性导航系统、飞行控制系统、机电管理系统和任务计算机等子系统。战机的作战性能与航电系统息息相关&#xff0c;可以说&#xff0c;没有高性能的空电系统&#x…

【Python爬虫开发基础⑭】Scrapy架构(组件介绍、架构组成和工作原理)

&#x1f680;个人主页&#xff1a;为梦而生~ 关注我一起学习吧&#xff01; &#x1f4a1;专栏&#xff1a;python网络爬虫从基础到实战 欢迎订阅&#xff01;后面的内容会越来越有意思~ &#x1f4a1;往期推荐&#xff1a; ⭐️前面比较重要的基础内容&#xff1a; 【Python爬…

数据库用户管理

数据库用户管理 一、创建&#xff1a; 1.新建用户&#xff1a; CREATE USER 用户名来源地址 [IDENTIFIED BY [PASSWORD] 密码];‘用户名’&#xff1a;指定将创建的用户名. ‘来源地址’&#xff1a;指定新创建的用户可在哪些主机上登录&#xff0c;可使用IP地址、网段、主机…

Docker 应用容器引擎

Docker 应用容器引擎 一、Docker是什么二、Docker安装和查看1、docker安装2、docker版本信息查看3、docker信息查看 三、镜像操作四、容器操作1、容器创建2、创建并启动容器3、容器的进入4、复制5、容器的导入和导出6、删除容器 一、Docker是什么 是一个开源的应用容器引擎&…

ROS:nodelet

目录 一、前言二、概念三、作用四、使用演示4.1案例简介4.2nodelet 基本使用语法4.3内置案例调用 五、nodelet实现5.1需求5.2流程5.3准备5.4创建插件类并注册插件5.5构建插件库5.6使插件可用于ROS工具链5.6.1配置xml5.6.2导出插件 5.7执行 一、前言 ROS通信是基于Node(节点)的…

【动手学深度学习】--15.含并行连结的网络GoogLeNet

文章目录 含并行连结的网络GoogLeNet1.Inception块2.GoogLeNet模型3.训练模型 含并行连结的网络GoogLeNet 学习视频&#xff1a;含并行连结的网络 GoogLeNet / Inception V3【动手学深度学习v2】 官方笔记&#xff1a;含并行连结的网络&#xff08;GoogLeNet&#xff09; 1.…

Appium-Python-Client 源码剖析 (一) driver 的元素查找方法

目录 前言 源码版本:0.9 结构图&#xff1a; mobileby.py appium 的 webdriver.py selenium 的 webdriver.py seleniumdriver appiumdriver 前言 Appium-Python-Client是一个用于Python语言的Appium客户端库&#xff0c;它提供了丰富的API和功能&#xff0c;用于编写和…