热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

深入解析数据结构与算法:基数排序的原理与应用

本文详细探讨了基数排序(RadixSort)的基本原理及其应用场景。作为一种非比较型整数排序算法,基数排序通过将元素按照位数分配到不同的桶中进行排序,最终合并各个桶中的元素得到有序序列。文章首先介绍了基数排序的核心思想和工作流程,随后通过具体代码示例展示了其实现过程。此外,还对基数排序在处理大规模数据集时的性能表现进行了测试,并讨论了在实际应用中需要注意的事项。

目录

  • 简单介绍
  • 基本思想
  • 思路分析
  • 代码实现
    • 推导实现
    • 完整实现
  • 大数据量耗时测试
  • 注意事项

简单介绍

基数排序(radix sort)属于 分配式排序(distribution sort),又称 桶子法(bucket sort 或 bin sort),顾名思义,它是通过键值的各个位的值,将要排序的 元素分配 至某些「桶」中,达到排序的作用。基数排序是对 传统桶排序 的扩展。

基数排序属于 稳定性 的排序,基数排序法是效率高的稳定性排序法。

其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。

稳定性简介

2,1,43,1 数组进行排序后变成:1,1,2,43

稳定性指的是:两个 1 的先后顺序不改变。

基数排序(Radix Sort)是 桶排序 的扩展。

基数排序是 1887 年赫尔曼·何乐礼发明的。实现方式:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

基本思想

  1. 将所有待比较数值 统一为同样的数位长度,数位较短的数 前面补零
  2. 然后从最低位开始(个位),依次进行一次排序
  3. 这样从最低位排序一直到最高位排序完成以后,序列就变成了一个有序序列

基本思想是抽象的,下面看看思路分析,你就明白是咋回事了。

思路分析

《数据结构与算法——排序算法-基数排序》

解析上面的图:

第一轮:判断 个位数

  1. 将每个元素的 个位数 取出,找到其 个位数 所对应的下标的桶,然后把这个数放到桶中(桶为一个一维数组

  2. 按照这个桶的顺序,依次取出数据,放回原来的数组

    注意:取出桶里的数据时,不仅要按桶的顺序依次来取,而且单个桶里的数据取出顺序也要按先放进去的先取出来的规则。

以上步骤中,每一轮除了用什么位数的值来判断放在哪个桶里不同外,其他的都相同。

第二轮:判断 十位数

需要注意的是:

  • 第一轮使用后的桶并未清理,上图为了讲解方便,并未展示桶中已有的数据,不过会进行覆盖。
  • 长度不足的数,用零表示。如 3,没有十位数,则归类到第一个桶中(0),即该数看成 03

第三轮:判断 百位数

一个流程下来,你会发现,在第三轮排序后,数组已经是有序的了

《数据结构与算法——排序算法-基数排序》

动图:

《数据结构与算法——排序算法-基数排序》

代码实现

推导实现

为了更容易理解,下面进行分步推导来找出里面的规律,从而实现算法。

/**
* 推导:推导每一步的状态,然后找规律
*/
@Test
public void processDemo() {
//这里我就随便一个数组了
int arr[] = {53, 3, 542, 748, 14, 214};
System.out.println("原始数组:" + Arrays.toString(arr));
processRadixSort(arr);
}
public void processRadixSort(int[] arr) {
// 第一轮
// 1. 将每个元素的 **个位数** 取出,找到其 个位数 所对应的下标的桶,然后把这个数放到桶中(**桶为一个一维数组**)
// 2. 按照这个桶的顺序,**依次取出数据**,放回原来的数组
// 定义 10 个桶,每个桶是一个一维数组
// 由于无法知道每个桶需要多少个元素,所以声明为数组长度
// 例如:加入10 个数字都是 1,那么只会分配到同一个通中
int[][] buckets = new int[10][arr.length];
// 定义每个桶中有效的数据个数
// 桶长度为数组大小,那么每一个桶中存放了几个有效的元素呢?就需要有这个变量来指示
int[] bucketCounts = new int[buckets.length];
// 开始第一轮的代码实现
// 1. 将每个元素的 **个位数** 取出,找到其 个位数 所对应的下标的桶,然后把这个数放到桶中(**桶为一个一维数组**)
for (int i = 0; i // 获取到个位数
int temp = arr[i] % 10;
// 根据规则,将该数放到对应的桶中
buckets[temp][bucketCounts[temp]] = arr[i];
// 并将该桶的有效个数+1
bucketCounts[temp]++;
}
// 2. 按照这个桶的顺序,**依次取出数据**,放回原来的数组
int index = 0; // 原始数组下标索引
for (int i = 0; i if (bucketCounts[i] == 0) {
// 标识该桶无数据
continue;
}
for (int j = 0; j arr[index++] = buckets[i][j];
}
// 取完数据后,要重置每个桶的有效数据个数,注意,这里一步一定要做!!!
bucketCounts[i] = 0;
}
System.out.println("第一轮排序后:" + Arrays.toString(arr));
// 第 2 轮:判断 十位数
for (int i = 0; i // 获取到十位数
int temp = arr[i] / 10 % 10;//就这一步不同
buckets[temp][bucketCounts[temp]] = arr[i];
bucketCounts[temp]++;
}
index = 0; // 原始数组下标索引
for (int i = 0; i if (bucketCounts[i] == 0) {
continue;
}
for (int j = 0; j arr[index++] = buckets[i][j];
}
bucketCounts[i] = 0;
}
System.out.println("第二轮排序后:" + Arrays.toString(arr));
// 第 3 轮:判断 百位数
for (int i = 0; i // 获取到十位数
int temp = arr[i] / 100 % 10;//就这一步不同
buckets[temp][bucketCounts[temp]] = arr[i];
bucketCounts[temp]++;
}
index = 0; // 标识当前放回原数组的哪一个了
for (int i = 0; i if (bucketCounts[i] == 0) {
continue;
}
for (int j = 0; j arr[index++] = buckets[i][j];
}
bucketCounts[i] = 0;
}
System.out.println("第三轮排序后:" + Arrays.toString(arr));
}

测试输出

原始数组:[53, 3, 542, 748, 14, 214]
第一轮排序后:[542, 53, 3, 14, 214, 748]
第二轮排序后:[3, 14, 214, 542, 748, 53]
第三轮排序后:[3, 14, 53, 214, 542, 748]

根据前面的推导,整理发现有如下的规律:整体代码比较固定,少数变量在变化,即判断的位数

  1. 要循环几轮?这个与待排序数组中的最大值有几位数有关系

    需要找到数组中的最大值,并且得到该值的位数

  2. 获取数组中每个数的个、十、百 位数的公式可以如下整理:

    // 获取个位数
    arr[i] % 10 -> arr[i] / 1 % 10
    // 获取十位数
    arr[i] / 10 % 10
    // 获取百位数
    arr[i] / 100 % 10

    可以发现规律,每一次变化的都是 10 的倍数

因此通过推到以及上面的分析整理,下面完成了该算法的具体代码

完整实现

@Test
public void radixSortTest() {
int arr[] = {53, 3, 542, 748, 14, 214};
System.out.println("原始数组:" + Arrays.toString(arr));
radixSort(arr);
System.out.println("排序后:" + Arrays.toString(arr));
}
/**
* 根据推导规律,整理出完整算法
*
* @param arr
*/
public void radixSort(int[] arr) {
// 1. 得到数组中的最大值,并获取到该值的位数。用于知道要循环几轮
int max = arr[0];
for (int i = 0; i if (arr[i] > max) {
max = arr[i];
}
}
// 得到位数
int maxLength = (max + "").length();//这里很巧妙!
// 定义桶 和 标识桶中元素个数
int[][] bucket = new int[10][arr.length];
int[] bucketCounts = new int[bucket.length];
// 总共需要进行 maxLength 轮
for (int k = 1, n = 1; k <= maxLength; k++, n *= 10) {//注意看这里,有两个变量,k控制循环,n用于后面获取到对应的位数
// 进行桶排序
for (int i = 0; i // 获取该轮的桶索引:n 每一轮按 10 的倍数递增,获取到对应数位数
// 这里额外使用一个步长为 10 的变量 n 来得到每一次递增后的值
int bucketIndex = arr[i] / n % 10;//这里很巧妙!
// 放入该桶中
bucket[bucketIndex][bucketCounts[bucketIndex]] = arr[i];
// 标识该桶元素多了一个
bucketCounts[bucketIndex]++;
}
// 将桶中元素获取出来,放到原数组中,注意,要按单个桶中的数据取出是先进先出的顺序
int index = 0;
for (int i = 0; i if (bucketCounts[i] == 0) {
// 该桶无有效元素,跳过不获取
continue;
}
// 获取桶中有效的个数
for (int j = 0; j arr[index++] = bucket[i][j];
}
// 取完后,重置该桶的元素个数为 0 ,下一次才不会错乱数据,这一步很重要!!!
bucketCounts[i] = 0;
}
System.out.println("第" + k + "轮排序后:" + Arrays.toString(arr));
}
}

测试输出

原始数组:[53, 3, 542, 748, 14, 214]
第1轮排序后:[542, 53, 3, 14, 214, 748]
第2轮排序后:[3, 14, 214, 542, 748, 53]
第3轮排序后:[3, 14, 53, 214, 542, 748]
排序后:[3, 14, 53, 214, 542, 748]

动图:

《数据结构与算法——排序算法-基数排序》

大数据量耗时测试

/**
* 大量数据排序时间测试
*/
@Test
public void bulkDataSort() {
int max = 80000;
// max = 8;
int[] arr = new int[max];
for (int i = 0; i arr[i] = (int) (Math.random() * 80000);
}
if (arr.length <10) {
System.out.println("原始数组:" + Arrays.toString(arr));
}
Instant startTime = Instant.now();
radixSort(arr);
if (arr.length <10) {
System.out.println("排序后:" + Arrays.toString(arr));
}
Instant endTime = Instant.now();
System.out.println("共耗时:" + Duration.between(startTime, endTime).toMillis() + " 毫秒");
}

多次测试输出信息

共耗时:31 毫秒
共耗时:29 毫秒
共耗时:22 毫秒
共耗时:39 毫秒

如果增加数据量到 800 万,也发现只会用时 400 毫秒左右,速度非常快。

但是,如果数据量增加到 8000 万,则会报错(这个就得看你的电脑内存大小了)

java.lang.OutOfMemoryError: Java heap space
at cn.mrcode.study.dsalgtutorialdemo.datastructure.sort.radix.RadixSortTest.radixSort(RadixSortTest.java:125)

这是为什么呢?原因就在于开启了 10 个桶,每个桶都是 8000 万个数据。那么换算下单位:

80000000 * 11 * 4 / 1024 /1024 / 1024 = 3.2 G 左右的堆空间
# 11 = 10 个桶 + 原始数组
# 4 :一个 int 占用 4 字节

也就是说,牺牲内存来提升速度。

注意事项

  • 基数排序是对 传统桶排序 的扩展,速度很快

  • 是经典的空间换时间的方式,占用内存空间很大

    当数据量太大的时候,所耗费的额外空间较大。是原始数据的 10 倍空间

  • 基数排序是稳定的

    相同的数,排序之后,他们的先后顺序没有发生变化。

  • 有负数时,不用基数排序来进行排序

    如果要支持负数可以参考 中文维基百科

    由于上述算法使用的按位比较,并未考虑负数,直接使用,将导致数组越界。

    改造支持负数的核心思想是:将负数取绝对值,然后再反转成负数。


推荐阅读
  • golang常用库:配置文件解析库/管理工具viper使用
    golang常用库:配置文件解析库管理工具-viper使用-一、viper简介viper配置管理解析库,是由大神SteveFrancia开发,他在google领导着golang的 ... [详细]
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • 本文介绍了Java并发库中的阻塞队列(BlockingQueue)及其典型应用场景。通过具体实例,展示了如何利用LinkedBlockingQueue实现线程间高效、安全的数据传递,并结合线程池和原子类优化性能。 ... [详细]
  • 本文详细介绍了Java中org.neo4j.helpers.collection.Iterators.single()方法的功能、使用场景及代码示例,帮助开发者更好地理解和应用该方法。 ... [详细]
  • Java 中 Writer flush()方法,示例 ... [详细]
  • 本文介绍了如何使用 Spring Boot DevTools 实现应用程序在开发过程中自动重启。这一特性显著提高了开发效率,特别是在集成开发环境(IDE)中工作时,能够提供快速的反馈循环。默认情况下,DevTools 会监控类路径上的文件变化,并根据需要触发应用重启。 ... [详细]
  • Java 中的 BigDecimal pow()方法,示例 ... [详细]
  • Java 类成员初始化顺序与数组创建
    本文探讨了Java中类成员的初始化顺序、静态引入、可变参数以及finalize方法的应用。通过具体的代码示例,详细解释了这些概念及其在实际编程中的使用。 ... [详细]
  • 题目描述:给定n个半开区间[a, b),要求使用两个互不重叠的记录器,求最多可以记录多少个区间。解决方案采用贪心算法,通过排序和遍历实现最优解。 ... [详细]
  • 深入解析Spring Cloud Ribbon负载均衡机制
    本文详细介绍了Spring Cloud中的Ribbon组件如何实现服务调用的负载均衡。通过分析其工作原理、源码结构及配置方式,帮助读者理解Ribbon在分布式系统中的重要作用。 ... [详细]
  • 本文深入探讨了 Java 中的 Serializable 接口,解释了其实现机制、用途及注意事项,帮助开发者更好地理解和使用序列化功能。 ... [详细]
  • 本文详细介绍了Java编程语言中的核心概念和常见面试问题,包括集合类、数据结构、线程处理、Java虚拟机(JVM)、HTTP协议以及Git操作等方面的内容。通过深入分析每个主题,帮助读者更好地理解Java的关键特性和最佳实践。 ... [详细]
  • 将Web服务部署到Tomcat
    本文介绍了如何在JDeveloper 12c中创建一个Java项目,并将其打包为Web服务,然后部署到Tomcat服务器。内容涵盖从项目创建、编写Web服务代码、配置相关XML文件到最终的本地部署和验证。 ... [详细]
  • XNA 3.0 游戏编程:从 XML 文件加载数据
    本文介绍如何在 XNA 3.0 游戏项目中从 XML 文件加载数据。我们将探讨如何将 XML 数据序列化为二进制文件,并通过内容管道加载到游戏中。此外,还会涉及自定义类型读取器和写入器的实现。 ... [详细]
  • ImmutableX Poised to Pioneer Web3 Gaming Revolution
    ImmutableX is set to spearhead the evolution of Web3 gaming, with its innovative technologies and strategic partnerships driving significant advancements in the industry. ... [详细]
author-avatar
墨镜DHED_304
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有