Структура даних: вставити, видалити, містити, отримати випадковий елемент, все в O (1)


94

Мені цю проблему дали в інтерв'ю. Як би ти відповів?

Створіть структуру даних, яка пропонує наступні операції за O (1) час:

  • вставити
  • видалити
  • містить
  • отримати випадковий елемент

Чи можна припустити додаткові обмеження щодо типу даних? начебто немає дублікатів тощо
Sanjeevakumar Hiremath

Звичайно, без дублікатів, ви навіть можете використовувати вбудовані структури даних такою мовою, як java або c #.
guildner

1
Зауважу, що немає жодної специфікації щодо: замовлений / не упорядкований
Чарльз Даффі

7
Я знаю, що на цю посаду відповіли, але для мене було б більше сенсу, якби вони хотіли, щоб ви дали (1) випадковий доступ, а не отримати випадковий елемент.
ramsinb

Ви знайшли для цього правильне рішення?
Баладжі Боггарам Раманараян

Відповіді:


142

Розглянемо структуру даних, що складається з хеш-таблиці H та масиву A. Ключі хеш-таблиці є елементами в структурі даних, а значення - їх положеннями в масиві.

  1. insert (value): додайте значення до масиву та дозвольте i бути його індексом у A. Set H [value] = i.
  2. remove (value): Ми збираємось замінити комірку, що містить значення в A, останнім елементом в A. нехай d буде останнім елементом масиву A з індексом m. нехай я буде H [значення], індекс у масиві значення, яке потрібно видалити. Встановіть A [i] = d, H [d] = i, зменшіть розмір масиву на одиницю та видаліть значення з H.
  3. містить (значення): return H.contains (значення)
  4. getRandomElement (): нехай r = випадковий (поточний розмір A). повернути A [r].

оскільки масив потребує автоматичного збільшення розміру, амортизувати O (1), щоб додати елемент, але я думаю, це нормально.


Це близько до того, що я мав, але я пропустив використання самих елементів як клавіш .... Я знав, що я поруч, але це справді прибиває це до голови!
guildner

Цікаво, що я потрапив до цього питання на екрані телефону Google і після певних зусиль дотримувався того самого рішення. Я трохи накрутив реалізацію та призначив на другий екран телефону.
Андрій Тальников

Додайте значення до масиву: як це O (1)?
Баладжі Боггарам Раманараян

4
@aamadmi - ну, на Java я думаю, що так і слід. У псевдо-коді, містити, має працювати просто чудово :)
r0u1i

4
Чому потрібен масив, чому ми не можемо використовувати хешмап?
Анкіт Залані

22

Облік O (1) передбачає хешовану структуру даних .

Для порівняння:

  • O (1) вставка / видалення за допомогою пошуку O (N) передбачає пов'язаний список.
  • 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,2x до ~ 10x в залежності від продуктивності / налаштування пам'яті). Це означає, що в середньому ми можемо розраховувати на пошук від 1,2 до 10 відер - абсолютно незалежних від загального розміру контейнера; амортизований O (1).

Я можу уявити два простих підходи (і набагато більш хитрий):

  • шукати лінійно з випадкового відра

    • розглядайте порожні / відра з утриманням цінності ала "--AC ----- B - D": ви можете сказати, що перший "випадковий" вибір справедливий, хоча він надає перевагу B, тому що B більше не було ймовірності прихильності ніж інші елементи, але якщо ви робите неодноразові "випадкові" вибори з використанням одних і тих же значень, то, очевидно, що B багаторазово надається перевагу, може бути небажаним (хоча нічого в питанні не вимагає навіть ймовірностей)
  • спробуйте випадкові відра кілька разів, поки не знайдете заселене

    • "лише" середні відра, що відвідуються (як вище), - але в практичному плані дорожчі, оскільки генерація випадкових чисел порівняно дорога і нескінченно погана, якщо нескінченно неправдоподібна поведінка в гіршому випадку ...
      • швидшим компромісом було б використання списку заздалегідь згенерованих випадкових зсувів від початкового випадково вибраного сегмента,% -увівши їх до кількості сегментів

Не чудове рішення, але все-таки може бути кращим загальним компромісом, ніж накладні витрати на пам’ять та продуктивність, щоб постійно підтримувати другий масив індексів.


3

Найкраще рішення - це, мабуть, хеш-таблиця + масив, це дуже швидко і детерміновано.

Але відповідь з найнижчою оцінкою (просто скористайтесь хеш-таблицею!) Насправді теж чудова!

  • хеш-таблиця з повторним хешуванням або новим вибором відра (тобто один елемент на відро, відсутні пов'язані списки)
  • getRandom () кілька разів намагається вибрати випадкове відро, поки воно не порожнє.
  • як невдала спроба, можливо, getRandom (), після N (кількість елементів) невдалих спроб, вибирає випадковий індекс i в [0, N-1], а потім лінійно проходить хеш-таблицю і вибирає # i-й елемент .

Людям це може не сподобатися через "можливих нескінченних циклів", і я бачив, що дуже розумні люди теж мають цю реакцію, але це неправильно! Нескінченно маловірогідні події просто не трапляються.

Якщо припустити хорошу поведінку вашого псевдовипадкового джерела - що не важко встановити для цієї конкретної поведінки - і що хеш-таблиці завжди наповнені щонайменше на 20%, легко помітити, що:

Він не буде ніколи так статися , що getRandom () повинен спробувати більш ніж в 1000 разів. Просто ніколи . Дійсно, ймовірність такої події дорівнює 0,8 ^ 1000, що становить 10 ^ -97 - тому нам доведеться повторити її 10 ^ 88 разів, щоб мати один шанс у мільярд її колись трапитися. Навіть якщо ця програма працюватиме на повний робочий день на всіх комп’ютерах людства, поки Сонце не вмирає, цього ніколи не станеться.


1
Якщо ви постійно вирішите вибрати випадкове відро, яке має значення, як на землі найгірший випадок,
приведіть

@ user1147505 - звідки ви взяли цей номер: "0,8 ^ 1000"?
Хенгаме

Як ви дійшли до цього: "хеш-таблиці завжди наповнені як мінімум на 20%"
Hengameh

Чи можете ви написати, будь ласка, метод, за допомогою якого ви можете вибрати випадкове відро?
Hengameh

3

Для цього питання я буду використовувати дві структури даних

  • 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 і видаліть останній елемент --Time складність 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

Він не обробляє дублікати, оскільки @guildner припускає, що в коментарях до цього питання немає дублікатів. Якщо до дубліката додано повідомлення ArgumentExceptionіз повідомленням "Елемент із тим самим ключем уже додано." буде кинуто (із нижнього словника індексу).
Скотт Лерч

1

Ми можемо використовувати хешування для підтримки операцій у Θ (1) час.

insert (x) 1) Перевірте, чи x вже присутній, зробивши пошук хеш-карти. 2) Якщо немає, то вставити його в кінці масиву. 3) Додайте також у хеш-таблицю, x додається як ключ, а останній масив - як індекс.

delete (x) 1) Перевірте наявність x, зробивши пошук хеш-карти. 2) Якщо він присутній, то знайдіть його індекс і видаліть його з хеш-карти. 3) Замініть останній елемент на цей елемент у масиві та видаліть останній елемент. Заміна робиться тому, що останній елемент можна видалити за O (1) час. 4) Оновити індекс останнього елемента в хеш-карті.

getRandom () 1) Створення випадкового числа від 0 до останнього індексу. 2) Повернути елемент масиву у випадково генерованому індексі.

пошук (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;
}

Однак, наскільки я знаю, Hashtable (або його потомство по словнику) не є реальним рішенням цієї проблеми, оскільки Put () може бути амортизований тільки O (1), а не правда O (1), тому що це O (N ) на межі динамічного розміру.

Чи реально вирішити цю проблему? Все, про що я можу придумати, - це якщо вказати початкову ємність словника / хештеля на порядок більше того, що, як ви очікуєте, коли-небудь знадобиться, тоді ви отримуєте операції O (1), оскільки вам ніколи не потрібно змінювати розмір.


Якщо ви дуже суворо ставитесь до того, що таке хеш-таблиця, то зміни розміру O (N) не уникнути. Деякі реалізації йдуть на компроміс, щоб зменшити вартість зміни розміру - наприклад, утримуючи існуючу таблицю при додаванні секунди подвійного розміру або намагаючись змінити розмір наявної таблиці на місці (після ретельного упорядкування віртуального адресного простору та розмірів таблиць за межами сторінки, так що ні потрібне копіювання, яке може потребувати карт пам'яті, а не new / malloc mem), потім пошук у новій більшої області перед тим, як повернутися назад на меншу (у місцевій моделі шляхом більш чіткого моделювання), з логікою міграції елементів.
Тоні Делрой

0

Я згоден з Аноном. За винятком останньої вимоги, коли отримання випадкового елемента з однаковою справедливістю, всі інші вимоги можуть бути вирішені лише за допомогою одного DS, заснованого на Хеші. Я виберу для цього HashSet на Java. Модуль хеш-коду елемента дасть мені номер індексу базового масиву за час O (1). Я можу використовувати це для операцій додавання, видалення та вмісту.


0

Не можемо це зробити за допомогою HashSet of Java? Він забезпечує вставку, дель, пошук за всіма параметрами 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] = вузол; O (1)
  2. delete (H, E): отримати адресу вузла H (E), перейти до попереднього цього вузла та видалити та зробити H (E) як NULL, так O (1)
  3. містить (H, E) і getRandom (H) заперечно O (1)

Це не має сенсу.
innosam
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.