Struttura dati: inserisci, rimuovi, contiene, ottieni elementi casuali, tutto in O (1)


95

Mi è stato dato questo problema in un'intervista. Come avresti risposto?

Progettare una struttura dati che offra le seguenti operazioni nel tempo O (1):

  • inserire
  • rimuovere
  • contiene
  • ottieni un elemento casuale

Possiamo assumere ulteriori restrizioni sul tipo di dati? come se non ci fossero duplicati, ecc.
Sanjeevakumar Hiremath

Certo, nessun duplicato, puoi persino usare strutture dati incorporate in un linguaggio come java o c #.
guildner

1
Prendo atto che non ci sono specifiche relative a: ordinato / non ordinato
Charles Duffy

7
So che a questo post è stata data una risposta, tuttavia per me avrebbe più senso se volessero che tu fornissi o (1) un accesso casuale invece di ottenere un elemento casuale.
ramsinb

Hai trovato la soluzione corretta per questo?
Balaji Boggaram Ramanarayan

Risposte:


143

Si consideri una struttura dati composta da una tabella hash H e un array A. Le chiavi della tabella hash sono gli elementi nella struttura dati ei valori sono le loro posizioni nell'array.

  1. insert (value): aggiunge il valore all'array e sia i il suo indice in A. Poni H [value] = i.
  2. remove (value): sostituiremo la cella che contiene il valore in A con l'ultimo elemento in A. sia d l'ultimo elemento dell'array A all'indice m. sia H [valore], l'indice nell'array del valore da rimuovere. Imposta A [i] = d, H [d] = i, diminuisci la dimensione dell'array di uno e rimuovi il valore da H.
  3. contiene (valore): restituisce H. contiene (valore)
  4. getRandomElement (): let r = random (dimensione corrente di A). restituire A [r].

poiché l'array deve aumentare automaticamente le dimensioni, verrà ammortizzato O (1) per aggiungere un elemento, ma immagino che sia OK.


Questo è vicino a quello che avevo, ma mi mancava l'uso degli elementi stessi come chiavi ... Sapevo di essere vicino, ma questo mi inchioda davvero sulla testa!
guildner

È interessante che ho ricevuto questa domanda sullo schermo di un telefono Google e dopo un po 'di fatica sono rimasto bloccato alla stessa soluzione. Ho rovinato un po 'un'implementazione e l'ho assegnata al secondo schermo del telefono.
Andrey Talnikov

APpendi valore all'array: com'è O (1)?
Balaji Boggaram Ramanarayan

4
@aamadmi - beh, in Java immagino che dovrebbe. Nello pseudo-codice, contiene dovrebbe funzionare bene :)
r0u1i

4
Perché è richiesto un array, perché non possiamo usare hashmap.
Ankit Zalani

22

La ricerca O (1) implica una struttura dati con hash .

A confronto:

  • O (1) inserire / eliminare con la ricerca O (N) implica un elenco collegato.
  • O (1) insert, O (N) delete e O (N) lookup implicano un elenco supportato da array
  • O (logN) insert / delete / lookup implica un albero o un heap.

Questo è un inizio, ma per quanto riguarda l'ultimo requisito? Puoi ottenere un elemento casuale (con uguale probabilità per ogni elemento nella struttura dati) da una struttura dati con hash?
guildner

1
@ lag1980, immagino che tu possa:hashtable.get((int)(Math.random()*hashtable.size()));
CMR

3
Hmmm, non conosco nessuna tabella hash che ti consenta di ottenere un elemento del genere e, se ce ne sono, non riesco a immaginare che sarebbe un'operazione a tempo costante. Sarei interessato a essere smentito in entrambi i casi.
guildner

@ lag1980 ... potresti facilmente farlo in tempo costante nello stesso modo in cui i vettori di Clojure sono "tempo costante" - log32 (N) quando i possibili valori di N sono vincolati dal tuo hardware in modo tale che il valore log32 () più grande possibile sia ... qualcosa come 7, che è effettivamente un tempo costante.
Charles Duffy

Con "elenco supportato da array" intendi: array?
Hengameh

5

Potrebbe non piacerti, perché probabilmente stanno cercando una soluzione intelligente, ma a volte vale la pena attenersi alle tue pistole ... Una tabella hash soddisfa già i requisiti , probabilmente nel complesso meglio di qualsiasi altra cosa (anche se ovviamente in una costante ammortizzata tempo e con diversi compromessi rispetto ad altre soluzioni).

Il requisito difficile è la selezione di "elementi casuali": in una tabella hash, dovresti scansionare o sondare per un tale elemento.

Per l'hashing chiuso / indirizzamento aperto, la possibilità che un determinato bucket sia occupato è size() / capacity() , ma in modo cruciale questo è tipicamente mantenuto in un intervallo moltiplicativo costante da un'implementazione della tabella hash (ad esempio la tabella può essere mantenuta più grande del suo contenuto corrente diciamo 1.2x a ~ 10x a seconda delle prestazioni / ottimizzazione della memoria). Ciò significa che in media possiamo aspettarci di cercare da 1,2 a 10 bucket, totalmente indipendente dalla dimensione totale del container; ammortizzato O (1).

Posso immaginare due semplici approcci (e moltissimi altri poco pratici):

  • cerca linearmente da un bucket casuale

    • considera i bucket vuoti / detentori di valore ala "--AC ----- B - D": puoi dire che la prima selezione "casuale" è giusta anche se favorisce B, perché B non aveva più probabilità di essere favorito rispetto agli altri elementi, ma se stai facendo ripetute selezioni "casuali" usando gli stessi valori, allora chiaramente avere B ripetutamente favorito potrebbe essere indesiderabile (nulla nella domanda richiede anche probabilità però)
  • prova ripetutamente i bucket casuali finché non ne trovi uno popolato

    • "solo" capacità () / dimensioni () segmenti medi visitati (come sopra) - ma in termini pratici più costosi perché la generazione di numeri casuali è relativamente costosa e un comportamento nel caso peggiore infinitamente negativo se infinitamente improbabile ...
      • un compromesso più veloce sarebbe utilizzare un elenco di offset casuali pre-generati dal bucket selezionato casualmente iniziale, inserendoli in% nel conteggio dei bucket

Non è un'ottima soluzione, ma può comunque essere un compromesso complessivo migliore rispetto ai costi generali di memoria e prestazioni derivanti dalla manutenzione di un secondo array di indici in ogni momento.


3

La soluzione migliore è probabilmente la tabella hash + array, è davvero veloce e deterministica.

Ma anche la risposta con il punteggio più basso (usa solo una tabella hash!) È davvero eccezionale!

  • tabella hash con re-hashing o nuova selezione di bucket (ad esempio un elemento per bucket, nessun elenco collegato)
  • getRandom () tenta ripetutamente di scegliere un bucket casuale finché non è vuoto.
  • come fail-safe, forse getRandom (), dopo N (numero di elementi) tentativi falliti, sceglie un indice casuale i in [0, N-1] e quindi passa attraverso la tabella hash linearmente e sceglie l'elemento # i-esimo .

Alla gente potrebbe non piacere a causa di "possibili cicli infiniti", e ho visto anche persone molto intelligenti avere questa reazione, ma è sbagliato! Eventi infinitamente improbabili semplicemente non accadono.

Supponendo il buon comportamento della tua fonte pseudo-casuale - che non è difficile da stabilire per questo particolare comportamento - e che le tabelle hash siano sempre piene almeno al 20%, è facile vedere che:

Non accadrà mai che getRandom () debba provare più di 1000 volte. Proprio mai . In effetti, la probabilità di un tale evento è 0,8 ^ 1000, che è 10 ^ -97, quindi dovremmo ripeterlo 10 ^ 88 volte per avere una possibilità su un miliardo che accada una volta. Anche se questo programma fosse in esecuzione a tempo pieno su tutti i computer dell'umanità fino alla morte del Sole, ciò non accadrà mai .


1
Se scegli continuamente di scegliere un secchio casuale che ha un valore, come
diavolo

@ user1147505 - dove hai preso questo numero: "0,8 ^ 1000"?
Hengameh

Come sei arrivato a questo: "le tabelle hash sono sempre piene almeno al 20%"
Hengameh

Potresti scrivere il metodo con cui puoi scegliere un secchio casuale?
Hengameh

3

Per questa domanda userò due strutture dati

  • HashMap
  • ArrayList / Array / Double LinkedList.

Passaggi: -

  1. Inserimento: - Controlla se X è già presente in HashMap - Complessità temporale O (1). se non presente, quindi aggiungere alla fine di ArrayList - Complessità temporale O (1). aggiungilo in HashMap anche x come chiave e ultimo Indice come valore - Complessità temporale O (1).
  2. Rimuovi: - Controlla se X è presente in HashMap - Complessità temporale O (1). Se presente, trova il suo indice e rimuovilo da HashMap --Time complex O (1). scambia questo elemento con l'ultimo elemento in ArrayList e rimuovi l'ultimo elemento - Complessità temporale O (1). Aggiorna l'indice dell'ultimo elemento in HashMap - Complessità temporale O (1).
  3. GetRandom: - Genera un numero casuale da 0 all'ultimo indice di ArrayList. restituisce l'elemento ArrayList all'indice casuale generato - Complessità temporale O (1).
  4. Cerca: - Vedi in HashMap x come chiave. - Complessità temporale O (1).

Codice :-

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;

            }
        }
    }

}

- Complessità temporale O (1). - Complessità spaziale O (N).


1

Ecco una soluzione in C # a questo problema che mi è venuta in mente un po 'di tempo fa quando mi è stata posta la stessa domanda. Implementa Aggiungi, Rimuovi, Contiene e Random insieme ad altre interfacce .NET standard. Non che avresti mai bisogno di implementarlo in modo così dettagliato durante un colloquio, ma è bello avere una soluzione concreta da guardare ...

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;

        }
    }
}

Non sono sicuro che funzionerà se hai numeri duplicati.
AlexIIP

Non gestisce i duplicati poiché @guildner ha detto di presumere che non ci siano duplicati nei commenti della domanda. Se viene aggiunto un duplicato e viene visualizzato ArgumentExceptionil messaggio "È già stato aggiunto un elemento con la stessa chiave". verrà lanciato (dall'indice Dictionary sottostante).
Scott Lerch

1

Possiamo usare l'hashing per supportare le operazioni in Θ (1) tempo.

insert (x) 1) Controlla se x è già presente eseguendo una ricerca sulla mappa hash. 2) Se non presente, inserirlo alla fine dell'array. 3) Aggiungi anche la tabella hash, x viene aggiunto come chiave e l'ultimo indice dell'array come indice.

remove (x) 1) Controlla se x è presente eseguendo una ricerca sulla mappa hash. 2) Se presente, trova il suo indice e rimuovilo dalla mappa hash. 3) Scambia l'ultimo elemento con questo elemento nell'array e rimuovi l'ultimo elemento. Lo scambio viene eseguito perché l'ultimo elemento può essere rimosso in tempo O (1). 4) Aggiorna l'indice dell'ultimo elemento nella mappa hash.

getRandom () 1) Genera un numero casuale da 0 all'ultimo indice. 2) Restituisce l'elemento della matrice all'indice generato casualmente.

search (x) Cerca x nella mappa hash.


1

Anche se questo è molto vecchio, ma poiché non c'è risposta in C ++, ecco i miei due centesimi.

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

Ecco un pezzo di codice client per testare la soluzione.

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

In C # 3.0 + .NET Framework 4, un generico Dictionary<TKey,TValue>è persino migliore di un Hashtable perché è possibile utilizzare il System.Linqmetodo di estensione ElementAt()per indicizzare nell'array dinamico sottostante in cui KeyValuePair<TKey,TValue>sono archiviati gli elementi:

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

Tuttavia, per quanto ne so, un Hashtable (o la sua progenie del Dizionario) non è una vera soluzione a questo problema perché Put () può essere ammortizzato solo O (1), non vero O (1), perché è O (N ) al confine di ridimensionamento dinamico.

C'è una vera soluzione a questo problema? Tutto quello a cui riesco a pensare è che se specifichi una capacità iniziale di Dictionary / Hashtable un ordine di grandezza oltre quello che prevedi di aver mai bisogno, allora ottieni operazioni O (1) perché non hai mai bisogno di ridimensionare.


Se sei molto severo su cosa sia una tabella hash, il ridimensionamento O (N) è inevitabile. Alcune implementazioni compromettono la riduzione dei costi di ridimensionamento, ad esempio mantenendo la tabella esistente aggiungendo un secondo del doppio delle dimensioni o cercando di ridimensionare la tabella esistente sul posto (dopo aver organizzato con cura lo spazio degli indirizzi virtuali e le dimensioni della tabella sui confini della pagina, quindi no è richiesta la copia, che potrebbe richiedere mappe di memoria piuttosto che new / malloc mem), quindi cercare nella nuova area più ampia prima di ricadere su quella più piccola (in un modello sul posto modding più strettamente), con logica di migrazione degli elementi.
Tony Delroy

0

Sono d'accordo con Anon. Ad eccezione dell'ultimo requisito in cui è richiesto ottenere un elemento casuale con la stessa correttezza, tutti gli altri requisiti possono essere risolti solo utilizzando un singolo DS basato su hash. Sceglierò HashSet per questo in Java. Il modulo del codice hash di un elemento mi darà il numero di indice dell'array sottostante in tempo O (1). Posso usarlo per aggiungere, rimuovere e contenere operazioni.


0

Non possiamo farlo usando HashSet di Java? Per impostazione predefinita, fornisce insert, del, search all in O (1). Per getRandom possiamo usare l'iteratore di Set che comunque fornisce un comportamento casuale. Possiamo semplicemente iterare il primo elemento dell'insieme senza preoccuparci del resto degli elementi

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

Perché non usiamo epoch% arraysize per trovare un elemento casuale. Trovare la dimensione dell'array è O (n) ma la complessità ammortizzata sarà O (1).


-3

Penso che possiamo usare doppiamente l'elenco dei collegamenti con la tabella hash. chiave sarà elemento e il suo valore associato sarà nodo in doppiamente linklist.

  1. insert (H, E): inserisce il nodo in doppiamente linklist e inserisce come H [E] = nodo; O (1)
  2. cancella (H, E): ottieni l'indirizzo del nodo da H (E), vai al precedente di questo nodo ed elimina e rendi H (E) NULL, quindi O (1)
  3. contiene (H, E) e getRandom (H) sono ovviamente O (1)

Questo non ha senso.
innosam
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.