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

推荐订阅源

B
Blog
V
Vulnerabilities – Threatpost
Apple Machine Learning Research
Apple Machine Learning Research
V
V2EX
博客园 - 叶小钗
阮一峰的网络日志
阮一峰的网络日志
人人都是产品经理
人人都是产品经理
Latest news
Latest news
博客园 - 三生石上(FineUI控件)
美团技术团队
aimingoo的专栏
aimingoo的专栏
Google Online Security Blog
Google Online Security Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
T
Threatpost
Y
Y Combinator Blog
T
Tailwind CSS Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
A
Arctic Wolf
C
Cyber Attacks, Cyber Crime and Cyber Security
小众软件
小众软件
Recent Commits to openclaw:main
Recent Commits to openclaw:main
T
Tenable Blog
W
WeLiveSecurity
L
LINUX DO - 热门话题
D
Docker
Cyberwarzone
Cyberwarzone
量子位
A
About on SuperTechFans
The Last Watchdog
The Last Watchdog
雷峰网
雷峰网
C
CERT Recently Published Vulnerability Notes
P
Palo Alto Networks Blog
The Hacker News
The Hacker News
Blog — PlanetScale
Blog — PlanetScale
P
Proofpoint News Feed
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
F
Full Disclosure
The Cloudflare Blog
T
The Blog of Author Tim Ferriss
T
The Exploit Database - CXSecurity.com
Engineering at Meta
Engineering at Meta
O
OpenAI News
Hacker News - Newest:
Hacker News - Newest: "LLM"
Scott Helme
Scott Helme
IT之家
IT之家
S
Secure Thoughts
MongoDB | Blog
MongoDB | Blog
L
Lohrmann on Cybersecurity
博客园 - 司徒正美
Google DeepMind News
Google DeepMind News

博客园_首页

Linux实操--组管理、权限管理和定时任务 Java + EasyExcel 实现单个接口导出多个Excel Mem0 源码解析系列(二):提示词工程的深度剖析 Openclaw TaskFlow究竟是什么?和普通Skill技能有什么区别 博文阅读密码验证 - 博客园 嘉立创开源:应该是全网MicroPython教程最多的开发板 Hermes Agent 集成实践:从协议到生产 2026年AI编程工具横评:Cursor、Codex、Claude Code、Zed、Windsurf Java程序员必看的RAG入门教程 2026 AI效率神器:Superpowers + Claude Code 保姆级教程 本地大模型部署全攻略:从 0 到 1 玩转 Ollama 【从0到1构建一个ClaudeAgent】内存管理-上下文压缩 .NET 高级开发 | 设计、实现一个事件总线框架 电子小白入门之NE555 3. WorkBuddy:隐藏玩法,一键召唤专家,让 AI 以"专家身份"给你干活 和AI一起搞事情#3:Claude Teammate 游戏开发翻车实录 【OpenClaw】通过 Nanobot 源码学习架构---(7)Memory C# .NET 周刊|2026年3月3期 我在 Debian 11 上把 K8s 单机搭起来了,过程没你想的那么顺(/opt 目录版) 深度学习进阶(七)Data-efficient Image Transformer CLI+Skill搭建浏览器AI自动化框架,告别一切重复枯燥任务 告别Token账单无底洞:OpenClaw本地部署,重塑企业数据主权的唯一解 FastAPI+Vue:文件分片上传+秒传+断点续传,这坑我帮你踩平了! SBTI 爆火后,我做了个程序员版的 CBTI。。已开源 + 附开发过程 多模态检索开始进入工程期:用 Sentence Transformers 搭建可落地的 Multimodal RAG 100多行代码实现一个最简单的Agent(用ReAct) Claude Code 通关手册(八):推荐 5 个 Hooks,代码质量提升 3 倍 老板:“有人截图了!”。安全部门:“收到,马上查暗水印!” - why技术 技术之外,皆是人间 C#/.NET/.NET Core技术前沿周刊 | 第 69 期(2026年4.01-4.12) Snack JSONPath 项目架构分析 Claude Code Buddy 小析:一个非核心功能,如何体现产品的细节完成度 AI新时代下的图床管理方案-Cloudflare图床+MCP+Skills方案指南 化繁为简:顺丰速运App如何通过 HarmonyOS SDK实现专业级空间测量 从零实现富文本编辑器#13-React非编辑节点的内容渲染 AI开发-python-langchain框架(3-23-OpenAI Functions风格Tool Calling智能助手) .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 PbootCMS 网站内容数量多导致访问慢?这些实用优化方案帮你提速! - 家兴网络技术工作室 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 网站漏洞怎么发现并修复?一篇实用指南(附完整流程) - 家兴网络技术工作室 开了 TUN 模式还是直连?90% 的人都踩过这个坑 Github日报|2026年04月12日 - AI一族 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术 Etsy 把 1000 个 MySQL 分片迁进 Vitess:425TB 数据背后的真正问题不是性能,而是运维规模 MicroPython LVGL基础知识和概念:底层渲染与性能优化 - FreakStudio 数据库草图算法 Python 潮流周刊#146:CPython 引入 Rust 的进展 - 豌豆花下猫 最小生成树 - mofei1116 红日靶场七:从外网入口、容器逃逸到 AD 接管的完整利用链复盘 - YouDiscovered1t 分享四款开源且实用的 Kafka 管理工具 - 追逐时光者 vLLM 权重加载机制全解析:从挑战到理想架构 LCT 学习笔记 - ACehomoxue Avalonia UI 12.0.0 正式发布:架构演进和性能飞跃 - 张善友 当 AI Agent 把调用链拉长,延迟开始成为一门生意 conhost.exe 无法显示 U+2717 - 145a 太秀了,我把自己蒸馏成了 Skill!已开源 - 程序员鱼皮 ASP.NET Core 内存缓存实战:一篇搞懂该怎么配、怎么避坑 基于 Ghostty 带有分割标签页和为 Claude 编程设计的通知终端 - BugShare AI 焊死入口:教育的“操作系统级”重塑 - 郝hai 初级Java开发工程师使用sql脚本编写代码的过程是简单而且不糊涂 - CoderOilStation Claude Code通关手册(六):MCP协议完全指南 - 暮色之狐 边框灯光环绕动画特效实现指南 - Newbe36524 开源:子木蒸馏版的 SEO 审计工具 seo-audit-skill v1.0 我所理解的Python元模型 【从0到1构建一个ClaudeAgent】规划与协调-TodoWrite - 程序员Seven Claude 和 Codex 在审计 Skill 上性能差异探究 - ACai_sec AScript如何实现中文脚本引擎 - rockey627 【渗透测试】HTB Season10 Garfield 全过程wp - dynasty_chenzi Android 开发者为什么必须掌握 AI 能力?端侧视角下的技术变革 树状数组正确性证明 - AC-wyr 你的 AI 焦虑,可能比 AI 本身更危险——ATM 机没有消灭银行柜员,但恐慌消灭了你的判断力 - 我没有三颗心脏 一个拉胯的分库分表方案有多绝望?整个部门都在救火! - 冰河团队 动态规划入门必学之走方格问题 - Ofnoname PostgREST 与 PostgreSQL 角色权限配置全解析(生产级实践) - SheepDog1998 使用 UEFI 图形输出协议 GOP 在屏幕上显示图像的方法 - 阿源- Claude Code通关手册(五):组建你的AI专家团队,子代理系统 - 暮色之狐 一个程序员到架构师的催婚路之感悟(整整10年后的催婚相亲感悟) - MisterLip 用 Agent Skill 自动生成工作周报 - 赵康
十种排序算法
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. 数据分布均匀:选择桶排序,可以达到线性时间复杂度

十二、结语

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