Когда вы в первый раз добавляете элемент (назовем его A
) в список, head
равен нулю, и вы проходите через часть if
. Это означает, что и head
, и tail устанавливаются так, чтобы указывать на A
при добавлении этого первого элемента.
Теперь давайте добавим еще один элемент B
. На этот раз head
не равно нулю, поэтому он проходит через часть else
, устанавливая tail
для указания на B
, но оставляя head
указывая на A
.
Это, как и ожидалось, теперь у вас есть head
, указывающий на A
, A
, указывающий на B
, B
, указывающий на ничто (ноль) и tail
, указывающий на B
.
Давайте рассмотрим шаг за шагом.
Initial state: head -+-> null
|
tail -+
Insert item A: head -+-> A ---> null
|
tail -+
Insert item B: head ---> A -+-> B ---> null
|
tail --------+
Insert item C: head ---> A ---> B -+-> C ---> null
|
tail ---------------+
Вы можете видеть на каждом этапе (кроме начального), что текущий хвост настроен так, чтобы указывать на новый узел (который уже указывает на NULL для его следующего узла), а затем указатель хвоста обновляется, чтобы указывать на новый последний узел.
На самом деле, давайте пройдем добавление C даже на больше деталей (строка за строкой), чтобы вы могли видеть, что делает каждая строка кода (я переименовал node_temp
в * 1036) * просто чтобы помочь с форматированием):
Starting state: head ---> A -+-> B ---> null
|
tail --------+
node = malloc(sizeof(*node)); node ---> C ----------> ?
(allocate node C) head ---> A -+-> B ---> null
|
tail --------+
node->next = NULL; node ---> C --------+
(ignore payload cel/fah |
for now since it's not head ---> A -+-> B -+-> null
relevant to the list |
structure) tail --------+
tail->next = node; node ---------------+
(first in else clause) |
head ---> A -+-> B -+-> C ---> null
|
tail --------+
tail = node; node ---------------+
(second in else clause) |
head ---> A ---> B -+-> C ---> null
|
tail ---------------+
Затем, в конце концов, node
исчезает, поскольку это локальная переменная, и у вас есть конечное состояние:
head ---> A ---> B -+-> C ---> NULL
|
tail ---------------+
Преимущество сохранения указателя tail
в односвязном списке состоит в том, что вам не нужно обходить весь список, чтобы найти конец при попытке добавить элемент в конец.
Обход по всему списку делает вставку в конце операции O(n)
времени (время зависит от количества элементов в списке). Использование указателя tail
делает эту операцию O(1)
времени (то же время независимо от размера списка).
Кроме того, двусвязный список имеет дополнительное использование для указателя tail
- он дает возможность быстро начать обход с конца списка до начала, используя tail
и prev
указатели вместо head
и указатели next
.