作为一名对技术充满热情的学习者,我一直以来都深刻地体会到知识的广度和深度。在这个不断演变的数字时代,我远非专家,而是一位不断追求进步的旅行者。通过这篇博客,我想分享我在某个领域的学习经验,与大家共同探讨、共同成长。请大家以开放的心态阅读,相信你们也会在这段知识之旅中找到启示。
前言
今天我来继续聊聊数据结构排序算法----基数排序
一、什么是基数排序
基数排序是一种非比较型整数排序算法,它的工作原理是按照数字的每一位来分配和收集元素
。这种排序方式通常用于排序数字(尽管它也可以用于排序其他类型的数据,比如字符串),它可以处理从小到大的各个数字位,这被称作“最低位优先”(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]
对上述数组进行基数排序的步骤如下:
- 分别排序每个位数
开始时,我们先对每个数的个位数进行排序。
原始数据: [170, 45, 75, 90, 802, 24, 2, 66] 个位数排序: [170, 90, 802, 02, 24, 45, 75, 66]
- 注意,170和90中的“0”布置在类似“桶”的数据结构中的同一位置,802都布置在“2”的桶中,如此类推。
- 对十位数排序
接着对十位数排序。对于不足十位的数,可以认为它的十位数是0。
个位数排序: [170, 90, 802, 02, 24, 45, 75, 66] 十位数排序: [802, 02, 24, 45, 66, 170, 75, 90]
- 对百位数排序
最后我们对百位数排序。不足百位的认为它的百位数是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面试题
面试题:
提供一种基数排序的实现,可以处理负数。展示和解释如何修改传统的基数排序算法,使它能够正确地排序包含负数的整数数组。请说明您的方法,并提供清晰、优化的代码实例。
解释:
传统的基数排序算法通常只处理非负数,因为它依赖于整数的位模式来排序而不是它们的实际值。对于包含负数的数组,我们需要稍微调整算法,来保证负数可以按照其数值大小逆序摆放在正数之前。这是因为在二进制形式中,负数表示为正数的二进制补码。如果直接对这样的二进制形式排序,将会导致对大小的判断出现逻辑错误。
为了处理负数,我们可以采用以下步骤:
- 分离正负数:首先将数组分离成负数和非负数两个子数组。
- 绝对值转化:对负数部分取绝对值。
- 独立排序:分别对两个子数组进行基数排序。
- 还原负数:对排序好的负数子数组,再次取反获取它们原来的补码形式。
- 合并结果:合并两个子数组,先放置转换后的负数子数组(即原始的负数),再放置非负数子数组。
代码示例:
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标准浮点数(例如单精度或双精度浮点数)以便使用基数排序的一种方法:
- 分析表示:IEEE浮点数由符号位、指数位和尾数位组成。正数和负数有不同的排序方式,而对于浮点数的排序,通常会处理其二进制表示。
- 处理符号位:由于浮点数可以是正数或负数,我们需要一个方法区分它们,以便保持排序的稳定性。你可以通过反转正浮点数的位模式的所有位来完成这一点,同时反转负浮点数的位模式的所有位并再反转一次符号位。这样做的结果是,所有的浮点数可以被排序为其实际大小的正确顺序。
- 制作整数表现形式:现在每个浮点数都有一个唯一的整数表示。这使得使用基数排序变得可能,因为你可以简单地对这些整数进行排序,如同对标准整数进行排序一样。
- 排序:传统的基数排序过程可以作用在转换后的整数集上。按照每一位(或多个位,取决于你排序算法的基数)依次对其排序。
- 还原浮点数:一旦整数排序完成,将这些整数重新转换为浮点数表示即可得到正确排序的浮点数序列。
处理浮点数的基数排序需要特别注意,尤其是与符号和指数相关的边界情况(例如,处理正负零、无穷大和NaN等特殊值)。正确地处理这些情况需要详细的IEEE浮点数标准知识和对二进制数据操作的小心处理。在实际应用中,很多排序库和函数已经包含了对浮点数排序的优化处理,因此,在需要对浮点数序列进行排序时,往往可以直接使用这些现成的工具,而不是自己从头实现基数排序算法。
总结
想象一下你在图书馆里的一大堆书籍,你需要把它们按照书籍编号进行排序。这些编号是从1到999的编号,而你的任务是将书籍排列得整整齐齐。
你可以使用基数排序的方式来完成这个任务:
- 第一轮分类(根据编号的个位数):你将所有书籍放到10个不同的桌子上,每个桌子对应于个位数的0到9。例如,以“5”为个位数的所有编号的书籍都会放在标记为“5”的桌子上。
- 收集书籍:一旦书籍按个位数分类,你将所有桌子上的书籍收集起来,保持每个桌子上的顺序。
- 第二轮分类(根据编号的十位数):接着,你再次将收集来的书籍分散到10个桌子上,这次根据十位数。所有十位数为“1”的书籍都会放在“1”的桌子上,以此类推。
- 再次收集:同样,你按顺序收集所有桌子上的书籍。
- 第三轮分类(根据编号的百位数):最后,你将书籍根据百位数分类到10个桌子上。
- 最终收集:进行最后一次收集,这时所有的书籍将按照完整的编号顺序排列好。
在整个过程中,你是按照从最小的数位(个位)到最大的数位(百位)的顺序进行排序的。“十位”和“百位”的分类只有在完成了更低位数位的分类和收集后才可能进行。基数排序正是通过这样一种分层的方式,先对数字的一部分进行排序,再逐步处理更高位的部分,最后得到完全有序的序列。
感谢大家抽出宝贵的时间来阅读博主的博客,新人博主,感谢大家关注点赞,祝大家未来的学习工作生活一帆风顺,加油!!!