【问题标题】:How to find all values that occur more than n/k times in an array?如何找到在数组中出现超过 n/k 次的所有值?
【发布时间】:2018-05-28 03:28:24
【问题描述】:

我在 Coursera 上攻读 Algorithms, Part I 课程,其中一个面试问题(未评分)如下:

十进制占优。给定一个有 n 个键的数组,设计一个算法来找出所有出现超过 n/10 次的值。预期的 算法的运行时间应该是线性的。

它有一个提示:

使用快速选择确定第 (n/10) 个最大的键并检查它是否 出现 n/10 次以上。

我不明白 n/10 最大键与 n/10 重复值有什么关系。它不会告诉我哪些值出现的次数超过 n/10 次。

有一个paper 为 n/k 找到了更通用的解决方案,但我很难理解论文中的代码。

解决此问题的一种方法是对输入数组进行排序,然后再计算每个不同值的出现次数。这将花费 O(nlogn) + O(n) 时间,这比问题所要求的要多。

想法?

【问题讨论】:

  • 当提示指的是线性时,意味着您正在对数组进行一次索引。如果您要从最大到最小对数组进行排序,则需要 N^2。数组中的每个数字可能有重复项。数组中的每个唯一数字都是键。所以这个问题意味着你有一个字典(一个带有键的数组),你需要在字典中搜索超过 10 个项目的键。
  • @jdweng 课程中还没有引入字典或者哈希表,所以不能使用这样的数据结构。此外,正如我在问题中所说,有比 n^2 更快的排序算法可用。
  • 你是对的。小数占优是 QuickSelect 之后的下一个候选对象:n/10, 2*n/10..9*n/10,因此仅检查 n/10 是不够的
  • @MBo 请详细说明,没有关注。
  • 如果一个元素出现超过 n/k 次,那么对于排序数组中 0 到 n/k 范围内的某些 i,它必须位于位置 i*n/k。 (如果它不在这些位置中的任何一个,则不可能有 n/k 个元素的副本,因为在两个连续的测试位置之间没有 n/k 个副本的空间。)快速选择可以在 O(n ),因此找到所有这些需要 O(kn),如果 K 是已知常数,则为 O(n)。但是通用的Boyer-Moore majority algorithm 可能更快更容易编码。

标签: arrays algorithm data-structures


【解决方案1】:

使用 QuickSelect 查找第 n/10 个最大的键(即,如果数组已排序,则该键将位于位置 n/10)需要线性时间。如果这个键的副本少于 n/10,那么你知道它上面的任何东西按排序顺序都没有 n/10 个副本,因为在排序的键上面没有任何 n/10 个副本的空间命令。如果有 n/10 或更多副本,那么您已经找到了出现超过 n/10 次的东西,并且再一次不可能有任何比它更大的东西出现超过 n/10 次,因为没有空间为它。

现在您有一个最多包含 9n/10 个值的数组,该值比您刚刚从 QuickSelect 中找到的键小。使用另一遍 QuickSelect 从这个剩余数组的顶部找到键 n/10。和以前一样,您可能会找到一个出现 n/10 次或更多次的键,无论您是否这样做,您都会从数组中删除至少 n/10 个条目。

因此,您可以通过 10 次 QuickSelect 调用来搜索整个数组,每次调用都需要线性时间。 10 是问题定义中固定的数字,因此整个操作仅计为线性时间。

【讨论】:

  • 我试图将我的头脑围绕以下语句“如果此密钥的副本少于 n/10,那么您知道它上面的任何内容都没有 n/10 个副本排序命令”。如果我们有[6, 9, 7, 2, 5, 7, 7, 4],n = 8,k = 3,n/k = 2。排序后的数组是[2, 4, 5, 6, 7, 7, 7, 9],所以索引 2 处的元素将是 5。即使 5 出现少于 2 次,也有7 发生 3 次。上述说法如何成立?
  • 我认为你的论点是倒退的;只要 n/k 键不是小数占优,我们就需要在右子数组中递归。显然,在 n/k 的左子数组中任何东西的键不能超过 n/k,否则它不会在位置 n/k。但是我们不能排除正确的子数组,所以我们需要一直寻找直到 n/k
  • 我们正在考虑按不同方向排序的数组,您还需要担心 n 不能被 k 整除的特殊情况。如果您从选择最大的键开始,请考虑以最大的第一个键对数组进行排序,因此 [9,7,7,7,6,5,4,2],实际上您会找到 7。为简单起见,请考虑 [9,8 ,7,6,5,4,3,2,1] 并且 n/k=3。如果您使用快速选择查找 7,您知道只有 2 个值大于它,并且您只关心出现至少 3 次的值,因此您知道可以忽略它们。
  • @mcdowella,你能举个例子吗?
  • 现在你有一个最多 9n/10 个值的数组,比你刚刚从 QuickSelect 中找到的键小 - 这是错误的。实际上小于或等于
【解决方案2】:

Boyer-Moore Voting 算法有一个变体,它可以在O(nk) 中运行的输入中找到超过n/k 出现的所有元素,并且由于k = 10 对于您的问题,我认为它应该在@ 中运行987654326@时间。

来自here

以下是一个有趣的 O(nk) 解决方案:我们可以解决上述问题 在 O(nk) 时间内使用 O(k-1) 额外空间的问题。请注意,可以 输出中永远不会超过 k-1 个元素(为什么?)。主要有 该算法的三个步骤。

1) 创建一个大小为 (k-1) 的临时数组来存储元素及其 计数(输出元素将在这些 k-1 个元素中)。 以下是临时数组元素的结构。

 struct eleCount {
     int element;
     int count; };  
 struct eleCount temp[];  This step takes O(k) time.

2) 遍历输入数组并更新 temp[](添加/删除一个 每个遍历的元素的元素或增加/减少计数)。这 数组 temp[] 在每一步存储潜在的 (k-1) 个候选对象。这 步骤需要 O(nk) 时间。

3) 遍历最终的 (k-1) 个潜在候选人(存储在 温度 [])。或每个元素,检查它是否实际计数超过 n/k。这一步需要 O(nk) 时间。

主要步骤是第 2 步,如何在 每一点?第 2 步中使用的步骤类似于著名的游戏:俄罗斯方块。我们 将每个数字视为俄罗斯方块中的一块,它在我们的 临时数组 temp[]。我们的任务是尽量保持相同的数字 堆叠在同一列上(临时数组中的计数递增)。

考虑 k = 4, n = 9 给定数组:3 1 2 2 2 1 4 3 3

i = 0

     3 _ _ temp[] has one element, 3 with count 1

i = 1

     3 1 _ temp[] has two elements, 3 and 1 with  counts 1 and 1 respectively

i = 2

     3 1 2 temp[] has three elements, 3, 1 and 2 with counts as 1, 1 and 1 respectively.

i = 3

     - - 2 
     3 1 2 temp[] has three elements, 3, 1 and 2 with counts as 1, 1 and 2 respectively.

i = 4

     - - 2 
     - - 2 
     3 1 2 temp[] has three elements, 3, 1 and 2 with counts as 1, 1 and 3 respectively.

i = 5

     - - 2 
     - 1 2 
     3 1 2 temp[] has three elements, 3, 1 and 2 with counts as 1, 2 and 3 respectively.  

现在问题来了,当 temp[] 已满,我们看到一个新元素——我们从 元素堆栈,即我们将每个元素的计数减少 1 in 温度[]。我们忽略当前元素。

i = 6

     - - 2 
     - 1 2  temp[] has two elements, 1 and 2 with counts as 1 and 2 respectively.

i = 7

       - 2 
     3 1 2  temp[] has three elements, 3, 1 and 2 with counts as 1, 1 and 2 respectively.

i = 8

     3 - 2
     3 1 2  temp[] has three elements, 3, 1 and 2 with counts as 2, 1 and 2 respectively. 

最后,我们最多有 k-1 个数字 温度[]。 temp 中的元素是 {3, 1, 2}。请注意,计数 temp[] 现在没用了,只在步骤 2 中需要计数。现在我们 需要检查 temp[] 中元素的实际数量是否更多 是否超过 n/k (9/4)。元素 3 和 2 的计数超过 9/4。 所以我们打印 3 和 2。

有关此方法的正确证明,请查看this answer from cs.stackexchange

【讨论】:

  • geeksforgeeks 是一个转储站点,供任何可以访问 Internet 的随机 Joe 使用,我不会在上面投入一毛钱。不过还是谢谢。
  • 该网站中提到的算法回答了您的问题,如果您仍然认为它是一个转储站点,这取决于您。
  • 充其量是有问题的,因为常数因子很小。此外,显示的代码基本上是一个人为的查找表,访问时间为 O(n)。如果有人使用此算法,他们最好使用哈希表,正如我在原始帖子的评论中指定的那样,这是不允许的。
  • Hashtable 方法使用O(n) 空间,而这使用常量空间O(k = 10)
  • @abhijit:如果对问题有限制,您应该将其放在问题中,而不是评论中。 SO cmets 是暂时的。此外,此解决方案肯定是可行的,并且与您引用的链接中的建议类似。因此,尝试理解它而不是立即拒绝它会帮助你理解那篇论文。
【解决方案3】:

你是对的。

QuickSelect 之后的下一个候选是十进制占优:n/10、2*n/10..9*n/10,因此仅检查 n/10 索引是不够的

请注意,在排序数组中占主导地位的元素占据了很长时间,并且肯定至少有一个具有提到索引的元素属于该运行。

k = 3, N = 11 的例子。设元素 b 至少占据数组的 1/3。在这种情况下,排序数组可能看起来像

b b b b * * * * * * * 
* b b b b * * * * * * 
* * b b b b * * * * *     
* * * b b b b * * * *     
* * * * b b b b * * *
* * * * * b b b b * *
* * * * * b b b b * *
* * * * * * b b b b *
* * * * * * * b b b b
      ^       ^       //positions for quickselect

请注意,在任何情况下,主导元素(如果确实存在 k 主导元素)至少占据一个标记位置。所以经过两轮 QuickSelect 我们有两个候选人

【讨论】:

  • 什么是候选人?你指的是Boyer–Moore majority vote algorithm吗?对于算法问题的答案,您应该列出解决方案的想法,或者提供伪代码,或者更好的实际代码。抱歉,但您的回答对您的评论没有任何帮助,而且似乎做出了未明确说明的假设(例如对候选人的引用)。
  • 查看较小k值的示例
  • @gstackoverflow 看看 rici cmets。我的例子中有什么不清楚的地方?它明确表明支配者必须至少占据所示位置之一。
【解决方案4】:

说实话,我在这里没有得到任何答案,但他们给出了如何解决问题的想法。根据前面提到的@MBo 的回答,您对每 10 个元素使用快速选择,但您使用的不是简单的快速选择,而是基于 3 路快速排序的快速选择。您不仅将每 10 个元素放置在它的位置上,而且将所有与其相等的元素放置在它的位置上。 并且经过一次迭代就足以计算出所有重复次数超过 9 个的元素。

【讨论】:

  • 这不是答案,更像是评论
  • 强烈反对这不是一个答案。在您询问的有关如何解决此问题的想法的问题中。所以我们的想法是采用最初提出这个问题的系列课程中讨论的方法。甚至命名了确切的方法。我同意这不是一个解决方案,但这肯定是一个答案。希望它会帮助某人)那是评论))))
【解决方案5】:

我用 Dijkstra 解决 DNF 问题的方法决定了这个问题。 请参阅下面的我的实施。可能对某人感兴趣和有用。

public void dCount(Comparable[] arr) {
    dCount(arr, 0, arr.length - 1);
}
private void dCount(Comparable[] arr, int lo, int hi) {
    if (lo >= hi) return;
    int curr = lo;
    int lt = lo;
    int rt = hi;
    Comparable pivot = arr[lo];

    while (curr <= rt) { // 3-way qSort main cycle
        if (less(arr[curr], pivot))
            swap(arr, curr++, lt++);
        else if (less(pivot, arr[curr]))
            swap(arr, curr, rt--);
        else curr++;
    }
    int count = curr - lt;
    if (count > arr.length / 10) { // you can change count value if it needs (n / 3, n / 2... just change 10 to your number)           
        System.out.printf("%s repeats %d times\n", arr[lt], count);
    }
    dCount(arr, lo, lt -1); // recur call for the left side from the pivot equal range
    dCount(arr, rt + 1, hi); // recur call for the right side from the pivot equal range
}
private static boolean less(Comparable a, Comparable b) {
    return a.compareTo(b) < 0;
}
private static void swap(Comparable[] arr, int i, int j) {
    Comparable swap = arr[i];
    arr[i] = arr[j];
    arr[j] = swap;
}

【讨论】:

  • 您能解释一下您的代码并提供时间复杂度分析吗?
  • 我从普林斯顿快速排序讲座cs.princeton.edu/courses/archive/spr18/cos226/lectures/…987654321@得到想法
  • 请看第 31-40 页。我刚刚输入了一个变量计数。您可以看到,当我们找到 pivot 的重复项时 - curr 和 lt 指针之间存在重复项。我们只是将计数与数组长度/10 进行比较。时间复杂度分析可以在上述 pdf 的第 40 页上找到。当我们说数组中有许多重复键时,我们可以假设时间复杂度为 O(n)。我试图找到一个证明——结果对我来说很难。
  • 链接不是解释。在 Stackoverflow,自包含答案是首选,因为链接几乎没有价值,并且可以通过 Google 搜索找到。
  • @AbhijitSarkar 谢谢你的解释。这对我来说非常有用,因为这是我在 Stackoverflow 的第一步。以后我会记住的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-03-16
  • 1970-01-01
  • 2014-09-01
  • 2016-12-21
相关资源
最近更新 更多