Эффективная структура данных, поддерживающая Insert, Delete и MostFrequent

14

Предположим, что у нас есть множество D и каждый член D является парой данных и ключей. Нам нужна структура данных, которая бы поддерживала следующие операции:

  • Вставьте (d,k) в D ,
  • Удалить член e (не нужно искать, чтобы найти e , например, e указывает на члена в D ),
  • MostFrequent, который возвращает член eD такой, что e.key - один из самых частых ключей в D (обратите внимание, что самый частый ключ не обязательно должен быть уникальным).

Какова будет эффективная реализация этой структуры данных?

Мое решение - это куча ключей и их частот с приоритетом частот, плюс хеш-таблица, в которой хеш-функция отображает элементы с одинаковым ключом в один и тот же слот в хеш-таблице (с указателями от каждой части к другой).

Это может дать Θ(lgn) для первых двух операций и Θ(1) для третьей (наихудшее время выполнения).

Мне интересно, есть ли более эффективное решение? (или более простое решение с той же эффективностью?)

Кава
источник
Вы можете использовать простое сбалансированное двоичное дерево поиска вместо хеш-таблицы, если хотите.
Джо
Хеш-таблица занимает много лишнего пространства, я бы предложил приоритетную очередь. Это даст вам одинаковую сложность при вставке и удалении, но сложность памяти будет лучше.
Бартош Прзыбыльски
@ Joe, использование BST вместо хеш-таблицы сделает операции MostFrequent менее эффективными, но это может быть разумным компромиссом для памяти.
Каве
2
Если используются только сравнения, по крайней мере один из Insert / MostFrequent должен быть амортизирован из-за нижних границ для проблемы отличимости элементов. Ω(logn)
Арьябхата
1
Есть также несколько интересных структур в потоковой модели. springerlink.com/content/t17nhd9hwwry909p
Джо,

Ответы:

7

В модели вычислений, основанной на сравнении, вы можете реализовать приоритетную очередь, используя кучу Фибоначчи вместо обычной кучи. Это даст вам следующие границы: амортизированное время для вставки и O ( log n ) амортизированное время для операций удаления.O(1)O(logn)

Если вы отойдете от модели, основанной на сравнении, и примете модель оперативной памяти, в которой ключи рассматриваются как двоичные строки, каждая из которых содержится в одном или нескольких машинных словах, вы можете реализовать свою очередь приоритетов в . Действительно, вы можете достичь как для операций вставки, так и удаления O ( o(logn)иO(1)время для операции findMin. Торуп доказал, чтоO(loglogn)O(1)

Если мы можем отсортировать ключей за время S ( n ) на ключ, то мы сможем реализовать очередь с приоритетом, поддерживающую find-min в постоянном времени, и обновления (вставка и удаление) за S ( n ) .nS(n)S(n)

Смотри М. Торуп. Эквивалентность между приоритетными очередями и сортировкой, 2002. в Proc. ФОКС 2002

Так как мы можем сортировать в ожидаемое время и линейное пространство, как показаноO(nloglogn)

Ю. Хан и М. Торуп. Целочисленная сортировка в ожидаемое время и линейное пространство. в учеб. ФОКС 2002O(nloglogn)

оценка доказана.

Массимо Кафаро
источник
1

Вы можете сделать все это за ожидаемое амортизированное время. Суть в том, что нам не нужна полная мощность очереди с приоритетами, поскольку частота клавиш изменяется только на 1 при каждой вставке или удалении.O(1)

Мое решение, приведенное ниже, на самом деле является просто вашим решением с «неэффективной» очередью приоритетов, которая в данном случае работает хорошо: очередь с максимальным приоритетом, реализованная в виде двусвязных списков блоков ключей, имеет O (1) insertMin, deleteMax, removeFromBucket и increaseKey.


Поддерживайте двусвязный список Buckets, где у каждого Bucket есть непустой хэш-набор ключей (который я назову когортой) и положительное целое число (которое я назову ValCount). В сегменте b каждый ключ k в когорте b имеет одинаковое количество уникальных значений, связанных с ним в поддерживаемом вами наборе. Например, если в вашем наборе есть пары (a, яблоко), (a, авокадо), (b, банан), (c, огурец), (d, фрукт дракона), где отдельные буквы - это ключи, а фрукты - значения, то у вас будет два Buckets: один Bucket будет иметь ValCount 2 и Cohort, состоящий только из одного ключа: a. Другой Bucket будет иметь ValCount 1 и Cohort, состоящий из трех ключей b, c и d.

Список Bucket с двойной связью должен быть упорядочен ValCount. Будет важно, чтобы мы могли найти начало и конец списка за время и чтобы мы могли соединить новый Bucket за время O ( 1 ), если мы знаем его соседей. Единственно, я буду называть список Buckets BucketList.O(1)O(1)

В дополнение к BucketList нам понадобится SetMap, представляющий собой ключи отображения хеш-карты для ValueBuckets. ValueBucket - это пара, состоящая из ValueSet (непустой хэш-набор значений) и ненулевого указателя на Bucket. Набор значений, связанный с ключом k, содержит все уникальные значения, связанные с k. Указатель Bucket, связанный с ValueSet, имеет Cohort, равный размеру ValueSet. Bucket, связанный с ключом k в SetMap, также связан с ключом k в BucketList.

В C ++:

struct Bucket {
    unsigned ValCount;
    unordered_set<Key> Cohort;
    Bucket * heavier;
    Bucket * lighter;
};
Bucket * BucketListHead;
Bucket * BucketListTail;

struct ValueBucket {
  unordered_set<Value> ValueSet;
  Bucket * bucket;
};
unordered_map<Key, ValueBucket> SetMap;

Чтобы найти пару ключ-значение максимальной частоты, нам просто нужно взглянуть на заголовок BucketList, найти ключ в когорте, найти этот ключ в SetMap и найти значение в ValueSet его ValueBucket. (Уф!)

Вставить и удалить пары ключ-значение сложнее.

Чтобы вставить или удалить пару ключ-значение, мы сначала вставляем или удаляем ее в SetMap. Это изменит размер ValueSet, поэтому нам нужно изменить Bucket, связанный с ключом. Единственными Buckets, на которые нам нужно обратить внимание, чтобы внести это изменение, будут непосредственные соседи Bucket, в котором раньше находился ключ. Здесь есть несколько случаев, и их, вероятно, не стоит описывать полностью, хотя я был бы рад чтобы уточнить, если у вас все еще есть проблемы.

jbapple
источник
Благодарю. На самом деле у нас была похожая идея для решения, но с этим была проблема. Я должен проверить, в чем была проблема, потому что я не помню это прямо сейчас. Я проверю это более тщательно на следующей неделе, чтобы увидеть, поможет ли это избежать проблемы, с которой столкнулось наше решение.
Каве
Вот еще один способ подумать о моем решении: на самом деле это просто ваше решение с «неэффективной» очередью приоритетов, которая в этом случае хорошо работает. Очередь с максимальным приоритетом, реализованная в виде двусвязных списков блоков ключей, имеет O (1) insertMin, deleteMax, removeFromBucket и увеличенияKey.
jbapple
Наиболее эффективным способом (с точки зрения наихудшего случая) для сохранения сопоставления ключей для ValueBuckets, вероятно, является дерево поиска.
Рафаэль
Рафаэль - я не уверен, к чему ты клонишь. Вы говорите, что на практике хеш-таблицы дороги, или в худшем случае они имеют плохую производительность, или что-то в этом роде?
Jbapple
-3

В худшем случае сложность

O(1)

O(1)

O(1)

O(loglogn)

O(n)

доказательство

[0,N) O(log log min{n,N})

Который устанавливается с помощью комбинации:

τ(n,N) n[0,N) τ(N,N)τ(N,N)τ

и:

n[0,N)O(1+log log nlog log q)q

Ссылка

Торуп, Миккель. «Целочисленные приоритетные очереди с уменьшением ключа в постоянном времени и проблема кратчайших путей из одного источника». В материалах тридцать пятого ежегодного симпозиума ACM по теории вычислений, 149–158. STOC '03. Нью-Йорк, штат Нью-Йорк, США: ACM, 2003.

В
источник
Обратите внимание, что во всех приоритетных очередях легко перейти к структуре, поддерживающей get-min и extract-min, к структуре, поддерживающей get-max и extract-max.
AT
Ping ...: @Kaveh
AT