当前位置: 首页 > news >正文

(C语言)单链表(2.0)数据结构(指针,单链表教程)

#include <stdio.h>
#include <stdlib.h>

//函数结果状态代码
#define OK 1
#define ERROR 0

typedef int Status;//函数返回状态,ok,error
typedef int Elemtype;//链表元素为整形
typedef struct Lnode//定义结构体
{
    Elemtype data;//数据域
    struct Lnode* next;//指针域
}Lnode,*LinkList;//单个结点,整个链表(指向结点的指针)

//初始化链表(建立一个头结点)
Status InitLinkList(LinkList* L){
    *L=(LinkList)malloc(sizeof(Lnode));//分配头结点内存
    if(*L==NULL){
        return ERROR;//判断是否分配成功
    }
    (*L)->next=NULL;//头结点的指针域为空
    return OK;
}

//判断链表是否为空
Status IsEmptyLinkList(const LinkList* L){
    if((*L)->next==NULL){
        printf("该链表为空!\n");
        return ERROR;
    }else{
        printf("该链表不为空!\n");
        return OK;
    }
}

//求链表的长度
Status LenLinkList(const LinkList* L){
    int i=0;
    Lnode* p;
    p=(*L)->next;
    while (p!=NULL)
    {
        i++;
        p=p->next;
    }
    printf("该链表的长度为:%d\n",i);
    return OK;
}

//清空链表
Status ClearLinkList(LinkList* L){
    Lnode* p;
    Lnode* q;
    p=(*L)->next;
    while (p!=NULL)
    {
        q=p;
        p=p->next;
        free(q);
    }
    (*L)->next=NULL;
    printf("该链表已清空!\n");
    return OK;
}

//销毁链表
Status DestoryLinkList(LinkList* L){
    LinkList p;//定义一个临时的指向结点的指针
    while (*L!=NULL)
    {
        p=*L;//储存原来的指针(结点)
        *L=(*L)->next;//往后移动结点
        free(p);//释放原来的指针
    }
    printf("该链表已销毁!\n");
    return OK;
}

//链表的插入,头插法
Status CreateLinkList_h(LinkList* L,int n){
    InitLinkList(L);//创建头结点
    for(int i=0;i<n;i++){
        LinkList newlnode;//创建一个新结点
        newlnode=(Lnode*)malloc(sizeof(Lnode));//为新节点分配内存
        if(newlnode==NULL){
            return ERROR;//判断是否分配成功
        }
        printf("请输入数据:\n");
        scanf("%d",&newlnode->data);
        newlnode->next=(*L)->next;//使新结点指向原指针
        (*L)->next=newlnode;//使头指针指向新结点
    }
    return OK;
}

//链表的插入,尾插入
Status CreateLinkList_r(LinkList* L,int n){
    InitLinkList(L);//创建头结点
    LinkList p=*L;//定义临时尾结点
    for(int i=0;i<n;i++){
        LinkList newlnode;
        newlnode=(Lnode*)malloc(sizeof(Lnode));//给新结点分配内存
        if(newlnode==NULL){
            return ERROR;//判断是否分配成功
        }
        printf("请输入数据:\n");
        scanf("%d",&newlnode->data);
        newlnode->next=NULL;//使新结点指向空
        p->next=newlnode;//使原结点指向新结点
        p=p->next;//后移一次,定义新的尾结点
    }
    return OK;
}

//查看链表
Status ShowLinkList(const LinkList* L){
    Lnode* p=(*L)->next;//定义个临时结点
    if(p==NULL){
        printf("链表为空!\n");
        return OK;
    }
    int i=1;
    printf("该链表的数据为:\n");
    while (p!=NULL)
    {
        printf("%d : %d\n", i, p->data);  // 打印序号和数据
        i++;            // 序号递增
        p = p->next;    // p 移动到下一个结点
    }
    return OK;
}

//单链表插入,在第i个结点之前插入一个结点
Status InsertLinkList(LinkList *L, int i, Elemtype value)
{
	Lnode* p;
	p = (*L)->next;
	int j = 1;
	while (p && j < i - 1)		//找到第i-1个结点
	{
		j++;
		p = p->next;
	}
	if (!p || j > i - 1)		//i大于表长+1,或者小于1,插入位置非法
		return ERROR;

	Lnode* newlnode;
	newlnode = (Lnode*)malloc(sizeof(Lnode));
	newlnode->data = value;
	newlnode->next = p->next;
	p->next = newlnode;
	return OK;
}

//删除第i个结点
Status DelLinkList(LinkList* L,int i){
    int j=i;
    i=2;
    Lnode* p=(*L)->next;
    Lnode* q;
    while (j!=i)
    {
        i++;
        p=p->next;
    }
    q=p->next;
    p->next=q->next;
    free(q);
    return OK;
}

//查看第i个元素
Status LocatElem(const LinkList* L,int i){
    int j=i;//赋值给j
    i=1;//初始化i
    LinkList p=(*L)->next;//创建临时结点表示第一个结点
    if(p==NULL){
        printf("链表为空!\n");//判断链表是否为空
        return OK;
    }
    //逐步后移,直到i和j相等
    while (i!=j)
    {
        i++;
        p=p->next;
    }
    printf("第%d个 : %d\n", j, p->data);  // 打印第i个序号,和数据
    return OK;
}

//主函数
int main(){
    LinkList mylist;
    mylist=NULL;
    //CreateLinkList_h(&mylist,4);//头插
    CreateLinkList_r(&mylist,4);//尾插
    ShowLinkList(&mylist);
    InsertLinkList(&mylist, 3, 9);
    ShowLinkList(&mylist);
    LenLinkList(&mylist);
    DelLinkList(&mylist,3);
    ShowLinkList(&mylist);
    LocatElem(&mylist,2);
    IsEmptyLinkList(&mylist);
    ClearLinkList(&mylist);
    DestoryLinkList(&mylist);
}

上篇博客我们解释了一部分单链表的原理,今天继续完善单链表的查询和删除等操作:

单链表插入操作的原理详解(新手友好版)


1. 函数功能

这个函数的作用是:在单链表的第 i 个位置前插入一个新结点,新结点的值为 value


2. 关键变量解析
  • LinkList *L:指向头指针的指针(二级指针),用于访问整个链表

  • int i:要插入的位置(从1开始计数)

  • Elemtype value:新结点的值

  • Lnode* p:临时指针,用于遍历链表

  • Lnode* newlnode:新创建的结点


3. 插入过程分步解析
步骤1:定位插入位置的前驱结点
Lnode* p = (*L)->next;  // p从第一个数据结点开始(跳过头结点)
int j = 1;              // 计数器从1开始
while (p && j < i-1) {  // 寻找第i-1个结点
    p = p->next;
    j++;
}

作用
让指针 p 最终指向 第i-1个结点(即要插入位置的前一个结点)。

示例
如果要在第3个位置前插入,p 需要指向第2个结点:

复制

头结点 → [结点1] → [结点2] → [结点3] → NULL
                  ↑
                  p(此时i=3, j=2)

步骤2:检查位置合法性
if (!p || j > i-1) 
    return ERROR;  // 插入位置非法

可能出错的情况

  1. p == NULLi 超出链表长度(如链表只有2个结点,但想插入到第4个位置前)

  2. j > i-1i < 1(如传入 i = 0


步骤3:创建新结点
Lnode* newlnode = (Lnode*)malloc(sizeof(Lnode));
newlnode->data = value;  // 设置新结点的值

内存变化
在堆内存中分配一个新结点,并存储数据 value


步骤4:链接新结点
newlnode->next = p->next;  // 新结点指向原第i个结点
p->next = newlnode;        // 前驱结点指向新结点

关键操作

  1. newlnode->next = p->next
    让新结点的指针指向原来第i个位置的结点。

  2. p->next = newlnode
    让前驱结点(第i-1个结点)指向新结点。

图示(在第2个位置前插入新结点):

插入前:
头结点 → [A] → [B] → [C] → NULL
           ↑
           p(指向第1个结点)

插入后:
头结点 → [A] → [新结点] → [B] → [C] → NULL

4. 时间复杂度分析
  • 最好情况:插入到第1个位置前 → O(1)

  • 最坏情况:插入到链表末尾 → O(n)

  • 平均情况:O(n)


5. 易错点提醒
  1. 边界条件

    • 插入位置 i = 1(第一个数据结点前)

    • 插入位置 i = 表长+1(相当于尾插)

  2. 内存泄漏
    如果插入失败(如位置非法),需确保已分配的 newlnode 被释放:

    if (!p || j > i-1) {
        free(newlnode);  // 防止内存泄漏
        return ERROR;
    }
  3. 头结点的作用
    头结点保证了即使插入位置 i=1,也能统一处理(无需特殊代码)。


6. 完整示例

假设现有链表:头结点 → [10] → [20] → [30] → NULL
调用 InsertLinkList(&L, 2, 15) 后:

复制

头结点 → [10] → [15] → [20] → [30] → NULL

7. 为什么需要二级指针?
  • 如果插入位置是第1个结点前,需要修改头结点的 next 指针。

  • 通过 LinkList* L(二级指针)可以修改外部头指针的值。


8. 对比头插法和尾插法
操作时间复杂度特点
按位置插入O(n)通用,但需要遍历
头插法O(1)始终插入头部,顺序与输入相反
尾插法O(n)顺序与输入一致

总结

  • 核心思想:通过遍历找到插入位置的前驱结点,修改指针链接关系。

  • 关键步骤:定位前驱、创建结点、重新链接。

  • 学习建议

    1. 画图辅助理解指针变化

    2. 尝试手动模拟插入过程

    3. 结合调试工具观察内存变化

单链表删除第i个结点的原理详解(新手版)


1. 函数功能

这个函数的作用是:删除单链表中第i个数据结点,并释放其内存空间。


2. 关键变量解析
  • LinkList* L:指向头指针的指针(二级指针)

  • int i:要删除的结点位置(从1开始计数)

  • Lnode* p:临时指针,最终指向第i-1个结点

  • Lnode* q:临时指针,指向要删除的第i个结点


3. 删除过程分步解析
步骤1:初始化指针
int j = i;  // 保存目标位置i
i = 1;      // 从第1个位置开始查找
Lnode* p = (*L);  // p初始指向头结点

作用
准备从头结点开始遍历,寻找第i-1个结点。

步骤2:定位前驱结点
while (j != i) {  // 循环直到i == j
    i++;
    p = p->next;
}

实际效果
让指针p最终指向第i-1个结点(要删除结点的前一个结点)。

示例(删除第3个结点):

初始:p → 头结点, i=1, j=3
第1次循环:p → 第1个结点, i=2
第2次循环:p → 第2个结点, i=3 (循环结束)
步骤3:执行删除操作
q = p->next;        // q指向要删除的第i个结点
p->next = q->next;  // 绕过要删除的结点
free(q);            // 释放内存

关键操作图示

删除前:
[第i-1个结点] → [第i个结点(q)] → [第i+1个结点]
                ↑
                p->next

删除后:
[第i-1个结点] → [第i+1个结点]

4. 时间复杂度分析
  • 最好情况:删除第1个结点 → O(1)

  • 最坏情况:删除最后一个结点 → O(n)

  • 平均情况:O(n)


5. 正确执行流程示例

链表结构

头结点 → [10] → [20] → [30] → NULL

操作DelLinkList(&L, 2)

执行过程

  1. p初始指向头结点

  2. 第一次循环后:p指向第1个结点(值为10)

  3. 循环结束(pos=2等于目标位置)

  4. 删除p->next(值为20的结点)

  5. 最终链表:

    复制

    头结点 → [10] → [30] → NULL

6. 关键注意事项
  1. 必须检查p->next
    避免删除不存在的结点导致程序崩溃。

  2. 必须释放内存
    防止内存泄漏(memory leak)。

  3. 头结点的作用
    保证删除第1个结点时也能统一处理(不需要特殊逻辑)。


7. 常见问题解答

Q:为什么p初始指向头结点而不是第一个数据结点?
A:因为要删除第i个结点需要修改它的前驱结点的next指针。当i=1时,前驱结点就是头结点。

Q:如果链表为空会怎样?
A:修正后的代码会因!p->next条件返回ERROR,避免访问空指针。

Q:为什么删除操作是O(n)时间复杂度?
A:因为需要遍历链表找到要删除的位置。只有删除头结点时是O(1)。


总结

  • 核心原理:通过遍历找到待删除结点的前驱结点,修改指针关系后释放内存。

  • 三个关键步骤:定位前驱、绕过目标、释放内存。

  • 学习建议

    1. 用纸笔画出指针变化过程

    2. 使用调试工具单步执行观察变量

    3. 尝试实现其他操作(如按值删除)

单链表清空操作的原理详解(新手友好版)


1. 函数功能

这个函数的作用是:清空单链表中的所有数据结点,但保留头结点,使链表恢复到初始空表状态。


2. 关键变量解析
  • LinkList* L:指向头指针的指针(二级指针)

  • Lnode* p:工作指针,用于遍历链表(初始指向第一个数据结点)

  • Lnode* q:临时指针,用于保存待释放的结点


3. 清空过程分步解析
步骤1:初始化指针
Lnode* p = (*L)->next;  // p指向第一个数据结点(跳过头结点)

作用:从第一个实际存储数据的结点开始处理。

步骤2:循环释放所有结点
while (p != NULL) {
    q = p;        // q记录当前要释放的结点
    p = p->next;  // p移动到下一个结点
    free(q);      // 释放当前结点内存
}

内存变化过程

初始状态:
[头结点] → [结点1] → [结点2] → ... → [结点N] → NULL
           ↑
           p

每次循环:
1. q = p(记录要释放的结点)
2. p = p->next(p后移)
3. free(q)(释放原结点)
步骤3:重置头结点指针
(*L)->next = NULL;  // 头结点的next置空

作用:将链表恢复为只有头结点的初始状态。

步骤4:输出提示
printf("该链表已清空!\n");

用户体验:提示用户操作已完成。


4. 时间复杂度分析
  • 时间复杂度:O(n)
    需要遍历所有n个数据结点,每个结点的释放操作是O(1)。


5. 内存管理关键点
  1. 必须逐个释放
    每个通过malloc分配的结点都必须通过free释放,否则会导致内存泄漏。

  2. 指针顺序不能错

    • 必须先保存pq

    • 然后才能移动p = p->next

    • 最后释放q

  3. 头结点保留
    清空后头结点仍然存在,可以继续使用该链表。


6. 与销毁链表的区别
清空链表销毁链表
保留头结点释放包括头结点在内的所有结点
链表可继续使用链表不可再使用
(*L)->next = NULL*L = NULL

7. 示例演示

清空前链表

头结点 → [10] → [20] → [30] → NULL

执行过程

  1. p指向[10]

  2. 释放[10],p移动到[20]

  3. 释放[20],p移动到[30]

  4. 释放[30],p变为NULL

  5. 头结点next置空

清空后链表

头结点 → NULL

8. 常见问题解答

Q:为什么不清空头结点?
A:保留头结点可以复用链表,避免重复初始化。若需要完全销毁应调用DestoryLinkList

Q:如果链表已经是空的会怎样?
A:p初始为NULL,while循环直接跳过,仅执行(*L)->next = NULL

Q:能否用递归实现清空?
A:技术上可以,但不推荐。递归深度过大会导致栈溢出,且效率不如迭代。


9. 代码改进建议
  1. 增加安全性检查

if (*L == NULL) return ERROR;  // 检查链表是否已初始化
  1. 优化提示信息

printf("成功清空%d个结点\n", count);  // 可添加计数器统计释放的结点数

总结

  • 核心思想:遍历链表逐个释放数据结点,最后重置头结点指针。

  • 关键操作:指针顺序移动、内存释放、头结点重置。

  • 学习建议

    1. 配合画图理解指针变化

    2. 在调试模式下观察内存地址变化

    3. 对比清空与销毁的操作差异

运行结果如下:

请输入数据:
1
请输入数据:
2
请输入数据:
3
请输入数据:
4
该链表的数据为:
1 : 1
2 : 2
3 : 3
4 : 4
该链表的数据为:
1 : 1
2 : 2
3 : 9
4 : 3
5 : 4
该链表的长度为:5
该链表的数据为:
1 : 1
2 : 2
3 : 3
4 : 4
第2个 : 2
该链表不为空!
该链表已清空!
该链表已销毁!

请按任意键继续. . .

注:该代码是本人自己所写,可能不够好,不够简便,欢迎大家指出我的不足之处。如果遇见看不懂的地方,可以在评论区打出来,进行讨论,或者联系我。上述内容全是我自己理解的,如果你有别的想法,或者认为我的理解不对,欢迎指出!!!如果可以,可以点一个免费的赞支持一下吗?谢谢各位彦祖亦菲!!!!!  

http://www.dtcms.com/a/110042.html

相关文章:

  • 栈和队列的概念
  • dfs递归回溯的两种体型
  • 水下声呐探测仪,应急救援中的高效水下定位技术|深圳鼎跃
  • Nuxt3项目的SEO优化(robots.txt,页面tdk,伪静态.html,sitemap.xml动态生成等)
  • 开源虚拟化管理平台Proxmox VE部署超融合
  • RHCSA LINUX系统文件管理
  • 市场交易策略优化与波动管理
  • 6.模型训练4-毕设篇
  • 【Prometheus】kube-state-metrics 的详细说明
  • 【学习笔记】计算机网络(七)—— 网络安全
  • Metasploit 反弹Shell
  • eplan许可证常见问题及解决方法
  • 数据结构(JAVA)单向,双向链表
  • 解析CSRF攻击
  • Transformer架构详解:从Encoder到Decoder的完整旅程
  • VSCode历史版本的下载安装
  • 破解AI编程瓶颈:上下文管理助力高效开发,以Cline为例
  • kornia,一个实用的 Python 库!
  • 环形链表相关题目
  • ARM架构安装MySQL8.0
  • 数据结构每日一题day11(链表)★★★★★
  • Python HTTP交互双剑客:requests与responses实战指南
  • 2025年消防设施操作员考试题库及答案
  • 矩池云使用指南
  • 高级IO模型
  • 华三H3C模拟器HCL搭建简单内网三层网络
  • Lua:第1-4部分 语言基础
  • Compose组件转换XML布局
  • 煤矿沿线 智能输入输出模块,一般用来干什么
  • 使用 Vue3 打造一个简易分类器演示工具