关于算法:数组可以比排序更有效地分组吗?

Can an array be grouped more efficiently than sorted?

在处理算法问题的示例代码时,我遇到了对输入数组进行排序的情况,尽管我只需要将相同的元素分组在一起,而不是以任何特定顺序进行分组,例如:

{1,2,4,1,4,3,2} → {1,1,2,2,4,4,3} or {1,1,2,2,3,4,4} or {3,1,1,2,2,4,4} or ...

这让我感到奇怪:与对数组进行排序相比,是否有可能将数组中的相同元素更有效地组合在一起?

一方面,不需要将元素移动到特定位置这一事实意味着,找到需要较少互换的订单的自由度更高。另一方面,跟踪组中每个元素的位置以及最佳的最终位置是什么,而不是简单地对数组进行排序可能需要更多的计算。

逻辑候选将是一种计数排序,但是如果数组长度和/或值范围不切实际地大怎么办?

为了便于讨论,我们假设数组很大(例如一百万个元素),包含32位整数,每个值中相同元素的数量可以是1到一百万。

更新:对于支持字典的语言,萨尔瓦多·达利(Salvador Dali)的答案显然是正确的方法。我仍然会对听到老式的比较和交换方法或使用较少空间(如果有的话)的方法感兴趣。


由于您询问了基于比较的方法,因此我将做出通常的假设,即(1)可以比较但不能散列的元素(2)唯一感兴趣的资源是三元操作。

从绝对意义上讲,分组比排序更容易。这是针对三个元素的分组算法,该算法使用一个比较(排序需要三个)。给定输入x, y, z,如果为x = y,则返回x, y, z。否则,返回x, z, y

渐近地,分组和排序都需要Omega(n log n)比较。下限技术是信息理论的:我们证明,对于表示为决策树的每个分组算法,都有3^Omega(n log n)个叶子,这表明树的高度(以及该算法的最坏运行时间) )是Omega(n log n)

修复决策树的任意叶子,在该叶子中找不到任何输入元素相等。输入位置由发现的不等式部分排序。

相反地假设i, j, k是成对的不可比的输入位置。让x = input[i], y = input[j], z = input[k],可能性x = y < zy = z < xz = x < y都与算法观察到的一致。这不可能,因为叶子选择的一个顺序不可能将x放在y旁边,然后放在x旁边。我们得出结论,偏序没有基数三的反链。

根据狄尔沃斯定理,偏序具有两条覆盖整个输入的链。通过考虑将这些链合并为总顺序的所有可能方式,最多有n choose m ≤ 2^n个排列映射到每个叶子。因此,叶数至少为n!/2^n = 3^Omega(n log n)


是的,您要做的就是创建字典并计算每次都有多少个元素。之后,只需遍历该字典中的键并输出与该键的值相同次数的键即可。

快速python实现:

1
2
3
4
5
6
7
from collections import Counter
arr = [1,2,4,1,4,3,2]
cnt, grouped = Counter(arr), []  # counter create a dictionary which counts the number of each element
for k, v in cnt.iteritems():
    grouped += [k] * v # [k] * v create an array of length v, which has all elements equal to k

print grouped

这将使用潜在的O(n)额外空间对O(n)时间中的所有元素进行分组。与排序相比,排序效率更高(就时间复杂度而言),排序可以在O(n logn)时间内实现,并且可以就地完成。


如何使用二维数组,其中第一维是每个值的频率,第二维是值本身。我们可以利用布尔数据类型和索引。这也使我们可以立即对原始数组进行排序,同时仅遍历原始数组一次即可提供O(n)解决方案。我认为这种方法可以很好地翻译成其他语言。观察下面的基本R代码(注意,R中的R方法比下面的要有效得多,我只是在给出一种更通用的方法)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GroupArray <- function(arr.in) {

    maxVal <- max(arr.in)

    arr.out.val <- rep(FALSE, maxVal)  ## F, F, F, F, ...
    arr.out.freq <- rep(0L, maxVal)     ## 0, 0, 0, 0, ...

    for (i in arr.in) {
        arr.out.freq[i] <- arr.out.freq[i]+1L
        arr.out.val[i] <- TRUE
    }

    myvals <- which(arr.out.val)   ##"which" returns the TRUE indices

    array(c(arr.out.freq[myvals],myvals), dim = c(length(myvals), 2), dimnames = list(NULL,c("freq","vals")))
}

上面代码的小例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
set.seed(11)
arr1 <- sample(10, 10, replace = TRUE)

arr1                                    
[1]  3  1  6  1  1 10  1  3  9  2     ## unsorted array

GroupArray(arr1)    
     freq vals       ## Nicely sorted with the frequency
[1,]    4    1
[2,]    1    2
[3,]    2    3
[4,]    1    6
[5,]    1    9
[6,]    1   10

较大的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
set.seed(101)
arr2 <- sample(10^6, 10^6, replace = TRUE)

arr2[1:10]       ## First 10 elements of random unsorted array
[1] 372199  43825 709685 657691 249856 300055 584867 333468 622012 545829

arr2[999990:10^6]     ## Last 10 elements of random unsorted array
[1] 999555 468102 851922 244806 192171 188883 821262 603864  63230  29893 664059

t2 <- GroupArray(arr2)
head(t2)
     freq vals        ## Nicely sorted with the frequency
[1,]    2    1
[2,]    2    2
[3,]    2    3
[4,]    2    6
[5,]    2    8
[6,]    1    9

tail(t2)
          freq    vals
[632188,]    3  999989
[632189,]    1  999991
[632190,]    1  999994
[632191,]    2  999997
[632192,]    2  999999
[632193,]    2 1000000


任何排序算法,即使是最有效的排序算法,都将要求您多次遍历数组。另一方面,分组可以只在一个迭代中完成,具体取决于您坚持将结果格式化为两种格式:

1
2
3
4
5
groups = {}
for i in arr:
    if i not in groups:
        groups[i] = []
    groups[i].append(i)

这是一个极其原始的循环,它忽略了可能在您选择的语言中提供的许多优化和习惯用法,但仅经过一次迭代就导致了这一结果:

1
{1: [1, 1], 2: [2, 2], 3: [3], 4: [4, 4]}

如果您有复杂的对象,则可以选择任意任意属性作为字典关键字进行分组,因此这是一种非常通用的算法。

如果您坚持将结果列为固定清单,则可以轻松实现:

1
2
3
result = []
for l in groups:
    result += l

(再次,忽略特定的语言优化和习惯用法。)

因此,您有一个恒定的时间解决方案,要求最多进行一次完整的输入迭代,并进行一次较小的中间分组数据结构迭代。空间要求取决于语言的具体要求,但通常仅是字典和列表数据结构所产生的少量开销。