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_index
        min_index = i
        for 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 = False
        for j in range(i):
            if nums[j] > nums[j+1]:
                nums[j], nums[j+1] = nums[j+1], nums[j]
                flag = True
        if 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个元素作为base
        base = 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, right
    while 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 = 1
    if (nums[left] < nums[mid]) ^ (nums[left] < nums[right]):
        return left
    elif (nums[mid] < nums[left]) ^ (nums[mid] < nums[right]):
        return mid
    else:
        return right

def 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, right
    while 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 += 1
        else:
            tmp[k] = nums[j]
            j += 1
        k += 1
    # 将左子数组和右子数组的剩余元素复制到临时数组中,这两个循环实际只会执行一次
    # 因为剩下的那个元素必定是左右数组中最大的元素,所以放在最后
    while i <= mid:
        tmp[k] = nums[i]
        i += 1
        k += 1
    while j <= right:
        tmp[k] = nums[j]
        j += 1
        k += 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中最大的节点,记为ma
        l = 2 * i + 1
        r = 2 * i + 2
        ma = i
        if l < n and nums[l] > nums[ma]:
            ma = l
        if 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 = ma

def 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) // 2
    buckets = [[] 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 = 0
    for bucket in buckets:
        for num in bucket:
            nums[i] = num
            i += 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. 统计数组最大元素 m
    m = 0
    for num in nums:
        m = max(m, num)
    # 2. 统计各数字的出现次数
    # counter[num] 代表 num 的出现次数
    counter = [0] * (m + 1)
    for num in nums:
        counter[num] += 1
    # 3. 遍历 counter ,将各元素填入原数组 nums
    idx = 0
    for num in range(m+1):
        # 没出现的数字,会跳过循环,因为counter[num]=0
        for _ in range(counter[num]):
            nums[idx] = num
            idx += 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) % 10

def counting_sort_digit(nums: list[int], exp: int):
    """计数排序(根据 nums 第 k 位排序)"""
    # 十进制的位范围为 0~9 ,因此需要长度为 10 的桶数组
    counter = [0] * 10
    n = len(nums)
    # 统计 0~9 各数字的出现次数
    for i in range(n):
        d = digit(nums[i], exp)  # 获取 nums[i] 第 k 位,记为 d
        counter[d] += 1  # 统计数字 d 的出现次数
    # 求前缀和,将“出现个数”转换为“数组索引”
    for i in range(1, 10):
        counter[i] += counter[i - 1]
    # 倒序遍历,根据桶内统计结果,将各元素填入 res
    res = [0] * n
    for i in range(n - 1, -1, -1):
        d = digit(nums[i], exp)
        j = counter[d] - 1  # 获取 d 在数组中的索引 j
        res[j] = nums[i]  # 将当前元素填入索引 j
        counter[d] -= 1  # 将 d 的数量减 1
    # 使用结果覆盖原数组 nums
    for i in range(n):
        nums[i] = res[i]

def radix_sort(nums: list[int]):
    """基数排序"""
    # 获取数组的最大元素,用于判断最大位数
    m = max(nums)
    # 按照从低位到高位的顺序遍历
    exp = 1
    while exp <= m:
        # 对数组元素的第 k 位执行计数排序
        # k = 1 -> exp = 1
        # k = 2 -> exp = 10
        # 即 exp = 10^(k-1)
        counting_sort_digit(nums, exp)
        exp *= 10

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Langchain-Chatchat在windows平台离线部署(1)

Langchain-Chatchat在windows平台离线部署&#xff08;1&#xff09; pwd的不兼容 在调用数据库初始化程序的时候&#xff0c;系统将会调用pebblo.py程序&#xff0c;在此程序中&#xff0c;需要调用基于linux平台的pwd程序。 在windows环境下&#xff0c;pwd模块不兼容&…

extends继承

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

WInForm —— 自定义画板

项目模板:要实现在背景和无背景上完成画线&#xff0c;画直线、矩形、椭圆、并能随意调整字体的大小 首先要定义绘制的类型 enum DrawMode {None, // 没有选择绘制型Pen, // 画笔 画直线Line,// 画直线Rectangle,// 画矩形Ellipse, // 画椭圆Rubber // 橡皮擦 } //如果要想…

【Linux】进程的优先级及linux下进程的调度于切换

目录 ​编辑 1.优先级是什么 2.linux中的优先级是怎么实现的 ps -la 命令查看当前用户启动的进程​编辑 linux下调整优先级&#xff1a; ①先top一下 ②点击r ③需要输入进程的pid ④回车 ​编辑 ⑤输入想将优秀级修改的值&#xff1a; linux进程优先级范围为什么必须是【60,9…

Navicat的安装与破解

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

《系统分析与设计》实验-----需求规格说明书 哈尔滨理工大学

文章目录 需求规格说明书1&#xff0e;引言1.1编写目的1.2项目背景1.3定义1.4参考资料 2&#xff0e;任务概述2.1目标2.2运行环境2.3条件与限制 3&#xff0e;数据描述3.1静态数据3.2动态数据3.3数据库介绍3.4数据词典3.5数据采集 4&#xff0e;功能需求4.1功能划分4.2功能描述…

Java——封装、访问修饰符、包

目录 一.封装的概念 二.访问限定符 三.封装扩展之包 1.包的概念 2.导入包中的类 3.自定义包 4.包的访问权限控制举例 5.常见的包 一.封装的概念 面向对象程序三大特性&#xff1a;封装、继承、多态。而类和对象阶段&#xff0c;主要研究的就是封装特性。何为封装呢&am…

linux进阶篇:文件查找的利器——grep命令+管道操作详解

Linux文件查找的利器——grep命令管道操作详解 1 grep简介 grep (global search regular expression(RE) and print out the line,全面搜索正则表达式并把行打印出来)是一种强大的文本搜索工具&#xff0c;它能使用正则表达式搜索文本&#xff0c;并把匹配的行打印出来。 Uni…

Java面试八股文(更新中emsp;)(❤❤)

Java面试八股文 一. 基础篇1. Java语言特点2. 面向对象和面向过程的区别3. 八种基本数据类型的大小&#xff0c;以及他们的封装类4. 标识符的命名规则5. instanceof 关键字的作用6. Java自动装箱与拆箱面试题1&#xff1a; 以下代码会输出什么&#xff1f;面试题2&#xff1a;以…

Linux 网络测速

1.开发背景 网络测速&#xff0c;为了测试开发板的网络速度是否达标的通用测试方法 2.开发需求 搭建 iperf3 &#xff0c;在 ubuntu 下安装服务端&#xff0c;在板卡上安装客户端&#xff0c;服务端和客户端互发 3.开发环境 ubuntu20.04 嵌入式开发板&#xff08;debian 千…

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

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

记录flume运行时报NullPointerException异常

【背景说明】 我要起一个将kafka上的topic_log主题中的数据上传到hdfs上的flume进程。 这是我的flume配置文件脚本&#xff1a; #定义组件 a1.sourcesr1 a1.channelsc1 a1.sinksk1#配置source1 a1.sources.r1.type org.apache.flume.source.kafka.KafkaSource a1.sources.r…

JAVA基础06-面向对象,构造器,递归以及对象创建时内存分析(内含代码与练习)

面向对象的概念以及特征 概念 实质上将 "数据" 与 "行为" 的过程, 以类的形式封装起来, 一切以对象为中心语言。 面向对象的程序设计过程中有两个重要概念&#xff1a;类&#xff08;class&#xff09;和对象&#xff08;也称为实例&#xff09;。 其中…

YOLO-World——S

文章目录 Abstract成果 MethodPre-training Formulation: Region-Text PairsModel ArchitectureYOLO DetectorText EncoderText Contrastive HeadTraining with Online VocabularyInference with Offline Vocabulary Re-parameterizable Vision-Language PANText-guided CSPLay…

强烈推荐 ——电脑终端管理系统

强烈推荐&#xff01;电脑终端管理系统 电脑终端管理系统使用的目的是为了管控电脑上硬件和软件资产&#xff0c;以及员工使用电脑的行为&#xff0c;最终目的是为了保护企业资产和信息&#xff0c;以下是一些推荐的电脑终端管理系统&#xff0c;这些系统为企业提供了强大的功…

C语言基础入门案例(2)

目录 第一题&#xff1a;编写一个基于switch语句的等级评估程序 第二题&#xff1a;学生成绩评定 第三题&#xff1a;计算圆的周长和面积 第四题&#xff1a;将三个整数按从大到小顺序输出 第五题&#xff1a;打印九九乘法表 第一题&#xff1a;编写一个基于switch语句的等…

TXT文本批量高效编辑,支持给文章结尾进行添加上相同的结语,轻松应对多个文本

在信息爆炸的时代&#xff0c;我们每天面对大量的文本信息&#xff0c;无论是工作文档、新闻稿件还是社交媒体内容&#xff0c;都需要进行高效、精准的编辑。而XT文本批量编辑器正是您理想的助手&#xff0c;它支持批量高效编辑&#xff0c;更能在文章结尾添加上您想要的相同结…

Knowledge Editing for Large Language Models: A Survey

目录 IntroductionProblem Formulation评估指标Methods数据集应用讨论挑战未来方向 大型语言模型&#xff08;LLMS&#xff09;最近由于其出色的理解&#xff0c;分析和生成文本的能力而根据其广泛的知识和推理能力来改变了学术和工业景观。然而&#xff0c;LLM的一个主要缺点是…

韩顺平Java | C25 JDBC和连接池(上)

概述 JDBC概述&#xff1a;JDBC为访问不同数据库提供统一接口&#xff0c;为使用者屏蔽细节问题。Java程序员使用JDBC可以连接任何提供了JDBC驱动程序的数据库系统&#xff0c;从而完成对数据库的各种操作。 // 模拟代码 //JdbcInterface.java --Java规定的JDBC接口(方法) p…

计算机网络——抓取icmp包

前言 本博客是博主用于记录计算机网络实验的博客&#xff0c;如果疏忽出现错误&#xff0c;还望各位指正。 抓包 我们是用Wireshark工具来进行抓包的。 ​在安装时候一路打勾安装即可&#xff0c;不过最后那个因为是英文&#xff0c;一定要看清&#xff0c;点了立即重启&am…