Правильный способ удалить элемент из связанного списка

10

В этом интервью Slashdot Линус Торвальдс цитирует слова:

Я видел слишком много людей, которые удаляли односвязную запись списка, отслеживая запись «prev», а затем удаляли запись, делая что-то вроде

if (prev)
  prev-> next = entry-> next;
иначе
  list_head = entry-> next;

и всякий раз, когда я вижу такой код, я просто говорю: «Этот человек не понимает указателей». И это, к сожалению, довольно часто.

Люди, которые понимают указатели, просто используют «указатель на указатель записи» и инициализируют его адресом list_head. И затем, проходя по списку, они могут удалить запись без использования каких-либо условий, просто выполнив «* pp = entry-> next».

Как разработчик PHP, я не касался указателей с момента введения в Си в университете десять лет назад. Тем не менее, я чувствую, что это тип ситуации, с которой я, по крайней мере, должен быть знаком. О чем говорит Линус? Если честно, если бы меня попросили реализовать связанный список и удалить элемент, то вышеупомянутый «неправильный» путь - это то, как я бы поступил. Что мне нужно знать, чтобы кодировать, как лучше всего говорит Линус?

Я спрашиваю здесь, а не о переполнении стека, поскольку у меня фактически нет проблем с этим в производственном коде.

dotancohen
источник
1
Он говорит, что когда вам нужно сохранить местоположение prev, а не весь узел, вы можете просто сохранить местоположение prev.next, так как это единственное, что вас интересует. Указатель на указатель. И если вы сделаете это, вы избежите глупости if, так как теперь у вас нет неловкого случая list_headбыть указателем извне узла. В этом случае указатель на начало списка семантически совпадает с указателем на следующий узел.
Ордос
@ Ордос: Понятно, спасибо. Почему комментарий? Это краткий, ясный и яркий ответ.
dotancohen
@Ordous Все, что связано с этим фрагментом кода, является указателем, поэтому его точка не может иметь ничего общего с сохранением всего узла, а с сохранением указателя на него.
Довал

Ответы:

9

Используя мои навыки L331 MS Paint:

введите описание изображения здесь

Первоначальное решение - указать на узлы через curr. В этом случае вы проверяете, имеет ли следующий узел currзначение удаления, и если это так, сбрасываете указатель currузла next. Проблема в том, что нет узла, который указывает на заголовок списка. Это означает, что должен быть особый случай, чтобы проверить это.

Вместо этого Линус (скорее всего) предлагает не сохранять указатель на текущий проверенный узел, а скорее указатель на указатель на текущий узел (помеченный pp). Операция такая же - если ppуказатель указывает на узел с правильным значением, вы сбрасываете ppуказатель.

Разница возникает в самом начале списка. Хотя нет узла, который указывает на заголовок списка, на самом деле есть указатель на заголовок списка. И это точно такой же указатель на узел, как и nextуказатель на другой узел . Следовательно, нет необходимости в специальном предложении для начала списка.

Ordous
источник
Ах, теперь я вижу ... вы узнаете что-то новое каждый день.
Лоуренс Айелло
1
Я думаю, что вы описываете вещи правильно, но я бы предположил, что правильное решение состоит в том, чтобы list_headуказывать на что-то с nextузлом, который указывает на первый реальный элемент данных (и prevинициализировать этот же фиктивный объект). Мне не нравится идея prevуказывать на что-то другого типа, поскольку такие приемы могут вводить неопределенное поведение с помощью псевдонимов и делать код ненужным образом чувствительным к структуре структуры.
Суперкат
@supercat В том-то и дело. Вместо того, чтобы prevуказывать на узлы, он указывает на указатели. Он всегда указывает на что-то одного типа, а именно указатель на узел. Ваше предложение по сути то же самое - имейте в prevвиду что-то "с nextузлом". Если вы отбрасываете оболочку, вы просто получаете начальный list_headуказатель. Или, другими словами, то, что определяется только указателем на следующий узел, семантически эквивалентно указателю на узел.
Ордус
@Ordous: Это имеет смысл, хотя он предполагает , что list_headи nextбудет держать такой же «вид» указателя. Возможно, не проблема в C, но, возможно, проблема в C ++.
суперкат
@supercat Я всегда предполагал, что это «каноническое» представление связанного списка, независимого от языка. Но я не достаточно опытен, чтобы судить, имеет ли это значение на самом деле между C и C ++, и каковы стандартные реализации там.
Ордус
11

введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь

Пример кода

// ------------------------------------------------------------------
// Start by pointing to the head pointer.
// ------------------------------------------------------------------
//    (next_ptr)
//         |
//         v
// [head]----->[..]----->[..]----->[..]----->[to_remove]----->[....]
Node** next_ptr = &list->head;

// ------------------------------------------------------------------
// Search the list for the matching entry.
// After searching:
// ------------------------------------------------------------------
//                                  (next_ptr)
//                                       |
//                                       v
// [head]----->[..]----->[..]----->[..]----->[to_remove]----->[next]
while (*next_ptr != to_remove) // or (*next_ptr)->val != to_remove->val
{
    Node* next_node = *next_ptr
    next_ptr = &next_node->next;
}

// ------------------------------------------------------------------
// Dereference the next pointer and set it to the next node's next
// pointer.
// ------------------------------------------------------------------
//                                           (next_ptr)
//                                                |
//                                                v
// [head]----->[..]----->[..]----->[..]---------------------->[next]
*next_ptr = to_remove->next;

Если нам нужна логика для уничтожения узла, тогда мы можем просто добавить строку кода в конце:

// Deallocate the node which is now stranded from the list.
free(to_remove);

источник