确定在O(n)时间和O(1)空间中发生最多的元素

时间:2014-04-24 06:13:12

标签: java c algorithm

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

6 个答案:

答案 0 :(得分:14)

不能有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)空间。

答案 1 :(得分:3)

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

  1. 迭代数组。
  2. 假设数组包含的最大元素是m。
  3. 对于每个索引,我得到它所持有的元素,让它为array [i]
  4. 现在转到索引数组[i]并添加m。
  5. 对数组中的所有索引执行上述操作。
  6. 最后迭代数组并返回带最大元素的索引。
  7. TC - >上) SC - > O(1)

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

答案 2 :(得分:3)

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

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

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

表示(i = 0到n-1)

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

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

考虑到哈希O(K)的实现,使用的空间为O(N),时间复杂度为O(1)。 因此,性能实际上取决于K。如果k << n,此方法效果不佳。

答案 3 :(得分:2)

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

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

  • 访问计数器
  • 具有此访问次数的密钥集(作为哈希集)
  • 下一个指针
  • prev pointer

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

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

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

答案 4 :(得分:2)

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

如你所说,如果你可以哈希,你可以这样做,因为你可以简单地按哈希计算。问题是你可能得到非唯一的哈希值。你提到20位数字,所以可能是2 ^ 20个可能的值,并且希望实际的哈希计数具有小的和固定数量的工作内存。因此,假设这将导致散列冲突并因此导致散列算法的破坏。但是你可以通过使用互补散列算法进行多次传递来解决这个问题。

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

假设单词对齐;这意味着我们有18位哈希值。

接下来,您可能认为最大缓存大小可能非常小。我将假设您分配的最大值为&lt; = 256项,因为我们可以使用单个字节进行计数。

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

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

稍后添加

我一直在考虑这个问题并且我已经意识到它有一个失败的条件:如果第一次传递将两个组计为具有相同数量的元素,则无法有效区分它们。好吧

答案 5 :(得分:1)

假设:所有元素都是整数,对于其他数据类型我们也可以实现这个,如果我们使用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个变量。