惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

月光博客
月光博客
Cyberwarzone
Cyberwarzone
L
LINUX DO - 最新话题
N
News and Events Feed by Topic
T
Troy Hunt's Blog
Help Net Security
Help Net Security
S
Security @ Cisco Blogs
Google DeepMind News
Google DeepMind News
Security Archives - TechRepublic
Security Archives - TechRepublic
M
MIT News - Artificial intelligence
G
Google Developers Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V2EX - 技术
V2EX - 技术
Y
Y Combinator Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
大猫的无限游戏
大猫的无限游戏
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Microsoft Security Blog
Microsoft Security Blog
Cisco Talos Blog
Cisco Talos Blog
T
Threatpost
Recent Commits to openclaw:main
Recent Commits to openclaw:main
S
SegmentFault 最新的问题
I
InfoQ
H
Hacker News: Front Page
D
Docker
Scott Helme
Scott Helme
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Blog — PlanetScale
Blog — PlanetScale
人人都是产品经理
人人都是产品经理
博客园 - 叶小钗
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
N
Netflix TechBlog - Medium
AWS News Blog
AWS News Blog
Know Your Adversary
Know Your Adversary
博客园 - 【当耐特】
T
Tor Project blog
U
Unit 42
H
Heimdal Security Blog
Microsoft Azure Blog
Microsoft Azure Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
P
Privacy & Cybersecurity Law Blog
PCI Perspectives
PCI Perspectives
美团技术团队
O
OpenAI News
T
Tailwind CSS Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
B
Blog
GbyAI
GbyAI
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
MyScale Blog
MyScale Blog

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 Claude code入门 - 陈彦斌 一文学习入门 ThingsBoard 开源物联网平台 GitHub 热门项目 | 2026年04月16日 如何为GIT设置全局勾子,为每次提交追加信息 Number.isFinite和isFinite与isNaN()和Number.isNaN的区别 PortSwigger SQL注入LAB2 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) GLM模型这么火,咱们用vllm也咧一个呗! 深入理解 AbortController:从底层原理到跨语言设计哲学 字符串学习笔记 多租户系统框架的基础模块设计和分析设计 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 ElasticSearch7.X版本配置密码 用Manim实现动态交点计算--从一个动点问题说起 团结引擎+Addressable+Instant Game打包抖音小游戏 function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 bubseek —— 让 Agent 的足迹,变成团队的洞察 通过 C# 读取并导出 PDF 书签 如何用 GitHub Actions 实现 Steam 自动化发布 【从0到1构建一个ClaudeAgent】并发-后台任务 .NET 高级开发 | 定制 ASP.NET Core 框架 电子小白:什么是运算放大器(运放) zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 工作的意义、工作的困难认知再思考 .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 开了 TUN 模式还是直连?90% 的人都踩过这个坑 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术
十种排序算法
myLv · 2026-05-17 · via 博客园_首页

引言

排序算法是计算机科学中最基础也最重要的算法之一,它的应用无处不在:从考试成绩排名到电商商品筛选,从数据库查询优化到操作系统任务调度。掌握经典排序算法不仅是编程入门的必经之路,更是理解算法设计思想(如分治、贪心、动态规划)的关键。

本文将详细讲解十种最常用的排序算法,包括它们的核心思想、实现步骤、代码实现、时间/空间复杂度分析以及适用场景。无论您是编程初学者还是准备算法竞赛的选手,都能从本文中获得有价值的内容。


一、冒泡排序

核心思想:通过相邻元素的比较和交换,将较大的元素逐步"冒泡"到数组的末尾,就像水中的气泡向上浮起一样。每一轮冒泡都会确定一个最大元素的最终位置。

算法步骤

  1. 从数组第一个元素开始,依次比较相邻的两个元素
  2. 如果前一个元素大于后一个元素,交换它们的位置
  3. 重复步骤1-2,直到本轮没有发生任何交换(说明数组已经有序)
  4. 每完成一轮冒泡,下一轮的比较次数减少1

代码实现

/*======= 冒泡排序 =======*/
void bubbleSort(int *arr, int n)
{
    for (int i = 0; i < n-1; i++) // i表示已确定位置的元素个数
    {
        bool swapped = false; // 优化:标记本轮是否发生交换
        for (int j = 0; j < n-i-1; j++) // 未排序部分的长度为n-i
        {
            if (arr[j] > arr[j+1]) // 前一个元素更大,需要交换
            {
                int tmp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = tmp;
                swapped = true;
            }
        }
        if (!swapped) break; // 本轮没有交换,数组已经有序,提前退出
    }
}

优缺点分析

  • ✅ 优点:实现简单,排序稳定,不需要额外空间
  • ❌ 缺点:时间复杂度高,效率低下,仅适用于小规模数据

适用场景:小规模数据集、要求排序稳定、教学演示场景


二、选择排序

核心思想:将数组分为已排序和未排序两部分,每次从未排序部分中选择最小的元素,放到已排序部分的末尾。与冒泡排序不同,选择排序每轮只进行一次交换。

算法步骤

  1. 初始时,已排序部分为空,未排序部分为整个数组
  2. 从未排序部分中找到最小元素的索引
  3. 将最小元素与未排序部分的第一个元素交换
  4. 已排序部分长度加1,未排序部分长度减1
  5. 重复步骤2-4,直到未排序部分为空

代码实现

/*======= 选择排序 =======*/
void selectSort(int *arr, int n)
{
    for (int i = 0; i < n-1; i++) // i表示已排序部分的末尾索引
    {
        int minIndex = i; // 假设当前元素是最小值
        for (int j = i+1; j < n; j++) // 在未排序部分找最小值
        {
            if (arr[j] < arr[minIndex])
            {
                minIndex = j; // 更新最小值索引
            }
        }
        // 将最小值交换到已排序部分的末尾
        if (minIndex != i)
        {
            int tmp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = tmp;
        }
    }
}

优缺点分析

  • ✅ 优点:实现简单,交换次数少(每轮最多一次),不需要额外空间
  • ❌ 缺点:不稳定,时间复杂度高,效率低下

适用场景:小规模数据集、内存受限、不需要稳定性的场景


三、插入排序

核心思想:将数组分为已排序和未排序两部分,每次从未排序部分取出第一个元素,插入到已排序部分的正确位置。类似于我们整理扑克牌的过程。

算法步骤

  1. 初始时,已排序部分只有第一个元素
  2. 取出未排序部分的第一个元素
  3. 从后往前遍历已排序部分,找到第一个小于等于当前元素的位置
  4. 将当前元素插入到该位置后面
  5. 重复步骤2-4,直到未排序部分为空

代码实现

/*======= 插入排序 =======*/
void insertSort(int *arr, int n)
{
    for (int i = 1; i < n; i++) // i表示未排序部分的第一个元素索引
    {
        int tmp = arr[i]; // 保存当前要插入的元素
        int j = i - 1;
        // 从后往前找插入位置
        while (j >= 0 && arr[j] > tmp)
        {
            arr[j+1] = arr[j]; // 元素后移
            j--;
        }
        arr[j+1] = tmp; // 插入到正确位置
    }
}

优缺点分析

  • ✅ 优点:实现简单,排序稳定,不需要额外空间,对基本有序的数据效率极高
  • ❌ 缺点:时间复杂度高,对于大规模无序数据效率低下

适用场景

  • 小规模数据集(通常n < 32)
  • 数据基本有序的情况
  • 作为其他高级排序算法的子程序(如快速排序的优化)

四、希尔排序

核心思想:插入排序的改进版,也称为"缩小增量排序"。通过设置不同的步长(gap)将数组分为多个子序列,分别进行插入排序。随着步长逐渐减小,数组变得越来越有序,最后当步长为1时,对整个数组进行一次插入排序。

算法步骤

  1. 选择一个初始步长gap(通常为数组长度的一半)
  2. 将数组按gap分为gap个子序列
  3. 对每个子序列进行插入排序
  4. 将gap缩小为原来的一半
  5. 重复步骤2-4,直到gap变为0

代码实现

/*======= 希尔排序 =======*/
void shellSort(int *arr, int n)
{
    // 初始步长为数组长度的一半,每次减半
    for (int gap = n/2; gap > 0; gap /= 2)
    {
        // 对每个子序列进行插入排序
        for (int i = gap; i < n; i++)
        {
            int tmp = arr[i];
            int j;
            for (j = i; j >= gap && arr[j-gap] > tmp; j -= gap)
            {
                arr[j] = arr[j-gap];
            }
            arr[j] = tmp;
        }
    }
}

优缺点分析

  • ✅ 优点:比插入排序效率高,实现简单,不需要额外空间
  • ❌ 缺点:不稳定,步长选择会影响排序效率

适用场景:中等规模数据集、内存受限环境、对稳定性无要求


五、快速排序

核心思想:采用分治策略,选择一个基准元素(pivot),将数组分为两部分:左边部分小于等于基准,右边部分大于等于基准。然后递归地对左右两部分进行排序。

算法步骤

  1. 选择一个基准元素(通常选择第一个元素、最后一个元素或中间元素)
  2. 分区(partition):将数组分为两部分,左边小于等于基准,右边大于等于基准
  3. 递归地对左右两部分进行快速排序
  4. 递归终止条件:子数组长度为0或1

代码实现

/*======= 快速排序 =======*/
void quickSort(int *arr, int start, int end)
{
    if (start >= end) return; // 递归终止条件

    int left = start, right = end;
    int pivot = arr[left]; // 选择第一个元素作为基准

    while (left < right)
    {
        // 从右往左找第一个小于基准的元素
        while (arr[right] > pivot && left < right) right--;
        if (left < right) arr[left++] = arr[right];

        // 从左往右找第一个大于基准的元素
        while (arr[left] < pivot && left < right) left++;
        if (left < right) arr[right--] = arr[left];
    }
    arr[left] = pivot; // 将基准放到正确位置

    // 递归排序左右两部分
    quickSort(arr, start, left-1);
    quickSort(arr, left+1, end);
}

// 对外提供的统一接口
void quickSort(int *arr, int size)
{
    quickSort(arr, 0, size-1);
}

优缺点分析

  • ✅ 优点:平均时间复杂度低,是实际应用中最快的排序算法之一,原地排序
  • ❌ 缺点:不稳定,最坏情况下时间复杂度为O(n²),递归调用需要栈空间

适用场景

  • 大规模数据集
  • 平均性能要求较高的场合
  • 内存充足且对最坏情况容忍度较高的应用

六、归并排序

核心思想:同样采用分治策略,将数组不断地分成两半,直到每个子数组只有一个元素(天然有序)。然后将两个有序的子数组合并成一个更大的有序数组,最终得到完全有序的数组。

算法步骤

  1. 分解(divide):将数组从中间分成左右两部分
  2. 递归地对左右两部分进行归并排序
  3. 合并(merge):将两个有序的子数组合并成一个有序数组

图解参考
归并排序过程

代码实现

/*======= 归并排序 =======*/
/*------- 合并两个有序子数组 -------*/
void merge(int *arr, int left, int mid, int right)
{
    int *temp = new int[right-left+1]; // 临时数组存储合并结果

    int i = left, j = mid + 1, k = 0;
    // 同时遍历两个子数组,将较小的元素放入临时数组
    while (i <= mid && j <= right)
    {
        if (arr[i] <= arr[j]) temp[k++] = arr[i++];
        else temp[k++] = arr[j++];
    }

    // 处理剩余元素
    while (i <= mid) temp[k++] = arr[i++];
    while (j <= right) temp[k++] = arr[j++];

    // 将临时数组的内容复制回原数组
    for (i = left, k = 0; i <= right; i++, k++)
    {
        arr[i] = temp[k];
    }

    delete[] temp; // 释放临时数组
}

/*------- 递归排序函数 -------*/
void mergeSort(int *arr, int start, int end)
{
    if (start >= end) return; // 递归终止条件

    int mid = (start + end) / 2;
    mergeSort(arr, start, mid); // 排序左半部分
    mergeSort(arr, mid+1, end); // 排序右半部分
    merge(arr, start, mid, end); // 合并两个有序部分
}

// 对外提供的统一接口
void mergeSort(int *arr, int size)
{
    mergeSort(arr, 0, size-1);
}

优缺点分析

  • ✅ 优点:排序稳定,时间复杂度稳定为O(nlogn),适合处理大规模数据和外部排序
  • ❌ 缺点:需要额外的O(n)空间,原地归并实现复杂

适用场景:需要稳定排序、链表排序、外部排序(数据无法完全加载到内存)


七、堆排序

核心思想:利用堆这种数据结构进行排序。堆是一棵完全二叉树,满足堆性质:大根堆中每个父节点的值都大于等于其子节点的值,小根堆则相反。我们可以利用大根堆的性质,每次取出堆顶的最大值,放到数组末尾,最终得到升序排列的数组。

基础知识

  1. 大根堆:父节点的值 ≥ 左右子节点的值
  2. 小根堆:父节点的值 ≤ 左右子节点的值
  3. 堆的数组表示:对于索引为i的节点:
    • 父节点索引:(i-1)/2
    • 左子节点索引:2i+1
    • 右子节点索引:2i+2
  4. 最后一个非叶子节点:索引为(n-1)/2,n为数组长度

算法步骤

  1. 建堆:从最后一个非叶子节点开始,自下而上地将数组调整为大根堆
  2. 排序:
    • 将堆顶元素(最大值)与堆的最后一个元素交换
    • 堆的大小减1
    • 对新的堆顶元素进行下沉调整,重新维护大根堆性质
    • 重复上述步骤,直到堆的大小为1

排序过程演示
建堆过程
交换与下沉
最终结果

代码实现

#include <algorithm> // 用于swap函数

/*======= 堆排序 =======*/
// 下沉操作:将索引为i的节点下沉到正确位置
void siftDown(int arr[], int size, int i)
{
    int value = arr[i]; // 保存当前节点的值
    while (i < size / 2) // 只要不是叶子节点就继续
    {
        int child = 2 * i + 1; // 左子节点索引
        // 如果右子节点存在且大于左子节点,选择右子节点
        if (child + 1 < size && arr[child+1] > arr[child])
        {
            child++;
        }
        // 如果子节点大于当前节点,将子节点上移
        if (arr[child] > value)
        {
            arr[i] = arr[child];
            i = child;
        }
        else
        {
            break; // 当前节点已经大于等于子节点,停止下沉
        }
    }
    arr[i] = value; // 将当前节点放到正确位置
}

void heapSort(int *arr, int size)
{
    // 第一步:建堆,从最后一个非叶子节点开始
    for (int i = (size-1)/2; i >= 0; i--)
    {
        siftDown(arr, size, i);
    }

    // 第二步:排序
    for (int i = size-1; i > 0; i--)
    {
        swap(arr[0], arr[i]); // 交换堆顶和最后一个元素
        siftDown(arr, i, 0); // 对新的堆顶进行下沉调整
    }
}

优缺点分析

  • ✅ 优点:时间复杂度稳定为O(nlogn),原地排序,不需要额外空间
  • ❌ 缺点:不稳定,建堆过程会打乱数组的原有顺序

适用场景:大规模数据集、内存有限、不需要稳定性、优先队列相关应用


八、桶排序

核心思想:将数组元素按照数值范围分配到多个桶中,每个桶内的元素再单独排序,最后将所有桶中的元素按顺序合并,得到有序数组。桶排序是一种非比较排序算法。

算法步骤

  1. 找出数组中的最大值和最小值,确定数值范围
  2. 根据数值范围和数据量确定桶的数量
  3. 将数组元素分配到对应的桶中
  4. 对每个桶内的元素进行排序(可以使用任意排序算法)
  5. 按顺序将所有桶中的元素合并到原数组中

图解参考
桶排序过程

代码实现

#include <vector>
#include <algorithm>
#include <cmath>

/*======= 桶排序 =======*/
void bucketSort(int *arr, int size)
{
    if (size <= 1) return;

    // 找出最大值和最小值
    int maxNum = arr[0], minNum = arr[0];
    for (int i = 1; i < size; i++)
    {
        if (arr[i] > maxNum) maxNum = arr[i];
        if (arr[i] < minNum) minNum = arr[i];
    }

    // 计算桶的数量(工业界通用经验:桶的数量为数据量的平方根+1)
    int bucketCount = (int)sqrt(size) + 1;
    vector<vector<int>> buckets(bucketCount);

    // 计算每个桶的数值范围
    double range = (double)(maxNum - minNum + 1) / bucketCount;

    // 将元素分配到对应的桶中
    for (int i = 0; i < size; i++)
    {
        int bucketIndex = (int)((arr[i] - minNum) / range);
        buckets[bucketIndex].push_back(arr[i]);
    }

    // 对每个桶内的元素进行排序,并合并到原数组
    int index = 0;
    for (int i = 0; i < bucketCount; i++)
    {
        // 使用标准库的sort函数对桶内元素排序
        sort(buckets[i].begin(), buckets[i].end());
        // 将桶内元素复制到原数组
        for (int j = 0; j < (int)buckets[i].size(); j++)
        {
            arr[index++] = buckets[i][j];
        }
    }
}

优缺点分析

  • ✅ 优点:当数据分布均匀时,时间复杂度接近O(n),排序稳定(如果桶内使用稳定排序)
  • ❌ 缺点:需要额外的空间,对数据分布敏感,如果数据集中在少数桶中,效率会急剧下降

适用场景:数据分布均匀、数值范围已知、大规模整数/浮点数排序


九、计数排序

核心思想:桶排序的特殊情况,当数组元素的数值范围较小时,可以用一个计数数组来统计每个数值出现的次数,然后根据计数数组将元素按顺序放回原数组。计数排序也是一种非比较排序算法。

算法步骤

  1. 找出数组中的最大值和最小值,确定数值范围
  2. 创建一个计数数组,长度为数值范围的大小,初始化为0
  3. 遍历原数组,统计每个数值出现的次数
  4. 遍历计数数组,根据计数将元素按顺序放回原数组

代码实现

/*======= 计数排序 =======*/
void countingSort(int *arr, int size)
{
    if (size <= 1) return;

    // 找出最大值和最小值
    int maxNum = arr[0], minNum = arr[0];
    for (int i = 1; i < size; i++)
    {
        if (arr[i] > maxNum) maxNum = arr[i];
        if (arr[i] < minNum) minNum = arr[i];
    }

    // 创建计数数组并初始化
    int range = maxNum - minNum + 1;
    int *count = new int[range](); // 初始化为0

    // 统计每个元素出现的次数
    for (int i = 0; i < size; i++)
    {
        count[arr[i] - minNum]++;
    }

    // 根据计数数组将元素放回原数组
    int index = 0;
    for (int i = 0; i < range; i++)
    {
        while (count[i] > 0)
        {
            arr[index++] = i + minNum;
            count[i]--;
        }
    }

    delete[] count; // 释放计数数组
}

优缺点分析

  • ✅ 优点:时间复杂度为O(n+k),其中k为数值范围大小,排序稳定
  • ❌ 缺点:需要额外的空间,只适用于数值范围较小的整数排序

适用场景:数据范围小且集中、整数排序、作为基数排序的子程序


十、基数排序

核心思想:按照数字的每一位进行排序,从最低位到最高位依次进行。每一位的排序可以使用计数排序或桶排序。基数排序也是一种非比较排序算法。

算法步骤

  1. 找出数组中的最大值,确定最大位数
  2. 从最低位(个位)开始,对每一位进行排序
  3. 重复步骤2,直到所有位都排序完成

图解参考
基数排序过程

代码实现

#include <vector>

/*======= 基数排序 =======*/
void radixSort(int *arr, int size)
{
    if (size <= 1) return;

    // 找出最大值,确定最大位数
    int maxNum = arr[0];
    for (int i = 1; i < size; i++)
    {
        if (arr[i] > maxNum) maxNum = arr[i];
    }

    // 从最低位开始,依次对每一位排序
    for (int exp = 1; maxNum / exp > 0; exp *= 10)
    {
        vector<vector<int>> buckets(10); // 创建10个桶,对应0-9

        // 将元素按当前位分配到对应的桶中
        for (int i = 0; i < size; i++)
        {
            int digit = (arr[i] / exp) % 10;
            buckets[digit].push_back(arr[i]);
        }

        // 将桶中的元素按顺序放回原数组
        int index = 0;
        for (int i = 0; i < 10; i++)
        {
            for (int j = 0; j < (int)buckets[i].size(); j++)
            {
                arr[index++] = buckets[i][j];
            }
        }
    }
}

优缺点分析

  • ✅ 优点:时间复杂度为O(d*(n+k)),其中d为最大位数,排序稳定
  • ❌ 缺点:需要额外的空间,只适用于整数排序(或可以转换为整数的类型)

适用场景:整数排序、位数固定且较少、大规模数据排序


十一、算法对比与选择指南

十种排序算法完整对比表

类型 时间复杂度-最好 时间复杂度-最坏 时间复杂度-平均 空间复杂度-最好 空间复杂度-最坏 空间复杂度-平均 稳定性 适用场景
冒泡 O(n) O(n²) O(n²) O(1) O(1) O(1) 稳定 小规模数据集、要求排序稳定、教学演示
选择 O(n²) O(n²) O(n²) O(1) O(1) O(1) 不稳定 小规模数据集、内存受限、不需要稳定性
插入 O(n) O(n²) O(n²) O(1) O(1) O(1) 稳定 小规模数据集(n<32)、数据基本有序、高级排序子程序
希尔 O(n) O(n²) O(n^1.3) O(1) O(1) O(1) 不稳定 中等规模数据集、内存受限、不需要稳定性
快速 O(nlogn) O(n²) O(nlogn) O(logn) O(n) O(logn) 不稳定 大规模数据集、平均性能要求高、内存充足
归并 O(nlogn) O(nlogn) O(nlogn) O(n) O(n) O(n) 稳定 需要稳定排序、链表排序、外部排序
O(nlogn) O(nlogn) O(nlogn) O(1) O(1) O(1) 不稳定 大规模数据集、内存有限、优先队列应用
O(n) O(n²) O(n+k) O(n+k) O(n+k) O(n+k) 稳定* 数据分布均匀、数值范围已知、大规模数值排序
计数 O(n+k) O(n+k) O(n+k) O(k) O(k) O(k) 稳定 数据范围小且集中、整数排序、基数排序子程序
基数 O(d(n+k)) O(d(n+k)) O(d(n+k)) O(n+k) O(n+k) O(n+k) 稳定 整数排序、位数固定且较少、大规模数据排序

注:桶排序的稳定性取决于桶内使用的排序算法

符号说明

  • n:待排序元素个数
  • k:桶的数量或数据范围大小
  • d:数字的最大位数

算法选择指南

  1. 小规模数据:优先选择插入排序或冒泡排序,实现简单且效率足够
  2. 中等规模数据:可以选择希尔排序,比插入排序效率更高
  3. 大规模数据:优先选择快速排序、归并排序或堆排序
  4. 需要稳定排序:选择冒泡排序、插入排序、归并排序、计数排序或基数排序
  5. 内存受限:选择选择排序、插入排序、希尔排序或堆排序(原地排序)
  6. 数值范围小的整数:选择计数排序或基数排序,效率极高
  7. 数据分布均匀:选择桶排序,可以达到线性时间复杂度

十二、结语

作者是大学生,到目前为止只会这十种排序算法,并且有些说明得不是很好,如果您发现文章中有任何错误或不足之处,或者有其他更好的排序算法推荐,欢迎在评论区留言交流。