我的数据结构3——链表(link list)

(叠甲:如有侵权请联系,内容都是自己学习的总结,一定不全面,仅当互相交流(轻点骂)我也只是站在巨人肩膀上的一个小卡拉米,已老实,求放过)

链表是一种线性动态数据结构 ,由若干个「节点(Node)」串联而成。与数组的「连续内存存储」本质不同,链表的节点在内存中可以分散存放,节点之间通过指针(引用) 建立连接,从而形成一条 "数据链"。

它的核心特性是:不需要预先分配固定内存,插入 / 删除元素时无需移动大量数据;但失去了随机访问能力,无法通过下标直接定位元素

常见链表类型

1. 单链表(Singly Linked List)

最基础的链表形态,每个节点只有一个 next 指针,指向后继节点。

  • 特点:只能从头到尾单向遍历,无法反向访问前驱节点
  • 尾节点的 next 指向 nullptr,表示链表结束

2. 双向链表(Doubly Linked List)

每个节点包含两个指针:next 指向后继节点,prev 指向前驱节点。

  • 特点:支持双向遍历,查找前驱节点的时间复杂度从 O (n) 降为 O (1)
  • 代价:每个节点多占用一个指针的内存空间
  • 典型场景:需要频繁前后遍历、快速删除已知节点的场景(如 LRU 缓存)

3. 循环链表(Circular Linked List)

尾节点的 next 不再指向空,而是回头指向头节点,形成闭环。

  • 分为单向循环链表双向循环链表
  • 特点:从任意节点出发都能遍历完整条链表
  • 典型场景:约瑟夫环问题、操作系统进程调度队列

核心操作与时间复杂度

注意:很多资料会说 "链表插入删除是 O (1)",这个结论有前提 ------必须提前定位到插入 / 删除位置的前驱节点。如果包含 "查找位置" 的过程,整体时间复杂度仍是 O (n)。

1. 遍历与查找

从头节点出发,逐个通过 next 指针向后移动,直到找到目标或到达链表末尾。

  • 时间复杂度:O(n),n 为链表长度
  • 不支持随机访问,不能像数组一样通过下标 O (1) 定位元素

2. 插入操作

分为头插、尾插、中间插入三种场景:

  1. 头部插入 :新节点的 next 指向原头节点,更新头指针 → O(1)
  2. 尾部插入 :需先遍历找到尾节点,再修改指针 → O (n)(若维护尾指针tail则为 O (1))
  3. 中间插入 :先找到插入位置的前驱节点,再修改两个指针
    • 核心步骤:newNode->next = prev->next; prev->next = newNode;
    • 查找前驱 O (n),修改指针 O (1),整体O(n)

3. 删除操作

  1. 找到待删除节点的前驱节点 prev
  2. 修改指针跳过待删节点:prev->next = prev->next->next;
  3. 手动释放待删除节点的内存(C++ 等手动内存管理语言)
  • 时间复杂度:查找前驱 O (n),删除操作 O (1),整体O(n)
  • 双向链表中,若已知待删节点本身,可直接通过前驱指针完成删除,时间为 O (1)

实用技巧:虚拟头节点

为了统一处理「头节点插入 / 删除」和「中间节点插入 / 删除」的逻辑,避免单独判断头节点的边界情况,常引入虚拟头节点(dummy node)

  • 在原链表头部前新增一个无实际意义的节点,让 dummy->next 指向原头节点
  • 最终返回结果时返回 dummy->next 即可
对比维度 数组(连续存储) 链表(链式存储)
内存分布 连续内存块,需预先分配大小 分散内存,动态分配,无固定容量
随机访问 O (1),通过下标直接定位 O (n),必须从头遍历
头部 / 中间插入删除 O (n),需要移动大量元素 O (n)(查找位置)+ O (1)(修改指针)
尾部插入删除 空间充足时 O (1) 维护尾指针时 O (1),无尾指针则 O (n)
空间利用率 固定大小易浪费,扩容成本高 按需分配,利用率高
缓存友好性 高,连续内存易命中 CPU 缓存 低,节点分散,缓存命中率低

常用的链表

链表总共有8种,但是用的多些的为一下两种

  1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结 构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

  2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向 循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。

一、无头单向非循环单链表(SList)

特点:无哨兵位头结点,节点只有后继指针,非循环结构。所有可能修改头节点的操作都需要传入二级指针。

cpp 复制代码
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>

// 数据类型定义
typedef int SLDataType;
// 链表节点结构体
typedef struct SListNode
{
    SLDataType data;          // 节点存储的数据
    struct SListNode* next;   // 指向后继节点的指针
} SListNode;

// 1. 创建新节点
SListNode* CreateNewNode(SLDataType val)
{
    SListNode* newNode = (SListNode*)malloc(sizeof(SListNode));
    if (newNode == NULL)
    {
        perror("CreateNewNode malloc failed");
        return NULL;
    }
    newNode->data = val;
    newNode->next = NULL;
    return newNode;
}

// 2. 头插:在链表头部插入新节点
void SLFrontInsert(SListNode** pphead, SLDataType val)
{
    assert(pphead); // 二级指针本身不能为空
    SListNode* newNode = CreateNewNode(val);
    if (newNode == NULL) return;

    newNode->next = *pphead;
    *pphead = newNode;
}

// 3. 尾插:在链表尾部插入新节点
void SLBackInsert(SListNode** pphead, SLDataType val)
{
    assert(pphead);
    SListNode* newNode = CreateNewNode(val);
    if (newNode == NULL) return;

    // 空链表:新节点直接作为头节点
    if (*pphead == NULL)
    {
        *pphead = newNode;
        return;
    }

    // 非空链表:遍历找到尾节点
    SListNode* cur = *pphead;
    while (cur->next != NULL)
    {
        cur = cur->next;
    }
    cur->next = newNode;
}

// 4. 查找元素:返回第一个值为val的节点指针,找不到返回NULL
SListNode* SLFindElement(SListNode* phead, SLDataType val)
{
    SListNode* cur = phead;
    while (cur != NULL)
    {
        if (cur->data == val)
        {
            return cur;
        }
        cur = cur->next;
    }
    return NULL;
}

// 5. 指定节点后插入:在值为posVal的节点之后插入新数据
void SLInsertAfter(SListNode** pphead, SLDataType posVal, SLDataType newVal)
{
    assert(pphead);
    SListNode* pos = SLFindElement(*pphead, posVal);
    if (pos == NULL)
    {
        printf("未找到指定位置节点,插入失败\n");
        return;
    }

    SListNode* newNode = CreateNewNode(newVal);
    if (newNode == NULL) return;

    // 先连后半段,再连前半段,防止断链
    newNode->next = pos->next;
    pos->next = newNode;
}

// 6. 头删:删除链表第一个有效节点
void SLFrontDelete(SListNode** pphead)
{
    assert(pphead);
    if (*pphead == NULL)
    {
        printf("链表为空,无法头删\n");
        return;
    }

    SListNode* del = *pphead;
    *pphead = (*pphead)->next; // 注意运算符优先级,必须加括号
    free(del);
}

// 7. 尾删:删除链表最后一个节点
void SLBackDelete(SListNode** pphead)
{
    assert(pphead);
    if (*pphead == NULL)
    {
        printf("链表为空,无法尾删\n");
        return;
    }

    // 只有一个节点的情况
    if ((*pphead)->next == NULL)
    {
        free(*pphead);
        *pphead = NULL;
        return;
    }

    // 多个节点:找到倒数第二个节点
    SListNode* cur = *pphead;
    while (cur->next->next != NULL)
    {
        cur = cur->next;
    }

    free(cur->next);
    cur->next = NULL;
}

// 8. 求链表长度
int SLLength(SListNode* phead)
{
    int len = 0;
    SListNode* cur = phead;
    while (cur != NULL)
    {
        len++;
        cur = cur->next;
    }
    return len;
}

// 9. 指定值删除:删除第一个值为val的节点
void SLRemoveByVal(SListNode** pphead, SLDataType val)
{
    assert(pphead);
    if (*pphead == NULL)
    {
        printf("链表为空,删除失败\n");
        return;
    }

    // 头节点就是目标节点,直接复⽤头删
    if ((*pphead)->data == val)
    {
        SLFrontDelete(pphead);
        return;
    }

    // 查找目标节点的前驱节点
    SListNode* prev = *pphead;
    while (prev->next != NULL && prev->next->data != val)
    {
        prev = prev->next;
    }

    // 遍历完未找到目标值
    if (prev->next == NULL)
    {
        printf("未找到值为%d的节点,删除失败\n", val);
        return;
    }

    // 移除并释放目标节点
    SListNode* del = prev->next;
    prev->next = del->next;
    free(del);
}

// 10. 指定位置删除:删除第pos个节点(pos从1开始计数)
void SLDeleteByPos(SListNode** pphead, int pos)
{
    assert(pphead);
    int len = SLLength(*pphead);
    if (pos < 1 || pos > len)
    {
        printf("位置%d非法,删除失败(有效范围1~%d)\n", pos, len);
        return;
    }

    // 第一个节点:复⽤头删
    if (pos == 1)
    {
        SLFrontDelete(pphead);
        return;
    }

    // 找到第pos-1个节点(前驱节点)
    SListNode* prev = *pphead;
    for (int i = 1; i < pos - 1; i++)
    {
        prev = prev->next;
    }

    SListNode* del = prev->next;
    prev->next = del->next;
    free(del);
}

// 11. 打印链表
void SLPrint(SListNode* phead)
{
    SListNode* cur = phead;
    while (cur != NULL)
    {
        printf("%d -> ", cur->data);
        cur = cur->next;
    }
    printf("NULL\n");
}

// 12. 销毁链表(释放所有节点,头指针置空)
void SLDestroy(SListNode** pphead)
{
    assert(pphead);
    SListNode* cur = *pphead;
    while (cur != NULL)
    {
        SListNode* next = cur->next;
        free(cur);
        cur = next;
    }
    *pphead = NULL;
    printf("链表销毁完成\n");
}

我们来看一下以下代码的经典错误

cpp 复制代码
#include<stdio.h>
#include<assert.h>
#include<stdbil.h>
//无头+单向+非循环链表
typedef int SLDataType;
typedef struct SListNode
{
SLDataType Data;
struct SListNode* next;
}SListNode;

SListNode* CreateNewNode(SLDataType val)
{
	SlistNode* temp = (SListNode*)malloc(sizeof(SListNode));
	if(!temp)
	{
	perror("Failed to create a new node!\n");
		return NULL;
	}
	temp->Data = val;
	temp->next = NULL;
	return temp;
}

void SLFrontInsert(SListNode** FirstNode,SLDataType val)
{
	assert(FirstNode);
	SListNode* temp = CreateNewNode(val);
	temp->next = *FirstNode;
	*FirstNode = temp;	
}

void SLBackInsert(SListNode** FirstNode,SLDataType val)
{
	assert(FirstNode);
	SListNode* temp = CreateNewNode(val);
	if(!*FirstNode)
	{
		*FirstNode = temp;	
	}
	SlistNode* Move = *FirstNode;
	while(Move->next)
	{
		Move = Move->next;
	}
	Move->next = temp;
}

SListNode* SLFindElement(SListNode* Head,SLDataType val)
{
	assert(Head);
	while(Head->next)
	{
		if(Head->Data==val)
		{
			return Head;
			printf("searching operation is successful!\n");
		}
		Head = Head->next;
	}
	Printf("searching operation is failed!\n");
	return NULL;
}

void SLInsertPointPosition(SListNode** FirstNode,SLDataType Val,SLDataType NewData)
{
	assert(FirstNode);
	SListNode* pointposition = SLFindElement(*FirstNode,val);
	if(!pointposition)
	{
		return;
	}
	SListNode* temp = pointpositon->next;
	SListNode* NewNode = CreateNewNode(NewData);
	if(!NewNode)
	{
		return;	
	}
	pointposition->next = NewNode;
	NewNode->next = temp;
}

void SLFrontDelete(SListNode** FirstNode)
{
	assert(FirstNode);
	if(!*FirstNode)
	{
		printf("此链表已为空\n");
		return;	
	}
	SListNode* temp = *FirstNode;
	*FirstNode = *FirstNode->next;
	free(temp);
}

void SLBackDelete(SListNode** FirstNode)
{
	assert(FirstNode);
	if(!*FirstNode)
	{
		printf("此链表已为空\n");
		return;	
	}
	SListNode* Move = *FirstNode;
	if(!Move->next)
	{
		free(Move);
	}
	while(Move->next->next)
	{
		Move = Move->next;	
	}
	SListNode* temp = Move->next;
	Move->next = NULL;
	free(temp);
}

void SLPrint(SListNode* FirstNode)
{
	assert(FirstNode);
	SListNode* Move = FirstNode;
	while(Move)
	(
		printf("d% ",Move->Data);
		Move = Move->next;
	)
	printf("\n");
}

void SLDestory(SListNode**FirstNode)
{
	assert(FirstNode);
	if(!*FirstNode)
	{
		printf("此链表已为空\n");
		return;	
	}
	SListNode* Move = *FirstNode;
	*FirstNode = NULL;
	SListNode* temp = NULL;
	while(Move)
	{
		temp = Move;
		Move = Move->next;
		free(temp);
	}
	printf("链表注销成功\n");
}

一、编译级错误(直接无法通过编译)

1. 头文件拼写错误

#include<stdbil.h> 拼写错误,内存申请 malloc/free、错误提示 perror 都在 stdlib.h 中。

  • 修正:#include <stdlib.h>

2. 结构体类型名大小写不匹配

C 语言大小写敏感,你定义的类型是 SListNode(L 大写),但多处写成 SlistNode(l 小写):

  • CreateNewNode 中:SlistNode* temp
  • SLBackInsert 中:SlistNode* Move

3. 变量名拼写 / 大小写不一致

  • SLInsertPointPosition 形参是 Val(大写),调用 SLFindElement 时传的是 val(小写),属于两个不同变量,编译报 "未定义标识符"。
  • 同函数内,变量定义为 pointposition,使用时写成 pointpositon(漏写字母 i),名称不匹配。

4. 运算符优先级错误(经典坑)

SLFrontDelete 中:

cpp 复制代码
*FirstNode = *FirstNode->next;

-> 优先级高于解引用 *,代码等价于 *(FirstNode->next),但 FirstNode 是二级指针,本身没有 next 成员,直接编译报错。

  • 修正:必须加括号 (*FirstNode)->next,先解引用得到一级指针,再访问成员。

5. 语法与库函数拼写错误

  • SLFindElementPrintf 大写 P,标准库函数是小写 printf
  • SLPrintwhile(Move) 后用了圆括号 () 包裹循环体,C 语言循环体必须用花括号 {},属于语法错误。
  • SLPrint 中格式化符写反:"d% " → 正确为 "%d "

二、运行时致命错误(编译通过也会崩溃 / 死循环)

1. 尾插空链表场景形成自环(死循环根源)

SLBackInsert 中,空链表插入时你设置了新节点为头节点,但没有 return,代码会继续向下执行遍历逻辑:

cpp 复制代码
if(!*FirstNode)
{
    *FirstNode = temp;	
}
// 没有return,继续执行
SListNode* Move = *FirstNode; // Move 就是新节点本身
while(Move->next) { ... }     // 新节点next为NULL,循环不执行
Move->next = temp;            // 节点自己指向自己,形成循环链表

后续遍历会直接死循环。

  • 修正:空链表处理完必须加 return 退出函数。

2. 尾删单节点场景野指针

SLBackDelete 中,只有一个节点时,你释放了节点,但:

  1. 没有把头指针 *FirstNode 置为 NULL,头指针变成野指针;
  2. 没有 return,代码继续向后执行,访问已释放内存的 Move->next->next,触发野指针崩溃。

3. 查找函数漏掉最后一个节点

SLFindElement 循环条件写为 while(Head->next),当 Head 指向最后一个节点时,Head->nextNULL,循环直接终止,最后一个节点永远不会被判断,漏查尾节点。

  • 修正:循环条件应为 while(Head),逐个判断每个节点。

4. 空指针解引用风险

多处调用 CreateNewNode 后,没有检查返回值是否为 NULL。如果内存申请失败,后续直接访问 temp->next 就是空指针解引用,程序直接崩溃。

三、逻辑与设计缺陷

1. assert 滥用

assert 应用于检查 "绝对不应该出现的非法入参"(比如二级指针本身为 NULL),但你在:

  • SLFindElementassert(Head):空链表查找是正常场景,应该返回 NULL,而不是直接断言终止程序。
  • SLPrintassert(FirstNode):空链表打印是正常需求,不应该崩溃。

2. 死代码

SLFindElementreturn Head; 之后的 printf 永远不会执行,属于无效代码。


四、修正后的完整可运行代码

cpp 复制代码
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>

// 无头+单向+非循环链表
typedef int SLDataType;
typedef struct SListNode
{
    SLDataType Data;
    struct SListNode* next;
} SListNode;

// 创建新节点
SListNode* CreateNewNode(SLDataType val)
{
    SListNode* temp = (SListNode*)malloc(sizeof(SListNode));
    if (!temp)
    {
        perror("Failed to create a new node!");
        return NULL;
    }
    temp->Data = val;
    temp->next = NULL;
    return temp;
}

// 头插
void SLFrontInsert(SListNode** FirstNode, SLDataType val)
{
    assert(FirstNode);
    SListNode* temp = CreateNewNode(val);
    if (temp == NULL) return; // 防御:内存申请失败直接返回
    temp->next = *FirstNode;
    *FirstNode = temp;
}

// 尾插
void SLBackInsert(SListNode** FirstNode, SLDataType val)
{
    assert(FirstNode);
    SListNode* temp = CreateNewNode(val);
    if (temp == NULL) return;

    // 空链表:新节点直接作为头节点
    if (*FirstNode == NULL)
    {
        *FirstNode = temp;
        return; // 关键:处理完直接返回,不执行后续遍历
    }

    // 非空链表:遍历找尾节点
    SListNode* Move = *FirstNode;
    while (Move->next)
    {
        Move = Move->next;
    }
    Move->next = temp;
}

// 查找元素
SListNode* SLFindElement(SListNode* Head, SLDataType val)
{
    SListNode* cur = Head;
    while (cur)
    {
        if (cur->Data == val)
        {
            printf("searching operation is successful!\n");
            return cur;
        }
        cur = cur->next;
    }
    printf("searching operation is failed!\n");
    return NULL;
}

// 在指定值的节点后面插入新节点
void SLInsertPointPosition(SListNode** FirstNode, SLDataType val, SLDataType NewData)
{
    assert(FirstNode);
    SListNode* pointposition = SLFindElement(*FirstNode, val);
    if (!pointposition)
    {
        return;
    }
    SListNode* NewNode = CreateNewNode(NewData);
    if (!NewNode)
    {
        return;
    }
    // 插入逻辑:先连后面,再连前面
    NewNode->next = pointposition->next;
    pointposition->next = NewNode;
}

// 头删
void SLFrontDelete(SListNode** FirstNode)
{
    assert(FirstNode);
    if (!*FirstNode)
    {
        printf("此链表已为空\n");
        return;
    }
    SListNode* temp = *FirstNode;
    *FirstNode = (*FirstNode)->next; // 修正运算符优先级
    free(temp);
}

// 尾删
void SLBackDelete(SListNode** FirstNode)
{
    assert(FirstNode);
    if (!*FirstNode)
    {
        printf("此链表已为空\n");
        return;
    }

    SListNode* Move = *FirstNode;
    // 只有一个节点的情况
    if (!Move->next)
    {
        free(Move);
        *FirstNode = NULL; // 头指针置空,避免野指针
        return;            // 处理完直接返回
    }

    // 多个节点:找到倒数第二个节点
    while (Move->next->next)
    {
        Move = Move->next;
    }
    SListNode* temp = Move->next;
    Move->next = NULL;
    free(temp);
}

// 打印链表
void SLPrint(SListNode* FirstNode)
{
    SListNode* Move = FirstNode;
    while (Move)
    {
        printf("%d ", Move->Data);
        Move = Move->next;
    }
    printf("\n");
}

// 销毁链表
void SLDestory(SListNode** FirstNode)
{
    assert(FirstNode);
    if (!*FirstNode)
    {
        printf("此链表已为空\n");
        return;
    }
    SListNode* Move = *FirstNode;
    *FirstNode = NULL;
    SListNode* temp = NULL;
    while (Move)
    {
        temp = Move;
        Move = Move->next;
        free(temp);
    }
    printf("链表注销成功\n");
}

五、补充测试建议

可以用以下主函数验证所有边界场景:空链表插入、单节点删除、尾节点查找、连续增删等:

cpp 复制代码
int main()
{
    SListNode* head = NULL;
    SLBackInsert(&head, 1);  // 空链表尾插
    SLBackInsert(&head, 2);
    SLBackInsert(&head, 3);
    SLFrontInsert(&head, 0); // 头插
    SLPrint(head);           // 预期输出:0 1 2 3

    SLInsertPointPosition(&head, 2, 99); // 在2后面插99
    SLPrint(head);           // 预期输出:0 1 2 99 3

    SLFrontDelete(&head);
    SLBackDelete(&head);
    SLPrint(head);           // 预期输出:1 2 99

    SLDestory(&head);
    SLPrint(head);           // 空链表,输出换行
    return 0;
}

二、有头循环双向链表(带头结点 + 双向 + 循环)

特点:带有哨兵位头结点(不存储有效数据),每个节点有前驱 + 后继双指针,首尾相连形成循环。 优势:尾插、尾删、指定位置插入删除均为 O (1) 时间复杂度,无需单独处理空链表边界,代码更简洁健壮。

cpp 复制代码
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>

// 数据类型定义
typedef int LTDataType;
// 双向链表节点结构体
typedef struct ListNode
{
    LTDataType data;          // 节点存储的数据
    struct ListNode* next;   // 后继指针
    struct ListNode* prev;   // 前驱指针
} ListNode;

// 1. 创建新节点
ListNode* CreateListNode(LTDataType val)
{
    ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
    if (newNode == NULL)
    {
        perror("CreateListNode malloc failed");
        return NULL;
    }
    newNode->data = val;
    newNode->next = NULL;
    newNode->prev = NULL;
    return newNode;
}

// 2. 链表初始化:创建哨兵位头结点,形成自循环
ListNode* ListInit()
{
    ListNode* phead = CreateListNode(0); // 头结点数据无实际意义
    if (phead == NULL) return NULL;
    // 循环结构:空链表时头结点的前驱、后继都指向自身
    phead->next = phead;
    phead->prev = phead;
    return phead;
}

// 3. 尾插:在链表尾部插入新节点(O(1),头结点的prev就是尾节点)
void ListPushBack(ListNode* phead, LTDataType val)
{
    assert(phead);
    ListNode* newNode = CreateListNode(val);
    if (newNode == NULL) return;

    ListNode* tail = phead->prev; // 直接获取尾节点
    // 链接新节点与原尾节点
    tail->next = newNode;
    newNode->prev = tail;
    // 链接新节点与头结点,保持循环结构
    newNode->next = phead;
    phead->prev = newNode;
}

// 4. 头插:在链表头部(头结点之后)插入新节点
void ListPushFront(ListNode* phead, LTDataType val)
{
    assert(phead);
    ListNode* newNode = CreateListNode(val);
    if (newNode == NULL) return;

    ListNode* first = phead->next; // 原第一个有效节点
    // 链接头结点与新节点
    phead->next = newNode;
    newNode->prev = phead;
    // 链接新节点与原第一个节点
    newNode->next = first;
    first->prev = newNode;
}

// 5. 查找元素:返回第一个值为val的节点指针,找不到返回NULL
ListNode* ListFind(ListNode* phead, LTDataType val)
{
    assert(phead);
    ListNode* cur = phead->next;
    // 循环遍历,回到头结点时表示遍历结束
    while (cur != phead)
    {
        if (cur->data == val)
        {
            return cur;
        }
        cur = cur->next;
    }
    return NULL;
}

// 6. 指定位置插入:在pos节点之前插入新节点(O(1))
void ListInsert(ListNode* pos, LTDataType val)
{
    assert(pos);
    ListNode* newNode = CreateListNode(val);
    if (newNode == NULL) return;

    ListNode* prev = pos->prev;
    // 链接前驱节点与新节点
    prev->next = newNode;
    newNode->prev = prev;
    // 链接新节点与pos节点
    newNode->next = pos;
    pos->prev = newNode;
}

// 7. 头删:删除第一个有效节点
void ListPopFront(ListNode* phead)
{
    assert(phead);
    // 空链表:只有头结点
    if (phead->next == phead)
    {
        printf("链表为空,无法头删\n");
        return;
    }

    ListNode* del = phead->next;
    ListNode* next = del->next;
    // 头结点直接链接第二个有效节点
    phead->next = next;
    next->prev = phead;
    free(del);
}

// 8. 尾删:删除最后一个有效节点(O(1))
void ListPopBack(ListNode* phead)
{
    assert(phead);
    if (phead->next == phead)
    {
        printf("链表为空,无法尾删\n");
        return;
    }

    ListNode* tail = phead->prev;
    ListNode* prev = tail->prev;
    // 倒数第二个节点直接链接头结点
    prev->next = phead;
    phead->prev = prev;
    free(tail);
}

// 9. 指定位置删除:删除pos节点(O(1))
void ListErase(ListNode* pos)
{
    assert(pos);
    // 禁止删除哨兵位头结点
    assert(pos->next != pos);

    ListNode* prev = pos->prev;
    ListNode* next = pos->next;
    // 前后节点直接相连,跳过目标节点
    prev->next = next;
    next->prev = prev;
    free(pos);
}

// 10. 求链表长度
int ListLength(ListNode* phead)
{
    assert(phead);
    int len = 0;
    ListNode* cur = phead->next;
    while (cur != phead)
    {
        len++;
        cur = cur->next;
    }
    return len;
}

// 11. 打印链表
void ListPrint(ListNode* phead)
{
    assert(phead);
    printf("头结点 <-> ");
    ListNode* cur = phead->next;
    while (cur != phead)
    {
        printf("%d <-> ", cur->data);
        cur = cur->next;
    }
    printf("头结点\n");
}

// 12. 销毁链表(释放所有节点,包括哨兵位头结点)
void ListDestroy(ListNode* phead)
{
    assert(phead);
    ListNode* cur = phead->next;
    while (cur != phead)
    {
        ListNode* next = cur->next;
        free(cur);
        cur = next;
    }
    free(phead); // 最后释放头结点
    printf("链表销毁完成\n");
}

补充说明

双向循环链表的 ListInsertListErase 是通用核心接口,其余增删操作都可以直接复用它们:

  • 尾插 = 在头结点前插入:ListInsert(phead, val)
  • 头插 = 在首节点前插入:ListInsert(phead->next, val)
  • 尾删 = 删除尾节点:ListErase(phead->prev)
  • 头删 = 删除首节点:ListErase(phead->next)
相关推荐
王老师青少年编程3 小时前
2026年6月GESP真题及题解(C++七级):消消乐
数据结构·c++·算法·真题·gesp·2026年6月
海清河晏1113 小时前
数据结构 | 二叉搜索树
数据结构·c++·visual studio
Yang_jie_034 小时前
笔记:数据结构(顺序表)
数据结构·windows·笔记
一个初入编程的小白8 小时前
数据结构:栈
数据结构
我命由我123459 小时前
方差(实例实操、与标准差的区别)
java·数据结构·算法·数据分析·java-ee·intellij-idea·idea
努力努力再努力wz9 小时前
【高性能网络库与HTTP Server系列】:基于主从 Reactor 模型实现高性能 C++ 网络库与 HTTP Server
开发语言·网络·数据结构·数据库·c++·网络协议·http
什巳9 小时前
JAVA练习275-乘积最大子数组
java·开发语言·数据结构·算法
码完就睡11 小时前
数据结构——串的模式匹配算法(BF算法、KMP算法)
数据结构·算法
tachibana211 小时前
hot100 排序链表(148)
java·数据结构·算法·leetcode·链表