データ構造:ランダム要素の挿入、削除、包含、取得、すべてO(1)で


94

私はこの問題をインタビューで受けました。どのように答えましたか?

O(1)時間で次の操作を提供するデータ構造を設計します。

  • インサート
  • 削除する
  • 含む
  • ランダムな要素を取得する

データの種類について追加の制限を想定できますか?重複がないようになど
Sanjeevakumar Hiremath '15

もちろん、重複はありません。javaやc#などの言語で組み込みのデータ構造を使用することもできます。
ギルド

1
re:注文/未注文
Charles Duffy

7
私はこの投稿が回答されていることを知っていますが、ランダムな要素を取得するのではなく、o(1)のランダムアクセスを提供するように依頼するほうが理にかなっています。
ramsinb 2013年

これに対する正しい解決策を見つけましたか?
Balaji Boggaram Ramanarayan 2013年

回答:


142

ハッシュテーブルHと配列Aで構成されるデータ構造を考えます。ハッシュテーブルキーはデータ構造の要素であり、値は配列内の位置です。

  1. insert(value):配列に値を追加し、Aのインデックスをiとします。H[value] = iを設定します。
  2. remove(value):Aの値を含むセルをAの最後の要素で置き換えます。dを配列mの配列Aの最後の要素とします。削除する値の配列のインデックスであるH [値]とします。A [i] = d、H [d] = iを設定し、配列のサイズを1つ減らし、Hから値を削除します。
  3. contains(value):H.contains(value)を返します
  4. getRandomElement():r = random(Aの現在のサイズ)にします。A [r]を返します。

配列のサイズは自動で増加する必要があるため、要素を追加するためにO(1)を償却することになりますが、これは問題ないと思います。


これは私が持っていたものに近いですが、要素自体をキーとして使用するのを逃しました。
ギルダー

興味深いのは、Googleの電話画面でこの質問をして、同じ解決策に苦労した後のことです。私は実装を少し台無しにして、2番目の電話画面に割り当てました。
Andrey Talnikov

配列に値を割り当てる:それはO(1)ですか?
Balaji Boggaram Ramanarayan 2013年

4
@aamadmi-まあ、Javaではそうだと思います。擬似コードでは、containsは問題なく動作するはずです:)
r0u1i

4
なぜ配列が必要なのか、なぜハッシュマップを使用できないのか。
Ankit Zalani 2014年

22

O(1)ルックアップはハッシュされたデータ構造を意味します

比較すると:

  • O(1)挿入/削除とO(N)ルックアップは、リンクリストを意味します。
  • O(1)挿入、O(N)削除、およびO(N)ルックアップは、配列に基づくリストを意味します
  • O(logN)挿入/削除/ルックアップは、ツリーまたはヒープを意味します。

それは始まりですが、最後の要件はどうですか?ハッシュされたデータ構造からランダムな要素を(データ構造の各要素に対して等しい確率で)取得できますか?
ギルダー

1
@ lag1980、私はあなたができると思います:hashtable.get((int)(Math.random()*hashtable.size()));
CMR

3
うーん、私はあなたがそのような要素を取得することを可能にするハッシュテーブルを知りません、そしてもしあれば、これが一定の時間の操作になるとは想像できません。私はどちらかの点で間違っていると証明されることに興味があります。
ギルダー

@ lag1980 ... Clojureのベクトルが「一定時間」であるのと同じように、一定の時間で簡単にそれを行うことができます。log32(N)は、Nの可能な値が、最大のlog32()値が... 7のようなもので、事実上一定の時間です。
Charles Duffy

「配列対応リスト」とは、配列ですか?
ヘンガメ2015年

5

彼らはおそらく賢い解決策を探しているので、あなたはこれが気に入らないかもしれませんが、時にはあなたの銃に固執するのにお金がかかります... ハッシュテーブルはすでに要件を満たしています -おそらく他のものよりも全体的に優れています時間、および他のソリューションへのさまざまな妥協により)。

トリッキーな要件は、「ランダムな要素」の選択です。ハッシュテーブルでは、そのような要素をスキャンまたはプローブする必要があります。

クローズドハッシュ/オープンアドレス指定の場合、特定のバケットが占有される可能性は size() / capacity()、これは通常、ハッシュテーブルの実装によって一定の乗法範囲に保たれます(たとえば、テーブルは、現在のコンテンツよりも1.2x大きくなることがあります)パフォーマンス/メモリ調整に応じて〜10xまで)。これは、平均して1.2から10のバケットを検索することを期待できることを意味します-コンテナーの合計サイズとは完全に無関係です。償却済みO(1)。

私は2つの単純なアプローチを想像できます(さらに多くの手間のかかるアプローチ)。

  • ランダムなバケットから線形検索

    • 「--AC ----- B - D」たala空/値保持バケットを考える:あなたができる Bが好まれているのこれ以上の確率を持っていたので、選択は、それがBを好むにもかかわらず、公正である最初の「ランダム」と言います他の要素よりも、同じ値を使用して繰り返し「ランダム」選択を行っている場合、明らかにBを繰り返し優先することは望ましくない可能性があります(ただし、質問では何も確率を要求しません)。
  • ランダムなバケットを繰り返して、入力されたバケットが見つかるまで繰り返してください

    • 「上記の」capacity()/ size()の平均バケットにアクセス(ただし、上記のように)-乱数の生成は比較的高価であるため、実際にはより高価であり、無限にありそうもない最悪の場合の動作では、無限に悪い...
      • より速く妥協するには、ランダムに選択された最初のバケットから事前に生成されたランダムオフセットのリストを使用し、それらをバケットカウントに%-入れます。

優れたソリューションではありませんが、2番目のインデックス配列を常に維持することによるメモリとパフォーマンスのオーバーヘッドよりも、全体的な妥協点として優れている可能性があります。


3

最善の解決策は、おそらくハッシュテーブル+配列です。これは非常に高速で決定論的です。

しかし、最も低い評価の回答(ハッシュテーブルを使用するだけ!)も実際には素晴らしいです。

  • 再ハッシュ付きのハッシュテーブル、または新しいバケット選択(つまり、バケットごとに1つの要素、リンクリストなし)
  • getRandom()は、空になるまでランダムなバケットを繰り返し選択しようとします。
  • フェイルセーフとして、おそらくgetRandom()として、N(要素数)の試行が失敗した後、ランダムなインデックスiを[0、N-1]から選択し、ハッシュテーブルを直線的に通過して#i番目の要素を選択します。

「無限ループの可能性」があるので、これは気に入らないかもしれません。私は非常に賢い人にもこの反応があるのを見てきましたが、それは間違っています!ありそうもない出来事起こりません。

この特定の動作を確立するのは難しくない、疑似ランダムソースの適切な動作と、ハッシュテーブルが常に少なくとも20%満たされていると仮定すると、次のことが簡単にわかります。

getRandom()が1000回以上試行する必要があることは決してありません。ただ、決してありません。実際、このようなイベントの確率は0.8 ^ 1000、つまり10 ^ -97です。つまり、10億回のイベントで1回のチャンスを得るには、これを10 ^ 88回繰り返す必要があります。このプログラムが太陽が死ぬまで人類のすべてのコンピュータでフルタイムで実行されていたとしても、これは決して起こりません。


1
値を持つランダムバケットを選択することを継続的に選択した場合、ランダム要素を選択している間、地球上での最悪のケースはO(1)になります
Balaji Boggaram Ramanarayan

@ user1147505-どこでこの番号を取得しましたか:「0.8 ^ 1000」?
ヘンガメ

どのようにしてこれに到達しましたか:「ハッシュテーブルは常に少なくとも20%いっぱいです」
ヘンガメ

ランダムなバケツを選ぶことができるメソッドを書いていただけませんか?
Hengameh

3

この質問では、2つのデータ構造を使用します

  • HashMap
  • ArrayList / Array / Double LinkedList。

手順:-

  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. 検索:-キーとしてのxについては、HashMapを参照してください。-時間の複雑さ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#ソリューションです。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;

        }
    }
}

番号が重複している場合、これが機能するかどうかはわかりません。
AlexIIP 2016年

@guildnerは質問のコメントに重複がないと想定しているため、重複は処理しません。複製が追加された場合、ArgumentException「同じキーを持つアイテムはすでに追加されています」というメッセージが表示されます。(基になるインデックスディクショナリから)スローされます。
Scott Lerch、2016年

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 ++には答えがないので、ここでは2セントです。

#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>System.Linq拡張メソッドElementAt()を使用して、KeyValuePair<TKey,TValue>要素が格納されている動的配列にインデックスを付けることができるため、ジェネリックはHashtableよりも優れています。

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;
}

ただし、私の知る限りでは、Hashtable(またはそのディクショナリの子孫)は、この問題の実際の解決策ではありません。これは、Put()が償却できるのはO(1)だけであり、真のO(1)ではないためです。 )動的サイズ変更境界で。

この問題の実際の解決策はありますか?私が考えることができるのは、Dictionary / Hashtableの初期容量を必要以上に桁違いに指定すると、サイズを変更する必要がないため、O(1)演算が得られるということです。


ハッシュテーブルとは何かに非常に厳しい場合は、O(N)のサイズ変更は避けられません。一部の実装は、サイズ変更のコストを削減するために妥協します-たとえば、2倍のサイズを追加しながら既存のテーブルを保持するか、既存のテーブルのサイズを変更しようとします(ページ境界に仮想アドレス空間とテーブルサイズを注意深く配置した後、コピーが必要です。これには、new / malloc memではなくメモリマップが必要になる場合があります)。次に、要素マイグレーションロジックを使用して、より小さな領域にフォールバックする前に、より大きな領域をシークします(インプレースモデルではより厳密に変更します)。
Tony Delroy、2013年

0

私はアノンに同意します。同等の公平性を持つランダム要素を取得する必要がある最後の要件を除いて、他のすべての要件は、単一のハッシュベースのDSを使用することでのみ対処できます。これにはJavaでHashSetを選択します。要素のハッシュコードの剰余は、O(1)時間での基になる配列のインデックスnoを与えます。これを追加、削除、および操作に使用できます。


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 2013
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.