Интересная идиома связанного списка C

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

Скажем, у нас есть запись связанного списка, определенная так:

typedef struct _record
{
    char* value;
    struct _record* next;
} record;

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

void insert_sorted(record** r, const char* value)
{
    record* newrec = NULL;
    while(*r && strcmp(value, (*r)->value) > 0)
        r = &((*r)->next); /* move r to point to the next field of the record */
    newrec = malloc(sizeof(record));
    newrec->value = strdup(value);
    newrec->next = *r;
    *r = newrec;
}

Когда функция вызывается, r указывает на указатель заголовка списка. Во время цикла while r обновляется, чтобы указать на поле next записи, которая находится непосредственно перед точкой, в которую мы хотим поместить новую запись. Последняя строка функции либо обновляет указатель заголовка списка (если вставка происходит в начале) или в поле next предыдущей записи, что довольно круто.

Пара вопросов:

  • Есть ли у этой идиомы название или она упоминается в какой-либо литературе?

  • Есть ли другие подобные в языке Си?

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

char* value вместо char *value? фу. Не работай там.
finnw 14.07.2010 13:36

@finnw Это вопрос личного (или рабочего) стиля. Для меня это тоже был бы char* value.

zentrunix 01.09.2013 18:53

@ JoséX. Как и большинство программистов на C, я несколько раз делал ошибку, записывая char* pointer1, pointer2;. 'char*' (без пробела) увеличивает вероятность того, что человек прочитает его иначе, чем его анализирует компилятор (что делает эту ошибку более вероятной).

finnw 03.09.2013 07:23

Этот метод обсуждается без указания имени в Написание твердого кода Стива Магуайра. Есть те, кто критикует книгу (см. ACCU обзор); Я думаю, что это разумно, хотя теперь местами довольно устарело (в первую очередь потому, что оно было написано до того, как стандартные компиляторы C стали доступны повсеместно).

Jonathan Leffler 10.11.2014 02:30
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
11
4
2 885
11
Перейти к ответу Данный вопрос помечен как решенный

Ответы 11

Я не вижу ничего, что я бы назвал идиомой как таковой. Это похоже на стандартное кодирование, когда вы имеете дело со структурами данных на C.

Моя единственная жалоба заключается в том, что указатель вызывающего абонента (* r) изменен. В зависимости от использования функции я ожидал бы неожиданного побочного эффекта. Помимо устранения неожиданного побочного эффекта, использование локальной переменной в качестве * r сделает код более читабельным.

Обновление * r возвращает указатель на новый узел. Это сделано намеренно, иначе нет четкого способа добраться до нового узла, если значения не уникальны.

ConcernedOfTunbridgeWells 02.12.2008 01:38

Почему бы просто не заставить функцию возвращать запись * новому узлу?

Adam Jaskiewicz 02.12.2008 01:48

r используется для изменения указателя заголовка списка, если он пуст. запись * newlist = NULL; insert_sorted (& newlist, "значение"); Теперь newlist указывает на одноэлементный связанный список.

aib 02.12.2008 02:19

аиб, это хороший сценарий / идея. Но когда новый элемент не становится заголовком списка, указатель вызывающего абонента указывает на что-то помимо заголовка. Если цель состоит в том, чтобы сообщить вызывающему объекту голову, он должен это сделать.

Frank Schwieterman 02.12.2008 02:55

Вы правы, я упустил из виду, что последнее присваивание * r = безусловное. Это должно происходить только тогда, когда нужно обновить указатель головы.

aib 02.12.2008 03:13

Какая здесь идиома? Уж точно не реализация связного списка. Использование указателя на конструкцию указателя? Компактная петля?

Лично я бы использовал возвращаемое значение указателя вместо того, чтобы работать с входным значением. Потому что, увидев этот тип входных данных, я позвоню в колокол, что заставит меня скопировать свое значение, прежде чем передать его вашей функции.

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

Во-первых, они всегда имеют двойную связь, поскольку это упрощает обход в обоих направлениях как для операций обработки, так и для операций вставки / удаления.

«Пустой» список фактически состоит из двух узлов: головы и хвоста. Таким образом, при вставке и удалении не нужно беспокоиться о том, является ли удаляемый узел головой или хвостом, они могут просто считать, что это средний узел.

Вставка нового узла y перед узлом x становится простой задачей:

x -> prev -> next = y
y -> next = x
y -> prev = x -> prev
x -> prev = y

Удаление узла x выполняется просто:

x -> prev -> next = x -> next
x -> next -> prev = x -> prev
free x

Обход настроен так, чтобы игнорировать посторонние голова и хвост:

n = head -> next
while n != tail
    process n
    n = n -> next

Все это упрощает понимание кода без особой обработки крайних случаев за счет пары узлов памяти.

Ваш обход не позволяет изменить указатель на n, чтобы завершить вставку.

Frank Schwieterman 02.12.2008 01:33

Ой, спасибо, @Frank, прошло много времени с тех пор, как я это делал, так как сейчас я в основном занимаюсь Java, и почти все структуры данных под солнцем уже где-то реализованы. Починил это.

paxdiablo 02.12.2008 02:22

AmigaOS широко использует этот вид двусвязных списков.

user52898 27.02.2009 09:42

Полезный шаблон, но это не ответ на вопрос

finnw 14.07.2010 13:51

@finnw, вопрос состоял из двух частей: (1) «У этой идиомы есть название или она упоминается в какой-либо литературе?» (2) «Есть ли другие подобные в языке C?» - Думаю, вы обнаружите, что я отвечал на вторую часть. Это из смутной памяти, конечно, года полтора назад :-)

paxdiablo 14.07.2010 15:51

Об этом упоминается у Кнута. Вы также можете использовать один и тот же узел как для головы, так и для хвоста.

Paul Hankin 05.08.2012 14:09

Я бы сказал, что эта идиома - это «такой код, который дал« c »плохую репутацию»

  • Неоправданно умный
  • Неоправданно компактный
  • Неожиданные побочные эффекты у звонящего
  • Нет обработки ошибок на malloc
  • Работает только для строк на английском языке (США)

В 2001 году я по ошибке устроился на работу в компанию, которая использовала C. Я знал, что это плохая идея, и все пошло еще хуже. Я ушел через несколько месяцев. Я содрогаюсь при мысли о том, что мне придется вернуться к такому языку.

Tim 02.12.2008 01:45

Он выглядит простым, не «умным» и типичным примером кода, который игнорирует проверку ошибок и использует очевидную библиотечную функцию.

Paul Nathan 02.12.2008 01:48

Да, это довольно просто. Я не вижу в нем ничего "умного", да и не особо компактный. Я могу не обращать внимания на отсутствие проверки ошибок и, например, кода на американском английском, но это довольно неприятный побочный эффект.

Adam Jaskiewicz 02.12.2008 02:00

Код в вопросе - плохая реализация хорошей идиомы. Я думаю, что все наоборот: именно такой код делает C.

user3458 02.12.2008 02:33

Вау, Дин действительно ненавидит C. Он не слишком умный и компактный, это типичный код C. Это делает для вызывающего абонента именно то, что он хочет. Рекомендуется опускать обработку ошибок при публикации здесь, если это не позволяет понять суть.

buti-oxa 02.12.2008 03:07

Видите ли, «именно то, что хочет вызывающий», для меня не означает «изменить указатель, который я передаю вам, чтобы он указывал на новый узел»; это неожиданный побочный эффект.

Adam Jaskiewicz 02.12.2008 04:16

Я думаю, что прошло довольно много времени, так как скорость кода была важнее читабельности кода. Вы ВСЕГДА можете добавить более быстрое оборудование, более умных людей не так-то просто найти.

paxdiablo 02.12.2008 04:24

@ Тим, мне нравится, что «Я по ошибке устроился на работу в 2001 году в компанию» - что, вы проходили мимо их дома, случайно зашли в дверь и подписали форму приема? :-)

paxdiablo 02.12.2008 04:26

Переданный указатель не помечен как const, поэтому, конечно, это сигнализирует вызывающему, что его можно изменить?

unwind 02.12.2008 11:58

Ха - забавно возвращаться на эту стоянку. Я совсем не ненавижу букву C, хотя думаю, что в наши дни это кажется очень старомодным и требует много тяжелой работы, которая уходит в прошлое с более современными языками. Но я, делать, считаю такие вещи является хорошим примером того, почему люди ушли.

Will Dean 02.12.2008 21:13

Люди не пошли дальше. Люди по-прежнему пишут кода на C больше, чем всего остального. Люди ушли туда, где это было необходимо. Операционные системы, встроенные и многое другое, написанное на C, и это не изменится в обозримом будущем. Хорошо, я обещал подождать. Это будет последний :)

Ilya 02.12.2008 22:08

Согласен с двумя последними пунктами. Категорически не согласен с третьим. Это обычный (и самый простой) способ имитации передачи по ссылке в C. Если бы подобный метод C# был определен как void InsertSorted(ref Record r, string value), вы бы не стали жаловаться на «неожиданный» побочный эффект, не так ли? Из сигнатуры функции очевидно, что она может модифицировать r.

finnw 14.07.2010 13:45

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

record* insert_sorted(const record* head, const char* value)

Вам не хватает обработки ошибок для случая сбоя malloc / strdup, кстати.

Я не думаю, что *head должен быть const, потому что этой функции может потребоваться изменить свой указатель next.

finnw 14.07.2010 13:49

Это хорошо известная штука - итерация с двойным указателем (так я ее называю, официального названия не знаю). Цель состоит в том, чтобы иметь возможность найти позицию в односвязном списке, а затем вставить до этой позиции (вставка после тривиальной). Наивно реализовано, для этого требуются два указателя (текущий и предыдущий) и специальный код для начала списка (когда prev == NULL).

Код, который я обычно пишу, похож на

void insertIntoSorted(Element *&head, Element *newOne)
{
  Element **pp = &head;
  Element *curr;
  while ((curr = *pp) != NULL && less(curr, newOne)) {
    pp = &(pp->next);
  }
  newOne->next = *pp;
  *pp = newOne;
}

Обновлять:

Я забыл о другой цели этого трюка - более важной. Используется для удаления элементов из односвязных списков:

// returns deleted element or NULL when key not found
Element *deleteFromList(Element *&head, const ElementKey &key)
{
  Element **pp = &head;
  Element *curr;
  while ((curr = *pp) != NULL && !keyMatches(curr, key)) {
    pp = &(pp->next);
  }
  if (curr == NULL) return NULL;
  *pp = (*pp)->next; // here is the actual delete
  return curr;
}

C не имеет ссылок, и тип pp должен быть Element **, а не Element *.

Evan Teran 02.12.2008 02:55

Хорошо, поэтому я позволил себе здесь некоторые вольности. Этот трюк так же ценен в C++, как и в C

user3458 02.12.2008 16:31
Ответ принят как подходящий

Я использовал подобное для вставки в двоичное дерево. Потому что при итерации дерева вы обычно останавливаетесь, когда ваш указатель становится NULL (вы сбежали с дерева).

Итак, чтобы вставить, у вас есть 3 варианта:

1: используйте переменную, которая отслеживает предыдущее значение вашего итерационного указателя.

2: остановитесь, когда указатель, за которым вы будете следовать, равен NULL, прежде чем следовать за ним, работает, но, на мой взгляд, немного менее элегантно.

3: или более элегантное решение - просто использовать указатель на указатель, так что вы можете просто сделать: *it = new_node();, и он добавит его туда, где раньше был NULL в вашем дереве.

Для связного списка, хотя этот код работает хорошо, я обычно просто использую двусвязный список, что упрощает вставку в любое место.

Это то, что я искал - как распознавание шаблона, так и другое его использование - в бинарных деревьях. Спасибо!

elifiner 02.12.2008 11:20

Чтобы ответить на исходный вопрос, это известен как подход, ориентированный на указатель, а не на наивный подход, ориентированный на узлы. Глава 3 «Продвинутых методов программирования» Рекса Барзи, доступная по адресу amazon.com, включает гораздо лучший пример реализации подхода, ориентированного на указатель.

Advanced Programming Techniques в настоящее время выпущена в 2011 году. Эта техника существует намного дольше. Это обсуждается (несколько мимоходом) в Написание твердого кода, например, S Maguire из 1993 года. (WSC имеет неоднозначную репутацию; это не рекомендация как таковая, хотя я думаю, что она лучше, чем позволяют ее критики; скорее, это просто «пример» предыдущей публикации. Я был бы удивлен, обнаружив, что WSC тоже был первым. Я просто знаю об этом.)

Jonathan Leffler 23.03.2017 01:01

Эта идиома приводится в главе 12 «Указатели на C». Она используется для вставки узла в связанный список без заголовка списка.

Я также придумал такое использование двойного указателя, я использовал его, но мне это не очень нравится. Код, который я придумал, содержит это ядро ​​для поиска определенных объектов и удаления их из списка:

Element** previous = &firstElement, *current;
while((current = *previous)) {
    if (shouldRemove(current)) {
        *previous = current->next;  //delete
    } else {
        previous = &current->next;  //point to next
    }
}

Причина, по которой мне не нравится мой код, заключается в тонкой разнице между двумя предложениями if: синтаксис почти идентичен, но эффект совершенно другой. Я не думаю, что мы должны писать такой тонкий код, как этот, но, делая это по-другому, код становится действительно длинным. Так что в любом случае это плохо - вы можете пойти для краткости или для удобочитаемости, это ваш выбор.

несмотря на уловки, не изменилась ли роль переменной «r»? как вызывающий абонент узнает, что означает «* r» после звонка? или после выполнения, каков заголовок списка?

Я не мог поверить, что это можно проиллюстрировать (даже в какой-нибудь книге ?!). Я что-нибудь пропустил?

Если вы не возвращаете указатель (как предлагали другие), я бы предложил следующие изменения, чтобы сохранить роль ввода.

void insert_sorted(record** head, const char* value)
{
    record** r = head;
    bool isSameHead = false;
    record* newrec = NULL;
    while(*r && strcmp(value, (*r)->value) > 0) {
        r = &((*r)->next); isSameHead = true; }
    newrec = malloc(sizeof(record));
    newrec->value = strdup(value);
    newrec->next = *r;
    *r = newrec;
    if (!isSameHead) *head = newrec;
}

на самом деле, вероятно, еще один лучший способ сделать это - использовать «фиктивный головной узел», который связывает его рядом с началом списка.

    void insert_sorted(record** head, const char* value)
    {
        record dummyHead;
        dummyHead.next = *head;
        record* r = &dummyHead;
        while(r->next) {
           if (strcmp(value, r->next->value) < 0) 
              break;
           r = r->next;}
        newrec = malloc(sizeof(record));
        newrec->value = strdup(value);
        newrec->next = r->next;
        r->next = newrec;
        *head = dummyHead.next;
    }

Другие вопросы по теме