当前位置: 首页 > news >正文

数据结构:基数排序 (Radix Sort)

目录

从“按位思考”而非“整体比较”

一个具体的排序过程

代码的逐步实现

构建核心工具 —— countingSortByDigit

构建总指挥 —— radixSort 主函数

复杂度与特性分析

时间复杂度 (Time Complexity)

空间复杂度 (Space Complexity)

稳定性 (Stability)


我们来探讨排序算法中的一个“奇才”——基数排序 (Radix Sort)。它再次刷新了我们对排序的认知,因为它甚至不需要完整地看待一个数字就能完成排序。

从“按位思考”而非“整体比较”

我们之前遇到的所有排序算法,无论是比较大小还是计算位置,都是将数字(例如 170)作为一个整体来看待的。

基数排序的革命性思想在于:

我们能不能不把数字当成一个整体,而是把它拆分成一个个独立的“位” (digit),然后逐位进行排序?

这个想法初听起来很奇怪。如果我只按个位数排序,那么 21 就会排在 19 的前面,这显然是错的。这个思路要怎样才能奏效呢?

💡核心洞察(The Magic): 这个“逐位排序”的思路要能成功,必须满足两个条件:

1️⃣ 排序的顺序:我们必须从最低位(个位)开始,一轮一轮地向最高位排序。这个策略被称为 LSD (Least Significant Digit first) Radix Sort

2️⃣ 排序的稳定性:每一轮“按位排序”时,所使用的排序算法必须是稳定的。也就是说,如果两个元素的当前位相同,它们在排序后必须保持上一轮的相对顺序。

为什么这样可行? 因为稳定性是关键。

当我们对“十位”进行排序时,所有“十位”相同的元素(例如 2428)会聚集在一起。但由于我们使用的排序算法是稳定的,它会保持这些元素在上一轮(按“个位”排序时)的相对顺序。

因为 24 的个位 4 小于 28 的个位 8,所以上一轮排序的结果是 ...24...28...。在按十位排完序后,它们的相对顺序依然是 ...24...28...

换句话说,低位的排序结果,成为了高位排序时解决“平局”的依据。当我们处理完最高位时,整个数组自然就完全有序了。

这个“利用稳定的低位排序结果作为高位排序的次要规则”的思想,就是基数排序的第一性原理。


一个具体的排序过程

我们来手动模拟这个过程。

待排序数组:arr = [170, 45, 75, 90, 802, 24, 2, 66]

第一轮:按“个位” (Least Significant Digit) 排序

  • 我们只看每个数字的个位数:0, 5, 5, 0, 2, 4, 2, 6

  • 我们需要一个稳定的算法来根据这些个位数(范围0-9)对原数组进行排序。什么算法最适合这个场景?计数排序!

  • 经过稳定的计数排序后,数组变为: [170, 90, 802, 2, 24, 45, 75, 66]

注意稳定性:17090 的个位都是0,但原数组中 17090 前面,所以新数组中它依然在前面。8022 也是同理。

第二轮:按“十位”排序

  • 我们基于上一轮的结果,按每个数字的十位数进行排序:7, 9, 0, 0, 2, 4, 7, 6

  • 再次使用稳定的计数排序: [802, 2, 24, 45, 66, 170, 75, 90]

  • 注意稳定性:8022 的十位都是0,但在上一轮结果中 8022 的前面,所以新数组中它依然在前面。17075 也是同理。

第三轮:按“百位”排序

  • 基于上一轮的结果,按百位数排序:1, 0, 0, 0, 8, 0, 0, 0

  • 再次使用稳定的计数排序: [2, 24, 45, 66, 75, 90, 170, 802]

现在,数组已经完全有序了!


代码的逐步实现

从上面的推导可以看出,基数排序本身是一个“总指挥”,它需要一个“特种兵”来完成每一轮的按位排序。这个特种兵就是计数排序。

我们将把这个任务分解成两个主要部分:

  1. 构建核心工具:一个能“按特定位”进行排序的特殊计数排序函数。

  2. 构建总指挥:一个能调用上述工具,从低位到高位循环,完成整个排序的主函数。

构建核心工具 —— countingSortByDigit

这个函数是基数排序的引擎。它的目标不是完全排序,而是根据数字的某一位(个位、十位…)来对整个数组进行一次稳定的重排。

第一阶段:函数框架和参数设计

我们需要告诉这个函数要排序哪个数组 (arr),数组有多大 (n),以及这次要根据哪一位来排序。我们用一个整数 exp 来代表“位权”。

  • exp = 1 代表个位

  • exp = 10 代表十位

  • exp = 100 代表百位

// 一个根据'位权(exp)'对数组arr进行稳定排序的函数
void countingSortByDigit(int arr[], int n, int exp) {// 笔算步骤: "准备一张最终答案纸和一张计数用的草稿纸"// 代码翻译: 创建 output 数组和 count 数组// 1. output 数组用来临时存放本次按位排序的结果int* output = new int[n];// 2. count 数组用来对 0-9 这十个数字进行计数int count[10] = {0}; // 直接初始化为0// ... 核心逻辑将在这里展开 ...// 释放内存delete[] output;
}

第二阶段: isolating the Digit and Counting Frequencies

这是本函数最关键的改动:如何从一个数中“取出”我们想要的那一位数字,并用它来计数。

关键技巧:digit = (number / exp) % 10

例子:number = 824, exp = 10 (我们想要十位)

  • number / exp => 824 / 10 = 82 (整型除法,小数部分被舍去)

  • (result) % 10 => 82 % 10 = 2 (取余数)

  • 我们成功地取出了十位数 2

现在我们把这个技巧放入代码中。

void countingSortByDigit(int arr[], int n, int exp) {int* output = new int[n];int count[10] = {0};// 步骤1: 统计频率// 笔算步骤: "看每个数字的特定位,在草稿纸上画正字"// 代码翻译: 遍历原数组,计算出特定位,然后在 count 数组中累加for (int i = 0; i < n; i++) {int digit = (arr[i] / exp) % 10;count[digit]++;}// ... 后续步骤 ...
}

第三阶段:计算累加和 & 构建输出数组

这部分和我们之前讲的标准计数排序完全一样。我们把代码直接放进来。

void countingSortByDigit(int arr[], int n, int exp) {int* output = new int[n];int count[10] = {0};for (int i = 0; i < n; i++) {count[(arr[i] / exp) % 10]++;}// 步骤2: 计算累加和,将频率转换为“排名”for (int i = 1; i < 10; i++) {count[i] += count[i - 1];}// 步骤3: 构建 output 数组,从后往前保证稳定性for (int i = n - 1; i >= 0; i--) {int digit = (arr[i] / exp) % 10;output[count[digit] - 1] = arr[i];count[digit]--;}// ... 最后一步 ...
}

第四阶段:将结果拷贝回原数组

我们的“按位排序”任务完成了,结果在 output 数组里。现在需要更新原数组 arr,以便下一轮排序(比如按十位排序)能在一个正确的基础上进行。

// 最终版本的 `countingSortByDigit`
void countingSortByDigit(int arr[], int n, int exp) {int* output = new int[n];int count[10] = {0};for (int i = 0; i < n; i++) {count[(arr[i] / exp) % 10]++;}for (int i = 1; i < 10; i++) {count[i] += count[i - 1];}for (int i = n - 1; i >= 0; i--) {output[count[(arr[i] / exp) % 10] - 1] = arr[i];count[(arr[i] / exp) % 10]--;}// 步骤4: 将本次排序的结果拷贝回 arr,为下一轮做准备for (int i = 0; i < n; i++) {arr[i] = output[i];}delete[] output;
}

至此,我们的核心工具(引擎)已经打造完毕。


构建总指挥 —— radixSort 主函数

这个“总指挥”函数的工作流程非常清晰。

第一阶段:函数框架与寻找最大值

总指挥需要知道什么时候该“收工”。什么时候收工?当我们处理完最大数字的最高位之后。所以,第一步是找到数组中的最大值。

#include <iostream>// (这里应该包含上面写好的 countingSortByDigit 函数)// 辅助函数,用来找到数组最大值
int getMax(int arr[], int n) {int max = arr[0];for (int i = 1; i < n; i++) {if (arr[i] > max) {max = arr[i];}}return max;
}// 基数排序主函数框架
void radixSort(int arr[], int n) {// 笔算步骤: "看一眼所有数字,找到那个位数最多的"// 代码翻译: 调用 getMax 函数int max_val = getMax(arr, n);// ... 接下来是调用核心工具的循环 ...
}

第二阶段:实现主循环

我们需要一个循环,不断地更新 exp 的值(从1到10,再到100...),并反复调用我们的核心工具 countingSortByDigit

循环应该什么时候停止?当 exp 的值已经超过了最大数的范围时。例如,最大数是 802,当 exp 变成 1000 时,802 / 1000 的结果是 0,这意味着在千位以及更高位上,所有数字都是0,没有必要再排序了。所以循环的条件就是 max_val / exp > 0

void radixSort(int arr[], int n) {int max_val = getMax(arr, n);// 笔算步骤: "先按个位排,再按十位排,再按百位排..."// 代码翻译: 用一个 for 循环来控制 exp (1, 10, 100...)//           并在循环中调用我们的核心工具for (int exp = 1; max_val / exp > 0; exp *= 10) {countingSortByDigit(arr, n, exp);}
}

这个 radixSort 函数本身非常简洁,因为它把所有复杂的工作都委托给了 countingSortByDigit。这就是良好的模块化设计。

最终组装:getMaxcountingSortByDigitradixSort 放在一起,就构成了完整的基数排序实现。每一步都清晰地对应了你笔算的思路,从准备工作到核心计算,再到最终的指挥调度。


复杂度与特性分析

时间复杂度 (Time Complexity)

  • n 是元素个数,d 是最大元素的位数。

  • 基数排序的主循环执行 d 次。

  • 循环内部调用 countingSortByDigit。这个函数的复杂度是多少?

  • 它的内部有几个循环,都是遍历 n 个元素或者遍历 count 数组(大小固定为10,即基数 k)。所以 countingSortByDigit 的复杂度是 O(n+k)。

  • 总的时间复杂度 = 位数 d × 每一位排序的成本 O (n+k)。

  • 通常,基数 k 是一个常数(比如十进制是10),所以复杂度可以简化为:O(d⋅n)

📍关键点:d 和什么什么有关?

它和最大值 max_val 有关,d 大约是 log₁₀(max_val)。所以时间复杂度也可以写成 O (n・log (max_val))。当数字很大时,d 也会变大。


空间复杂度 (Space Complexity)

  • 基数排序的空间开销主要来自于其内部使用的计数排序。

  • countingSortByDigit 函数中,我们需要一个大小为 noutput 数组和一个大小为 k(在这里是10)的 count 数组。

  • 因此,空间复杂度是:O(n+k)


稳定性 (Stability)

  • 正如我们从第一性原理中推导的那样,基数排序的正确性完全依赖于其内部排序算法的稳定性。

  • 我们选用了计数排序作为内部排序,而我们已经知道计数排序是稳定的。

  • 因此,基数排序也是一个稳定的排序算法 (Stable Sort)

基数排序是一个非常聪明的非比较排序算法。它不是将数字作为一个整体来处理,而是将其分解,逐位征服。它通过巧妙地利用稳定的子排序算法(如计数排序),成功地对大范围的整数(或字符串等可以按位比较的数据)进行了高效的线性时间排序。


文章转载自:

http://D0G7bLIA.dzdtj.cn
http://a3JGIzU3.dzdtj.cn
http://dMQMpcSg.dzdtj.cn
http://UGL7wc2N.dzdtj.cn
http://A8vSvYC8.dzdtj.cn
http://wg29g4FA.dzdtj.cn
http://kf78yoj5.dzdtj.cn
http://QN5yD4SG.dzdtj.cn
http://bYC8X7vI.dzdtj.cn
http://SeMaoXzp.dzdtj.cn
http://3WsxMvwS.dzdtj.cn
http://54aNW3WD.dzdtj.cn
http://tZaKNC1h.dzdtj.cn
http://TJIva9I9.dzdtj.cn
http://VvYkiXs3.dzdtj.cn
http://rhNtjqpu.dzdtj.cn
http://dOfuUFp5.dzdtj.cn
http://IZIcDqKo.dzdtj.cn
http://VE8q9wfm.dzdtj.cn
http://VpaY3EXw.dzdtj.cn
http://JsMYqZfV.dzdtj.cn
http://XswhsrHR.dzdtj.cn
http://kCKP4146.dzdtj.cn
http://JnTCuJhb.dzdtj.cn
http://pOLWpcEf.dzdtj.cn
http://xX9ffxka.dzdtj.cn
http://3ZrPZCiq.dzdtj.cn
http://HmrZWrj1.dzdtj.cn
http://qjuyDipu.dzdtj.cn
http://ez2hsucL.dzdtj.cn
http://www.dtcms.com/a/362562.html

相关文章:

  • uni-app iOS 性能监控与调试全流程:多工具协作的实战案例
  • Qt中QSettings的键值使用QDataStream进行存储
  • 【Vue2 ✨】Vue2 入门之旅(七):事件处理
  • 从spring MVC角度理解HTTP协议及Request-Response模式
  • 自学嵌入式第三十二天:网络编程-UDP
  • 基于单片机醉酒驾驶检测系统/酒精检测/防疲劳驾驶设计
  • Angular事件处理全攻略:从基础到进阶的完整指南
  • GEO 应用实践研讨会:探索行业新路径,激发企业新活力
  • IoT Power软件 -- 每次开启强制升级解决方法
  • DVWA靶场通关笔记-DOM型XSS(Impossible级别)
  • CentOS7.6
  • 基于Force-closure评估的抓取计算流程
  • gitlab中回退代码,CI / CD 联系运维同事处理
  • RAGFlow——知识库检索系统开发实战指南(包含聊天和Agent模式)
  • 微信小程序备忘
  • ResponseBodyEmitter介绍
  • HarmonyOS 鸿蒙系统自带的 SymbolGlyph 图标组件详解
  • 【学Python自动化】 8.1 Python 与 Rust 错误处理对比学习笔记
  • 拔河(蓝桥杯)(前缀和)
  • Docker CI/CD 自动化部署配置指南
  • 【Datawhale之Happy-LLM】3种常见的decoder-only模型——Github最火大模型原理与实践教程task07
  • C#---共享项目
  • 【C++变量和数据类型:从基础到高级】
  • AI 在教育领域的落地困境:个性化教学与数据隐私的平衡之道
  • 线程特定存储
  • 【Go语言入门教程】 Go语言的起源与技术特点:从诞生到现代编程利器(一)
  • 深入浅出 RabbitMQ-TTL+死信队列+延迟队列
  • idea上传本地项目代码到Gitee仓库教程
  • 【论文阅读】Deepseek-VL:走向现实世界的视觉语言理解
  • 【Web前端】JS+DOM来实现乌龟追兔子小游戏