头结点与头指针

在这里插入图片描述

判空条件

在这里插入图片描述
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);
}

优势总结

带头结点的链表提供了以下优势:

  1. 代码统一:不需要为首结点操作编写特殊代码
  2. 参数简化:操作函数只需要接收普通指针,而不是指针的指针(二级指针)
  3. 条件判断减少:不需要判断是否为空链表的特殊情况
  4. 逻辑清晰:所有结点(包括第一个数据结点)都有前驱结点

这种统一性使得链表操作更加简洁和不易出错,特别是在复杂的链表操作(如合并、分割等)中,带头结点的设计可以显著简化代码逻辑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值