Estructura de datos: insertar, eliminar, contener, obtener elemento aleatorio, todo en O (1)

94

Me dieron este problema en una entrevista. ¿Cómo habrías respondido?

Diseñe una estructura de datos que ofrezca las siguientes operaciones en O (1) tiempo:

  • insertar
  • eliminar
  • contiene
  • obtener elemento aleatorio
gremio
fuente
¿Podemos asumir restricciones adicionales sobre el tipo de datos? como si no hubiera duplicados, etc.
Sanjeevakumar Hiremath
Claro, sin duplicados, incluso puede usar estructuras de datos integradas en un lenguaje como java o c #.
guildner
1
Observo que no hay ninguna especificación sobre: ​​pedido / desordenado
Charles Duffy
7
Sé que esta publicación ha sido respondida, sin embargo, para mí tendría más sentido si quisieran que brindes o (1) acceso aleatorio en lugar de obtener un elemento aleatorio.
ramsinb
¿Encontraste la solución correcta para esto?
Balaji Boggaram Ramanarayan

Respuestas:

142

Considere una estructura de datos compuesta por una tabla hash H y una matriz A. Las claves de la tabla hash son los elementos de la estructura de datos y los valores son sus posiciones en la matriz.

  1. insertar (valor): agregue el valor a la matriz y sea i su índice en A. Establezca H [valor] = i.
  2. eliminar (valor): vamos a reemplazar la celda que contiene valor en A con el último elemento en A. sea d el último elemento en la matriz A en el índice m. sea ​​yo H [valor], el índice en la matriz del valor a eliminar. Establezca A [i] = d, H [d] = i, disminuya el tamaño de la matriz en uno y elimine el valor de H.
  3. contiene (valor): devuelve H.contains (valor)
  4. getRandomElement (): sea r = aleatorio (tamaño actual de A). devuelve A [r].

dado que la matriz necesita aumentar automáticamente de tamaño, se amortizará O (1) para agregar un elemento, pero supongo que está bien.

r0u1i
fuente
Esto está cerca de lo que tenía, pero extrañaba el uso de los elementos en sí mismos como teclas ... ¡Sabía que estaba cerca, pero esto realmente lo clava en la cabeza!
gremio
Es interesante que obtuve esta pregunta en la pantalla de un teléfono de Google y después de algunos problemas me quedé con la misma solución. Arruiné un poco una implementación y la asigné a la segunda pantalla del teléfono.
Andrey Talnikov
APpend valor a la matriz: ¿cómo es O (1)?
Balaji Boggaram Ramanarayan
4
@aamadmi - bueno, en Java supongo que debería. En pseudocódigo, el contenido debería funcionar bien :)
r0u1i
4
Por qué se requiere una matriz, por qué no podemos usar hashmap.
Ankit Zalani
22

La búsqueda de O (1) implica una estructura de datos hash .

En comparación:

  • O (1) insertar / eliminar con búsqueda O (N) implica una lista vinculada.
  • O (1) insertar, O (N) eliminar y la búsqueda O (N) implica una lista respaldada por una matriz
  • O (logN) insertar / eliminar / buscar implica un árbol o montón.
Luego
fuente
Eso es un comienzo, pero ¿qué pasa con el último requisito? ¿Puede obtener un elemento aleatorio (con la misma probabilidad para cada elemento en la estructura de datos) de una estructura de datos hash?
guildner
1
@ lag1980, supongo que puedes:hashtable.get((int)(Math.random()*hashtable.size()));
CMR
3
Hmmm, no conozco ninguna tabla hash que te permita obtener un elemento como ese, y si hay alguno, no puedo imaginar que esta sea una operación de tiempo constante. Me interesaría que se demuestre que estoy equivocado en cualquiera de los dos aspectos.
guildner
@ lag1980 ... podría hacerlo fácilmente en tiempo constante de la misma manera que los vectores de Clojure son "tiempo constante" - log32 (N) cuando los valores posibles de N están restringidos por su hardware de modo que el valor log32 () más grande posible es ... algo así como 7, que es efectivamente un tiempo constante.
Charles Duffy
Por "lista respaldada por matriz" te refieres a: matriz?
Hengameh
5

Puede que esto no le guste, porque probablemente estén buscando una solución inteligente, pero a veces vale la pena ceñirse a sus armas ... Una tabla hash ya satisface los requisitos , probablemente mejor en general que cualquier otra cosa (aunque obviamente en constante amortizada tiempo, y con compromisos diferentes a otras soluciones).

El requisito que es complicado es la selección del "elemento aleatorio": en una tabla hash, necesitaría escanear o sondear dicho elemento.

Para el hash cerrado / direccionamiento abierto, la posibilidad de que se ocupe un cubo determinado es size() / capacity(), pero lo más importante es que esto se mantiene en un rango multiplicativo constante mediante una implementación de tabla hash (por ejemplo, la tabla puede mantenerse más grande que su contenido actual, por ejemplo, 1.2x a ~ 10x dependiendo del rendimiento / ajuste de la memoria). Esto significa que, en promedio, podemos esperar buscar entre 1,2 y 10 cubos, totalmente independiente del tamaño total del contenedor; amortizado O (1).

Puedo imaginar dos enfoques simples (y muchos más complicados):

  • buscar linealmente desde un depósito aleatorio

    • considere baldes vacíos / de retención de valor ala "--AC ----- B - D": puede decir que la primera selección "aleatoria" es justa aunque favorece a B, porque B no tenía más probabilidades de ser favorecido que los otros elementos, pero si está haciendo selecciones "aleatorias" repetidas usando los mismos valores, entonces claramente tener B favorecido repetidamente puede ser indeseable (nada en la pregunta exige incluso probabilidades)
  • intente cubos aleatorios repetidamente hasta que encuentre uno poblado

    • "solo" capacidad () / tamaño () cubos promedio visitados (como arriba) - pero en términos prácticos más costoso porque la generación de números aleatorios es relativamente costosa, e infinitamente mala aunque infinitamente improbable comportamiento en el peor de los casos ...
      • un compromiso más rápido sería usar una lista de compensaciones aleatorias pregeneradas del grupo inicial seleccionado aleatoriamente, comparándolas con el recuento de grupos

No es una gran solución, pero aún puede ser un mejor compromiso general que los gastos generales de memoria y rendimiento de mantener una segunda matriz de índices en todo momento.

Tony Delroy
fuente
3

La mejor solución es probablemente la tabla hash + matriz, es realmente rápida y determinista.

Pero la respuesta con la calificación más baja (¡solo use una tabla hash!) ¡También es excelente!

  • tabla hash con re-hash o nueva selección de cubos (es decir, un elemento por cubeta, sin listas vinculadas)
  • getRandom () intenta repetidamente elegir un depósito aleatorio hasta que está vacío.
  • como a prueba de fallas, tal vez getRandom (), después de N (número de elementos) intentos fallidos, elige un índice aleatorio i en [0, N-1] y luego recorre la tabla hash linealmente y elige el elemento # i-ésimo .

Puede que a la gente no le guste esto debido a "posibles bucles infinitos", y he visto a personas muy inteligentes que también tienen esta reacción, ¡pero está mal! Los eventos infinitamente improbables simplemente no suceden.

Suponiendo el buen comportamiento de su fuente pseudoaleatoria, que no es difícil de establecer para este comportamiento en particular, y que las tablas hash siempre están llenas al menos en un 20%, es fácil ver que:

Será Nunca suceder que getRandom () tiene que probar más de 1000 veces. Simplemente nunca . De hecho, la probabilidad de que se produzca un evento de este tipo es 0,8 ^ 1000, que es 10 ^ -97, por lo que tendríamos que repetirlo 10 ^ 88 veces para tener una posibilidad entre mil millones de que ocurra una vez. Incluso si este programa se ejecutara a tiempo completo en todas las computadoras de la humanidad hasta que el Sol muera, esto nunca sucederá.

usuario1147505
fuente
1
Si continuamente elige elegir un cubo aleatorio que tiene valor, ¿cómo diablos es el peor de los casos que conduce a O (1) mientras elige un elemento aleatorio
Balaji Boggaram Ramanarayan
@ user1147505: ¿de dónde sacaste este número: "0.8 ^ 1000"?
Hengameh
¿Cómo llegó a esto? "Las tablas hash siempre están llenas al menos en un 20%"
Hengameh
¿Podría escribir el método con el que puede elegir un cubo al azar?
Hengameh
3

Para esta pregunta utilizaré dos estructuras de datos

  • HashMap
  • ArrayList / Array / Double LinkedList.

Pasos: -

  1. Inserción: - Compruebe si X ya está presente en HashMap - Complejidad de tiempo O (1). si no está presente, agregue al final de ArrayList - Complejidad de tiempo O (1). agréguelo en HashMap también x como clave y último índice como valor - Complejidad de tiempo O (1).
  2. Eliminar: - Compruebe si X está presente en HashMap - Complejidad de tiempo O (1). Si está presente, busque su índice y elimínelo de HashMap --Time Complex O (1). intercambie este elemento con el último elemento en ArrayList y elimine el último elemento - Complejidad de tiempo O (1). Actualice el índice del último elemento en HashMap - Complejidad de tiempo O (1).
  3. GetRandom: - Genera un número aleatorio desde 0 hasta el último índice de ArrayList. devuelve el elemento ArrayList en un índice aleatorio generado - Complejidad temporal O (1).
  4. Buscar: - Vea en HashMap para x como clave. - Complejidad temporal O (1).

Código: -

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;

            }
        }
    }

}

- Complejidad temporal O (1). - Complejidad espacial O (N).

Cabeza y cola
fuente
1

Aquí hay una solución de C # para ese problema que se me ocurrió hace un tiempo cuando me hicieron la misma pregunta. Implementa Agregar, Eliminar, Contiene y Aleatorio junto con otras interfaces .NET estándar. No es que necesite implementarlo con tanto detalle durante una entrevista, pero es bueno tener una solución concreta para analizar ...

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;

        }
    }
}
Scott Lerch
fuente
No estoy seguro de que esto funcione si tiene números duplicados.
AlexIIP
No maneja duplicados ya que @guildner dijo que asume que no hay duplicados en los comentarios de la pregunta. Si se agrega un duplicado ArgumentExceptioncon el mensaje "Ya se agregó un elemento con la misma clave". se lanzará (desde el diccionario de índice subyacente).
Scott Lerch
1

Podemos usar hash para respaldar operaciones en Θ (1) tiempo.

insert (x) 1) Verifique si x ya está presente haciendo una búsqueda de mapa hash. 2) Si no está presente, insértelo al final de la matriz. 3) Agregue también la tabla hash, x se agrega como clave y el último índice de matriz como índice.

remove (x) 1) Verifique si x está presente haciendo una búsqueda de mapa hash. 2) Si está presente, busque su índice y elimínelo del mapa hash. 3) Cambie el último elemento con este elemento en la matriz y elimine el último elemento. El intercambio se realiza porque el último elemento se puede eliminar en O (1) tiempo. 4) Actualizar el índice del último elemento en el mapa hash.

getRandom () 1) Genera un número aleatorio desde 0 hasta el último índice. 2) Devuelve el elemento de la matriz en el índice generado aleatoriamente.

search (x) Realiza una búsqueda de x en el mapa hash.

Shobhit Raj
fuente
1

Aunque esto es muy antiguo, pero como no hay respuesta en C ++, aquí está mi granito de arena.

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

Aquí hay un fragmento de código de cliente para probar la solución.

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;
}
Antítesis
fuente
0

En C # 3.0 + .NET Framework 4, un genérico Dictionary<TKey,TValue>es incluso mejor que un Hashtable porque puede usar el System.Linqmétodo de extensión ElementAt()para indexar en la matriz dinámica subyacente donde KeyValuePair<TKey,TValue>se almacenan los elementos:

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

Sin embargo, hasta donde yo sé, una tabla hash (o su progenie del diccionario) no es una solución real a este problema porque Put () solo se puede amortizar O (1), no O (1) verdadero, porque es O (N ) en el límite de cambio de tamaño dinámico.

¿Existe una solución real a este problema? Todo lo que puedo pensar es que si especifica una capacidad inicial de Dictionary / Hashtable en un orden de magnitud más allá de lo que anticipa que necesitará, entonces obtendrá operaciones O (1) porque nunca necesita cambiar el tamaño.

BaltoStar
fuente
Si es muy estricto con lo que es una tabla hash, entonces el cambio de tamaño O (N) es inevitable. Sin embargo, algunas implementaciones se comprometen a reducir el costo de cambiar el tamaño, por ejemplo, reteniendo la tabla existente mientras se agrega un segundo del doble del tamaño, o tratando de cambiar el tamaño de la tabla existente en su lugar (después de organizar cuidadosamente el espacio de direcciones virtuales y los tamaños de las tablas en los límites de la página para que no se requiere copiar, lo que puede requerir mapas de memoria en lugar de new / malloc mem), luego buscar en la nueva área más grande antes de recurrir a la más pequeña (en un modelo in situ modificando más estrictamente), con lógica de migración de elementos.
Tony Delroy
0

Estoy de acuerdo con Anon. Excepto por el último requisito, donde se requiere obtener un elemento aleatorio con la misma imparcialidad, todos los demás requisitos se pueden abordar solo utilizando un único DS basado en Hash. Elegiré HashSet para esto en Java. El módulo de código hash de un elemento me dará el número de índice de la matriz subyacente en el tiempo O (1). Puedo usar eso para agregar, eliminar y contiene operaciones.

Ayaskant
fuente
0

¿No podemos hacer esto usando HashSet of Java? Proporciona insertar, eliminar, buscar todo en O (1) de forma predeterminada. Para getRandom podemos hacer uso del iterador de Set que de todos modos da un comportamiento aleatorio. Podemos simplemente iterar el primer elemento del conjunto sin preocuparnos por el resto de los elementos

public void getRandom(){
    Iterator<integer> sitr = s.iterator();
    Integer x = sitr.next();    
    return x;
}
usuario1108687
fuente
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`
    }
}
Ahmed Taha
fuente
-2

¿Por qué no usamos epoch% arraysize para encontrar un elemento aleatorio? Encontrar el tamaño de la matriz es O (n) pero la complejidad amortizada será O (1).

pranay yadav
fuente
-3

Creo que podemos usar doble lista de enlaces con tabla hash. la clave será elemento y su valor asociado será nodo en doble lista de enlaces.

  1. insertar (H, E): insertar el nodo en la lista de enlaces doble y hacer la entrada como H [E] = nodo; O (1)
  2. eliminar (H, E): obtener la dirección del nodo por H (E), ir al anterior de este nodo y eliminar y hacer H (E) como NULL, entonces O (1)
  3. contiene (H, E) y getRandom (H) son obviamente O (1)
Abhinav Jaiswal
fuente
Esto no tiene sentido.
innosam