【问题标题】:Determining the element that occurred the most in O(n) time and O(1) space确定在 O(n) 时间和 O(1) 空间中出现最多的元素
【发布时间】:2014-04-24 06:13:11
【问题描述】:

首先让我说这不是一个家庭作业问题。我正在尝试设计一个缓存,其驱逐策略取决于缓存中出现最多的条目。用软件术语来说,假设我们有一个包含不同元素的数组,我们只想找到出现次数最多的元素。例如:{1,2,2,5,7,3,2,3} 应该返回 2。由于我正在使用硬件,因此简单的 O(n^2) 解决方案将需要巨大的硬件开销。使用哈希表的更聪明的解决方案适用于软件,因为哈希表的大小可以改变,但在硬件中,我将有一个固定大小的哈希表,可能不会那么大,所以冲突会导致错误的决定。我的问题是,在软件中,我们可以在 O(n) 时间复杂度和 O(1) 空间中解决上述问题吗?

【问题讨论】:

  • 你对可以出现的元素有最大限制吗?
  • @Aniket 它很大,因为它是物理地址的一部分,大约 20 位
  • 数组中元素的最大数量是否有限制?
  • @NikunjBanka 是的,限制是缓存大小。它很大但有限
  • 是否允许对数组进行变异 - 例如对其进行排序?另外,既然它是一个缓存,那么不精确的启发式方法是否可行?

标签: java c algorithm


【解决方案1】:

不可能有O(n) 时间、O(1) 空间的解决方案,至少对于一般情况来说不是。

作为amit points out,通过解决这个问题,我们找到了element distinctness problem(确定列表的所有元素是否不同)的解决方案,这已被证明在不使用元素索引时需要Θ(n log n)时间计算机的内存。如果我们要使用元素来索引计算机的内存,给定无限范围的值,这至少需要Θ(n) 空间。考虑到将这个问题简化为那个问题,该问题的边界对该问题强制执行相同的边界。

但是,实际上,如果没有其他原因,通常用于存储每个元素的类型具有固定大小(例如 32 位整数),则该范围大部分是有界的。如果是这种情况,这将允许O(n) 时间、O(1) 空间解决方案,尽管可能太慢并且由于涉及的大常数因素而使用太多空间(因为时间和空间复杂性将取决于范围值)。

2 个选项:

  • Counting sort

    保存每个元素出现次数的数组(数组索引为元素),输出最频繁的。

    如果您有一个有限范围的值,则此方法将是O(1) 空间(和O(n) 时间)。但从技术上讲,哈希表方法也是如此,因此这里的常数因子可能太大而无法接受。

    相关选项是radix sort(具有就地变体,类似于快速排序)和bucket sort

  • Quicksort

    根据选定的枢轴(通过交换)重复对数据进行分区并在分区上递归。

    排序后,我们可以遍历数组,跟踪连续元素的最大数量。

    这需要O(n log n) 时间和O(1) 空间。

【讨论】:

  • I don't believe an O(n) time, O(1) space 你可以去掉“我不相信”,没有。如果存在解决差异性问题,则可以在 O(n) 时间和 O(1) 空间内完成,通过运行算法来获取候选者,然后检查其出现次数。您的回答几乎概括了各种可能性,所以 +1。
  • @amit 谢谢。我(希望准确)将此纳入我的答案。
  • 我不知道使用哈希表等随机算法无法在预期时间和空间内解决 ED 的下限。参考?
  • @NiklasB。根据维基百科,哈希表解决方案属于使用元素来索引计算机内存,因此不受这些时间和空间限制。但维基百科也提到,同样的下限也被证明适用于不属于此范围的随机算法,即随机代数决策树模型算法。
  • 我无法从维基百科的文章中提取出来,但如果你这么说
【解决方案2】:

正如您所说,缓存中的最大元素可能是一个非常大的数字,但以下是解决方案之一。

  1. 遍历数组。
  2. 假设数组包含的最大元素是 m。
  3. 对于每个索引,我得到它所持有的元素,让它成为 array[i]
  4. 现在转到索引数组 [i] 并将 m 添加到它。
  5. 对数组中的所有索引执行上述操作。
  6. 最后遍历数组并返回最大元素的索引。

TC -> O(N) SC -> O(1)

对于您的情况,对于较大的 m 可能不可行。但是看看你是否可以优化或改变这个算法。

【讨论】:

    【解决方案3】:

    我头顶上的解决方案:
    由于数字可能很大,所以我考虑散列,而不是直接将它们存储在数组中。

    0n-1有n个数字。
    假设出现最大次数的次数,出现K次。
    让我们创建 n/k buckets ,最初都是空的。

    hash(num) 告诉num 是否存在于任何存储桶中。
    hash_2(num) 存储num 出现在任何存储桶中的次数。

    for(i = 0 to n-1)

    • 如果该数字已经存在于其中一个桶中,则增加 input[i] 的计数,例如 Hash_2(input[i]) ++
    • 否则找到一个空桶,在第一个空桶中插入input[i]Hash(input[i])=true
    • 否则,如果所有存储桶已满,则将存储桶中 all 数字的计数减 1,请勿在任何存储桶中添加 input[i]
      如果任何数字的计数变为零 [参见 hash_2(number)],Hash(number) = false

    这样,你最终会得到最多k个元素,并且需要的数字就是其中之一,所以你需要再次遍历输入O(N)才能最终找到实际的数字。

    使用的空间是O(K),时间复杂度是O(N),考虑到哈希O(1)的实现。
    所以,性能真的取决于K。如果k << n,这个方法执行的很差。

    【讨论】:

      【解决方案4】:

      我认为这不能回答标题中所述的问题,但实际上您可以使用最少使用的驱逐策略来实现缓存,该策略具有恒定的平均放置、获取和删除操作时间。如果您正确维护数据结构,则无需扫描所有项目以找到要驱逐的项目。

      这个想法是有一个将键映射到值记录的哈希表。值记录包含值本身以及对“计数器节点”的引用。计数器节点是双向链表的一部分,包括:

      • 访问计数器
      • 具有此访问计数的键集(作为哈希集)
      • 下一个指针
      • 上一个指针

      维护该列表,使其始终按访问计数器(头部为 min)排序,并且计数器值是唯一的。具有访问计数器 C 的节点包含具有此访问计数的所有密钥。请注意,这不会增加数据结构的整体空间复杂度。

      get(K) 操作涉及通过将 K 迁移到另一个计数器记录(新记录或列表中的下一个记录)来提升 K。

      由 put 操作触发的驱逐操作大致包括检查列表的头部,从其键集中删除任意键,然后将其从哈希表中删除。

      【讨论】:

        【解决方案5】:

        如果我们对您的数据集做出合理的(无论如何对我而言)假设,这是可能的。

        正如你所说,如果你可以散列,你就可以做到,因为你可以简单地按散列计数。问题是您可能会得到非唯一的哈希值。您提到了 20 位数字,因此大概有 2^20 个可能的值,并且希望为实际哈希计数提供少量且固定数量的工作内存。有人推测,这将因此导致散列冲突,从而导致散列算法的崩溃。但是您可以通过使用互补散列算法进行多次遍历来解决此问题。

        因为这些是内存地址,所以实际上可能并非所有位都能够被设置。例如,如果您只分配字(4 字节)块,则可以忽略两个最低有效位。我怀疑,但不知道,你实际上只是在处理更大的分配边界,所以它可能比这更好。

        假设单词对齐;这意味着我们有 18 位要散列。

        接下来,您大概有一个最大缓存大小,大概相当小。我将假设您最多分配

        好的,为了制作我们的哈希,我们将缓存中的数字分成两个九位数字,按照重要性从高到低的顺序,并丢弃最后两位,如上所述。获取这些块中的第一个并将其用作散列以给出第一部分计数。然后我们取出这些块中的第二个并将其用作散列,但这次我们只计算第一部分散列是否与我们确定为具有最高散列的部分匹配。具有最高哈希值的那个现在被唯一标识为具有最高计数。

        这在 O(n) 时间内运行,需要一个 512 字节的哈希表进行计数。如果表格太大,您可以分成三个块并使用 64 字节的表格。

        稍后添加

        我一直在考虑这个问题,我意识到它有一个失败的条件:如果第一遍将两个组视为具有相同数量的元素,它无法有效地区分它们。哦,好吧

        【讨论】:

          【解决方案6】:

          假设:所有元素都是整数,对于其他数据类型我们也可以使用 hashCode() 来实现

          我们可以实现时间复杂度O(nlogn),空间为O(1)

          首先对数组进行排序,时间复杂度为O(nlog n)(为了保持空间复杂度,我们应该使用快速排序之类的就地排序算法)

          使用四个整数变量,current表示我们所指的值,count表示current的出现次数,result表示最终结果,resultCount表示result的出现次数

          从数组的开头到结尾迭代data

            int result = 0;
            int resultCount = -1;
            int current = data[0];
            int count = 1;
          
            for(int i = 1; i < data.length; i++){
                 if(data[i] == current){
                      count++;
                }else{
                     if(count > resultCount){
                         result = current;
                         resultCount = count;
                     }
                     current = data[i];
                     count = 1;
                 }
            }
            if(count > resultCount){
                result = current;
                resultCount = count;
            }
            return result;
          

          所以,最后只用到了4个变量。

          【讨论】:

          • log(1) + log(2) + ... + log(n) = log(1*2*....*n) = log(n!) 这是 Theta (nlogn)。我不明白你几乎 O(n) 的说法是从哪里来的。
          • 此外,将一个元素插入到数组中的任意位置是 O(n),因为您需要进一步将所有元素“推”到其右侧。如果您想使用树/跳过列表来获得 O(logn),那么您将面临更高的常数。
          • (我在答案确定之前投了反对票,当它是时撤销了我的反对票)
          • @PhamTrung:对不起,我做错了。我没有看到修复。请在您的答案中进行虚拟编辑,以便我可以取消我的反对票。
          • @EyalSchneider 没关系 :)
          猜你喜欢
          • 1970-01-01
          • 2015-05-25
          • 2011-08-09
          • 1970-01-01
          • 1970-01-01
          • 2020-01-28
          • 1970-01-01
          • 1970-01-01
          • 2015-11-17
          相关资源
          最近更新 更多