Я был на собеседовании на должность С, на котором мне представили идиому, с которой я раньше не сталкивался. Это уловка, которая упрощает реализацию различных алгоритмов, включающих связанные списки, и мне интересно, сталкивался ли с этим кто-нибудь еще.
Скажем, у нас есть запись связанного списка, определенная так:
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 довольно хорошо, и у меня довольно хорошо разобрались указатели и косвенное обращение, но мне потребовалось время, чтобы полностью понять.
@finnw Это вопрос личного (или рабочего) стиля. Для меня это тоже был бы char* value.
@ JoséX. Как и большинство программистов на C, я несколько раз делал ошибку, записывая char* pointer1, pointer2;. 'char*' (без пробела) увеличивает вероятность того, что человек прочитает его иначе, чем его анализирует компилятор (что делает эту ошибку более вероятной).
Этот метод обсуждается без указания имени в Написание твердого кода Стива Магуайра. Есть те, кто критикует книгу (см. ACCU обзор); Я думаю, что это разумно, хотя теперь местами довольно устарело (в первую очередь потому, что оно было написано до того, как стандартные компиляторы C стали доступны повсеместно).





Я не вижу ничего, что я бы назвал идиомой как таковой. Это похоже на стандартное кодирование, когда вы имеете дело со структурами данных на C.
Моя единственная жалоба заключается в том, что указатель вызывающего абонента (* r) изменен. В зависимости от использования функции я ожидал бы неожиданного побочного эффекта. Помимо устранения неожиданного побочного эффекта, использование локальной переменной в качестве * r сделает код более читабельным.
Обновление * r возвращает указатель на новый узел. Это сделано намеренно, иначе нет четкого способа добраться до нового узла, если значения не уникальны.
Почему бы просто не заставить функцию возвращать запись * новому узлу?
r используется для изменения указателя заголовка списка, если он пуст. запись * newlist = NULL; insert_sorted (& newlist, "значение"); Теперь newlist указывает на одноэлементный связанный список.
аиб, это хороший сценарий / идея. Но когда новый элемент не становится заголовком списка, указатель вызывающего абонента указывает на что-то помимо заголовка. Если цель состоит в том, чтобы сообщить вызывающему объекту голову, он должен это сделать.
Вы правы, я упустил из виду, что последнее присваивание * r = безусловное. Это должно происходить только тогда, когда нужно обновить указатель головы.
Какая здесь идиома? Уж точно не реализация связного списка. Использование указателя на конструкцию указателя? Компактная петля?
Лично я бы использовал возвращаемое значение указателя вместо того, чтобы работать с входным значением. Потому что, увидев этот тип входных данных, я позвоню в колокол, что заставит меня скопировать свое значение, прежде чем передать его вашей функции.
Я не знаю, есть ли у этого имя или даже это какая-то особая идиома, но, поскольку в настоящее время памяти относительно много, мои связанные списки (где языковые библиотеки не делают их доступными) являются особым вариантом, который значительно упрощает код .
Во-первых, они всегда имеют двойную связь, поскольку это упрощает обход в обоих направлениях как для операций обработки, так и для операций вставки / удаления.
«Пустой» список фактически состоит из двух узлов: головы и хвоста. Таким образом, при вставке и удалении не нужно беспокоиться о том, является ли удаляемый узел головой или хвостом, они могут просто считать, что это средний узел.
Вставка нового узла 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, прошло много времени с тех пор, как я это делал, так как сейчас я в основном занимаюсь Java, и почти все структуры данных под солнцем уже где-то реализованы. Починил это.
AmigaOS широко использует этот вид двусвязных списков.
Полезный шаблон, но это не ответ на вопрос
@finnw, вопрос состоял из двух частей: (1) «У этой идиомы есть название или она упоминается в какой-либо литературе?» (2) «Есть ли другие подобные в языке C?» - Думаю, вы обнаружите, что я отвечал на вторую часть. Это из смутной памяти, конечно, года полтора назад :-)
Об этом упоминается у Кнута. Вы также можете использовать один и тот же узел как для головы, так и для хвоста.
Я бы сказал, что эта идиома - это «такой код, который дал« c »плохую репутацию»
В 2001 году я по ошибке устроился на работу в компанию, которая использовала C. Я знал, что это плохая идея, и все пошло еще хуже. Я ушел через несколько месяцев. Я содрогаюсь при мысли о том, что мне придется вернуться к такому языку.
Он выглядит простым, не «умным» и типичным примером кода, который игнорирует проверку ошибок и использует очевидную библиотечную функцию.
Да, это довольно просто. Я не вижу в нем ничего "умного", да и не особо компактный. Я могу не обращать внимания на отсутствие проверки ошибок и, например, кода на американском английском, но это довольно неприятный побочный эффект.
Код в вопросе - плохая реализация хорошей идиомы. Я думаю, что все наоборот: именно такой код делает C.
Вау, Дин действительно ненавидит C. Он не слишком умный и компактный, это типичный код C. Это делает для вызывающего абонента именно то, что он хочет. Рекомендуется опускать обработку ошибок при публикации здесь, если это не позволяет понять суть.
Видите ли, «именно то, что хочет вызывающий», для меня не означает «изменить указатель, который я передаю вам, чтобы он указывал на новый узел»; это неожиданный побочный эффект.
Я думаю, что прошло довольно много времени, так как скорость кода была важнее читабельности кода. Вы ВСЕГДА можете добавить более быстрое оборудование, более умных людей не так-то просто найти.
@ Тим, мне нравится, что «Я по ошибке устроился на работу в 2001 году в компанию» - что, вы проходили мимо их дома, случайно зашли в дверь и подписали форму приема? :-)
Переданный указатель не помечен как const, поэтому, конечно, это сигнализирует вызывающему, что его можно изменить?
Ха - забавно возвращаться на эту стоянку. Я совсем не ненавижу букву C, хотя думаю, что в наши дни это кажется очень старомодным и требует много тяжелой работы, которая уходит в прошлое с более современными языками. Но я, делать, считаю такие вещи является хорошим примером того, почему люди ушли.
Люди не пошли дальше. Люди по-прежнему пишут кода на C больше, чем всего остального. Люди ушли туда, где это было необходимо. Операционные системы, встроенные и многое другое, написанное на C, и это не изменится в обозримом будущем. Хорошо, я обещал подождать. Это будет последний :)
Согласен с двумя последними пунктами. Категорически не согласен с третьим. Это обычный (и самый простой) способ имитации передачи по ссылке в C. Если бы подобный метод C# был определен как void InsertSorted(ref Record r, string value), вы бы не стали жаловаться на «неожиданный» побочный эффект, не так ли? Из сигнатуры функции очевидно, что она может модифицировать r.
Вместо того, чтобы возвращать значение нового узла в качестве параметра ввода / вывода, вам лучше иметь это значение, возвращаемое функцией. Это упрощает как вызывающий код, так и код внутри функции (вы можете избавиться от всех этих уродливых двойных косвенных указаний).
record* insert_sorted(const record* head, const char* value)
Вам не хватает обработки ошибок для случая сбоя malloc / strdup, кстати.
Я не думаю, что *head должен быть const, потому что этой функции может потребоваться изменить свой указатель next.
Это хорошо известная штука - итерация с двойным указателем (так я ее называю, официального названия не знаю). Цель состоит в том, чтобы иметь возможность найти позицию в односвязном списке, а затем вставить до этой позиции (вставка после тривиальной). Наивно реализовано, для этого требуются два указателя (текущий и предыдущий) и специальный код для начала списка (когда 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 *.
Хорошо, поэтому я позволил себе здесь некоторые вольности. Этот трюк так же ценен в C++, как и в C
Я использовал подобное для вставки в двоичное дерево. Потому что при итерации дерева вы обычно останавливаетесь, когда ваш указатель становится NULL (вы сбежали с дерева).
Итак, чтобы вставить, у вас есть 3 варианта:
1: используйте переменную, которая отслеживает предыдущее значение вашего итерационного указателя.
2: остановитесь, когда указатель, за которым вы будете следовать, равен NULL, прежде чем следовать за ним, работает, но, на мой взгляд, немного менее элегантно.
3: или более элегантное решение - просто использовать указатель на указатель, так что вы можете просто сделать: *it = new_node();, и он добавит его туда, где раньше был NULL в вашем дереве.
Для связного списка, хотя этот код работает хорошо, я обычно просто использую двусвязный список, что упрощает вставку в любое место.
Это то, что я искал - как распознавание шаблона, так и другое его использование - в бинарных деревьях. Спасибо!
Чтобы ответить на исходный вопрос, это известен как подход, ориентированный на указатель, а не на наивный подход, ориентированный на узлы. Глава 3 «Продвинутых методов программирования» Рекса Барзи, доступная по адресу amazon.com, включает гораздо лучший пример реализации подхода, ориентированного на указатель.
Advanced Programming Techniques в настоящее время выпущена в 2011 году. Эта техника существует намного дольше. Это обсуждается (несколько мимоходом) в Написание твердого кода, например, S Maguire из 1993 года. (WSC имеет неоднозначную репутацию; это не рекомендация как таковая, хотя я думаю, что она лучше, чем позволяют ее критики; скорее, это просто «пример» предыдущей публикации. Я был бы удивлен, обнаружив, что WSC тоже был первым. Я просто знаю об этом.)
Эта идиома приводится в главе 12 «Указатели на C». Она используется для вставки узла в связанный список без заголовка списка.
Я также придумал такое использование двойного указателя, я использовал его, но мне это не очень нравится. Код, который я придумал, содержит это ядро для поиска определенных объектов и удаления их из списка:
Element** previous = &firstElement, *current;
while((current = *previous)) {
if (shouldRemove(current)) {
*previous = current->next; //delete
} else {
previous = ¤t->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;
}
char* valueвместоchar *value? фу. Не работай там.