В вашем случае голова и хвост могут просто указывать на начало и конец связанного списка. С односвязным списком действительно нужна только голова. По своей сути, связанный список может быть создан с использованием такой структуры, как:
typedef struct listnode
{
//some data
struct listnode *next;
}listnodeT;
listnodeT *list;
listnodeT *current_node;
list = (listnodeT*)malloc(sizeof(listnodeT));
current_node = list;
и пока список всегда указывает на начало списка, а последний элемент имеет следующий значение NULL, вы в порядке и можете использовать current_node для обхода списка. Но иногда, чтобы упростить обход списка и хранить любые другие данные о списке, используются жетоны головы и хвоста, которые обертываются в их собственную структуру, как вы это сделали. Тогда ваши функции добавления и инициализации будут выглядеть примерно так (минус обнаружение ошибок)
// Initialize linked list
void initialize(list_t *list)
{
list->head = NULL;
list->tail = NULL;
}
void add(list_t *list, int code, char name[], int cost)
{
// set up the new node
product_data_t *node = (product_data_t*)malloc(sizeof(product_data_t));
node->code = code;
node->cost = cost;
strncpy(node->product_name, name, sizeof(node->product_name));
node->next = NULL;
if(list->head == NULL){ // if this is the first node, gotta point head to it
list->head = node;
list->tail = node; // for the first node, head and tail point to the same node
}else{
tail->next = node; // append the node
tail = node; // point the tail at the end
}
}
В этом случае, поскольку это односвязный список, хвост очень полезен только для добавления элементов в список. Чтобы вставить элемент, вам придется пройти по списку, начиная с заголовка. Если хвост действительно пригодится, это список с двойными связями, он позволяет просматривать список, начиная с любого конца. Вы можете пройти этот список как
// return a pointer to element with product code
product_data_t* seek(list_t *list, int code){
product_data_t* iter = list->head;
while(iter != NULL)
if(iter->code == code)
return iter;
iter = iter->next;
}
return NULL; // element with code doesn't exist
}
Часто голова и хвост являются полностью построенными узлами, которые сами используются в качестве часового для обозначения начала и конца списка. Они сами не хранят данные (ну, скорее, их данные представляют собой маркер стража), они просто являются заполнителями для передней и задней частей. Это может упростить кодирование некоторых алгоритмов, связанных со связанными списками, за счет необходимости иметь два дополнительных элемента. В целом, связанные списки представляют собой гибкие структуры данных с несколькими способами их реализации.
о да, и Ник прав, игра со связанными списками - отличный способ справиться с указателями и косвенными ссылками. И они также отличный способ практиковать рекурсию! После того, как вы освоитесь со связным списком, попробуйте построить следующее дерево и использовать рекурсию для обхода дерева.