Hello算法11:排序

https://www.hello-algo.com/chapter_sorting/

选择排序

  1. 初始未排序的区间是[0,n-1]
  2. 在[0,n-1]中查找最小元素,和索引0交换,此时未排序的区间是[1,n-1]
  3. 在[1,n-1]中查找最小元素,和索引1交换,此时未排序区间是[2,n-1]
  4. 以此类推,在n-1轮后,数组前n-1个元素已排序
  5. 剩下的元素是最大的元素
def selection_sort(nums: list[int]):n = len(nums)for i in range(n-1):# 假设最小元素的索引是min_indexmin_index = ifor j in range(i+1, n):if nums[j] < nums[min_index]:# 如果出现比min_index还小的元素,就将索引记录下来min_index = j# 最终,将i和[i+1, n]中的最小元素交换nums[i], nums[min_index] = nums[min_index], nums[i]

时间复杂度O(n²),空间复杂度O(1),原地排序,非稳定排序(因为排序中相等的元素,相对位置可能发生变化)

冒泡排序

  1. 对n个元素进行冒泡,将最大的元素移动至索引n-1的位置
  2. 对n-1个元素进行冒泡,将最大元素移动至索引n-2的位置
  3. 以此类推,第n-1轮之后,除第一个元素外,所有的元素都已排序
  4. 最终剩下的是最小的元素
def bubble_sort(nums: list[int]):n = len(nums)for i in range(n-1, 0, -1):for j in range(i):if nums[j] > nums[j+1]:nums[j], nums[j+1] = nums[j+1], nums[j]

优化,如果某轮么有进行交换,那说明已经排序完成了,可以提前结束循环:

def bubble_sort(nums: list[int]):n = len(nums)for i in range(n-1, 0, -1):flag = Falsefor j in range(i):if nums[j] > nums[j+1]:nums[j], nums[j+1] = nums[j+1], nums[j]flag = Trueif not flag:break

时间复杂度O(n²),空间复杂度O(1),原地排序,稳定排序。

插入排序

  1. 假设第一个元素已排序
  2. 将第二个元素作为base,将它插入到第一个元素的正确位置,这样前2个元素已排序
  3. 将第三个元素作为base,插入前面的正确位置,前3已排序
  4. 以此类推,第n轮,所有元素已排序。
def insertion_sort(nums: list[int]):n = len(nums)for i in range(1, n):# 将第i个元素作为basebase = nums[i]j = i-1# 第j个元素大于base,就把该元素往后移动,空出位置while j >= 0 and nums[j] > base:nums[j+1] = nums[j]j -= 1# 最终将base移动到正确位置nums[j+1] = base

在数据量小的时候,插入排序比较快。

时间复杂度O(n²),自适应,空间复杂度O(1),原地排序,稳定排序。

快速排序

哨兵划分,最终的结果是将数组划分成左子数组,哨兵,和右子数组,并满足a<b<c的关系。

def partition(nums: list[int], left: int, right: int) -> int:"""哨兵划分"""# 以 nums[left] 为基准数i, j = left, rightwhile i < j:# 从右向左,找首个小于基准数的数字while i < j and nums[j] >= nums[left]:j -= 1# 从左到右,找首个大于基准数的数字while i < j and nums[i] <= nums[left]:i += 1# 交换这两个数字nums[i], nums[j] = nums[j], nums[i]# 将基准数交换至边界nums[i], nums[left] = nums[left], nums[i]return i

接下来就是递归进行哨兵划分,直到不能再分

  1. 首先,对原数组执行一次“哨兵划分”,得到未排序的左子数组和右子数组。
  2. 然后,对左子数组和右子数组分别递归执行“哨兵划分”。
  3. 持续递归,直至子数组长度为 1 时终止,从而完成整个数组的排序。
def quick_sort(nums: list[int], left: int, right: int):"""快速排序"""# 子数组长度为 1 时终止递归if left >= right:return# 哨兵划分pivot = partition(nums, left, right)# 递归左子数组、右子数组quick_sort(nums, left, pivot - 1)quick_sort(nums, pivot + 1, right)

时间复杂度:O(nlogn)

空间复杂度:O(n),原地排序,非稳定排序

快速排序为什么快

  • 出现最差情况的概率很低
  • 缓存使用效率高
  • 复杂度的常数系数小:在前三种算法中,比较、复制、交换的总数最少。

基准数优化

快速排序在某些输入的情况下效率很低,例如数组完全倒序的情况下。这样每次哨兵划分,右子数组都是一个空数组。为了避免这种情况,一般取数组的首尾和中间三个元素,计算中位数作为基准数。

def median_three(nums: list[int], left: int, mid: int, right: int) -> int:"""选取三个候选元素的中位数"""# 此处使用异或运算来简化代码# 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1if (nums[left] < nums[mid]) ^ (nums[left] < nums[right]):return leftelif (nums[mid] < nums[left]) ^ (nums[mid] < nums[right]):return midelse:return rightdef partition2(nums: list[int], left: int, right: int) -> int:"""哨兵划分(三数取中值)"""# 以 nums[left] 为基准数med = self.median_three(nums, left, (left + right) // 2, right)# 将中位数交换至数组最左端nums[left], nums[med] = nums[med], nums[left]# 以 nums[left] 为基准数i, j = left, rightwhile i < j:while i < j and nums[j] >= nums[left]:j -= 1  # 从右向左找首个小于基准数的元素while i < j and nums[i] <= nums[left]:i += 1  # 从左向右找首个大于基准数的元素# 元素交换nums[i], nums[j] = nums[j], nums[i]# 将基准数交换至两子数组的分界线nums[i], nums[left] = nums[left], nums[i]return i  # 返回基准数的索引

尾递归优化

在某些输入下,比如数组完全有序的情况下,快速排序的空间占有率很高。

解决方法是在每轮哨兵排序完成后,比较两个子数组的长度,仅对较短的子数组进行递归

def quick_sort2(self, nums: list[int], left: int, right: int):"""快速排序(尾递归优化)"""# 子数组长度为 1 时终止while left < right:# 哨兵划分操作pivot = self.partition(nums, left, right)# 对两个子数组中较短的那个执行快速排序if pivot - left < right - pivot:self.quick_sort(nums, left, pivot - 1)  # 递归排序左子数组left = pivot + 1  # 剩余未排序区间为 [pivot + 1, right]else:self.quick_sort(nums, pivot + 1, right)  # 递归排序右子数组right = pivot - 1  # 剩余未排序区间为 [left, pivot - 1]

归并排序

原理:

划分阶段:通过递归,不断把数组从中点分开,将长数组排序转化为短数组排序问题。

合并阶段:当数组长度为1时停止划分,持续将两个较短的有序数组进行合并

def merge(nums: list[int], left: int, mid: int, right: int):"""合并左子数组和右子数组"""# 左子数组区间为 [left, mid], 右子数组区间为 [mid+1, right]# 创建一个临时数组 tmp ,用于存放合并后的结果tmp = [0] * (right - left + 1)# 初始化左子数组和右子数组的起始索引i, j, k = left, mid + 1, 0# 当左右子数组都还有元素时,进行比较并将较小的元素复制到临时数组中while i <= mid and j <= right:if nums[i] <= nums[j]:tmp[k] = nums[i]i += 1else:tmp[k] = nums[j]j += 1k += 1# 将左子数组和右子数组的剩余元素复制到临时数组中,这两个循环实际只会执行一次# 因为剩下的那个元素必定是左右数组中最大的元素,所以放在最后while i <= mid:tmp[k] = nums[i]i += 1k += 1while j <= right:tmp[k] = nums[j]j += 1k += 1# 将临时数组tmp中的元素复制回原数组nums的对应区间for k in range(0, len(tmp)):nums[left + k] = tmp[k]def merge_sort(nums: list[int], left: int, right: int):"""归并排序"""# 终止条件,当子数组长度为1时终止递归if left >= right:return# 划分阶段mid = (left + right) // 2  # 计算中点merge_sort(nums, left, mid)  # 递归左子数组merge_sort(nums, mid + 1, right)  # 递归右子数组# 合并阶段merge(nums, left, mid, right)

时间复杂度:O(nlogn) 空间复杂度:O(n) 非原地排序 稳定排序

堆排序

原理:

  1. 输入数组建立小顶堆,此时最小元素位于堆顶。
  2. 不断执行出堆操作,记录出堆元素,即可获取排序数组。

在实际操作中,上述方法需要占用额外存储空间,所以采用以下方法实现:

  1. 输入数组,建立大顶堆
  2. 将堆顶元素和最右叶节点的元素交换,也就是数组首元素和尾元素交换,此时已排序元素数量为1,堆长度减1
  3. 对堆执行从顶到底堆化操作。恢复堆的性质。
  4. 重复步骤2,3。循环n-1次后,就完成了排序
def sift_down(nums: list[int], n: int, i: int):"""堆的长度为 n ,从节点 i 开始,从顶至底堆化"""while True:# 找出i, l , r中最大的节点,记为mal = 2 * i + 1r = 2 * i + 2ma = iif l < n and nums[l] > nums[ma]:ma = lif r < n and nums[r] > nums[ma]:ma = r# 若节点i最大,或索引 l, r 越界,则无须继续堆化,跳出循环if ma == i:break# 交换两节点nums[i], nums[ma] = nums[ma], nums[i]# 循环向下堆化i = madef heap_sort(nums: list[int]):"""堆排序"""# 建堆操作:堆化除叶节点以外的其他所有节点for i in range(len(nums) // 2 - 1, -1, -1):sift_down(nums, len(nums), i)# 从堆中提取最大元素,循环n-1轮for i in range(len(nums) -1, 0, -1):# 交换根节点与最右叶节点(也就是数组首位元素nums[0], nums[i] = nums[i], nums[0]# 以根节点为起点,从顶至底进行堆化sift_down(nums, i, 0)

时间复杂度:O(nlogn) 空间复杂度O(1),原地排序 ,非稳定排序

桶排序

之前的排序都是基于“比较”的排序,接下来的是“非比较排序”算法。

桶排序首先设置一些有大小顺序的桶,每个桶对应一个数据范围,将数据评价分配到各个桶中,然后在各个桶内进行排序,最后将数据合并。

例如将一个长度为n的数组,元素范围是[0,1)

  1. 初始化k个桶,将n个元素分配到k个桶中
  2. 对每个桶分别指定排序
  3. 按照桶从小到大的顺序合并结果
def bucket_sort(nums: list[float]):"""桶排序"""# 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素k = len(nums) // 2buckets = [[] for _ in range(k)]# 1,将数组元素分配到各个桶中for num in nums:# 输入数据范围为[0,1),使用num*k映射到索引范围[0, k-1]# 因为num的值是小于1的,所以num*k 始终在 [0, k-1] 范围内i = int(num * k)buckets[i].append(num)# 2.对各个桶执行排序for bucket in buckets:bucket.sort()# 3.遍历桶合并结果i = 0for bucket in buckets:for num in bucket:nums[i] = numi += 1

时间复杂度:O(n+k),自适应排序,空间复杂度O(n+k),稳定性取决于桶内排序算法。

桶排序适用于数据量非常大的数据。比如系统内存无法一次性加载百万条数据的时候。

桶排序的关键在于,如何将数据平均地分配到各个桶中

方法一:首先将数据大致分到三个桶中,然后再对数据较多的桶往下划分

在这里插入图片描述

方法二:知道数据的大概分布,我们就可以设置合理的价格区间

在这里插入图片描述

计数排序

简单实现思路:

  1. 遍历数组,找出其中最大的数字m,创建一个长度为m+1的数组counter
  2. 借助counter统计nums中各数字的出现次数,其中counter[num]对应各数字的出现次数
  3. 由于counter的索引是天然有序的,所以所有元素已经排序完毕。只需遍历counter,按照次数填入nums即可
def counting_sort_naive(nums: list[int]):"""计数排序"""# 简单实现,无法用于排序对象# 1. 统计数组最大元素 mm = 0for num in nums:m = max(m, num)# 2. 统计各数字的出现次数# counter[num] 代表 num 的出现次数counter = [0] * (m + 1)for num in nums:counter[num] += 1# 3. 遍历 counter ,将各元素填入原数组 numsidx = 0for num in range(m+1):# 没出现的数字,会跳过循环,因为counter[num]=0for _ in range(counter[num]):nums[idx] = numidx += 1

适用于非负整数排序数据量大但数据范围较小的情况

时间复杂度O(n+m),空间复杂度O(n+m),非原地排序,简单实现不稳定,如果需要稳定请见库中的高级实现。

基数排序

基数排序是对基数排序的一种优化,利用数字各位数之间的关系进行递进排序,适用于数字位数比较多的情况。

def digit(num: int, exp: int) -> int:"""获取元素 num 的第 k 位,其中 exp = 10^(k-1)"""# 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算return (num // exp) % 10def counting_sort_digit(nums: list[int], exp: int):"""计数排序(根据 nums 第 k 位排序)"""# 十进制的位范围为 0~9 ,因此需要长度为 10 的桶数组counter = [0] * 10n = len(nums)# 统计 0~9 各数字的出现次数for i in range(n):d = digit(nums[i], exp)  # 获取 nums[i] 第 k 位,记为 dcounter[d] += 1  # 统计数字 d 的出现次数# 求前缀和,将“出现个数”转换为“数组索引”for i in range(1, 10):counter[i] += counter[i - 1]# 倒序遍历,根据桶内统计结果,将各元素填入 resres = [0] * nfor i in range(n - 1, -1, -1):d = digit(nums[i], exp)j = counter[d] - 1  # 获取 d 在数组中的索引 jres[j] = nums[i]  # 将当前元素填入索引 jcounter[d] -= 1  # 将 d 的数量减 1# 使用结果覆盖原数组 numsfor i in range(n):nums[i] = res[i]def radix_sort(nums: list[int]):"""基数排序"""# 获取数组的最大元素,用于判断最大位数m = max(nums)# 按照从低位到高位的顺序遍历exp = 1while exp <= m:# 对数组元素的第 k 位执行计数排序# k = 1 -> exp = 1# k = 2 -> exp = 10# 即 exp = 10^(k-1)counting_sort_digit(nums, exp)exp *= 10

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

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

相关文章

基于springboot+vue+Mysql的藏区特产销售平台

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

TC387实现SPI自通讯

TC387实现SPI自通讯 预期效果&#xff1a; TC387上定义两个SPI通讯接口&#xff0c;一个用于发数据一个用于收数据。准确无误的收到一次数据就对核心板led灯的状态进行一次翻转。 由于实验设备有限&#xff0c;只能想办法通过现有设备进行实验。 实现过程&#xff1a; 最开…

linux 设置定时任务---学习

1、设置定时任务 crontab -e 设置格式参考&#xff1a;【Linux】Linux crontab 命令定时任务设置_crontab 设置每天10:30执行-CSDN博客 测试过程&#xff1a; */1 * * * * /root/cronjob.sh 脚本内容: echo "hell0 cronjob" >> /root/test/hello.txt 实现…

extends继承

目录 什么时候用继承? 继承的格式? 继承的特点 子类可以继承父类的哪些呢&#xff1f; 是否可以继承父类的构造方法呢&#xff1f; 是否可以继承成员变量&#xff1f; 是否可以继承成员方法&#xff1f; 在Java中&#xff0c;extends关键字用于实现继承关系。通过使用…

24年重庆三支一扶报名照不通过怎么处理?

24年重庆三支一扶报名照不通过怎么处理&#xff1f;

Laravel 11入门:使用ServBay打造高效开发环境

Laravel 11发布&#xff0c;改进了不少功能。 它引入了更加流畅的应用结构、每秒限速、健康路由等特性。 此外&#xff0c;Laravel还推出了第一方可扩展的WebSocket服务器Laravel Reverb&#xff0c;为你的应用提供强大的实时功能。 在今天的指南中&#xff0c;我将设置一个…

ASP.NET基于BS课件发布系统

摘 要&#xff1a;本文在分析建立动态网站的技术细节和课件发布工作流程的基础上&#xff0c;阐述了网站的结构与功能设计。本网站采用的是B/S结构&#xff0c;网站使用 Microsoft Visual Studio .NET2003作为主要开发工具&#xff0c;采用Dreamweaver 作为辅助开发工具实现网…

ELK日志分析系统之Zookeeper

一、Zookeeper简介 ZooKeeper是一种为分布式应用所设计的高可用、高性能且一致的开源协调服务&#xff0c;它提供了一项基本服务&#xff1a;分布式锁服务。分布式应用可以基于它实现更高级的服务&#xff0c;实现诸如同步服务、配置维护和集群管理或者命名的服务。 Zookeepe…

3_3.Apache的管理及优化web

### 一.Apache的作用 ### 在web被访问时通常使用http://的方式 http:// ##超文本传输协议 http:// 超文本传输协议提供软件&#xff1a; Apache nginx stgw jfe Tengine ### 二.Apache的安装 ### dnf install httpd.x86_64 -y ### 三.Apache的启用 ### systemctl enable --…

C语言 03 VSCode开发

安装好 C 语言的开发环境后&#xff0c;就需要创建项目进行开发了。 使用 IDE&#xff08;集成开发环境&#xff09;进行开发了。 C 语言的开发工具很多&#xff0c;现在主流的有 Clion、Visual Studio、VSCode。 这里以 VSCode 作为演示。 创建项目 安装 VSCode。 推荐直接在…

【C语言】<动态内存管理>我的C语言终末章

&#xff1c;动态内存管理&#xff1e; 1. 为什么要有动态内存分配2. malloc和free2.1 malloc2.2 free 3. calloc和realloc3.1 calloc3.2 realloc 4.常见的动态内存错误4.1 对NULL指针的解引用操作4.2 对动态开辟空间的越界访问4.3 对非动态开辟内存使用free释放4.4 使用free释…

Navicat的安装与破解

个人介绍 hello hello~ &#xff0c;这里是 code袁~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f981;作者简介&#xff1a;一名喜欢分享和记录学习的…

linux下常见解压命令gz、tar、zip详解

常见解压缩命令汇总 # .tar.gz解压 tar -zxvf jdk-17_linux-aarch64_bin.tar.gz # .tar.gz压缩 tar -czvf archive.tar.gz /path/to/directory# .gz解压 gzip -d file.gz # .gz压缩 gzip filename# zip解压 unzip filename.zip # zip压缩 zip archive.zip /path/to/file.tar.g…

累积分布函数图(CDF)的介绍、matlab的CDF图绘制方法(附源代码)

在对比如下两个误差的时候&#xff0c;怎么直观地分辨出来谁的误差更低一点&#xff1f;&#xff1a; 通过这种误差时序图往往不容易看出来。 但是如果使用CDF图像&#xff0c;以误差绝对值作为横轴&#xff0c;以横轴所示误差对应的累积概率为纵轴&#xff0c;绘制曲线图&am…

SpringBoot启动时banner设置

SpringBoot启动时banner设置 1.操作步骤2.各种banner图像 1.操作步骤 在application.properties文件中设置新的banner对于的文件位置&#xff0c;最好放在resources目录下 spring.banner.locationbanner.txt2.各种banner图像 &#xff08;1&#xff09;经典大佛图 具体txt文…

【Linux C | 多线程编程】线程同步 | 互斥量(互斥锁)介绍和使用

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; ⏰发布时间⏰&#xff1a; 本文未经允许…

洛谷P1364医院设置

洛谷P1364 医院设置 医院设置 题目描述 设有一棵二叉树&#xff0c;如图&#xff1a; 其中&#xff0c;圈中的数字表示结点中居民的人口。圈边上数字表示结点编号&#xff0c;现在要求在某个结点上建立一个医院&#xff0c;使所有居民所走的路程之和为最小&#xff0c;同时约…

vue--双向数据绑定原理

Vue采用数据劫持 发布者-订阅者模式实现双向数据绑定&#xff0c;实现逻辑图如下所示&#xff1a; 数据劫持 Vue 借助Object.defineProperty()来劫持各个属性&#xff0c;这样一来属性存取过程都会被监听到 发布者-订阅者模式 主要实现三个对象&#xff1a;Observer&#…

包装类初识泛型

一.包装类 在Java中, 基本类型不继承于Object类. 所以为了在泛型代码中可以支持基本类型,Java给每个基本类型都对应了一个包装类型.(包装类型相当于引用类型) 1.基本类型对应的包装类 byte -- Byteshort -- Shortint -- Integerlong -- Longfloat -- Floatdouble -- Doublech…

用c++实现串匹配问题、选择排序

5.2.2 串匹配问题 【问题】 给定两个字符串S和T&#xff0c;在主串S中查找子串T的过程称为串匹配(string matching,也称模式匹配&#xff09;&#xff0c;T称为模式。在文本处理系统、操作系统、编译系统、数据库系统以及 Internet 信息检索系统中&#xff0c;串匹配是使用最频…