Skip to the content.

23 ноября 2021

Связный список

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

struct ListNode {
    int value;
    struct ListNode *next;
};
typedef struct ListNode ListNode;

Цепочка из таких структур и является связным списком:

Поле next последнего элемента списка имеет значение NULL.

Первый элемент списка называют головой (head), а последний - хвостом (tail). Удобно иметь быстрый доступ к голове и хвосту списка, поэтому определим еще одну структуру:

typedef struct {
    size_t size;
    ListNode* head;
    ListNode* tail;
} List;

Поле size содержит информацию о количестве элементов в списке. Инициализировать пустой список будем с помощью функции:

List init_list() {
    List l = {0, NULL, NULL};
    return l;
}

Удаление элемента из списка выполняется посредством изменения ссылок:

Удаление головы и хвоста списка следует обрабатывать специальным образом. Аналогично выполняется вставка нового элемента:

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

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

Реализуем следующие функции:

// Создание элемента списка
ListNode* init_listnode(int value, ListNode* next) {
    ListNode* newnode = malloc(sizeof(ListNode));
    newnode->value = value;
    newnode->next = next;
    return newnode;
}
// Вставка элемента после данного
void listnode_insert_next(ListNode* node, int value) {
    ListNode* current_next = node->next;
    ListNode* newnode = init_listnode(value, current_next);
    node->next = newnode;
}
// Удаление элемента, следующего за данным
void listnode_remove_next(ListNode* node) {
    if (node == NULL || node->next == NULL) return;
    ListNode* current_next = node->next;
    node->next = node->next->next;
    free(current_next);
}
// Вывод в консоль всех значений элементов, начинася с данного
void listnode_print(ListNode* node) {
    while (node != NULL) {
        printf("%d%s", node->value, node->next == NULL ? "\n" : " -> ");
        node = node->next;
    }
}
// Вставка элемента в начало списка
void list_push_front(List* l, int value) {
    ListNode* new_head = init_listnode(value, l->head);
    l->head = new_head;
    if (!l->size) l->tail = new_head;
    ++l->size;
}
// Вставка элемента в конец списка
void list_push_back(List* l, int value) {
    // Реализуйте эту функцию самостоятельно
}
// Вывод всех значений списка в стандартный поток вывода
void list_print(List* l) {
    if (l == NULL) return;
    listnode_print(l->head);
}
// Удаление первого элемента и возвращение его значения
int list_pop_front(List* l) {
    if (l == NULL || !l->size) return 0;
    ListNode* current_head = l->head;
    int value = current_head == NULL ? 0 : current_head->value;
    l->head = l->head->next;
    free(current_head);
    if (!--l->size) l->tail = NULL;
    return value;
}
// Поиск первого элемента списка с данным значением.
// Функция возвращает указатель на найденный элемент.
// Функция возвращает NULL, если элемент не найден
ListNode* list_find(List* l, int value) {
    if (l == NULL) return NULL;
    ListNode* node = l->head;
    while (node != NULL) {
        if (node->value == value) return node;
        node = node->next;
    }
    return NULL;
}
// Обмен местами двух элементов списка
void list_swap(ListNode* node1, ListNode* node2) {
    // Реализуйте эту функцию самостоятельно
}

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

Упражнение 1. Найти первые 10000 чисел, составленные только из нулей и девяток, и вывести их в порядке возрастания.