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

目录

[1. 什么是单链表?](#1. 什么是单链表?)

[2. 单链表的代码表示](#2. 单链表的代码表示)

[3. 单链表的基本操作](#3. 单链表的基本操作)

[3.1 初始化链表](#3.1 初始化链表)

[3.2 插入结点(头插法)](#3.2 插入结点(头插法))

[3.3 插入结点(尾插法)](#3.3 插入结点(尾插法))

[3.4 遍历链表](#3.4 遍历链表)

[4. 单链表的优缺点](#4. 单链表的优缺点)

代码:*L=(LinkList)malloc(sizeof(LNode))

[1. malloc 的作用](#1. malloc 的作用)

[2. sizeof(LNode) 的作用](#2. sizeof(LNode) 的作用)

[3. 类型转换 (LinkList)](#3. 类型转换 (LinkList))

[4. *L 的含义](#4. *L 的含义)

[5. 整体流程](#5. 整体流程)

[6. 实际效果](#6. 实际效果)

[7. 常见问题解答](#7. 常见问题解答)

[Q1:为什么用 malloc 而不是直接声明变量?](#Q1:为什么用 malloc 而不是直接声明变量?)

[Q2:头结点的 data 字段有意义吗?](#Q2:头结点的 data 字段有意义吗?)

[Q3:为什么要用二级指针 LinkList* L?](#Q3:为什么要用二级指针 LinkList* L?)

[8. 图解过程](#8. 图解过程)

[DestoryLinkList 函数原理详解](#DestoryLinkList 函数原理详解)

[1. 函数参数 LinkList* L(二级指针)](#1. 函数参数 LinkList* L(二级指针))

[2. while (*L != NULL) 循环](#2. while (*L != NULL) 循环)

[3. 销毁过程图解](#3. 销毁过程图解)

循环步骤:

[4. 最终效果](#4. 最终效果)

[5. 为什么需要这样实现?](#5. 为什么需要这样实现?)

[6. 对比 ClearLinkList(清空链表)](#6. 对比 ClearLinkList(清空链表))

[7. 常见问题](#7. 常见问题)

[Q1:为什么用 while (*L) 而不是 while ((*L)->next)?](#Q1:为什么用 while (*L) 而不是 while ((*L)->next)?)

Q2:如果链表为空(只有头结点),会发生什么?

Q3:为什么不用递归实现?

[8. 代码验证](#8. 代码验证)

总结


cpp 复制代码
#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){
        return ERROR;
    }else{
        return OK;
    }
}

//销毁链表
Status DestoryLinkList(LinkList* L){
    LinkList p;//定义一个临时的指向结点的指针
    while (*L!=NULL)
    {
        p=*L;//储存原来的指针(结点)
        *L=(*L)->next;//往后移动结点
        free(p);//释放原来的指针
    }
    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;
    while (p!=NULL)
    {
        printf("%d : %d\n", i, p->data);  // 打印序号和数据
        i++;            // 序号递增
        p = p->next;    // p 移动到下一个结点
    }
    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,3);//头插
    CreateLinkList_r(&mylist,3);//尾插
    ShowLinkList(&mylist);
    LocatElem(&mylist,2);
}

下面来解释相关知识点和部分代码:

单链表是数据结构中最基础的一种链式存储结构,非常适合新手学习指针和动态内存管理的概念。下面我会用最易懂的方式讲解单链表的核心知识。

1. 什么是单链表?

单链表就像一列火车:

  • 每节车厢(结点)包含两部分:货物(数据)和连接钩(指针)

  • 车头(头结点)不装货物,只负责带领整列火车

  • 最后一节车厢的连接钩是空的(NULL)

2. 单链表的代码表示

复制代码
typedef struct Node {
    int data;           // 数据域(可以是任意类型)
    struct Node* next;  // 指针域(指向下一个结点)
} Node, *LinkedList;    // Node表示单个结点,LinkedList表示整个链表

3. 单链表的基本操作

3.1 初始化链表

复制代码
LinkedList initList() {
    LinkedList L = (LinkedList)malloc(sizeof(Node)); // 创建头结点
    L->next = NULL;     // 初始为空链表
    return L;
}

3.2 插入结点(头插法)

复制代码
void headInsert(LinkedList L, int data) {
    Node* newNode = (Node*)malloc(sizeof(Node)); // 创建新结点
    newNode->data = data;
    newNode->next = L->next;  // 新结点指向原第一个结点
    L->next = newNode;        // 头结点指向新结点
}

特点:新结点总是插在最前面,链表顺序与插入顺序相反

3.3 插入结点(尾插法)

复制代码
void tailInsert(LinkedList L, int data) {
    Node* p = L;
    while(p->next != NULL) {  // 找到最后一个结点
        p = p->next;
    }
    
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = data;
    newNode->next = NULL;
    p->next = newNode;  // 最后一个结点指向新结点
}

特点:新结点总是插在最后面,链表顺序与插入顺序相同

3.4 遍历链表

复制代码
void printList(LinkedList L) {
    Node* p = L->next;  // 跳过头结点
    while(p != NULL) {
        printf("%d ", p->data);
        p = p->next;    // 移动到下一个结点
    }
    printf("\n");
}

4. 单链表的优缺点

优点

  • 插入/删除速度快(O(1)时间复杂度)

  • 不需要预先知道数据规模

  • 不需要连续的内存空间

缺点

  • 查找速度慢(O(n)时间复杂度)

  • 需要额外的空间存储指针

下面来解释代码,更加了解单链表:

代码:*L=(LinkList)malloc(sizeof(LNode))

这行代码是单链表初始化中的核心操作,涉及指针、动态内存分配和类型转换。让我们用新手能理解的方式逐步拆解:


1. malloc 的作用

malloc 是内存分配函数(memory allocation),它的作用是从堆(Heap)内存中申请一块指定大小的内存空间。

  • 比喻:就像在仓库里预订一个储物柜,告诉管理员你需要多大的空间。

  • 语法void* malloc(size_t size);

    • 参数 size:需要申请的字节数

    • 返回值:成功时返回指向分配内存的指针,失败返回 NULL


2. sizeof(LNode) 的作用

sizeof 是一个运算符,用来计算数据类型或变量占用的字节数。

  • sizeof(LNode) :计算结构体 LNode 的大小。

    • 假设 LNode 包含一个 int(4字节)和一个指针(8字节,64位系统),则 sizeof(LNode) = 12 字节。

    • 意义 :告诉 malloc 要申请一个足够存放 LNode 的内存块。


3. 类型转换 (LinkList)

malloc 返回的是 void*(通用指针),需要强制转换为 LinkList 类型。

  • LinkList 是什么

    根据代码中的定义:

    复制代码
    typedef struct Signle_Link_List LNode, *LinkList;
    • LNode 是结构体类型(结点)

    • LinkListLNode* 的别名(指向结点的指针)

  • 转换目的 :明确告诉编译器,这块内存将被当作 LinkList(即指向 LNode 的指针)使用。


4. *L 的含义

函数的参数是 LinkList* L(二级指针):

  • LinkList* L 可以理解为:指向头指针的指针。

  • *L 解引用后得到的是头指针(LinkList 类型)。

  • 操作目的 :通过 *L = ... 修改外部的头指针,使其指向新分配的内存。


5. 整体流程

这行代码的完整意义是:

  1. 申请内存 :在堆内存中申请一块大小为 sizeof(LNode) 的内存。

  2. 类型转换 :将返回的 void* 转换为 LinkList 类型(即 LNode*)。

  3. 赋值给头指针 :让外部的头指针 *L 指向这块内存。


6. 实际效果

这行代码执行后:

  • 创建了一个头结点 :头结点的 data 字段未初始化(可能是垃圾值),但 next 指针会被初始化为 NULL(见后续代码 (*L)->next = NULL;)。

  • 链表结构

    复制代码
    *L(头指针)
      │
      ▼
    [头结点] → next = NULL

7. 常见问题解答

Q1:为什么用 malloc 而不是直接声明变量?
  • :链表结点需要动态增减,malloc 允许在运行时按需申请内存。

  • 对比

    复制代码
    LNode node;        // 栈内存,函数结束后自动释放
    LNode* p = malloc(...); // 堆内存,需要手动释放(用 free)
Q2:头结点的 data 字段有意义吗?
  • :在标准实现中,头结点的 data 通常不存储有效数据(仅作为链表入口),但代码中可能用它存储元信息(如长度)。
Q3:为什么要用二级指针 LinkList* L
  • :需要修改外部传入的头指针的值。C语言中,若想通过函数修改指针的值,必须传递指针的地址(即二级指针)。

  • 示例

    复制代码
    void Init(LinkList* L) {
        *L = malloc(...); // 修改外部的头指针
    }
    int main() {
        LinkList myList;  // 此时 myList 是野指针
        Init(&myList);    // 传递头指针的地址
    }

8. 图解过程

复制代码
Before malloc:
+------+
|  L  | --> 随机值(野指针)
+------+

After malloc:
+------+       +---------------------+
|  L  | -->   | data(未初始化)     |
+------+       | next = NULL         |
               +---------------------+

DestoryLinkList 函数原理详解

这个函数的作用是 销毁整个单链表 ,包括 头结点 和所有 数据结点,并释放它们占用的内存。让我们一步步解析它的工作原理:


1. 函数参数 LinkList* L(二级指针)

  • LinkListLNode* 的别名(指向结点的指针)。

  • LinkList* L 是一个 指向头指针的指针 (二级指针),目的是 修改外部的头指针 ,使其最终变为 NULL

    • 如果只传 LinkList L(一级指针),函数内部修改 L 不会影响外部的头指针,导致内存泄漏。

2. while (*L != NULL) 循环

  • 循环条件 :只要 *L(当前头指针)不为 NULL,就继续释放内存。

  • 循环过程

    1. p = *L :临时指针 p 保存当前要释放的结点(头结点或数据结点)。

    2. *L = (*L)->next :让头指针 *L 指向下一个结点(相当于链表"跳过"当前结点)。

    3. free(p) :释放 p 指向的结点内存。


3. 销毁过程图解

假设链表结构如下:

复制代码
头指针 *L
   │
   ▼
[头结点] → [结点1] → [结点2] → NULL
循环步骤:
  1. 第一次循环

    • p = *Lp 指向头结点)

    • *L = (*L)->next(头指针 *L 指向结点1)

    • free(p)(释放头结点)

    复制代码
    *L → [结点1] → [结点2] → NULL
  2. 第二次循环

    • p = *Lp 指向结点1)

    • *L = (*L)->next(头指针 *L 指向结点2)

    • free(p)(释放结点1)

    复制代码
    *L → [结点2] → NULL
  3. 第三次循环

    • p = *Lp 指向结点2)

    • *L = (*L)->next(头指针 *L 指向 NULL

    • free(p)(释放结点2)

    复制代码
    *L → NULL
  4. 循环结束

    • *LNULL,退出循环。

4. 最终效果

  • 链表被完全销毁:所有结点(包括头结点)的内存被释放。

  • 头指针 *L 被置为 NULL:防止外部代码误用已释放的内存(避免野指针)。


5. 为什么需要这样实现?

  • 防止内存泄漏:必须逐个释放所有结点,否则未释放的内存会一直占用堆空间。

  • 安全性 :将头指针置为 NULL,避免后续代码误操作已释放的内存。


6. 对比 ClearLinkList(清空链表)

ClearLinkList 只释放数据结点,保留头结点(链表可复用):

复制代码
Status ClearLinkList(LinkList* L) {
    LNode* p, *q;
    q = (*L)->next;  // q 指向第一个数据结点
    while (q != NULL) {
        p = q;
        q = q->next;
        free(p);
    }
    (*L)->next = NULL;  // 头结点的 next 置空
    return OK;
}
  • 区别

    • DestoryLinkList:销毁整个链表(头结点+数据结点)。

    • ClearLinkList:只清空数据结点,保留头结点。


7. 常见问题

Q1:为什么用 while (*L) 而不是 while ((*L)->next)
  • *L 是当前头指针,需要释放所有结点(包括头结点)。如果检查 (*L)->next,会漏掉头结点。
Q2:如果链表为空(只有头结点),会发生什么?
  • *L 指向头结点,第一次循环释放头结点后,*L 被置为 NULL,循环结束。
Q3:为什么不用递归实现?
  • :递归实现可能因链表过长导致栈溢出。迭代(循环)更安全高效。

8. 代码验证

可以通过打印结点地址验证释放过程:

复制代码
Status DestoryLinkList(LinkList* L) {
    LNode* p;
    while (*L != NULL) {
        p = *L;
        printf("Freeing node at address: %p\n", p);  // 打印释放的结点地址
        *L = (*L)->next;
        free(p);
    }
    return OK;
}

总结

  • 核心操作:循环遍历链表,逐个释放结点,并更新头指针。

  • 关键点 :二级指针修改头指针、free 释放内存、防止野指针。

  • 适用场景:当确定链表不再使用时调用,避免内存泄漏。

CreateLinkList_H 函数原理详解(头插法创建单链表)

这个函数的作用是 用头插法创建一个包含 n 个结点的单链表 。特点是 新结点总是插入在头结点之后 ,因此链表的顺序与输入顺序 相反。下面逐步解析其工作原理:


1. 函数参数

  • LinkList* L:二级指针,用于修改外部的头指针。

  • int n:要创建的结点数量。


2. 创建头结点

复制代码
*L = (LinkList)malloc(sizeof(LNode));  // 分配头结点内存
(*L)->next = NULL;                     // 头结点的 next 初始化为 NULL
  • 作用:初始化一个空链表,只有头结点(不存储实际数据)。

  • 图示

    复制代码
    *L(头指针)
      │
      ▼
    [头结点] → NULL

3. 头插法循环(for (i = n; i > 0; i--)

循环 n 次,每次创建一个新结点并插入到 头结点之后

步骤拆解
  1. 申请新结点内存

    复制代码
    LNode* newlnode = (LNode*)malloc(sizeof(LNode));
    • 为新结点分配内存,并通过 scanf 输入数据。
  2. 插入新结点

    复制代码
    newlnode->next = (*L)->next;  // 新结点的 next 指向原第一个结点
    (*L)->next = newlnode;        // 头结点的 next 指向新结点
    • 关键点 :新结点插入后成为链表的 第一个数据结点
插入过程图示
  • 初始状态(只有头结点):

    复制代码
    [头结点] → NULL
  • 插入第一个结点(值为 1)

    复制代码
    [头结点] → [1] → NULL
  • 插入第二个结点(值为 2)

    复制代码
    [头结点] → [2] → [1] → NULL
  • 插入第三个结点(值为 3)

    复制代码
    [头结点] → [3] → [2] → [1] → NULL

4. 输入顺序与链表顺序的关系

  • 输入顺序 :假设依次输入 1, 2, 3

  • 链表顺序3 → 2 → 1(与输入相反)。

  • 原因:每次新结点都插入在链表头部。


5. 与尾插法(CreateLinkList_R)的区别

头插法 尾插法
新结点插入头结点之后 新结点追加到链表末尾
链表顺序与输入顺序相反 链表顺序与输入顺序相同
无需维护尾指针 需要维护尾指针 tail
时间复杂度:O(n)(每次插入为 O(1)) 时间复杂度:O(n)

6. 关键代码解析

复制代码
newlnode->next = (*L)->next;  // 新结点的 next 指向原第一个结点
(*L)->next = newlnode;        // 头结点的 next 指向新结点
  • 类比:像排队时每次都让新来的人站到队伍最前面。

  • 操作顺序 :必须先设置 newlnode->next,再修改 (*L)->next,否则会丢失原链表的引用。


7. 内存管理注意事项

  • 每个 malloc 分配的内存必须在链表销毁时通过 free 释放(如调用 DestoryLinkList)。

  • 如果输入 n 为 0,函数会创建一个只有头结点的空链表。


8. 示例输入输出

输入
复制代码
CreateLinkList_H(&myList, 3);
// 依次输入:10, 20, 30
链表结构
复制代码
头结点 → [30] → [20] → [10] → NULL

9. 常见问题

Q1:为什么头插法会导致顺序相反?
  • :每次新结点都插入在链表头部,类似"后来居上"。
Q2:如果 n 为负数会发生什么?
  • :循环不会执行,链表只有头结点(需在函数开头添加参数检查)。
Q3:头插法的时间复杂度是多少?
  • :O(n),因为每个结点的插入操作是 O(1),共 n 次。

总结

  • 核心思想:通过每次在头部插入新结点构建链表。

  • 特点:简单高效,但顺序与输入相反。

  • 适用场景:不需要保持输入顺序,或需要频繁在头部插入的场景(如栈的实现)。

ShowLinkList 函数原理详解(显示单链表内容)

这个函数的作用是 遍历并打印单链表中的所有数据结点,同时显示每个结点的序号。如果链表为空,会提示用户。以下是逐步解析:


1. 函数参数 const LinkList* L
  • const 修饰符:表示不会修改链表内容(安全保护)。

  • LinkList* L:二级指针,用于访问头结点(但这里只读不修改)。


2. 初始化指针 p
复制代码
LNode* p = (*L)->next;  // p 指向第一个数据结点(跳过头结点)
  • 为什么从 (*L)->next 开始

    头结点(*L)不存储实际数据,它的 next 才指向第一个有效结点。


3. 检查链表是否为空
复制代码
if (!p) {  // 等价于 if (p == NULL)
    puts("The LinkList is empty");
    return;
}
  • 逻辑 :如果 pNULL,说明头结点的 next 为空,链表无数据结点。

4. 遍历链表并打印数据
复制代码
int i = 1;          // 结点序号从1开始
while (p != NULL) {  // 遍历直到链表末尾
    printf("%d : %d\n", i, p->data);  // 打印序号和数据
    i++;            // 序号递增
    p = p->next;    // p 移动到下一个结点
}
  • 关键点

    • p->data:当前结点的数据。

    • p = p->next:指针后移,实现遍历。


5. 示例输出

假设链表结构:

复制代码
头结点 → [10] → [20] → [30] → NULL

调用 ShowLinkList(&list) 输出

复制代码
1 : 10
2 : 20
3 : 30

如果链表为空,输出:

复制代码
The LinkList is empty

6. 遍历过程图解
复制代码
初始状态:
p = 头结点->next → [10] → [20] → [30] → NULL

第一次循环:
打印 1:10,p 移动到 [20]

第二次循环:
打印 2:20,p 移动到 [30]

第三次循环:
打印 3:30,p 移动到 NULL(循环结束)

7. 为什么用 while (p) 而不是 while (p->next)
  • while (p) :确保当前结点 p 有效时才打印数据(包括最后一个结点)。

  • 如果写成 while (p->next),会漏掉最后一个结点的数据!


8. 时间复杂度
  • O(n) :需要遍历所有 n 个数据结点,每个结点访问一次。

9. 安全性注意事项
  1. const 保护:防止函数内意外修改链表。

  2. 空指针检查 :避免访问 NULL->next(已通过 if (!p) 处理)。


总结

  • 功能:按顺序显示链表所有结点的数据和序号。

  • 关键操作 :指针遍历 (p = p->next)、空链表检查。

  • 适用场景:调试、查看链表内容、交互式程序输出。

CreateLinkList_R 函数原理详解(尾插法创建单链表)

这个函数的作用是 用尾插法创建一个包含 n 个结点的单链表 。特点是 新结点总是插入在链表末尾 ,因此链表的顺序与输入顺序 一致。下面逐步解析其工作原理:


1. 函数参数

  • LinkList* L:二级指针,用于修改外部的头指针。

  • int n:要创建的结点数量。


2. 创建头结点

复制代码
*L = (LinkList)malloc(sizeof(LNode));  // 分配头结点内存
(*L)->next = NULL;                     // 头结点的 next 初始化为 NULL
  • 作用:初始化一个空链表,只有头结点(不存储实际数据)。

  • 图示

    复制代码
    *L(头指针)
      │
      ▼
    [头结点] → NULL

3. 尾指针 p 的初始化

复制代码
LNode* p = *L;  // p 初始指向头结点
  • p 的作用 :始终指向当前链表的 最后一个结点(尾结点)。

  • 初始时 :链表只有头结点,所以 p 指向头结点。


4. 尾插法循环(for (i = n; i > 0; i--)

循环 n 次,每次创建一个新结点并插入到 链表末尾

步骤拆解
  1. 申请新结点内存

    复制代码
    LNode* newlnode = (LNode*)malloc(sizeof(LNode));
    • 为新结点分配内存,并通过 scanf 输入数据。
  2. 初始化新结点

    复制代码
    newlnode->next = NULL;  // 新结点的 next 置空(因为它将是新的尾结点)
  3. 插入新结点到末尾

    复制代码
    p->next = newlnode;  // 原尾结点的 next 指向新结点
    p = newlnode;        // p 移动到新结点(更新尾指针)
    • 关键点 :通过 p 直接找到链表末尾,实现 O(1) 时间复杂度的插入。
插入过程图示
  • 初始状态(只有头结点):

    复制代码
    [头结点] → NULL
    p → [头结点]
  • 插入第一个结点(值为 1)

    复制代码
    [头结点] → [1] → NULL
    p → [1]
  • 插入第二个结点(值为 2)

    复制代码
    [头结点] → [1] → [2] → NULL
    p → [2]
  • 插入第三个结点(值为 3)

    复制代码
    [头结点] → [1] → [2] → [3] → NULL
    p → [3]

5. 输入顺序与链表顺序的关系

  • 输入顺序 :假设依次输入 1, 2, 3

  • 链表顺序1 → 2 → 3(与输入一致)。

  • 原因:每次新结点都追加到链表尾部。


6. 与头插法(CreateLinkList_H)的区别

尾插法 头插法
新结点插入链表末尾 新结点插入头结点之后
链表顺序与输入顺序相同 链表顺序与输入顺序相反
需要维护尾指针 p 无需维护尾指针
时间复杂度:O(n) 时间复杂度:O(n)

7. 关键代码解析

复制代码
p->next = newlnode;  // 将新结点链接到末尾
p = newlnode;        // 更新尾指针
  • 类比:像排队时每次都让新来的人站到队伍最后面。

  • 必要性 :必须更新 p,否则下次插入无法找到新的末尾。


8. 内存管理注意事项

  • 每个 malloc 分配的内存必须在链表销毁时通过 free 释放(如调用 DestoryLinkList)。

  • 如果输入 n 为 0,函数会创建一个只有头结点的空链表。


9. 示例输入输出

输入
复制代码
CreateLinkList_R(&myList, 3);
// 依次输入:10, 20, 30
链表结构
复制代码
头结点 → [10] → [20] → [30] → NULL

10. 常见问题

Q1:为什么需要尾指针 p
  • :直接通过头指针找到链表末尾需要 O(n) 时间,而维护 p 可以在 O(1) 时间内访问末尾。
Q2:如果 p 不更新会怎样?
  • :所有新结点都会插入到原尾结点之后,但原尾结点不会更新,导致链表断裂。
Q3:尾插法的时间复杂度是多少?
  • :O(n),因为每个结点的插入操作是 O(1),共 n 次。

总结

  • 核心思想:通过维护尾指针,每次在链表末尾插入新结点。

  • 特点:保持输入顺序,适合需要顺序一致的场景。

  • 关键操作 :尾指针更新 (p = newlnode)、内存分配与释放。

运行结果如下:

cpp 复制代码
请输入数据:
90
请输入数据:
60
请输入数据:
45
1 : 90
2 : 60
3 : 45
第2个 : 60

请按任意键继续. . .

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

相关推荐
我命由我123451 小时前
Spring Boot 自定义日志打印(日志级别、logback-spring.xml 文件、自定义日志打印解读)
java·开发语言·jvm·spring boot·spring·java-ee·logback
徐小黑ACG2 小时前
GO语言 使用protobuf
开发语言·后端·golang·protobuf
0白露3 小时前
Apifox Helper 与 Swagger3 区别
开发语言
Tanecious.4 小时前
机器视觉--python基础语法
开发语言·python
叠叠乐4 小时前
rust Send Sync 以及对象安全和对象不安全
开发语言·安全·rust
想跑步的小弱鸡4 小时前
Leetcode hot 100(day 3)
算法·leetcode·职场和发展
Tttian6226 小时前
Python办公自动化(3)对Excel的操作
开发语言·python·excel
xyliiiiiL6 小时前
ZGC初步了解
java·jvm·算法
爱的叹息6 小时前
RedisTemplate 的 6 个可配置序列化器属性对比
算法·哈希算法
独好紫罗兰7 小时前
洛谷题单2-P5713 【深基3.例5】洛谷团队系统-python-流程图重构
开发语言·python·算法