Двусвязные списки
Двунаправленные связи
«Двусвязные списки» — бесплатный урок C Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C Academy содержит 4 уроков всего.
Двунаправленные связи
Двусвязный список даёт каждому узлу два указателя: один на узел next, другой на предыдущий узел prev.
Это позволяет обходить список в обоих направлениях и упрощает удаление.
#include <stdio.h>
struct Node {
int value;
struct Node *prev;
struct Node *next;
};
int main(void) {
printf("Each node links forward and backward\n");
return 0;
}Определение узла
Структура содержит указатель prev наряду с next. Оба указателя равны NULL на концах списка.
#include <stdio.h>
#include <stdlib.h>
struct Node { int value; struct Node *prev; struct Node *next; };
int main(void) {
struct Node *n = malloc(sizeof(struct Node));
n->value = 1; n->prev = NULL; n->next = NULL;
printf("%d\n", n->value);
free(n);
return 0;
}Вспомогательная функция создания
Как и прежде, вспомогательная функция объединяет выделение памяти в одном месте. Она устанавливает для prev и next значение NULL.
#include <stdio.h>
#include <stdlib.h>
struct Node { int value; struct Node *prev; struct Node *next; };
struct Node *make(int v) {
struct Node *n = malloc(sizeof(struct Node));
n->value = v; n->prev = NULL; n->next = NULL;
return n;
}
int main(void) {
struct Node *n = make(42);
printf("%d\n", n->value);
free(n);
return 0;
}Двунаправленное связывание узлов
При соединении двух узлов необходимо обновить оба направления: next первого узла и prev второго.
#include <stdio.h>
#include <stdlib.h>
struct Node { int value; struct Node *prev; struct Node *next; };
struct Node *make(int v){struct Node*n=malloc(sizeof*n);n->value=v;n->prev=NULL;n->next=NULL;return n;}
int main(void) {
struct Node *a = make(1), *b = make(2);
a->next = b;
b->prev = a;
printf("forward %d, back %d\n", a->next->value, b->prev->value);
free(a); free(b);
return 0;
}Вставка в начало
При добавлении в начало: next нового узла указывает на старую голову, prev старой головы — на новый узел, затем голова перемещается на новый узел.
#include <stdio.h>
#include <stdlib.h>
struct Node { int value; struct Node *prev; struct Node *next; };
struct Node *make(int v){struct Node*n=malloc(sizeof*n);n->value=v;n->prev=NULL;n->next=NULL;return n;}
void push(struct Node **head, int v) {
struct Node *n = make(v);
n->next = *head;
if (*head) (*head)->prev = n;
*head = n;
}
int main(void) {
struct Node *head = NULL;
push(&head, 2); push(&head, 1);
printf("%d %d\n", head->value, head->next->value);
return 0;
}Прямой обход
Прямой обход аналогичен обходу односвязного списка: переходите по next, пока не достигнете NULL.
#include <stdio.h>
#include <stdlib.h>
struct Node { int value; struct Node *prev; struct Node *next; };
struct Node *make(int v){struct Node*n=malloc(sizeof*n);n->value=v;n->prev=NULL;n->next=NULL;return n;}
int main(void) {
struct Node *a = make(1), *b = make(2);
a->next = b; b->prev = a;
for (struct Node *p = a; p; p = p->next) printf("%d ", p->value);
printf("\n");
free(a); free(b);
return 0;
}Обратный обход
Главное преимущество: из любого узла можно двигаться назад, переходя по указателям prev, пока не достигнете головы.
#include <stdio.h>
#include <stdlib.h>
struct Node { int value; struct Node *prev; struct Node *next; };
struct Node *make(int v){struct Node*n=malloc(sizeof*n);n->value=v;n->prev=NULL;n->next=NULL;return n;}
int main(void) {
struct Node *a = make(1), *b = make(2), *c = make(3);
a->next = b; b->prev = a; b->next = c; c->prev = b;
for (struct Node *p = c; p; p = p->prev) printf("%d ", p->value);
printf("\n");
free(a); free(b); free(c);
return 0;
}Удаление проще
Поскольку каждый узел знает своего предшественника, его можно удалить без поиска предыдущего узла.
Просто соедините node->prev с node->next в обоих направлениях.
#include <stdio.h>
#include <stdlib.h>
struct Node { int value; struct Node *prev; struct Node *next; };
struct Node *make(int v){struct Node*n=malloc(sizeof*n);n->value=v;n->prev=NULL;n->next=NULL;return n;}
void del(struct Node **head, struct Node *n) {
if (n->prev) n->prev->next = n->next; else *head = n->next;
if (n->next) n->next->prev = n->prev;
free(n);
}
int main(void) {
struct Node *a = make(1), *b = make(2), *c = make(3);
a->next=b; b->prev=a; b->next=c; c->prev=b;
struct Node *head = a;
del(&head, b);
printf("%d %d\n", head->value, head->next->value);
return 0;
}Обновление обоих соседей
При удалении узла всегда исправляйте next предыдущего узла и prev следующего узла.
Проверяйте NULL на каждом конце, чтобы не разыменовать отсутствующего соседа.
#include <stdio.h>
#include <stdlib.h>
struct Node { int value; struct Node *prev; struct Node *next; };
struct Node *make(int v){struct Node*n=malloc(sizeof*n);n->value=v;n->prev=NULL;n->next=NULL;return n;}
int main(void) {
struct Node *a = make(1), *b = make(2);
a->next = b; b->prev = a;
a->next = NULL;
free(b);
printf("now only %d remains\n", a->value);
free(a);
return 0;
}Хранение указателя на хвост
Во многих двусвязных списках также хранится указатель tail на последний узел, что позволяет добавлять элементы в конец за O(1) и выполнять обратный обход от конца.
#include <stdio.h>
#include <stdlib.h>
struct Node { int value; struct Node *prev; struct Node *next; };
struct Node *make(int v){struct Node*n=malloc(sizeof*n);n->value=v;n->prev=NULL;n->next=NULL;return n;}
int main(void) {
struct Node *head = make(1), *tail = head;
struct Node *n = make(2);
tail->next = n; n->prev = tail; tail = n;
printf("tail = %d\n", tail->value);
free(head); free(n);
return 0;
}Компромиссы
Двусвязные списки требуют больше памяти — по одному дополнительному указателю на узел — и обновления двух связей при каждом изменении.
Взамен Вы получаете двунаправленный обход и удаление известного узла за O(1). Выбирайте структуру с учётом своих задач.
#include <stdio.h>
int main(void) {
printf("Singly: less memory, one-way\n");
printf("Doubly: more memory, two-way + easy delete\n");
return 0;
}Быстрая проверка
Проверьте своё понимание двусвязных списков.
Итоги
Вы изучили двусвязные списки:
- У каждого узла есть указатели
prevиnext. - При связывании необходимо обновлять оба направления.
- Можно выполнять прямой и обратный обход, а также удалять известный узел за O(1).
- Недостатки — дополнительные память и обновления указателей; указатель tail позволяет добавлять элементы в конец за O(1).
Часто задаваемые вопросы
Урок «Двусвязные списки» бесплатный?
Да — полный текст урока «Двусвязные списки» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C Academy, подпишись на CoddyKit PRO. Курс C Academy содержит 4 уроков всего.
Чему я научусь в уроке «Двусвязные списки»?
Двунаправленные связи Ты практикуешь C Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать C Academy?
Предыдущий опыт не требуется. C Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Двусвязные списки»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке C Academy?
Да. Каждый урок C Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Односвязные списки
- Вставка и удаление
- Обход и поиск
- Двусвязные списки