【数据结构排序算法篇】----基数排序【实战演练】

简介: 【数据结构排序算法篇】----基数排序【实战演练】

作为一名对技术充满热情的学习者,我一直以来都深刻地体会到知识的广度和深度。在这个不断演变的数字时代,我远非专家,而是一位不断追求进步的旅行者。通过这篇博客,我想分享我在某个领域的学习经验,与大家共同探讨、共同成长。请大家以开放的心态阅读,相信你们也会在这段知识之旅中找到启示。



前言

今天我来继续聊聊数据结构排序算法----基数排序


一、什么是基数排序

基数排序是一种非比较型整数排序算法,它的工作原理是按照数字的每一位来分配和收集元素。这种排序方式通常用于排序数字(尽管它也可以用于排序其他类型的数据,比如字符串),它可以处理从小到大的各个数字位,这被称作“最低位优先”(LSD)方法,或者从大到小的各个数字位,称为“最高位优先”(MSD)方法。

基数排序的基本思想是将所有待比较的数字统一为相同的位数长度,位数较短的数字前面补零。然后,从最低位开始,依次进行一次分配和收集。对于每一个位数,排序时将数字分配至对应的桶中,并按照这些桶的顺序一次性收集起来,放回原数组,这就完成了一次排序。之后,用相同的方法对更高位进行排序。这个过程一直重复,直到最高位排序完成,整个数组就变成了有序的状态。

基数排序中使用的是一种临时的存储空间,它们是按数位上每个可能的值(例如,在十进制中就是0到9)来创建的。基数排序具有如下特点:

1. 稳定性:基数排序是一种稳定的排序算法,即具备相同值的元素,在排序后保持它们原有的相对顺序。

2. 时间复杂度:基数排序的时间复杂度是O(nk),n是排序数组的长度,k是数字的最大位数。

3. 空间复杂度:由于需要额外的空间来创建“桶”,其空间复杂度大概是O(n+k)。

尽管基数排序在理论上对于某些特定类型的数据排序时非常高效,但其性能强烈依赖于数据的分布以及基数(或位的基数)。换句话说,它适合于位数较少的整数排序,当数字范围特别广时,使用传统的比较排序可能更为高效。

二、基数排序可以用于排序哪些类型的数据

基数排序最初被设计用于整数排序,因为它们具有易于定义位的特征(例如,个位、十位、百位等)。然而,基数排序也可以扩展用于任何可以被分成较小部分的数据类型,并且这些部分可以被独立排序。以下是一些可以使用基数排序的数据类型:

1. 整数:基数排序对于非负整数尤其高效,包括小范围和大范围的值。

2. 浮点数:经过适当的转换(如IEEE标准浮点数表示),我们也可以对浮点数使用基数排序。

3. 字符串:字符串可以看作由字符组成的序列,可以对字符串集合使用基数排序,例如按字典顺序排列单词。

4. 定长字符串:如电话号码、日期等,可以通过每个字符的ASCII值进行排序。

5. 复合结构:比如说含有多个字段的数据结构,如果这些字段都可以单独排序,那么整个数据结构也可以使用基数排序进行排序。

值得注意的是,基数排序对数据的格式和划分有一定的要求。排序的数据必须能够分割成可以比较和排序的“基数字”,并且排序算法必须知道从哪一位到哪一位进行排序,以及每一位的基数(如十进制中每一位的基数是10,二进制是2等)。此外,对于那些不能明显分成独立部分或其部分大小不统一的数据来说,基数排序可能并不适宜。在处理这类数据时,可能需要其他类型的排序算法,比如比较排序或者其他非比较排序算法。

三、如何使用基数排序进行排序

当然,让我们来看一个简单的基数排序示例。

假设我们有以下数组:

[170, 45, 75, 90, 802, 24, 2, 66]

对上述数组进行基数排序的步骤如下:

  1. 分别排序每个位数
    开始时,我们先对每个数的个位数进行排序。
原始数据: [170, 45, 75, 90, 802, 24, 2, 66]
个位数排序: [170, 90, 802, 02, 24, 45, 75, 66]
  1. 注意,170和90中的“0”布置在类似“桶”的数据结构中的同一位置,802都布置在“2”的桶中,如此类推。
  2. 对十位数排序
    接着对十位数排序。对于不足十位的数,可以认为它的十位数是0。
个位数排序: [170, 90, 802, 02, 24, 45, 75, 66]
十位数排序: [802, 02, 24, 45, 66, 170, 75, 90]
  1. 对百位数排序
    最后我们对百位数排序。不足百位的认为它的百位数是0。
十位数排序: [802, 02, 24, 45, 66, 170, 75, 90]
百位数排序: [002, 024, 045, 066, 075, 090, 170, 802]

最终的排序结果(转换回没有前导零的形式)为:

[2, 24, 45, 66, 75, 90, 170, 802]

每一步的排序都是稳定的,即元素的相对位置被保持;如果它们在输入时具有相同的键值(在这里是指数字位),这对于每一轮都是正确的。在这个例子中,我们的数组是根据每个数的个位、十位、百位等按照顺序排列的。这个过程通常使用队列(桶)来收集每一位的相同数字,并以此顺序输出到下一阶段。

记住在每个步骤中,排序只影响处理的当前位。在移动到下一位之前,我们需要完整的一轮,以确保当前位已经被完全排序。在十进制的情况下,我们可能需要十个这样的“桶”来排序每个数字。对于对二进制数排序,我们只需要两个“桶”。步骤的数量取决于正在排序的项中最大位数的个数。

四、Java面试题

面试题:

提供一种基数排序的实现,可以处理负数。展示和解释如何修改传统的基数排序算法,使它能够正确地排序包含负数的整数数组。请说明您的方法,并提供清晰、优化的代码实例。

解释:

传统的基数排序算法通常只处理非负数,因为它依赖于整数的位模式来排序而不是它们的实际值。对于包含负数的数组,我们需要稍微调整算法,来保证负数可以按照其数值大小逆序摆放在正数之前。这是因为在二进制形式中,负数表示为正数的二进制补码。如果直接对这样的二进制形式排序,将会导致对大小的判断出现逻辑错误。

为了处理负数,我们可以采用以下步骤:

  1. 分离正负数:首先将数组分离成负数和非负数两个子数组。
  2. 绝对值转化:对负数部分取绝对值。
  3. 独立排序:分别对两个子数组进行基数排序。
  4. 还原负数:对排序好的负数子数组,再次取反获取它们原来的补码形式。
  5. 合并结果:合并两个子数组,先放置转换后的负数子数组(即原始的负数),再放置非负数子数组。

代码示例:

import java.util.Arrays;
public class RadixSortWithNegatives {
    // 使用基数排序算法排序负数和非负数
    public static void radixSortWithNegatives(int[] arr) {
        if (arr.length == 0) {
            return;
        }
        // 找出最大值和最小值
        int max = arr[0], min = arr[0];
        for (int i : arr) {
            if (i > max) {
                max = i;
            }
            if (i < min) {
                min = i;
            }
        }
        // 独立排序非负数和负数
        int[] from = new int[arr.length];
        int[] to = new int[arr.length];
        System.arraycopy(arr, 0, from, 0, arr.length);
        // 计算排序的总轮次,由最大值决定
        for (int r = 1; max / r > 0; r *= 10) {
            countingSort(from, to, r);
        }
        // 如果有负数存在
        if (min < 0) {
            // 反转数组以放置负数
            reverse(to);
            // 重新计算最大值(实际上是负数部分的最小值的绝对值)
            max = -min;
            // 复制负数到新的临时数组
            System.arraycopy(to, 0, from, 0, arr.length);
            // 再次进行基数排序,只针对负数
            for (int r = 1; max / r > 0; r *= 10) {
                countingSort(from, to, r);
            }
            // 再次反转已排序的负数部分以恢复正确的顺序
            reverse(to);
        }
        // 把排序的数字复制回原数组
        System.arraycopy(to, 0, arr, 0, arr.length);
    }
    // 计数排序 - 基数排序的一个轮次
    private static void countingSort(int[] from, int[] to, int r) {
        int[] count = new int[10];
        Arrays.fill(count, 0);
        // 计算出现次数
        for (int i : from) {
            ++count[absolute(i / r) % 10];
        }
        // 调整计数数组
        for (int i = 1; i < 10; i++) {
            count[i] += count[i - 1];
        }
        // 根据计数数组和位值进行排序
        for (int i = from.length - 1; i >= 0; i--) {
            to[--count[absolute(from[i] / r) % 10]] = from[i];
        }
        // 复制回原数组以进行下一个轮次
        System.arraycopy(to, 0, from, 0, from.length);
    }
    // 数字的绝对值
    private static int absolute(int i) {
        return (i < 0) ? -i : i;
    }
    // 反转数组
    private static void reverse(int[] arr) {
        for (int i = 0; i < arr.length / 2; i++) {
            int temp = arr[i];
            arr[i] = arr[arr.length - 1 - i];
            arr[arr.length - 1 - i] = temp;
        }
    }
    public static void main(String[] args) {
        int[] arr = { -5, -1, 0, 3, -8, 2, 4, -2 };
        radixSortWithNegatives(arr);
        System.out.println(Arrays.toString(arr)); // [-8, -5, -2, -1, 0, 2, 3, 4]
    }
}

面试时,重要的是能够解释代码的每个部分以及它们为什么是必要的。上面的实现考虑了处理负数的特殊情况,并且在整个排序过程中保持了稳定的排序。还要注意,上述代码是为了分别展示基数排序的负数扩展的,实际应用中可能需要进一步的优化。

五、思考

  • 在基数排序中,如何对浮点数进行适当转换以便排序?

在基数排序中,浮点数的转换通常涉及将浮点数的位模式解释为整数,以便可以使用整数排序的方法对其进行排序。这种转换必须保持浮点数的顺序关系,即在转换后的整数表示中,如果一个浮点数小于另一个,那么其对应的整数也应当小于另一个整数的表示。

下面是处理IEEE标准浮点数(例如单精度或双精度浮点数)以便使用基数排序的一种方法:

  1. 分析表示:IEEE浮点数由符号位、指数位和尾数位组成。正数和负数有不同的排序方式,而对于浮点数的排序,通常会处理其二进制表示。
  2. 处理符号位:由于浮点数可以是正数或负数,我们需要一个方法区分它们,以便保持排序的稳定性。你可以通过反转正浮点数的位模式的所有位来完成这一点,同时反转负浮点数的位模式的所有位并再反转一次符号位。这样做的结果是,所有的浮点数可以被排序为其实际大小的正确顺序。
  3. 制作整数表现形式:现在每个浮点数都有一个唯一的整数表示。这使得使用基数排序变得可能,因为你可以简单地对这些整数进行排序,如同对标准整数进行排序一样。
  4. 排序:传统的基数排序过程可以作用在转换后的整数集上。按照每一位(或多个位,取决于你排序算法的基数)依次对其排序。
  5. 还原浮点数:一旦整数排序完成,将这些整数重新转换为浮点数表示即可得到正确排序的浮点数序列。

处理浮点数的基数排序需要特别注意,尤其是与符号和指数相关的边界情况(例如,处理正负零、无穷大和NaN等特殊值)。正确地处理这些情况需要详细的IEEE浮点数标准知识和对二进制数据操作的小心处理。在实际应用中,很多排序库和函数已经包含了对浮点数排序的优化处理,因此,在需要对浮点数序列进行排序时,往往可以直接使用这些现成的工具,而不是自己从头实现基数排序算法。


总结

想象一下你在图书馆里的一大堆书籍,你需要把它们按照书籍编号进行排序。这些编号是从1到999的编号,而你的任务是将书籍排列得整整齐齐。

你可以使用基数排序的方式来完成这个任务:

  1. 第一轮分类(根据编号的个位数):你将所有书籍放到10个不同的桌子上,每个桌子对应于个位数的0到9。例如,以“5”为个位数的所有编号的书籍都会放在标记为“5”的桌子上。
  2. 收集书籍:一旦书籍按个位数分类,你将所有桌子上的书籍收集起来,保持每个桌子上的顺序。
  3. 第二轮分类(根据编号的十位数):接着,你再次将收集来的书籍分散到10个桌子上,这次根据十位数。所有十位数为“1”的书籍都会放在“1”的桌子上,以此类推。
  4. 再次收集:同样,你按顺序收集所有桌子上的书籍。
  5. 第三轮分类(根据编号的百位数):最后,你将书籍根据百位数分类到10个桌子上。
  6. 最终收集:进行最后一次收集,这时所有的书籍将按照完整的编号顺序排列好。

在整个过程中,你是按照从最小的数位(个位)到最大的数位(百位)的顺序进行排序的。“十位”和“百位”的分类只有在完成了更低位数位的分类和收集后才可能进行。基数排序正是通过这样一种分层的方式,先对数字的一部分进行排序,再逐步处理更高位的部分,最后得到完全有序的序列。

感谢大家抽出宝贵的时间来阅读博主的博客,新人博主,感谢大家关注点赞,祝大家未来的学习工作生活一帆风顺,加油!!!

目录
相关文章
|
22天前
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
34 1
|
25天前
|
机器学习/深度学习 算法 数据挖掘
K-means聚类算法是机器学习中常用的一种聚类方法,通过将数据集划分为K个簇来简化数据结构
K-means聚类算法是机器学习中常用的一种聚类方法,通过将数据集划分为K个簇来简化数据结构。本文介绍了K-means算法的基本原理,包括初始化、数据点分配与簇中心更新等步骤,以及如何在Python中实现该算法,最后讨论了其优缺点及应用场景。
77 4
|
2月前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
92 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
2月前
|
存储 Java 开发者
Java Map实战:用HashMap和TreeMap轻松解决复杂数据结构问题!
【10月更文挑战第17天】本文深入探讨了Java中HashMap和TreeMap两种Map类型的特性和应用场景。HashMap基于哈希表实现,支持高效的数据操作且允许键值为null;TreeMap基于红黑树实现,支持自然排序或自定义排序,确保元素有序。文章通过具体示例展示了两者的实战应用,帮助开发者根据实际需求选择合适的数据结构,提高开发效率。
74 2
|
23天前
|
存储 算法 搜索推荐
Python 中数据结构和算法的关系
数据结构是算法的载体,算法是对数据结构的操作和运用。它们共同构成了计算机程序的核心,对于提高程序的质量和性能具有至关重要的作用
|
23天前
|
数据采集 存储 算法
Python 中的数据结构和算法优化策略
Python中的数据结构和算法如何进行优化?
|
1月前
|
算法
数据结构之路由表查找算法(深度优先搜索和宽度优先搜索)
在网络通信中,路由表用于指导数据包的传输路径。本文介绍了两种常用的路由表查找算法——深度优先算法(DFS)和宽度优先算法(BFS)。DFS使用栈实现,适合路径问题;BFS使用队列,保证找到最短路径。两者均能有效查找路由信息,但适用场景不同,需根据具体需求选择。文中还提供了这两种算法的核心代码及测试结果,验证了算法的有效性。
96 23
|
1月前
|
算法
数据结构之蜜蜂算法
蜜蜂算法是一种受蜜蜂觅食行为启发的优化算法,通过模拟蜜蜂的群体智能来解决优化问题。本文介绍了蜜蜂算法的基本原理、数据结构设计、核心代码实现及算法优缺点。算法通过迭代更新蜜蜂位置,逐步优化适应度,最终找到问题的最优解。代码实现了单链表结构,用于管理蜜蜂节点,并通过适应度计算、节点移动等操作实现算法的核心功能。蜜蜂算法具有全局寻优能力强、参数设置简单等优点,但也存在对初始化参数敏感、计算复杂度高等缺点。
59 20
|
22天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
52 1
|
1月前
|
机器学习/深度学习 算法 C++
数据结构之鲸鱼算法
鲸鱼算法(Whale Optimization Algorithm,WOA)是由伊朗研究员Seyedali Mirjalili于2016年提出的一种基于群体智能的全局优化算法,灵感源自鲸鱼捕食时的群体协作行为。该算法通过模拟鲸鱼的围捕猎物和喷出气泡网的行为,结合全局搜索和局部搜索策略,有效解决了复杂问题的优化需求。其应用广泛,涵盖函数优化、机器学习、图像处理等领域。鲸鱼算法以其简单直观的特点,成为初学者友好型的优化工具,但同时也存在参数敏感、可能陷入局部最优等问题。提供的C++代码示例展示了算法的基本实现和运行过程。
49 0
下一篇
DataWorks