Algorithm 单链表尾
如果要创建如下所示的单链接列表:Algorithm 单链表尾,algorithm,list,data-structures,nodes,Algorithm,List,Data Structures,Nodes,如果要创建如下所示的单链接列表: struct Node { int data; Node *next; }; struct List{ Node *head; // Node *tail; --> necessary? Node *last; }; 该列表有“追加”、“删除”、“打印列表”和“findElement”方法。 有必要有一条尾巴吗?因为使用“last”可以寻址最后一个节点 那么,什么时候有必要将所有三个节点都设为“头”、“尾”和
struct Node {
int data;
Node *next;
};
struct List{
Node *head;
// Node *tail; --> necessary?
Node *last;
};
该列表有“追加”、“删除”、“打印列表”和“findElement”方法。
有必要有一条尾巴吗?因为使用“last”可以寻址最后一个节点
那么,什么时候有必要将所有三个节点都设为“头”、“尾”和“尾”?例如,当您要插入排序到列表中的节点时?否,不需要这样做。尾部等于
head->next
,因此它将是多余的,并增加簿记开销以保持此字段的更新
还要注意的是,
last
字段有点不寻常。在大多数用例中,您可以将元素添加到单个链接列表的开头,并在确实需要添加到末尾时使用不同的数据结构。如果您以FIFO方式(而非后进先出方式)的队列方式处理链接列表,或者希望能够从一个列表中传输整个元素列表,则尾部可能非常有用在不破坏元素相对顺序的情况下直奔他人的尾巴
请注意,我所指的“tail”是对列表中最后一个节点的引用,我认为可以安全地假设问题是关于这个节点的
许多非常微观优化的SLL实现通常都是无尾的,就像一个堆栈一样工作,同时有一个高效的固定分配器支持引用的局部性(缓存友好性)和更快的节点分配/释放。与基于数组的可变大小序列相比,SLL的主要优点是,只要更改next
指针/引用的值,就可以开始移动,并且如果您使用的是涉及指针的本机低级语言,则插入/删除元素时不会失效。通过减少从堆栈中推送和弹出操作所需的分支指令量,缺少尾部可以大大提高性能
对于您列出的需求,如果您的
append
和remove
操作可以严格地从前面以后进先出的方式工作,或者如果您希望能够以后进先出的方式追加到后面,但以先进先出的方式从前面删除,那么尾部是有帮助的还是只会增加不必要的复杂性和开销,例如,在后一种情况下,如果没有尾部,其中一个操作将从恒定时间复杂度变为线性时间复杂度,您可以通过将线性时间算法复杂性转换为相对较小的维护尾部的微观开销来改进您的用例。我认为这取决于您想要使用的操作
实际上,您可以实现排队(在尾部追加)、推(在头部前置)、出列(从头部移除),当然还可以使用一个指针头查找和打印。诀窍是使列表循环并使标题指向尾部。然后
tail->next
是头部
#include <stdio.h>
#include <stdlib.h>
typedef struct node_s {
struct node_s *next;
int data;
} Node;
typedef struct list_s {
Node *tail;
} List;
Node *new_node(int data) {
Node *node = malloc(sizeof *node);
node->data = data;
node->next = node;
return node;
}
void init_list(List *list) {
list->tail = NULL;
}
int is_empty(List *list) {
return list->tail == NULL;
}
void enqueue(List *list, Node *node) {
if (list->tail) {
Node *head = list->tail->next;
node->next = head;
list->tail->next = node;
list->tail = node;
} else list->tail = node->next = node;
}
void push(List *list, Node *node) {
if (list->tail) {
Node *head = list->tail->next;
node->next = head;
list->tail->next = node;
} else list->tail = node->next = node;
}
Node *dequeue(List *list) {
Node *head = list->tail->next;
if (head == list->tail)
list->tail = NULL;
else
list->tail->next = head->next;
return head;
}
void print_list(List *list) {
printf("The list:\n");
if (list->tail) {
Node *head = list->tail->next;
Node *p = head;
do {
printf("%d\n", p->data);
p = p->next;
} while (p != head);
}
}
int main(int argc, char *argv[]) {
List list[1];
init_list(list);
// Build the list in order and print it.
for (int i = 0; i < 4; i++) enqueue(list, new_node(i));
print_list(list);
// Remove elements from head until empty.
printf("Dequeueing:\n");
while (!is_empty(list)) {
Node *node = dequeue(list);
printf("%d\n", node->data);
free(node);
}
// Build the list in reverse order and print it.
for (int i = 0; i < 4; i++) push(list, new_node(i));
print_list(list);
return 0;
}
#包括
#包括
类型定义结构节点{
结构节点*next;
int数据;
}节点;
类型定义结构列表{
节点*尾部;
}名单;
节点*新节点(整数数据){
Node*Node=malloc(sizeof*Node);
节点->数据=数据;
节点->下一步=节点;
返回节点;
}
无效初始列表(列表*列表){
list->tail=NULL;
}
int为空(列表*列表){
返回列表->尾部==NULL;
}
无效排队(列表*列表,节点*节点){
如果(列表->尾部){
节点*头部=列表->尾部->下一步;
节点->下一步=头部;
列表->尾部->下一步=节点;
列表->尾部=节点;
}else list->tail=node->next=node;
}
无效推送(列表*列表,节点*节点){
如果(列表->尾部){
节点*头部=列表->尾部->下一步;
节点->下一步=头部;
列表->尾部->下一步=节点;
}else list->tail=node->next=node;
}
节点*出列(列表*列表){
节点*头部=列表->尾部->下一步;
如果(头部==列表->尾部)
list->tail=NULL;
其他的
列表->尾部->下一步=头部->下一步;
回流头;
}
作废打印列表(列表*列表){
printf(“列表:\n”);
如果(列表->尾部){
节点*头部=列表->尾部->下一步;
节点*p=头部;
做{
printf(“%d\n”,p->data);
p=p->next;
}而(p!=头);
}
}
int main(int argc,char*argv[]){
清单[1];
初始列表(列表);
//按顺序建立列表并打印。
对于(inti=0;i<4;i++)排队(列表,新_节点(i));
打印列表(列表);
//从头部移除元件,直到其为空。
printf(“出列:\n”);
而(!为空(列表)){
节点*节点=出列(列表);
printf(“%d\n”,节点->数据);
自由(节点);
}
//按相反顺序构建列表并打印。
对于(inti=0;i<4;i++)推送(列表,新_节点(i));
打印列表(列表);
返回0;
}
tail
和last
是相同的。同一节点的不同常规名称。Tail可以表示“而不是head”,即列表的其余部分。“last”可以表示在列表末尾之前。例如,这是在Haskell中完成的。我不认为last
是不寻常的,连接两个列表,附加到一个列表,使用子列表与last
相比是便宜的,这些是列表的常见操作。