Почему удаление элементов хэш-таблицы с использованием двусвязного списка равно O (1)?
в учебнике CLRS "введение в алгоритм"есть такой абзац на стр. 258.
мы можем удалить элемент в O (1) раз, если списки дважды связаны. (Обратите внимание, что CHAINED-HASH-DELETE принимает в качестве входных данных элемент x, а не его ключ k, так что нам не нужно искать x сначала. Если хэш-таблица поддерживает удаление, то ее связанный список должен быть дважды связан, чтобы мы могли быстро удалить элемент. Если бы списки были связаны только по отдельности, то для удаления элемента x мы бы сначала нужно найти x в списке, чтобы мы могли обновить далее атрибут предшественника X. С одиночно связанными списками удаление и поиск будут иметь одинаковое асимптотическое время выполнения).
что загадка для меня это большой parenthses, мне не удалось понять его логику. С двусвязным списком все еще нужно найти x, чтобы удалить его, как это отличается от односвязного списка? Пожалуйста, помогите мне понять это!
7 ответов
проблема, представленная здесь: рассмотрим, что вы смотрите на конкретный элемент хэш-таблицы. Сколько стоит его удаление?
Предположим, у вас есть простой список ссылок :
v ----> w ----> x ----> y ----> z
|
you're here
теперь если убрать x
, вам необходимо подключить w
to y
чтобы ваш список был связан. Вам нужно получить доступ w
и скажите ему указать на y
(вы хотите иметь w ----> y
). Но вы не можете получить доступ w
С x
потому что это просто связано! Таким образом вы должны пройти через весь свой список, чтобы найти w
в o (n) операциях, а затем скажите ему ссылку на y
. Это плохо.
тогда предположим, что вы дважды связаны:
v <---> w <---> x <---> y <---> z
|
you're here
круто, вы можете получить доступ к w и y отсюда, так что вы можете подключить два (w <---> y
) в O(1) операция!
В общем, вы правы-алгоритм, который вы разместили, принимает элемент сам как ввод, хотя и не только его ключ:
обратите внимание, что CHAINED-HASH-DELETE принимает за вход элемент x, а не его ключ k, так что нам не нужно искать x сначала.
У вас есть элемент x-поскольку это двойной связанный список, у вас есть указатели на предшественника и преемника, поэтому вы можете исправить эти элементы в O ( 1) - с помощью одного связанный список будет доступен только преемнику, поэтому вам придется искать предшественника в O(n).
Мне кажется, что часть хэш-таблицы это в основном отвлекающий маневр. Вопрос: "мы можем удалить текущий элемент из связанного списка в постоянное время, и если да, то как?"
ответ: это немного сложно, но на самом деле да, мы можем-по крайней мере, обычно. Мы делаем не (обычно) должны пройти весь связанный список, чтобы найти предыдущий элемент. Вместо этого мы можем поменять данные между текущим элементом и следующим элементом, а затем удалить следующий элемент.
единственное исключение - это когда/если нам нужно / нужно удалить последние пункт в списке. В этом случае там is нет следующего элемента для замены. Если вам действительно нужно это сделать, нет реального способа избежать поиска предыдущего элемента. Однако есть способы, которые обычно работают, чтобы избежать этого - один из них-завершить список с помощью sentinel вместо нулевого указателя. В этом случае, так как мы никогда не удаляем узел с помощью sentinel value, нам никогда не придется иметь дело с удалением последнего элемента в списке. Это оставляет нам относительно простой код, что-то вроде этого:
template <class key, class data>
struct node {
key k;
data d;
node *next;
};
void delete_node(node *item) {
node *temp = item->next;
swap(item->key, temp->key);
swap(item->data, temp->data);
item ->next = temp->next;
delete temp;
}
предположим, вы хотите удалить элемент x, используя дважды список ссылок, вы можете легко подключить предыдущий элемент x к следующему элементу X. поэтому не нужно проходить весь список, и он будет в O (1).
Find(x)
, Как правило, O (1) для цепной хэш-таблицы-неважно, используете ли вы односвязные списки или двусвязные списки. Они идентичны по производительности.
Если после запуска Find(x)
, вы решите, что хотите удалить возвращаемый объект, вы обнаружите, что внутри хэш-таблицы может потребоваться снова искать ваш объект. Обычно это будет O (1) и не имеет большого значения, но вы обнаружите, что удаляете очень много, вы можете сделать немного лучше. Вместо того чтобы возвращать элемент пользователя напрямую, верните указатель на базовый хэш-узел. Тогда вы можете воспользоваться некоторыми внутренними структурами. Поэтому, если в этом случае вы выбрали двусвязный список как способ выразить свою цепочку, то во время процесса удаления нет необходимости пересчитывать хэш и снова искать коллекцию-вы можете опустить этот шаг. У вас есть достаточно информации, чтобы выполнить удаление с того места, где вы сидите. Дополнительный уход должен быть берется, если узел, который вы отправляете, является головным узлом, поэтому целое число может использоваться для обозначения местоположения вашего узла в исходном массиве, если это глава связанного списка.
компромисс-это гарантированное пространство, занятое дополнительным указателем против возможного более быстрого удаления (и немного более сложного кода). С современными настольными компьютерами пространство обычно очень дешево, поэтому это может быть разумным компромиссом.
точка зрения кодирования:
можно использовать unordered_map
в C++ для реализации этого.
unordered_map<value,node*>mp;
здесь node*
является указателем на структуру, хранящую ключ, левый и правый указатели!
как использовать:
если у вас есть значение v
и вы хотите удалить этот узел вобще:
доступ к этому значению узлов, как
mp[v]
.теперь просто сделайте его левым указателем на узел на его право.
и вуаля, вы сделали.
(просто чтобы напомнить, в C++ unordered_map
принимает среднее значение O (1) для доступа к определенному хранящемуся значению.)
учебник ошибается. Первый член списка не имеет годного к употреблению "предыдущего" указателя, поэтому дополнительный код необходим для поиска и отсоединения элемента, если он оказывается первым в цепочке (обычно 30% элементов являются головкой их цепи, если N=M, (при отображении N элементов в M слотов; каждый слот имеет отдельную цепочку.))
EDIT:
лучший способ использовать обратную ссылку-использовать указатель к ссылке, которая указывает на нас (обычно - >следующая ссылка предыдущего узла в списке)
struct node {
struct node **pppar;
struct node *nxt;
...
}
удаление становится:
*(p->pppar) = p->nxt;
и хорошей особенностью этого метода является то, что он одинаково хорошо работает для первого узла в цепочке (указатель pppar которого указывает на некоторый указатель, который не часть узла.
обновление 2011-11-11
поскольку люди не понимают моей точки зрения, я попытаюсь проиллюстрировать. В качестве примера можно привести hashtable table
(в основном массив указателей)
и куча узлов one
, two
, three
один из которых должен быть удален.
struct node *table[123];
struct node *one, *two,*three;
/* Initial situation: the chain {one,two,three}
** is located at slot#31 of the array */
table[31] = one, one->next = two , two-next = three, three->next = NULL;
one->prev = NULL, two->prev = one, three->prev = two;
/* How to delete element one :*/
if (one->prev == NULL) {
table[31] = one->next;
}
else {
one->prev->next = one->next
}
if (one->next) {
one->next->prev = one->prev;
}
теперь очевидно, что код obove-O (1), но есть что-то неприятное: ему все еще нужно array
и 31
, поэтому в большинство случаях узел является "автономным", и указатель на узел достаточно, чтобы удалить его из цепи, за исключением когда это будет первый узел в его цепочке; дополнительная информация будет необходима найти table
и 31
.
Далее рассмотрим эквивалентную структуру с указателем на указатель в качестве обратных.
struct node {
struct node *next;
struct node **ppp;
char payload[43];
};
struct node *table[123];
struct node *one, *two,*three;
/* Initial situation: the chain {one,two,three}
** is located at slot#31 of the array */
table[31] = one, one-next = two , two-next = three, three->next = NULL;
one->ppp = &table[31], two->ppp = &one->next, three->ppp = &two-next;
/* How to delete element one */
*(one->ppp) = one->next;
if (one->next) one->next->ppp = one->ppp;
Примечание: нет особых случаев и нет необходимости знать родительскую таблицу. (рассмотрим случай, когда существует несколько хэш-таблиц, но с одинаковыми типами узлов: операция удаления все равно должна знать из таблицы узел должен быть удален).
часто в сценарии {prev,next} особые случаи избегается путем добавления фиктивного узла в начале двойного связанного списка; но это также должно быть выделено и инициализировано.