如何在Java中进行Radix排序

93 阅读7分钟

简介

排序是用于解决问题的基本技术之一,特别是在那些与编写和实现高效算法有关的问题中。

通常情况下,排序是与搜索结合在一起的--这意味着我们首先对给定集合中的元素进行排序,然后在其中搜索一些东西,因为在一个已排序的集合中搜索一些东西通常比在一个未排序的集合中搜索更容易,因为我们可以做出有根据的猜测并对数据进行假设。

有许多算法可以有效地对元素进行排序,但在本指南中,我们将看看如何在Java中实现Radix排序

Java中的Radix排序

弧形排序是一种非比较性排序算法,意味着它不通过比较集合中的每个元素来排序,而是依靠一种叫做 弧度来对集合进行排序。

弧度(通常称为基数)是位置数字系统中唯一的数字,用来表示数字。

对于众所周知的二进制系统,拉德数是2(它只使用两个数字--0和1)。对于可以说是更著名的十进制系统,小数是10(它使用10位数字来表示所有的数字--从0到9)。

弧度排序是如何利用这个优势的?

实际上,Radix排序并不是靠自己来排序。它使用任何稳定的、非比较性的排序算法作为其子程序--在大多数情况下,该子程序是计数排序

stable sorting

如果n 代表我们要排序的元素数量,而k 是这些元素的允许值范围,那么当k1...n 的范围内时,Counting Sort 的时间复杂度是O(n+k) ,这比典型的比较排序算法的时间复杂度O(nlogn) 要快很多。

但这里的问题是--如果范围是1...n² ,时间复杂度就会急剧恶化到O(n²) ,非常快。

径向排序的总体思路是,从最小的有效数字到最大的有效数字逐个排序(LSD Radix Sort),你也可以反其道而行之 (MSD Radix Sort).它允许Counting Sort通过对输入进行分区,并在集合上多次运行Counting Sort,不让k 接近

因为它不是基于比较的,所以不受O(nlogn) 的限制 - 它甚至可以在线性时间内执行。

由于繁重的工作是由计数排序完成的,所以在深入研究Radix排序本身之前,让我们先去看看它是如何工作和实现的吧!

Java中的计数排序--理论与实现

计数排序是一种非比较性稳定排序算法,它的主要用途是对整数数组进行排序。

它的工作方式是,计算具有不同键值的对象的数量,然后对这些相同的计数应用前缀和,以确定每个键值在输出中的位置。由于是稳定的,在对集合进行排序时,具有相同键值的记录的顺序会被保留下来。

输出本质上是一个整数出现的列表。

输出数组中的每个索引代表输入数组中的一个元素。与该索引相关的值是输入数组中该元素的出现次数(计数)。

展示计数排序工作原理的最好方法是通过一个例子。考虑到我们有以下数组。

int[] arr = {3, 0, 1, 1, 8, 7, 5, 5};

为了简单起见,我们将使用从09的数字。我们可以考虑的数字的最大值显然是9,所以我们将设置一个max = 9

这很重要,因为我们需要一个额外的、由max + 1 元素组成的辅助数组。这个数组将被用来计算每个数字在我们的数组arr 中出现的次数,所以我们需要将整个计数数组countingArray 初始化为0

int[] countingArray = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
// there are 10 digits, so one zero for every element

现在我们已经定义了我们要使用的数组并初始化了计数数组,我们需要做以下步骤来实现计数排序。

1.遍历我们的arr 数组,并对每一个元素的出现进行计数,同时在我们的countingArray 数组中对位置arr[i] 的元素进行递增。

for(int i = 0; i < arr.length; i++)
    countingArray[arr[i]]++;

在这一步之后,countingArray 有以下元素:[1, 2, 0, 1, 0, 2, 0, 1, 1, 0]

2.下一步是在countingArray 上应用前缀和,我们得到以下结果。

for(int i=1; i < countingArray.length; i++)
    countingArray[i] += countingArray[i-1];

在对计数数组进行修改后,它现在由countingArray = {1, 3, 3, 4, 4, 6, 6, 7, 8, 8}

3.第三步也是最后一步是根据countingArray 中的值计算出排序输出中的元素位置。为此,我们需要一个新的数组,我们称之为outputArray ,并将其初始化为m 零,其中m 是我们原始数组中的元素数arr

int[] outputArray = {0, 0, 0, 0, 0, 0, 0, 0};
// there are 8 elements in the arr array

由于计数排序是一种稳定的排序算法,我们将以相反的顺序遍历arr ,以免最终导致元素的交换。

我们将在我们的countingArray 中找到与当前元素的值相等的索引arr[i] 。然后,在countingArray[arr[i]] - 1 的位置,我们将放置元素arr[i]

这就保证了这个排序的稳定性,以及将每个元素放在它在排序顺序中的正确位置。之后,我们将把countingArray[i] 的值减去1。

最后,我们将把outputArray 复制到arr ,这样排序后的元素就被包含在arr

让我们把所有这些片段统一起来,完全实现计数排序。

int[] arr = {3, 0, 1, 1, 8, 7, 5, 5};
int[] countingArray = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

for(int i = 0; i < arr.length; i++)
    countingArray[arr[i]]++;

for(int i=1; i < countingArray.length; i++)
	countingArray[i] += countingArray[i-1];

int[] outputArray = {0, 0, 0, 0, 0, 0, 0, 0};
for(int i = arr.length-1; i >= 0; i--){
    outputArray[countingArray[arr[i]] - 1] = arr[i];
    countingArray[arr[i]]--;
}

for(int i = 0; i < arr.length; i++){
    arr[i] = outputArray[i];
    System.out.print(arr[i] + " ");
}

运行这个将给我们一个排序的数组。

0, 1, 1, 3, 5, 5, 7, 8

如前所述,这个算法的时间复杂度是O(n+k) ,其中narr 中的元素数,k 是数组中max 元素的值。然而,当k 接近 ,这个算法就会向O(n²) 恶化,这是该算法的一个主要缺点。

既然我们已经简单地解释了计数排序的工作原理,那么让我们继续讨论本文的主要话题--Radix排序。

Java中的Radix排序--理论与实现

同样,Radix Sort典型地将Counting Sort作为一个子程序,所以Radix Sort本身也是一种稳定的排序算法。

Counting Sort使用的键将是我们要排序的数组内的整数的数字。

Radix排序有两种变体--一种是从**最小有效数字(LSD)开始排序,另一种是从最大有效数字(MSD)**开始排序--我们将重点讨论LSD的方法。

一旦我们理解了计数排序的工作原理,Radix排序本身并不复杂,所以实现它的步骤也相当简单。

  1. 找到输入数组中的max 元素。
  2. 确定数字的数量,dmax 元素有。数字d 代表我们要用计数排序法对数组进行多少次排序。
  3. 在开始时将数字s 初始化为1,代表最小有效位,并通过每次乘以10来增加它的值。

例如,假设我们有以下的输入数组arr = {73, 481, 57, 23, 332, 800, 754, 125}max 我们在数组中循环的次数是3次,因为我们的arr 数组中的元素是800,有3个数字。

让我们通过一个数组被这样排序的直观例子,一步一步地看看Radix Sort是如何在每次迭代中对元素进行排序的。

输入的数组被分解成构成其原始元素的数字。然后--要么使用最重要的数字并向下移动,要么使用最不重要的数字并向上移动,该序列通过计数排序进行排序。

在第一遍中,只有右手边被用来排序,这就是为什么Radix Sort/Counting Sort的稳定性是关键。如果没有稳定性,这种排序方式就没有意义。在第二遍中,我们使用中间的行,最后--左边的行被使用,数组就完全排序了。

最后,我们来实现Radix排序。

static void radixSort(int[] arr) {
  int max = arr[0];
  for (int i = 1; i < arr.length; i++) {
    if (max < arr[i])
      max = arr[i];
  }

  for (int s = 1; max / s > 0; s *= 10)
    countingSortForRadix(arr, s);
}

我们还要略微修改一下Countinng Sort。

对Counting Sort的修改与之前的实现完全相同,只是它一次只关注整数中不同位置的数字。

static void countingSortForRadix(int[] arr, int s) {
  int[] countingArray = {0,0,0,0,0,0,0,0,0,0};
  for (int i = 0; i < arr.length; i++)
    countingArray[(arr[i] / s) % 10]++;

  for (int i = 1; i < 10; i++)
    countingArray[i] += countingArray[i - 1];

  int[] outputArray = {0,0,0,0,0,0,0,0};
  for (int i = arr.length - 1; i >= 0; i--)
    outputArray[--countingArray[(arr[i] / s) % 10]] = arr[i];

  for (int i = 0; i < arr.length; i++)
    arr[i] = outputArray[i];
}

让我们创建一个数组,并尝试对其进行排序。

public static void main(String[] args) {
  int[] arr = {73,481,57,23,332,800,754,125};

  radixSort(arr);
  for (int i = 0; i < arr.length; i++)
    System.out.print(arr[i] + " ");
}

这样的结果是

23, 57, 73, 125, 332, 481, 754, 800

由于我们使用计数排序作为主子程序,对于一个包含n 元素的数组,该数组的max 元素有d 数字,在一个有b 基数的系统中,我们的时间复杂性为O(d(n+b))

这是因为我们在重复计数排序过程d ,其复杂度为O(n+b)

结论

尽管Radix Sort可以非常高效、美妙地运行,但它需要一些特定的情况才能做到。因为它要求你把要排序的项目表示为整数,所以很容易看出为什么其他一些基于比较的排序算法在很多情况下可以证明是更好的选择。

与其他一些基于比较的算法相比,Radix Sort的额外内存要求也是这种排序算法更少被使用的原因之一。

另一方面,当输入数组的键较短,或者元素范围较小时,这种算法的表现非常好。