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

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

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



前言

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


一、什么是基数排序

基数排序是一种非比较型整数排序算法,它的工作原理是按照数字的每一位来分配和收集元素。这种排序方式通常用于排序数字(尽管它也可以用于排序其他类型的数据,比如字符串),它可以处理从小到大的各个数字位,这被称作“最低位优先”(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. 最终收集:进行最后一次收集,这时所有的书籍将按照完整的编号顺序排列好。

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

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

目录
相关文章
|
2月前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
90 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
2月前
|
存储 Java 开发者
Java Map实战:用HashMap和TreeMap轻松解决复杂数据结构问题!
【10月更文挑战第17天】本文深入探讨了Java中HashMap和TreeMap两种Map类型的特性和应用场景。HashMap基于哈希表实现,支持高效的数据操作且允许键值为null;TreeMap基于红黑树实现,支持自然排序或自定义排序,确保元素有序。文章通过具体示例展示了两者的实战应用,帮助开发者根据实际需求选择合适的数据结构,提高开发效率。
65 2
|
2月前
|
机器学习/深度学习 存储 缓存
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
文章主要介绍了排序算法的分类、时间复杂度的概念和计算方法,以及常见的时间复杂度级别,并简单提及了空间复杂度。
38 1
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
|
2月前
|
存储 算法 Java
Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性
Java Set因其“无重复”特性在集合框架中独树一帜。本文解析了Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性,并提供了最佳实践建议,包括选择合适的Set实现类和正确实现自定义对象的hashCode()与equals()方法。
40 4
|
2月前
|
搜索推荐 算法
数据结构与算法学习十四:常用排序算法总结和对比
关于常用排序算法的总结和对比,包括稳定性、内排序、外排序、时间复杂度和空间复杂度等术语的解释。
24 0
数据结构与算法学习十四:常用排序算法总结和对比
|
2月前
|
算法 搜索推荐 Java
数据结构与算法学习十三:基数排序,以空间换时间的稳定式排序,速度很快。
基数排序是一种稳定的排序算法,通过将数字按位数切割并分配到不同的桶中,以空间换时间的方式实现快速排序,但占用内存较大,不适合含有负数的数组。
33 0
数据结构与算法学习十三:基数排序,以空间换时间的稳定式排序,速度很快。
|
2月前
|
机器学习/深度学习 搜索推荐 算法
探索数据结构:初入算法之经典排序算法
探索数据结构:初入算法之经典排序算法
|
2月前
|
算法 Java 索引
数据结构与算法学习十五:常用查找算法介绍,线性排序、二分查找(折半查找)算法、差值查找算法、斐波那契(黄金分割法)查找算法
四种常用的查找算法:顺序查找、二分查找(折半查找)、插值查找和斐波那契查找,并提供了Java语言的实现代码和测试结果。
27 0
|
1月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
138 9
|
26天前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
24 1