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)唯一感兴趣的资源是三元操作。
从绝对意义上讲,分组比排序更容易。这是针对三个元素的分组算法,该算法使用一个比较(排序需要三个)。给定输入
渐近地,分组和排序都需要
修复决策树的任意叶子,在该叶子中找不到任何输入元素相等。输入位置由发现的不等式部分排序。
相反地假设
根据狄尔沃斯定理,偏序具有两条覆盖整个输入的链。通过考虑将这些链合并为总顺序的所有可能方式,最多有
是的,您要做的就是创建字典并计算每次都有多少个元素。之后,只需遍历该字典中的键并输出与该键的值相同次数的键即可。
快速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 |
这将使用潜在的
如何使用二维数组,其中第一维是每个值的频率,第二维是值本身。我们可以利用布尔数据类型和索引。这也使我们可以立即对原始数组进行排序,同时仅遍历原始数组一次即可提供
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 |
(再次,忽略特定的语言优化和习惯用法。)
因此,您有一个恒定的时间解决方案,要求最多进行一次完整的输入迭代,并进行一次较小的中间分组数据结构迭代。空间要求取决于语言的具体要求,但通常仅是字典和列表数据结构所产生的少量开销。