判空条件
head == NULL 是用于判断不带头结点的链表是否为空的条件表达式。
在不带头结点的链表中:
- 头指针
head
直接指向链表的第一个有效数据结点 - 如果链表中没有任何结点(即空链表),则
head
的值为NULL
- 因此,
head == NULL
为真表示链表为空
head->next == NULL 是一个条件表达式,用于判断链表是否为空(不包含任何数据结点)。
-
head
是头指针,指向链表的第一个结点(在带头结点的链表中,它指向头结点) -
->
是C/C++中的指针成员访问运算符,用于访问指针所指向结构体的成员 -
next
是结点结构体中的指针成员,指向下一个结点 -
head->next
表示头指针指向的结点(即头结点)的next
指针的值 -
== NULL
判断这个next指针是否为空
在带头结点的链表中:
头指针head
永远指向头结点(图中显示的地址是1000)
当链表为空时,头结点的next
指针为NULL
因此,通过检查head->next == NULL
来判断链表是否为空
不带头结点的链表操作
不带头结点时,需要特殊处理首结点操作
在不带头结点的链表中,头指针直接指向第一个数据结点。当我们需要在链表头部插入或删除结点时,必须修改头指针本身,这与在链表中间操作的逻辑不同
// 定义链表结点结构
typedef struct Node {
int data; // 结点的数据域
struct Node* next; // 指向下一个结点的指针
} Node;
// 在链表中间插入结点
// 参数:prev - 插入位置的前一个结点
// value - 要插入的数据值
void insertMiddle(Node* prev, int value) {
// 1. 创建新结点
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = value; // 设置新结点的数据
// 2. 调整指针,完成插入
newNode->next = prev->next; // 新结点指向前一结点的后继
prev->next = newNode; // 前一结点指向新结点
}
// 在链表头部插入结点
// 注意:需要修改头指针,因此使用二级指针
// 参数:head - 指向头指针的指针
// value - 要插入的数据值
void insertHead(Node** head, int value) {
// 1. 创建新结点
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = value; // 设置新结点的数据
// 2. 调整指针,完成插入
newNode->next = *head; // 新结点指向原来的第一个结点
*head = newNode; // 更新头指针,指向新结点
// 注意:这里修改了头指针本身,与链表中间插入操作逻辑不同
}
// 删除链表中间的结点
// 参数:prev - 要删除结点的前一个结点
void deleteMiddle(Node* prev) {
// 1. 保存要删除的结点
Node* temp = prev->next;
// 2. 调整指针,跳过要删除的结点
prev->next = temp->next; // 前一结点直接指向被删结点的后继
// 3. 释放内存
free(temp);
}
// 删除链表头部结点
// 注意:需要修改头指针,因此使用二级指针
// 参数:head - 指向头指针的指针
void deleteHead(Node** head) {
// 空链表检查
if (*head == NULL) return; // 如果链表为空,直接返回
// 1. 保存要删除的头结点
Node* temp = *head;
// 2. 更新头指针,指向第二个结点
*head = temp->next;
// 注意:这里修改了头指针本身,与链表中间删除操作逻辑不同
// 3. 释放内存
free(temp);
}
带头结点的链表操作
带头结点时,操作逻辑统一
带头结点的链表中,头指针始终指向不变的头结点,而头结点的next指针指向第一个数据结点。这样,插入或删除第一个数据结点时,只需修改头结点的next指针,而不需要修改头指针本身。
// 定义链表结点结构(与前面相同)
typedef struct Node {
int data; // 结点的数据域
struct Node* next; // 指向下一个结点的指针
} Node;
// 创建带头结点的空链表
Node* createList() {
// 创建头结点
Node* head = (Node*)malloc(sizeof(Node));
head->next = NULL; // 头结点的next指向NULL,表示空链表
// 头结点的data域通常不使用,可以设置为特殊值或存储链表信息
return head; // 返回头结点指针(即头指针)
}
// 在任意结点后插入新结点(统一操作)
// 参数:prev - 插入位置的前一个结点(可以是头结点)
// value - 要插入的数据值
void insert(Node* prev, int value) {
// 1. 创建新结点
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = value; // 设置新结点的数据
// 2. 调整指针,完成插入
newNode->next = prev->next; // 新结点指向前一结点的后继
prev->next = newNode; // 前一结点指向新结点
// 注意:插入第一个数据结点和中间结点的操作完全相同
// 若在第一个数据结点前插入,只需传入头结点作为prev
}
// 删除任意结点后的结点(统一操作)
// 参数:prev - 要删除结点的前一个结点(可以是头结点)
void delete(Node* prev) {
// 检查是否有结点可删除
if (prev->next == NULL) return; // 如果后面没有结点,直接返回
// 1. 保存要删除的结点
Node* temp = prev->next;
// 2. 调整指针,跳过要删除的结点
prev->next = temp->next; // 前一结点直接指向被删结点的后继
// 3. 释放内存
free(temp);
// 注意:删除第一个数据结点和中间结点的操作完全相同
// 若删除第一个数据结点,只需传入头结点作为prev
}
// 判断链表是否为空
bool isEmpty(Node* head) {
return head->next == NULL; // 检查头结点的next是否为NULL
}
// 释放整个链表(包括头结点)
void freeList(Node* head) {
Node* current = head;
Node* next;
// 逐个释放所有结点
while (current != NULL) {
next = current->next; // 保存下一个结点
free(current); // 释放当前结点
current = next; // 移动到下一个结点
}
}
使用示例
// 不带头结点的链表操作示例
void demoWithoutHeadNode() {
Node* head = NULL; // 初始化空链表
// 插入操作
insertHead(&head, 10); // 插入第一个结点,需要传递头指针的地址
insertHead(&head, 20); // 继续在头部插入,仍需传递头指针的地址
// 假设我们找到了值为20的结点
Node* node20 = head;
insertMiddle(node20, 15); // 在20后插入15,不需要修改头指针
// 删除操作
deleteHead(&head); // 删除头部结点,需要传递头指针的地址
// 删除中间结点...
}
// 带头结点的链表操作示例
void demoWithHeadNode() {
Node* head = createList(); // 创建带头结点的空链表
// 插入操作,所有插入操作都使用同一个函数
insert(head, 10); // 在头结点后插入10,成为第一个数据结点
insert(head, 20); // 在头结点后插入20,成为新的第一个数据结点
// 假设我们找到了值为20的结点
Node* node20 = head->next;
insert(node20, 15); // 在20后插入15,使用同样的插入函数
// 删除操作,所有删除操作都使用同一个函数
delete(head); // 删除第一个数据结点
// 删除其他结点...
// 检查链表是否为空
if (isEmpty(head)) {
printf("链表为空\n");
}
// 最后释放整个链表
freeList(head);
}
优势总结
带头结点的链表提供了以下优势:
- 代码统一:不需要为首结点操作编写特殊代码
- 参数简化:操作函数只需要接收普通指针,而不是指针的指针(二级指针)
- 条件判断减少:不需要判断是否为空链表的特殊情况
- 逻辑清晰:所有结点(包括第一个数据结点)都有前驱结点
这种统一性使得链表操作更加简洁和不易出错,特别是在复杂的链表操作(如合并、分割等)中,带头结点的设计可以显著简化代码逻辑。