带头结点的单链表基础操作(C语言版)

1. 什么是链表?(线性表的链式存储结构)

链表是由节点(Node) 组成的线性结构,每个节点包含两部分:

  • 数据域(data):存储实际业务数据(如整数、字符串、结构体等);
  • 指针域(next):存储下一个节点的内存地址,相当于“链条”,将分散的节点串联成一个整体。 与数组相比,链表的存储单元可以是不连续的——这意味着它无需提前申请大块连续内存,数据新增时动态分配内存即可,极大提升了内存利用率。在这里插入图片描述

2. 定义链表结点结构

  1. 创建一个数据域data来存储数据
  2. 创建一个指向目标类型的指针next来存储后继结点的地址,实现连接操作
typedef struct Node{
	int data;
	struct Node *next;
}Node; 

3. 初始化链表,创建头结点

头结点是链表的“哨兵节点”,不存储实际业务数据,仅用于简化操作。没有头结点的链表(无头链表)会面临以下问题:

  • 插入第一个数据节点时,需要修改头指针(空表时头指针为NULL);
  • ​删除第一个数据节点时,同样需要修改头指针(设计二级指针的问题),逻辑判断复杂。 而带头结点的链表,无论链表是否为空,头指针始终指向头结点——这让“插入/删除第一个节点”与“插入/删除其他节点”的逻辑完全统一,无需额外处理边界条件。
Node * Init_List(){
   //分配头结点内存
   Node * head = (Node*)malloc(sizeof(Node));
   if (head == NULL){
       printf("分配失败!\n");
       return NULL;
   }
   head->next = NULL;//指向的后继在链表为空时为NULL
   return head;
}

定义了一个如图所示的head头结点
在这里插入图片描述

4. 插入操作

因为进行的都是地址层面的操作(链表的本质还是通过指针指向各个结点的地址实现连接操作),所以函数参数自然也是指针类型

头插法(在链表头部插入新节点)

void Head_Insert(Node *head,int data)
{
    Node * newNode = (Node*)malloc(sizepf(Node));
    if (newNode == NULL)
    {
        printf("插入失败,内存分配错误!\n");
        return ;
    }
    newNode->data = data;
    //接下来为了防止head后继所有节点的丢失,需要先让newNode的后继节点指向head的后继节点
    newNode->next = head->next;
    //此时只需要让head的后继节点指向newNode自身的地址便实现了头插法插入结点
    head->next = newNode;
}

在这里插入图片描述
在这里插入图片描述


尾插法(在链表尾部插入新结点)

实现思路与头插法类似,但是由于需要找到整个链表的尾部,需要先经历一次遍历寻找尾部
可以新定义一个指针p赋以head中的地址,用while循环来寻找尾部,当不满足条件时,指针p后移,直到找到尾部

为啥不能直接用head = head->next呢?其实在这并无所谓,出于函数值传递的特性,虽然不影响外部head,但函数内部也无法保留遍历结果(副本会销毁),且逻辑上这是对参数的误用(参数的作用是传入链表入口,而非作为遍历的临时指针)。

void Tail_Insert(Node *head,int data)
{
    Node * newNode = (Node*)malloc(sizepf(Node));
    if (newNode == NULL)
    {
        printf("插入失败,内存分配错误!\n");
        return ;
    }
    newNode->data = data;
    Node *p = head;
    while (head->next != NULL)
    {
        p = p->next;
    }
    p->next = newNode;
    newNode->next = NULL;
}

在这里插入图片描述
在这里插入图片描述

指定位置插入

与尾插法的逻辑其实是类似的,只不过要在循环遍历中找到‘要插入的位置’
这里以在70 80两个链表间插入新结点为例
在这里插入图片描述

void Insert_ByPos(Node *head,int data,int pos)//pos为实际存储数据的结点位置
{
    if (pos < 1) {
        printf("插入失败:位置不合法(pos >= 1)\n");
        return;
    }
    // 找到第pos-1个节点(插入位置的前驱)
    Node* p = head;
    int i = 0;
    while (p != NULL && i < pos - 1) {
        p = p->next;
        i++;
    }
    // 若p为NULL,说明pos超过链表长度
    if (p == NULL) {
        printf("插入失败:位置超过链表长度\n");
        return;
    }
    // 创建新节点并插入
    Node* newNode = (Node*)malloc(sizeof(Node));
    if (newNode == NULL) {
        printf("插入失败:内存分配错误\n");
        return;
    }
    newNode->data = data;
    newNode->next = p->next;  // 新节点指向前驱的后继
    p->next = newNode;        // 前驱指向新节点
}

在这里插入图片描述
在这里插入图片描述

5. 删除结点

按位置删除

其实原理与插入也差不多,只不过多了一步释放结点的步骤,那就要有一个临时指针指q向此结点,调用free函数

  1. 找到要删除节点的前置节点 p
  2. 用指针 q 记录要删除的节点
  3. 通过改变 p 的后继节点实现删除
  4. 释放删除节点的空间
void Delete_ByPos(Node *head,int pos)
{
    if (pos < 1)
    {
        printf("删除失败,位置不合法");
        return ;
    }
    Node *p = head;
    int i = 0;
    while (p != NULL && i < pos - 1)
    {
        p = p->next;
        i++;
    }
    Node *q = p->next;
    p->next = q->next;
    free(q);
    q = NULL;
}

为啥要有这个temp呢?其实还是为了保存后继节点不让其丢失

步骤类似于:
在这里插入图片描述

在这里插入图片描述


按值删除

思路同上

void Delete_ByValue(Node* head, int value) {
    // 查找值为value的节点的前驱
    Node* p = head;
    while (p->next != NULL && p->next->data != value) {
        p = p->next;
    }
    // 若p->next为NULL,说明未找到该值
    if (p->next == NULL) {
        printf("删除失败:值 %d 不存在\n", value);
        return;
    }
    // 删除节点
    Node* temp = p->next;
    p->next = temp->next;
    free(temp);
    temp = NULL;
}

适用场景:仅知道数据内容,不知道位置(如删除“成绩为60的学生节点”)。

6. 查找结点

其实接下来的几个操作都是基本思路都是一样的,都离不开遍历链表—寻找目标的步骤,接下来直接给出相关代码

按位置查找(返回该位置的值,-1表示位置无效)

int Find_ByPos(Node *head,int pos)
{
    Node *p = head->next;
    int i = 1;
    while (p != NULL && i < pos)
    {
        p = p->next;
        i++;
    }
    if (p == NULL)
    {
        printf("位置不存在!");
        return -1;
    }
    return p->data;
}

按值查找(返回第一个匹配节点的位置,-1表示不存在)

int Find_ByValue(Node * head, int value){
    Node * p = head->next;
    int pos = 1;
    while(p != NULL){
        if(p->data == value){
            return pos;
        }
        p = p->next;
    }
    return -1;
}

7. 修改结点数据

按位置修改

void Update_ByPos(Node *head, int pos, int newData){
    if(pos < 1){
        printf("位置错误");
        return ;
    }
    Node *p = head->next;
    int i = 1;
    while(p != NULL && i < pos){
        p = p->next;
        i++;
    }
    if(p == NULL){
        printf("位置不存在");
        return ;
    }
    p->data = newData;
}

按值修改(修改第一个匹配的节点为新值)

void Update_ByValue(Node * head, int oldValue, int newValue){
    Node * p = head->next;
    while (p != NULL && p->data != oldValue){
        p = p->next;
    }
    if(p == NULL){
        printf("没有此值");
        return;
    }
    p->data = newValue;
}

8. 遍历链表(打印所有节点数据)

void Traverse_List(Node* head){
    Node *p = head->next;
    if(p == NULL){
        printf("链表为空");
        return;
    }
    while (p != NULL){
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

9. 销毁链表(释放所有节点内存,包括头结点)

链表的节点是动态分配的( malloc ),程序结束前必须手动释放所有节点,否则会导致内存泄漏。销毁逻辑是“从表头开始,逐个释放节点”。

void Free_List(Node *head){
    if(head == NULL){
        return;
    }
    Node *p = head;
    Node *temp;
    while(p != NULL){
        temp = p;
        p = p->next;
        free(temp);
    }
    printf("链表节点已全部释放");
}

最后别忘了在main函数主体内调用head = NULL;`来释放头结点避免野指针的问题!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值