【问题标题】:Data structure: insert, remove, contains, get random element, all at O(1)数据结构:插入、删除、包含、获取随机元素,都在 O(1)
【发布时间】:2011-08-06 15:15:20
【问题描述】:

我在一次采访中遇到了这个问题。你会怎么回答?

设计一个在 O(1) 时间内提供以下操作的数据结构:

  • 插入
  • 移除
  • 包含
  • 获取随机元素

【问题讨论】:

  • 我们可以假设对数据种类有额外的限制吗?就像没有重复,等等。
  • 当然,没有重复,您甚至可以使用 java 或 c# 等语言中的内置数据结构。
  • 我注意到没有规范 re:ordered/unordered
  • 我知道这篇文章已经得到答复,但是对我来说,如果他们希望您提供 o(1) 随机访问而不是获取随机元素,这将更有意义。
  • 您找到正确的解决方案了吗?

标签: data-structures


【解决方案1】:

我们为什么不使用 epoch%arraysize 来查找随机元素。查找数组大小为 O(n),但摊销复杂度为 O(1)。

【讨论】:

    【解决方案2】:

    对于这个问题,我将使用两个数据结构

    • 哈希映射
    • ArrayList / Array / 双链表。

    步骤:-

    1. 插入:- 检查 X 是否已经存在于 HashMap 中——时间复杂度 O(1)。如果不存在则添加到 ArrayList 的末尾——时间复杂度 O(1)。 将其添加到 HashMap 中,并将 x 作为键和最后一个索引作为值 - 时间复杂度 O(1)。
    2. 删除:- 检查 X 是否存在于 HashMap 中——时间复杂度 O(1)。如果存在,则找到其索引并将其从 HashMap 中删除——时间复杂度 O(1)。将此元素与 ArrayList 中的最后一个元素交换并删除最后一个元素 -- 时间复杂度 O(1)。更新HashMap中最后一个元素的索引——时间复杂度O(1)。
    3. GetRandom :- 生成从 0 到 ArrayList 的最后一个索引的随机数。返回随机索引处生成的 ArrayList 元素 -- 时间复杂度 O(1)。
    4. 搜索:- 在 HashMap 中查看 x 作为键。 --时间复杂度 O(1)。

    代码:-

    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.LinkedList;
    import java.util.List;
    import java.util.Random;
    import java.util.Scanner;
    
    
    public class JavaApplication1 {
    
        public static void main(String args[]){
           Scanner sc = new Scanner(System.in);
            ArrayList<Integer> al =new ArrayList<Integer>();
            HashMap<Integer,Integer> mp = new HashMap<Integer,Integer>();  
            while(true){
                System.out.println("**menu**");
                System.out.println("1.insert");
                System.out.println("2.remove");
                System.out.println("3.search");
                System.out.println("4.rendom");
                int ch = sc.nextInt();
                switch(ch){
                    case 1 : System.out.println("Enter the Element ");
                            int a = sc.nextInt();
                            if(mp.containsKey(a)){
                                System.out.println("Element is already present ");
                            }
                            else{
                                al.add(a);
                                mp.put(a, al.size()-1);
    
                            }
                            break;
                    case 2 : System.out.println("Enter the Element Which u want to remove");
                            a = sc.nextInt();
                            if(mp.containsKey(a)){
    
                                int size = al.size();
                                int index = mp.get(a);
    
                                int last = al.get(size-1);
                                Collections.swap(al, index,  size-1);
    
                                al.remove(size-1);
                                mp.put(last, index);
    
                                System.out.println("Data Deleted");
    
                            }
                            else{
                                System.out.println("Data Not found");
                            }
                            break;
                    case 3 : System.out.println("Enter the Element to Search");
                            a = sc.nextInt();
                            if(mp.containsKey(a)){
                                System.out.println(mp.get(a));
                            }
                            else{
                                System.out.println("Data Not Found");
                            }
                            break;
                    case 4 : Random rm = new Random();
                            int index = rm.nextInt(al.size());
                            System.out.println(al.get(index));
                            break;
    
                }
            }
        }
    
    }
    

    -- 时间复杂度 O(1)。 -- 空间复杂度 O(N)。

    【讨论】:

      【解决方案3】:
      /* Java program to design a data structure that support folloiwng operations
         in Theta(n) time
         a) Insert
         b) Delete
         c) Search
         d) getRandom */
      import java.util.*;
      
      // class to represent the required data structure
      class MyDS
      {
         ArrayList<Integer> arr;   // A resizable array
      
         // A hash where keys are array elements and vlaues are
         // indexes in arr[]
         HashMap<Integer, Integer>  hash;
      
         // Constructor (creates arr[] and hash)
         public MyDS()
         {
             arr = new ArrayList<Integer>();
             hash = new HashMap<Integer, Integer>();
         }
      
         // A Theta(1) function to add an element to MyDS
         // data structure
         void add(int x)
         {
            // If ekement is already present, then noting to do
            if (hash.get(x) != null)
                return;
      
            // Else put element at the end of arr[]
            int s = arr.size();
            arr.add(x);
      
            // And put in hash also
            hash.put(x, s);
         }
      
         // A Theta(1) function to remove an element from MyDS
         // data structure
         void remove(int x)
         {
             // Check if element is present
             Integer index = hash.get(x);
             if (index == null)
                return;
      
             // If present, then remove element from hash
             hash.remove(x);
      
             // Swap element with last element so that remove from
             // arr[] can be done in O(1) time
             int size = arr.size();
             Integer last = arr.get(size-1);
             Collections.swap(arr, index,  size-1);
      
             // Remove last element (This is O(1))
             arr.remove(size-1);
      
             // Update hash table for new index of last element
             hash.put(last, index);
          }
      
          // Returns a random element from MyDS
          int getRandom()
          {
             // Find a random index from 0 to size - 1
             Random rand = new Random();  // Choose a different seed
             int index = rand.nextInt(arr.size());
      
             // Return element at randomly picked index
             return arr.get(index);
          }
      
          // Returns index of element if element is present, otherwise null
          Integer search(int x)
          {
             return hash.get(x);
          }
      }
      
      // Driver class
      class Main
      {
          public static void main (String[] args)
          {
              MyDS ds = new MyDS();
              ds.add(10);
              ds.add(20);
              ds.add(30);
              ds.add(40);
              System.out.println(ds.search(30));
              ds.remove(20);
              ds.add(50);
              System.out.println(ds.search(50));
              System.out.println(ds.getRandom());`enter code here`
          }
      }
      

      【讨论】:

        【解决方案4】:

        我们不能使用 Java 的 HashSet 来做到这一点吗?默认情况下,它在 O(1) 中提供插入、删除、搜索所有功能。 对于 getRandom 我们可以使用 Set 的迭代器,它无论如何都会给出随机行为。我们可以只从集合中迭代第一个元素而不必担心其余元素

        public void getRandom(){
            Iterator<integer> sitr = s.iterator();
            Integer x = sitr.next();    
            return x;
        }
        

        【讨论】:

          【解决方案5】:

          我同意 Anon 的观点。除了最后一个要求获得具有相同公平性的随机元素之外,所有其他要求只能使用单个基于哈希的 DS 来解决。我将在 Java 中为此选择 HashSet。元素哈希码的模数将为我提供 O(1) 时间内底层数组的索引号。我可以将它用于添加、删除和包含操作。

          【讨论】:

            【解决方案6】:

            考虑一个由哈希表 H 和数组 A 组成的数据结构。哈希表的键是数据结构中的元素,值是它们在数组中的位置。

            1. insert(value):将值追加到数组中,并让 i 为其在 A 中的索引。设置 H[value]=i。
            2. remove(value):我们将用 A 中的最后一个元素替换 A 中包含 value 的单元格。设 d 是数组 A 中索引 m 处的最后一个元素。设 i 为 H[value],即要删除的值在数组中的索引。设A[i]=d,H[d]=i,数组大小减一,从H中取出值。
            3. contains(value):返回 H.contains(value)
            4. getRandomElement():让 r=random(A 的当前大小)。返回 A[r]。

            由于数组需要自动增加大小,因此添加一个元素需要 O(1) 分期,但我想这没关系。

            【讨论】:

            • 这与我所拥有的很接近,但我错过了使用元素本身作为键......我知道我很接近,但这真的是一针见血!
            • 有趣的是,我在 Google 手机屏幕上收到了这个问题,经过一番努力,我坚持使用相同的解决方案。我稍微搞砸了一个实现并分配到第二个手机屏幕。
            • @aamadmi - 好吧,在 Java 中我想应该如此。在伪代码中,包含应该可以正常工作:)
            • 为什么需要数组,为什么不能使用hashmap。
            • @BalajiBoggaramRamanarayan,我猜这是我们在这种情况下讨论的向向量添加单个元素的摊销时间复杂度。 stackoverflow.com/questions/200384/constant-amortized-time(提到向量的追加操作的答案)
            【解决方案7】:

            虽然这已经很老了,但由于在 C++ 中没有答案,这是我的两分钱。

            #include <vector>
            #include <unordered_map>
            #include <stdlib.h>
            
            template <typename T> class bucket{
                int size;
                std::vector<T> v;
                std::unordered_map<T, int> m;
            public:
                bucket(){
                    size = 0;
                    std::vector<T>* v = new std::vector<T>();
                    std::unordered_map<T, int>* m = new std::unordered_map<T, int>();
                }
                void insert(const T& item){
                    //prevent insertion of duplicates
                    if(m.find(item) != m.end()){
                        exit(-1);
                    }
                    v.push_back(item);
                    m.emplace(item, size);
                    size++;
            
                }
                void remove(const T& item){
                    //exits if the item is not present in the list
                    if(m[item] == -1){
                        exit(-1);
                    }else if(m.find(item) == m.end()){
                        exit(-1);
                    }
            
                    int idx = m[item];
                    m[v.back()] = idx;
                    T itm = v[idx];
                    v.insert(v.begin()+idx, v.back());
                    v.erase(v.begin()+idx+1);
                    v.insert(v.begin()+size, itm);
                    v.erase(v.begin()+size);
                    m[item] = -1;
                    v.pop_back();
                    size--;
            
                }
            
                 T& getRandom(){
                  int idx = rand()%size;
                  return v[idx];
            
                 }
            
                 bool lookup(const T& item){
                   if(m.find(item) == m.end()) return false;
                   return true;
            
                 }
                //method to check that remove has worked
                void print(){
                    for(auto it = v.begin(); it != v.end(); it++){
                        std::cout<<*it<<" ";
                    }
                }
            };
            

            这是一段测试解决方案的客户端代码。

            int main() {
            
                bucket<char>* b = new bucket<char>();
                b->insert('d');
                b->insert('k');
                b->insert('l');
                b->insert('h');
                b->insert('j');
                b->insert('z');
                b->insert('p');
            
                std::cout<<b->random()<<std::endl;
                b->print();
                std::cout<<std::endl;
                b->remove('h');
                b->print();
            
                return 0;
            }
            

            【讨论】:

              【解决方案8】:

              我们可以使用散列来支持 Θ(1) 时间内的运算。

              插入(x) 1) 通过进行哈希映射查找来检查 x 是否已经存在。 2)如果不存在,则将其插入数组的末尾。 3) 也加入hash table,x作为key,最后一个数组索引作为index。

              移除(x) 1) 通过进行哈希映射查找来检查 x 是否存在。 2)如果存在,则找到其索引并将其从哈希映射中删除。 3) 将数组中的最后一个元素与该元素交换并删除最后一个元素。 完成交换是因为可以在 O(1) 时间内删除最后一个元素。 4) 更新哈希图中最后一个元素的索引。

              getRandom() 1) 生成一个从 0 到最后一个索引的随机数。 2) 返回随机生成索引处的数组元素。

              搜索(x) 在哈希图中查找 x。

              【讨论】:

                【解决方案9】:

                你可能不喜欢这个,因为他们可能正在寻找一个聪明的解决方案,但有时坚持你的枪是值得的......哈希表已经满足要求 - 总体上可能更好比其他任何东西都好(尽管显然是在摊销的常数时间内,并且对其他解决方案有不同的妥协)。

                棘手的要求是“随机元素”选择:在哈希表中,您需要扫描或探测这样的元素。

                对于封闭式散列/开放式寻址,任何给定存储桶被占用的机会为size() / capacity(),但至关重要的是,这通常通过散列表实现保持在恒定的乘法范围内(例如,表可能保持大于其根据性能/内存调整,将当前内容提高 1.2 倍到 ~10 倍)。这意味着我们平均可以预期搜索 1.2 到 10 个桶——完全独立于容器的总大小;摊销 O(1)。

                我可以想象两种简单的方法(还有很多更复杂的方法):

                • 从随机桶中线性搜索

                  • 考虑空的/持有价值的桶 ala “--AC-----B--D”:你可以说第一个“随机”选择是公平的,即使它有利于 B ,因为 B 没有比其他元素更受青睐的可能性,但是如果您使用相同的值进行重复的“随机”选择,那么显然让 B 反复受青睐可能是不可取的(尽管问题中没有任何东西需要概率)
                • 反复尝试随机存储桶,直到找到一个已填充的存储桶

                  • “仅”容量()/大小()平均桶访问(如上)-但实际上更昂贵,因为随机数生成相对昂贵,如果无限不可能最坏,则无限糟糕-案件行为...
                    • 更快的折衷方案是使用从初始随机选择的存储桶中预先生成的随机偏移量列表,将它们%-ing 到存储桶计数中

                不是一个很好的解决方案,但与始终维护第二个索引数组的内存和性能开销相比,它可能仍然是一个更好的整体折衷方案。

                【讨论】:

                  【解决方案10】:

                  我认为我们可以使用带有哈希表的双向链表。键将是元素,其关联值将是双向链表中的节点。

                  1. insert(H,E) : 在双向链表中插入节点并将条目设为 H[E]=node; O(1)
                  2. delete(H,E) : 通过H(E)获取节点地址,跳转到该节点的前一个节点,删除H(E)为NULL,所以O(1)
                  3. contains(H,E) 和 getRandom(H) 显然是 O(1)

                  【讨论】:

                  • 这没有意义。
                  【解决方案11】:

                  这是我不久前在被问到相同问题时提出的该问题的 C# 解决方案。它实现了 Add、Remove、Contains 和 Random 以及其他标准 .NET 接口。并不是说您需要在面试中如此详细地实施它,但很高兴有一个具体的解决方案来查看......

                  using System;
                  using System.Collections;
                  using System.Collections.Generic;
                  using System.Linq;
                  using System.Threading;
                  
                  /// <summary>
                  /// This class represents an unordered bag of items with the
                  /// the capability to get a random item.  All operations are O(1).
                  /// </summary>
                  /// <typeparam name="T">The type of the item.</typeparam>
                  public class Bag<T> : ICollection<T>, IEnumerable<T>, ICollection, IEnumerable
                  {
                      private Dictionary<T, int> index;
                      private List<T> items;
                      private Random rand;
                      private object syncRoot;
                  
                      /// <summary>
                      /// Initializes a new instance of the <see cref="Bag&lt;T&gt;"/> class.
                      /// </summary>
                      public Bag()
                          : this(0)
                      {
                      }
                  
                      /// <summary>
                      /// Initializes a new instance of the <see cref="Bag&lt;T&gt;"/> class.
                      /// </summary>
                      /// <param name="capacity">The capacity.</param>
                      public Bag(int capacity)
                      {
                          this.index = new Dictionary<T, int>(capacity);
                          this.items = new List<T>(capacity);
                      }
                  
                      /// <summary>
                      /// Initializes a new instance of the <see cref="Bag&lt;T&gt;"/> class.
                      /// </summary>
                      /// <param name="collection">The collection.</param>
                      public Bag(IEnumerable<T> collection)
                      {
                          this.items = new List<T>(collection);
                          this.index = this.items
                              .Select((value, index) => new { value, index })
                              .ToDictionary(pair => pair.value, pair => pair.index);
                      }
                  
                      /// <summary>
                      /// Get random item from bag.
                      /// </summary>
                      /// <returns>Random item from bag.</returns>
                      /// <exception cref="System.InvalidOperationException">
                      /// The bag is empty.
                      /// </exception>
                      public T Random()
                      {
                          if (this.items.Count == 0)
                          {
                              throw new InvalidOperationException();
                          }
                  
                          if (this.rand == null)
                          {
                              this.rand = new Random();
                          }
                  
                          int randomIndex = this.rand.Next(0, this.items.Count);
                          return this.items[randomIndex];
                      }
                  
                      /// <summary>
                      /// Adds the specified item.
                      /// </summary>
                      /// <param name="item">The item.</param>
                      public void Add(T item)
                      {
                          this.index.Add(item, this.items.Count);
                          this.items.Add(item);
                      }
                  
                      /// <summary>
                      /// Removes the specified item.
                      /// </summary>
                      /// <param name="item">The item.</param>
                      /// <returns></returns>
                      public bool Remove(T item)
                      {
                          // Replace index of value to remove with last item in values list
                          int keyIndex = this.index[item];
                          T lastItem = this.items[this.items.Count - 1];
                          this.items[keyIndex] = lastItem;
                  
                          // Update index in dictionary for last item that was just moved
                          this.index[lastItem] = keyIndex;
                  
                          // Remove old value
                          this.index.Remove(item);
                          this.items.RemoveAt(this.items.Count - 1);
                  
                          return true;
                      }
                  
                      /// <inheritdoc />
                      public bool Contains(T item)
                      {
                          return this.index.ContainsKey(item);
                      }
                  
                      /// <inheritdoc />
                      public void Clear()
                      {
                          this.index.Clear();
                          this.items.Clear();
                      }
                  
                      /// <inheritdoc />
                      public int Count
                      {
                          get { return this.items.Count; }
                      }
                  
                      /// <inheritdoc />
                      public void CopyTo(T[] array, int arrayIndex)
                      {
                          this.items.CopyTo(array, arrayIndex);
                      }
                  
                      /// <inheritdoc />
                      public bool IsReadOnly
                      {
                          get { return false; }
                      }
                  
                      /// <inheritdoc />
                      public IEnumerator<T> GetEnumerator()
                      {
                          foreach (var value in this.items)
                          {
                              yield return value;
                          }
                      }
                  
                      /// <inheritdoc />
                      IEnumerator IEnumerable.GetEnumerator()
                      {
                          return this.GetEnumerator();
                      }
                  
                      /// <inheritdoc />
                      public void CopyTo(Array array, int index)
                      {
                          this.CopyTo(array as T[], index);
                      }
                  
                      /// <inheritdoc />
                      public bool IsSynchronized
                      {
                          get { return false; }
                      }
                  
                      /// <inheritdoc />
                      public object SyncRoot
                      {
                          get
                          {
                              if (this.syncRoot == null)
                              {
                                  Interlocked.CompareExchange<object>(
                                      ref this.syncRoot,
                                      new object(),
                                      null);
                              }
                  
                              return this.syncRoot;
                  
                          }
                      }
                  }
                  

                  【讨论】:

                  • 如果您有重复的号码,我不确定这是否有效。
                  • 它不处理重复项,因为@guildner 说假设问题的 cmets 中没有重复项。如果添加了重复项,则添加 ArgumentException 并显示消息“已添加具有相同密钥的项目”。将被抛出(来自底层索引字典)。
                  【解决方案12】:

                  在 C# 3.0 + .NET Framework 4 中,泛型 Dictionary&lt;TKey,TValue&gt; 甚至比 Hashtable 更好,因为您可以使用 System.Linq 扩展方法 ElementAt() 来索引到 KeyValuePair&lt;TKey,TValue&gt; 元素所在的底层动态数组存储:

                  using System.Linq;
                  
                  Random _generator = new Random((int)DateTime.Now.Ticks);
                  
                  Dictionary<string,object> _elements = new Dictionary<string,object>();
                  
                  ....
                  
                  Public object GetRandom()
                  {
                       return _elements.ElementAt(_generator.Next(_elements.Count)).Value;
                  }
                  

                  但是,据我所知,哈希表(或其字典后代)并不是这个问题的真正解决方案,因为 Put() 只能摊销 O(1) ,而不是真正的 O(1) ,因为它是在动态调整大小边界处 O(N)。

                  这个问题有真正的解决方案吗?我能想到的是,如果您指定一个字典/哈希表的初始容量超出您预期的一个数量级,那么您将获得 O(1) 操作,因为您永远不需要调整大小。

                  【讨论】:

                  • 如果您对什么是哈希表非常严格,那么 O(N) 调整大小是不可避免的。一些实现会妥协以降低调整大小的成本 - 例如,通过保留现有表同时添加第二个大小的两倍,或者尝试调整现有表的大小(在页面边界上仔细安排虚拟地址空间和表大小之后,所以没有需要复制,这可能需要内存映射而不是 new/malloc mem),然后在返回到较小的区域之前在新的较大区域中寻找(在原地模型中,通过更紧密地修改),使用元素迁移逻辑。跨度>
                  【解决方案13】:

                  最好的解决方案可能是哈希表+数组,它真正快速且确定性。

                  但评分最低的答案(只需使用哈希表!)实际上也很棒!

                  • 具有重新散列或新存储桶选择的哈希表(即每个存储桶一个元素,无链表)
                  • getRandom() 反复尝试选择一个随机存储桶,直到它为空。
                  • 作为故障保险,可能是 getRandom(),在 N(元素数)不成功尝试后,在 [0, N-1] 中选择一个随机索引 i,然后线性遍历哈希表并选择 #i -th 元素。

                  人们可能不喜欢这个,因为“可能存在无限循环”,我见过非常聪明的人也有这种反应,但这是错误的!无限可能的事件只是不会发生。

                  假设您的伪随机源的良好行为(对于这种特定行为并不难确定)并且哈希表总是至少 20% 已满,很容易看出:

                  永远不会发生 getRandom() 必须尝试超过 1000 次的情况。只是从不。事实上,这种事件的概率是 0.8^1000,即 10^-97——所以我们必须重复 10^88 次才能有十亿分之一的机会发生一次。即使这个程序在人类所有计算机上全时运行直到太阳灭亡,这永远不会发生。

                  【讨论】:

                  • 如果你不断地选择一个随机的有 value 的桶,那么在你选择一个随机元素时,最坏的情况是如何导致 O(1)
                  • @user1147505 - 你从哪里得到这个数字:“0.8^1000”?
                  • 你是怎么做到的:“哈希表总是至少有 20% 满了”
                  • 你能写出你可以随机选择一个桶的方法吗?
                  【解决方案14】:

                  O(1) 查找意味着 hashed data structure

                  对比:

                  • O(1) 插入/删除和 O(N) 查找意味着一个链表。
                  • O(1) 插入、O(N) 删除和 O(N) 查找意味着一个数组支持的列表
                  • O(logN) 插入/删除/查找意味着树或堆。

                  【讨论】:

                  • 这是一个开始,但最后一个要求呢?你能从哈希数据结构中得到一个随机元素(数据结构中每个元素的概率相等)吗?
                  • @lag1980,我想你可以:hashtable.get((int)(Math.random()*hashtable.size()));
                  • 嗯,我不知道有任何哈希表可以让您获得这样的元素,如果有的话,我无法想象这将是一个恒定时间操作。我很想在这两个方面都被证明是错误的。
                  • @lag1980 ...你可以很容易地在恒定时间内做到这一点,就像 Clojure 的向量是“恒定时间”一样——当 N 的可能值受到硬件限制时,log32(N) 使得最大可能的 log32() 值是......类似于 7,这实际上是恒定时间。
                  • “数组支持的列表”是指:数组?
                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 2013-11-16
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2015-05-22
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多