Линейный список. Логическое описание: голова и хвост

Модуль "Линейные списки" • урок 2 из 9 • ~5 минут

Суть урока. Логически список - это рекурсивная конструкция, которая состоит из головы (первого элемента) и хвоста (всего остального списка). База такой рекурсии - пустой список [].
Линейный список. Логическое описание: список из 1, 2 и 3 раскладывается на голову 1 и хвост из 2 и 3, тот - на голову 2 и хвост из 3, и так до пустого списка
Отщепляем голову, а хвост разбираем тем же правилом - до пустого списка

Исторический контекст (Prolog и Lisp)

В языке C встроенного логического типа "список" не существует - его логика слита с физической реализацией через указатели. Однако в функциональных (Lisp) и логических (Prolog) языках программирования список является главной встроенной структурой данных.

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

Как описывается список в Лиспе (Lisp)?

В языке Lisp списки описываются круглыми скобками, а элементы внутри них разделяются пробелами. Например: (1 2 3) или (A B C).

Главная особенность Лиспа заключается в том, что списки служат одновременно и структурой данных, и исполняемым кодом. По умолчанию интерпретатор пытается вычислить любой переданный список, воспринимая первый элемент как имя функции, а остальные - как ее аргументы (например, список (+ 1 2) вычислится в 3).

Чтобы создать список именно как набор данных, в коде используются два разных инструмента:

Оператор quote (одинарная кавычка ') - полностью блокирует вычисление. Он дает команду интерпретатору воспринимать содержимое буквально, "как есть".

Пример: '(A B C) вернет список (A B C). Без кавычки интерпретатор выдал бы ошибку, попытавшись вызвать несуществующую функцию A.

Функция list - в отличие от quote, сначала вычисляет каждый переданный ей аргумент, и только затем собирает полученные результаты в новый список.

Пример: (list 1 (+ 1 1) 3) вычислит центральное выражение и вернет готовый список данных (1 2 3).

Линейный список. Логическое описание: разделение на голову и хвост

Базовая операция в Прологе - отделение головы от хвоста. Если мы применим эту операцию к списку [1, 2, 3], то:

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

А как в Python? Для программиста доступно удобное обращение по индексу: list[5]. Однако в классическом логическом понимании списков, обращение к i-му элементу должно занимать время O(N), так как программе нужно пройти от головы до нужного узла по цепочке. В Python этот долгий проход просто скрыт внутри перегруженного оператора (хотя физически CPython оптимизирует это массивом указателей, позволяя доступ за O(1)).

Эмуляция логики Пролога на C

Чтобы лучше понять суть разделения на "голову" и "хвост", посмотрим, как список можно построить в C в функциональном стиле. Для этого понадобится функция Cons, склеивающая элемент-голову и хвост:

Список в функциональном стиле: код целиком C, 26 строк
#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
    int head;             /* Голова (элемент) */
    struct Node* tail;    /* Хвост (весь остальной список) */
} Node;

/* Функция склеивания Головы и Хвоста */
Node* Cons(int head, Node* tail) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->head = head;
    newNode->tail = tail;
    return newNode;
}

int main(void) {
    /* Строим список [1, 2, 3] рекурсивно, как в Lisp/Prolog: */
    Node* list = Cons(1, Cons(2, Cons(3, NULL)));
    
    /* Логическое отделение головы от хвоста: */
    int first_element = list->head;      /* 1 */
    Node* rest_of_list = list->tail;     /* Список [2, 3] */
    
    return 0;
}

Коротко: главное

  • В декларативных языках (Prolog, Lisp) список - базовая структура данных.
  • Логически список - это рекурсивная конкатенация головы (первого элемента) и хвоста (остального подсписка).
  • Обработка такого списка всегда идет рекурсивно: отщепляем голову, обрабатываем, рекурсивно вызываемся для хвоста.
  • В императивных языках (например, в C) такое описание отсутствует как встроенное и эмулируется через структуры и указатели.

Проверьте себя

Чему будет равен Хвост, если отделить его от списка [4, 8]?

Частые вопросы (FAQ) про линейный список

Частый вопрос из тестов: Какая базовая структура используется в Lisp для списков?

На низком уровне (в памяти) любой список в Лиспе представляет собой классический односвязный список, который конструируется из базовых строительных блоков - cons-ячеек (также известных как "точечные пары").

Каждая cons-ячейка состоит строго из двух указателей:

  • CAR (голова) - хранит указатель на само значение (текущий элемент списка).
  • CDR (хвост) - хранит указатель на следующую cons-ячейку в цепочке.

Списки выстраиваются путем связывания этих ячеек друг с другом. Чтобы цепочка считалась корректно завершенным списком (proper list), указатель CDR самой последней ячейки должен ссылаться на пустой список - маркер NIL.

Таким образом, привычный нам синтаксис (1 2 3) - это лишь синтаксический сахар. Интерпретатор "под капотом" разворачивает его в структуру из вложенных точечных пар, которая структурно выглядит так:

(1 . (2 . (3 . NIL)))

(Подсказка для тестов: списки собираются базовой функцией cons, а разбираются на элементы историческими функциями car и cdr).

Что такое "голова" и "хвост" списка?
Логически список разделяется на первый элемент, который называется головой, и весь остальной подсписок, который называется хвостом. Базой этой рекурсии выступает пустой список.
Зачем нужно отделение головы от хвоста?
Это базовая операция для обработки списков в функциональном и логическом программировании. Мы отщепляем голову, выполняем действие с ней, а затем рекурсивно вызываем ту же функцию для оставшегося хвоста.
Есть ли встроенный логический тип "список" в языке C?
Нет, в классическом C такого встроенного типа не существует. Его логика эмулируется физически через структуры (struct) и указатели на следующий узел.
Какая алгоритмическая сложность доступа к элементу списка по индексу?
В классическом логическом списке обращение к i-му элементу занимает время O(N), так как программе необходимо последовательно пройти от головы до нужного узла по цепочке ссылок.
Как вам урок? Спасибо за отзыв!