C Language
Связанные списки
Поиск…
замечания
Язык C не определяет структуру данных связанного списка. Если вы используете C и вам нужен связанный список, вам нужно либо использовать связанный список из существующей библиотеки (например, GLib), либо написать свой собственный интерфейс связанных списков. В этом разделе приведены примеры связанных списков и двойных связанных списков, которые можно использовать в качестве отправной точки для написания собственных связанных списков.
Одиночный список
Список содержит узлы, которые состоят из одной ссылки, называемой следующей.
Структура данных
struct singly_node
{
struct singly_node * next;
};
Двусторонний список
Список содержит узлы, которые состоят из двух ссылок, называемых предыдущими и последующими. Ссылки обычно ссылаются на узел с той же структурой.
Структура данных
struct doubly_node
{
struct doubly_node * prev;
struct doubly_node * next;
};
Topoliges
Линейный или открытый
Круговое или кольцевое
процедуры
привязывать
void doubly_node_bind (struct doubly_node * prev, struct doubly_node * next)
{
prev->next = next;
next->prev = prev;
}
Создание кругового списка
void doubly_node_make_empty_circularly_list (struct doubly_node * head)
{
doubly_node_bind (head, head);
}
Создание линейно связанного списка
void doubly_node_make_empty_linear_list (struct doubly_node * head, struct doubly_node * tail)
{
head->prev = NULL;
tail->next = NULL;
doubly_node_bind (head, tail);
}
вставка
Предположим, что пустой список всегда содержит один узел вместо NULL. Тогда процедуры ввода не должны принимать NULL.
void doubly_node_insert_between
(struct doubly_node * prev, struct doubly_node * next, struct doubly_node * insertion)
{
doubly_node_bind (prev, insertion);
doubly_node_bind (insertion, next);
}
void doubly_node_insert_before
(struct doubly_node * tail, struct doubly_node * insertion)
{
doubly_node_insert_between (tail->prev, tail, insertion);
}
void doubly_node_insert_after
(struct doubly_node * head, struct doubly_node * insertion)
{
doubly_node_insert_between (head, head->next, insertion);
}
Вставка узла в начале отдельного списка
Приведенный ниже код будет запрашивать цифры и продолжать добавлять их в начало связанного списка.
/* This program will demonstrate inserting a node at the beginning of a linked list */
#include <stdio.h>
#include <stdlib.h>
struct Node {
int data;
struct Node* next;
};
void insert_node (struct Node **head, int nodeValue);
void print_list (struct Node *head);
int main(int argc, char *argv[]) {
struct Node* headNode;
headNode = NULL; /* Initialize our first node pointer to be NULL. */
size_t listSize, i;
do {
printf("How many numbers would you like to input?\n");
} while(1 != scanf("%zu", &listSize));
for (i = 0; i < listSize; i++) {
int numToAdd;
do {
printf("Enter a number:\n");
} while (1 != scanf("%d", &numToAdd));
insert_node (&headNode, numToAdd);
printf("Current list after your inserted node: \n");
print_list(headNode);
}
return 0;
}
void print_list (struct Node *head) {
struct node* currentNode = head;
/* Iterate through each link. */
while (currentNode != NULL) {
printf("Value: %d\n", currentNode->data);
currentNode = currentNode -> next;
}
}
void insert_node (struct Node **head, int nodeValue) {
struct Node *currentNode = malloc(sizeof *currentNode);
currentNode->data = nodeValue;
currentNode->next = (*head);
*head = currentNode;
}
Объяснение для вставки узлов
Чтобы понять, как мы добавляем узлы в начале, давайте рассмотрим возможные сценарии:
- Список пуст, поэтому нам нужно добавить новый узел. В этом случае наша память выглядит так, где
HEAD
является указателем на первый узел:
| HEAD | --> NULL
Строка currentNode->next = *headNode;
будет присваивать значение currentNode->next
с NULL
так как headNode
изначально начинается со значения NULL
.
Теперь мы хотим, чтобы наш указатель на главном узле указывал на наш текущий узел.
----- -------------
|HEAD | --> |CURRENTNODE| --> NULL /* The head node points to the current node */
----- -------------
Это делается с помощью *headNode = currentNode;
- Список уже заполнен; нам нужно добавить новый узел в начало. Для простоты, давайте начнем с 1 узла:
----- -----------
HEAD --> FIRST NODE --> NULL
----- -----------
С currentNode->next = *headNode
структура данных выглядит следующим образом:
--------- ----- ---------------------
currentNode --> HEAD --> POINTER TO FIRST NODE --> NULL
--------- ----- ---------------------
Который, очевидно, должен быть изменен, так как *headNode
должен указывать на currentNode
.
---- ----------- ---------------
HEAD -> currentNode --> NODE -> NULL
---- ----------- ---------------
Это делается с помощью *headNode = currentNode;
Вставка узла в n-ое положение
До сих пор мы рассматривали вставку узла в начале отдельного списка . Однако в большинстве случаев вы захотите вставлять узлы в другое место. В приведенном ниже коде показано, как можно написать функцию insert()
для вставки узлов в любом из связанных списков.
#include <stdio.h>
#include <stdlib.h>
struct Node {
int data;
struct Node* next;
};
struct Node* insert(struct Node* head, int value, size_t position);
void print_list (struct Node* head);
int main(int argc, char *argv[]) {
struct Node *head = NULL; /* Initialize the list to be empty */
/* Insert nodes at positions with values: */
head = insert(head, 1, 0);
head = insert(head, 100, 1);
head = insert(head, 21, 2);
head = insert(head, 2, 3);
head = insert(head, 5, 4);
head = insert(head, 42, 2);
print_list(head);
return 0;
}
struct Node* insert(struct Node* head, int value, size_t position) {
size_t i = 0;
struct Node *currentNode;
/* Create our node */
currentNode = malloc(sizeof *currentNode);
/* Check for success of malloc() here! */
/* Assign data */
currentNode->data = value;
/* Holds a pointer to the 'next' field that we have to link to the new node.
By initializing it to &head we handle the case of insertion at the beginning. */
struct Node **nextForPosition = &head;
/* Iterate to get the 'next' field we are looking for.
Note: Insert at the end if position is larger than current number of elements. */
for (i = 0; i < position && *nextForPosition != NULL; i++) {
/* nextForPosition is pointing to the 'next' field of the node.
So *nextForPosition is a pointer to the next node.
Update it with a pointer to the 'next' field of the next node. */
nextForPosition = &(*nextForPosition)->next;
}
/* Here, we are taking the link to the next node (the one our newly inserted node should
point to) by dereferencing nextForPosition, which points to the 'next' field of the node
that is in the position we want to insert our node at.
We assign this link to our next value. */
currentNode->next = *nextForPosition;
/* Now, we want to correct the link of the node before the position of our
new node: it will be changed to be a pointer to our new node. */
*nextForPosition = currentNode;
return head;
}
void print_list (struct Node* head) {
/* Go through the list of nodes and print out the data in each node */
struct Node* i = head;
while (i != NULL) {
printf("%d\n", i->data);
i = i->next;
}
}
Смена связанного списка
Вы также можете выполнить эту задачу рекурсивно, но я выбрал в этом примере использовать итеративный подход. Эта задача полезна, если вы вставляете все свои узлы в начале связанного списка . Вот пример:
#include <stdio.h>
#include <stdlib.h>
#define NUM_ITEMS 10
struct Node {
int data;
struct Node *next;
};
void insert_node(struct Node **headNode, int nodeValue, int position);
void print_list(struct Node *headNode);
void reverse_list(struct Node **headNode);
int main(void) {
int i;
struct Node *head = NULL;
for(i = 1; i <= NUM_ITEMS; i++) {
insert_node(&head, i, i);
}
print_list(head);
printf("I will now reverse the linked list\n");
reverse_list(&head);
print_list(head);
return 0;
}
void print_list(struct Node *headNode) {
struct Node *iterator;
for(iterator = headNode; iterator != NULL; iterator = iterator->next) {
printf("Value: %d\n", iterator->data);
}
}
void insert_node(struct Node **headNode, int nodeValue, int position) {
int i;
struct Node *currentNode = (struct Node *)malloc(sizeof(struct Node));
struct Node *nodeBeforePosition = *headNode;
currentNode->data = nodeValue;
if(position == 1) {
currentNode->next = *headNode;
*headNode = currentNode;
return;
}
for (i = 0; i < position - 2; i++) {
nodeBeforePosition = nodeBeforePosition->next;
}
currentNode->next = nodeBeforePosition->next;
nodeBeforePosition->next = currentNode;
}
void reverse_list(struct Node **headNode) {
struct Node *iterator = *headNode;
struct Node *previousNode = NULL;
struct Node *nextNode = NULL;
while (iterator != NULL) {
nextNode = iterator->next;
iterator->next = previousNode;
previousNode = iterator;
iterator = nextNode;
}
/* Iterator will be NULL by the end, so the last node will be stored in
previousNode. We will set the last node to be the headNode */
*headNode = previousNode;
}
Объяснение для метода обратного списка
Мы запускаем previousNode
как NULL
, так как мы знаем на первой итерации цикла, если мы ищем узел перед первым головным узлом, он будет NULL
. Первый узел станет последним узлом в списке, а следующая переменная должна, естественно, быть NULL
.
В принципе, концепция обратного связывания списка здесь заключается в том, что мы фактически реверсируем сами ссылки. Следующий член следующего узла станет узлом перед ним, например:
Head -> 1 -> 2 -> 3 -> 4 -> 5
Где каждый номер представляет собой узел. Этот список станет следующим:
1 <- 2 <- 3 <- 4 <- 5 <- Head
Наконец, голова должна указывать на 5-й узел вместо этого, и каждый узел должен указывать на предыдущий узел.
Узел 1 должен указывать на NULL
поскольку перед ним ничего не было. Узел 2 должен указывать на узел 1, узел 3 должен указывать на узел 2 и т. Д.
Однако с этим методом существует одна небольшая проблема . Если мы разорвем ссылку на следующий узел и изменим ее на предыдущий узел, мы не сможем перейти к следующему узлу в списке, так как ссылка на него исчезла.
Решение этой проблемы состоит в том, чтобы просто сохранить следующий элемент в переменной ( nextNode
) перед изменением ссылки.
Список с двойной связью
Пример кода, показывающий, как узлы могут быть вставлены в двусвязный список, как список можно легко отменить и как его можно напечатать в обратном порядке.
#include <stdio.h>
#include <stdlib.h>
/* This data is not always stored in a structure, but it is sometimes for ease of use */
struct Node {
/* Sometimes a key is also stored and used in the functions */
int data;
struct Node* next;
struct Node* previous;
};
void insert_at_beginning(struct Node **pheadNode, int value);
void insert_at_end(struct Node **pheadNode, int value);
void print_list(struct Node *headNode);
void print_list_backwards(struct Node *headNode);
void free_list(struct Node *headNode);
int main(void) {
/* Sometimes in a doubly linked list the last node is also stored */
struct Node *head = NULL;
printf("Insert a node at the beginning of the list.\n");
insert_at_beginning(&head, 5);
print_list(head);
printf("Insert a node at the beginning, and then print the list backwards\n");
insert_at_beginning(&head, 10);
print_list_backwards(head);
printf("Insert a node at the end, and then print the list forwards.\n");
insert_at_end(&head, 15);
print_list(head);
free_list(head);
return 0;
}
void print_list_backwards(struct Node *headNode) {
if (NULL == headNode)
{
return;
}
/*
Iterate through the list, and once we get to the end, iterate backwards to print
out the items in reverse order (this is done with the pointer to the previous node).
This can be done even more easily if a pointer to the last node is stored.
*/
struct Node *i = headNode;
while (i->next != NULL) {
i = i->next; /* Move to the end of the list */
}
while (i != NULL) {
printf("Value: %d\n", i->data);
i = i->previous;
}
}
void print_list(struct Node *headNode) {
/* Iterate through the list and print out the data member of each node */
struct Node *i;
for (i = headNode; i != NULL; i = i->next) {
printf("Value: %d\n", i->data);
}
}
void insert_at_beginning(struct Node **pheadNode, int value) {
struct Node *currentNode;
if (NULL == pheadNode)
{
return;
}
/*
This is done similarly to how we insert a node at the beginning of a singly linked
list, instead we set the previous member of the structure as well
*/
currentNode = malloc(sizeof *currentNode);
currentNode->next = NULL;
currentNode->previous = NULL;
currentNode->data = value;
if (*pheadNode == NULL) { /* The list is empty */
*pheadNode = currentNode;
return;
}
currentNode->next = *pheadNode;
(*pheadNode)->previous = currentNode;
*pheadNode = currentNode;
}
void insert_at_end(struct Node **pheadNode, int value) {
struct Node *currentNode;
if (NULL == pheadNode)
{
return;
}
/*
This can, again be done easily by being able to have the previous element. It
would also be even more useful to have a pointer to the last node, which is commonly
used.
*/
currentNode = malloc(sizeof *currentNode);
struct Node *i = *pheadNode;
currentNode->data = value;
currentNode->next = NULL;
currentNode->previous = NULL;
if (*pheadNode == NULL) {
*pheadNode = currentNode;
return;
}
while (i->next != NULL) { /* Go to the end of the list */
i = i->next;
}
i->next = currentNode;
currentNode->previous = i;
}
void free_list(struct Node *node) {
while (node != NULL) {
struct Node *next = node->next;
free(node);
node = next;
}
}
Обратите внимание, что иногда полезно хранить указатель на последний узел (более эффективно просто прыгать прямо до конца списка, чем нужно перебирать до конца):
struct Node *lastNode = NULL;
В этом случае необходимо обновить его при внесении изменений в список.
Иногда ключ используется для идентификации элементов. Он просто является членом структуры узла:
struct Node {
int data;
int key;
struct Node* next;
struct Node* previous;
};
Затем ключ используется, когда любые задачи выполняются над конкретным элементом, например удалением элементов.