数据结构:插入,删除,包含,获取随机元素,全部在O(1)


94

采访中给了我这个问题。您将如何回答?

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

  • 去掉
  • 包含
  • 获取随机元素

我们可以对数据种类承担其他限制吗?像有没有重复,等等
Sanjeevakumar Hiremath

当然,没有重复项,您甚至可以使用Java或c#等语言的内置数据结构。
guildner

1
我注意到没有规范:有序/无序
Charles Duffy 2012年

7
我知道这篇文章已经被回答了,但是对我来说,如果他们希望您提供o(1)随机访问而不是获取随机元素,那将更有意义。
ramsinb

您是否找到了正确的解决方案?
Balaji Boggaram Ramanarayan

Answers:


142

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

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

由于数组需要自动增加大小,因此将分摊O(1)来添加元素,但是我想那是可以的。


这接近于我所拥有的,但是我错过了使用元素本身作为键的操作。...我知道我已经接近了,但这确实把它钉在了头上!
guildner

有趣的是,我在Google手机屏幕上遇到了这个问题,并且在同样的解决方案上苦苦挣扎之后。我稍微搞砸了一个实现,然后分配给第二个手机屏幕。
安德烈·塔尔尼科夫

AP将值附加到数组:O(1)如何?
Balaji Boggaram Ramanarayan

4
@aamadmi-好吧,在Java中,我认为应该。在伪代码中,包含应该可以正常工作:)
r0u1i 2014年

4
为什么需要数组,为什么不能使用哈希图。
Ankit Zalani 2014年

22

O(1)查找表示散列数据结构

通过比较:

  • 使用O(N)查找的O(1)插入/删除表示一个链表。
  • O(1)插入,O(N)删除和O(N)查找表示数组支持的列表
  • O(logN)插入/删除/查找表示树或堆。

这是一个开始,但是最后一个要求呢?您是否可以从散列数据结构中获得随机元素(数据结构中每个元素的概率相同)?
guildner

1
@ lag1980,我想您可以:hashtable.get((int)(Math.random()*hashtable.size()));
CMR

3
嗯,我不知道有什么哈希表可以让你得到这样的元素,如果有的话,我无法想象这将是一个恒定时间的操作。我有兴趣在两种情况下都被证明是错误的。
guildner

@ lag1980 ...您可以很容易地以恒定时间执行此操作,就像Clojure的向量是“恒定时间”时一样-log32(N),当N的可能值受到硬件的限制时,最大log32()值是...大约是7,这实际上是恒定时间。
查尔斯·达菲

所谓“支持数组的列表”是指:数组?
亨伽美

5

您可能不喜欢这样,因为他们可能正在寻找一个聪明的解决方案,但有时还是值得坚持…… 哈希表已经满足要求 -总体上可能比其他任何方法都更好(尽管显然摊销后的常量时间,以及对其他解决方案的不同妥协)。

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

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

我可以想像两种简单的方法(还有很多更巧妙的方法):

  • 从随机桶中线性搜索

    • 考虑空的/保持价值的桶ala“ --AC ----- B--D”:您可以说第一个“随机”选择即使对B有利,也很公平,因为B不再有被偏爱的可能性而不是其他元素,但是如果您使用相同的值进行重复的“随机”选择,那么显然不希望B反复受到青睐(尽管问题中甚至没有要求概率)
  • 反复尝试随机存储桶,直到找到一个填充的桶

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

这不是一个很好的解决方案,但是与始终保持第二个索引数组的内存和性能开销相比,这可能是一个更好的总体折衷方案。


3

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

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

  • 带有重新哈希或选择新存储桶的哈希表(即每个存储桶一个元素,没有链接列表)
  • getRandom()反复尝试选择一个随机存储桶,直到它为空。
  • 作为一种故障安全方法,可能是getRandom(),在尝试了N次(元素数)未成功之后,在[0,N-1]中选择了一个随机索引i,然后线性遍历哈希表并选择了第#i个元素。

人们可能不会因为“可能的无限循环”而喜欢这种方法,而且我已经看到非常聪明的人也有这种反应,但这是错误的!绝对不可能发生的事件根本不会发生。

假设您的伪随机源的行为良好-对于这种特定行为而言,这并不难建立-并且哈希表始终至少已满20%,很容易看到:

它将永远不会发生getRandom()有尝试1000次以上。只是从来没有。确实,此类事件的概率为0.8 ^ 1000,即10 ^ -97,因此我们必须重复执行10 ^ 88次,才能使十亿分之一的事件发生一次。即使该程序在人类所有计算机上全天候运行,直到太阳死了,这也永远不会发生。


1
如果您连续选择一个具有值的随机存储桶,那么在选择随机元素的同时,最糟糕的情况是导致O(1)
Balaji Boggaram Ramanarayan

@ user1147505-您从何处获得此号码:“ 0.8 ^ 1000”?
亨伽美

您是如何做到这一点的:“哈希表始终至少已满20%”
Hengameh 2015年

您能不能写一个随机桶的方法?
亨伽美

3

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

  • 哈希图
  • ArrayList / Array / Double LinkedList。

脚步 :-

  1. 插入:-检查X是否已存在于HashMap中-时间复杂度O(1)。如果不存在,则添加到ArrayList的末尾-时间复杂度O(1)。将其添加到HashMap中,还x作为键,最后一个index作为值-时间复杂度O(1)。
  2. 删除:-检查HashMap中是否存在X-时间复杂度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)。


1

这是针对该问题的C#解决方案,我前不久被问到相同的问题时想到了。它与其他标准.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;

        }
    }
}

如果您有重复的号码,我不确定是否可以使用。
AlexIIP '16

它不处理重复项,因为@guildner说要假设问题的注释中没有重复项。如果添加了重复项ArgumentException,则显示消息“已添加具有相同密钥的项目”。将被抛出(从基础索引Dictionary中)。
Scott Lerch

1

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

insert(x) 1)通过执行哈希映射查找来检查x是否已经存在。2)如果不存在,则将其插入数组的末尾。3)还要在哈希表中添加x作为键,最后一个数组索引作为索引。

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

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

search(x) 在哈希图中查找x。


1

尽管这已经很老了,但是由于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;
}

0

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

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 )在动态调整大小边界。

是否有解决此问题的真正方法?我能想到的是,如果您指定的Dictionary / Hashtable初始容量超出预期的数量级,那么您将获得O(1)操作,因为您无需调整大小。


如果您对什么是哈希表非常严格,那么O(N)的大小调整是不可避免的。但是,某些实现会折衷以降低调整大小的成本-例如,通过保留现有表,同时增加两倍的大小,或者尝试在适当位置调整现有表的大小(在页面边界上仔细安排虚拟地址空间和表大小之后,需要复制,这可能需要内存映射,而不是new / malloc mem),然后在元素较大的逻辑上寻找新的较大区域,然后再退回到较小的区域(在就地模型中通过更紧密地修改)。
Tony Delroy 2013年

0

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


0

不能使用Java的HashSet做到这一点吗?默认情况下,它在O(1)中提供插入,删除和搜索。对于getRandom,我们可以使用Set的迭代器,该迭代器始终会产生随机行为。我们可以从集合中迭代第一个元素,而不必担心其余元素

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

0
/* 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`
    }
}

-2

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


-3

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

  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)

这没有道理。
innosam
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.