Cyber骇客的脑机双链回流码 ——【初阶数据结构与算法】线性表之双向链表



点击下面查看作者专栏 🔥🔥C语言专栏🔥🔥 🌊🌊编程百度🌊🌊 🌠🌠如何获取自己的代码仓库🌠🌠

🌐索引与导读

💻链表分类

🔗Lucy的空间骇客裂缝:链表分类


💻双向链表的作用

选择双向链表 场景分析 需要双向遍历 频繁中间操作 需要快速删除 浏览器历史
文本编辑器 LRU缓存
内存管理 游戏对象管理
GUI系统


💻双向链表的概念和结构

🚩双向链表是一种链表数据结构

每个节点除了包含数据域(用于存储数据) 之外,还包含两个指针域一个指向前一个节点(prev),另一个指向后一个节点(next

最后一个节点有指向开头的指针next,开头的节点有指向结尾的指针prev,形成循环


📶双向链表的头节点

单链表的头节点和双向链表的头节点不是一个概念
带头链表的头节点实际上是哨兵位,不存储任何有效数据,只是在这里放哨的

📶双向链表节点的组成部分

初始定义

c 复制代码
struct ListNode {
	int data;
	struct ListNode* next;	//指向下一个节点
	struct ListNode* prev;	//指向上一个节点
};

重新定义数据类型后

c 复制代码
typedef int LTDataType;
typedef struct ListNode {
	LTDataType data;
	struct ListNode* next;	//指向下一个节点
	struct ListNode* prev;	//指向上一个节点
}LTNode;


初始情况下:
plist(头节点)为空,next指针和prev指针都指向自己


分文件编写双向链表

test.c 接口声明 lisNode.c 功能实现 listNode.h 函数的声明与头文件定义


listNode.c

🌠双向链表节点的申请

一个新的节点的申请,结构如下:

前驱指针和后驱指针都要指向自己

c 复制代码
/*双向链表节点的申请*/
LTNode* LTBuyNode(LTDataType x){
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL) {
		perror("malloc fail!");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = newnode->prev = newnode;

	return newnode;
}

🌠双向链表的初始化

c 复制代码
void LTInit(LTNode** pphead) {
		assert(pphead);
		*pphead = LTBuyNode(-1);
}

🔥🔥🔥🔥讲解代码要点:

  • ❗注意 :我们需要二级指针(LTNode**)的唯一场景是:我们需要修改头指针本身的值

判断双向链表是否为空

c 复制代码
bool LTEmpty(LTNode* phead){
	assert(phead);
	return (phead->next == phead && phead->prev == phead);
	}

🔥🔥🔥🔥讲解代码要点:
return (phead->next == phead && phead->prev == phead);

  • 如果链表的头节点的前驱指针和后继指针都指向自己,说明链表为空
  • assert(phead)防范的是链表没有初始化 或者传参传错了
    简单来说,就是保证指针有效不为空
  • assert(!LTEmpty(phead))保证链表还有数据不会只剩下头节点(不然头删尾删操作会把头节点删掉)

🌠双向链表的头插

❗插入的数据是在headd1之间❗

c 复制代码
void LTPushFront(LTNode* phead, LTDataType x) {
	assert(phead);

	//申请一个新节点
	LTNode* newnode = LTBuyNode(x);

	newnode->prev = phead;
	newnode->next = phead->next;

	phead->next->prev = newnode;
	phead->next = newnode;
}

🔥🔥🔥🔥讲解代码要点:

  • 连接秘诀:
    先连接新节点
    再重置头节点

🌠双向链表的头删

c 复制代码
void LTPopFront{LTNode* phead}{
	assert(!LTEmpty(phead));

	LTNode* del = phead->next;
	phead->next = del->next;
	del->next->prev = phead;

	free(del);
	del = NULL;
	}

🔥🔥🔥🔥讲解代码要点:

LTNode* del = phead->next;

  • 链表的头删是删除phead的下一个节点

为何不直接assert(phead);

  • assert(phead)防范的是链表没有初始化 或者传参传错了
    简单来说,就是保证指针有效不为空
  • assert(!LTEmpty(phead))保证链表还有数据不会只剩下头节点(不然头删尾删操作会把头节点删掉)

🌠双向链表的尾插

c 复制代码
void LTPushBack(LTNode* phead, LTDataType X) {
		assert(phead);

		LTNode* newnode = LTBuyNode(X);

		newnode->prev = phead->prev;
		newnode->next = phead;

		//此时phead的前驱指针还是指向原来的节点,需要进行修改
		phead->prev->next = newnode;
		phead->prev = newnode;
	}

由于是带头循环

每个节点都有4条逻辑线连接着,通常先修改两个节点间的链接,再修改循环的链接


🌠双向链表的尾删

c 复制代码
void LTPopBack(LTNode* phead){
	assert(!LTEmpty(phead));

	LTNode* del = phead->prev;
	del->prev->next = phead;  		//让d2指向phead

	phead->prev = del->prev;
	
	free(del);
	del = NULL;
	}

🔥🔥🔥🔥讲解代码要点:

链表的prevnext的顺序不要搞错了

  • 如上图
    headprev节点是d3d3prev节点是d2
    headnext节点是d1d3next节点是head

🌠双向链表的遍历打印

c 复制代码
void LTPint(LTNode* phead){
	assert(phead); 
	LTNode* pcur = phead->next;
	while(pcur != phead){
		printf("%d ->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

🌠双向链表的查找

c 复制代码
LTNode* LTFind(LTNode* phead, LTDataType x){
	assert(phead);
	LTNode* pcur = phead->next;
	while(pcur != phead) {
		if(pcur->data == x) {
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

🌠双向链表在pos之前插入节点

c 复制代码
void LTInsert(LTNode* pos, LTDataType x) {
		assert(pos);

		//申请一个新节点
		LTNode* newnode = LTBuyNode(x);
		LTNode* prev = pos->prev;
		
		//1. 处理 prev 和 newnode 的关系
		prev->next = newnode;
		newnode->prev = prev;

		//2. 处理 newnode 和 pos 的关系
		newnode->next = pos;
		pos->prev = newnode;
}

🤔代码核心逻辑🤔
建立 prev <-> newnode <-> pos 的连接


🌠双向链表任意位置的删除

c 复制代码
void LTErase(LTNode* pos){
	assert(pos);
	assert(phead);
	assert(phead != pos);

			LTNode* prev = pos->prev;
			LTNode* next = pos->next;
		
		prev->next = next;
		next->prev = prev;

	free(pos);
	}
  • assert(phead != pos);
    确保phead不等于pos

🌠双向链表的销毁

c 复制代码
/*双向链表的销毁*/
void LTDestroy(LTNode** pphead) {
	assert(pphead);
	assert(*pphead);

	LTNode* pcur = (*pphead)->next;
	while (pcur != *pphead) {
			LTNode* next = pcur->next;
			free(pcur);						//pcur只是内存释放,还可以改变指针指向
			pcur = next;
		}
	free(*pphead);
	*pphead = NULL;
	}

🔥🔥🔥🔥讲解代码要点:

  • 重点搞清楚二级指针free的操作原理
    二级指针只有在修改头文件的值的时候才使用
    free释放的指针指向的数据内存,但是原指针指向的未知的内存,会成为悬空指针

listNode.h

c 复制代码
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

typedef int LTDataType;
typedef struct ListNode {
	LTDataType data;
	struct ListNode* next;	//指向下一个节点
	struct ListNode* prev;	//指向上一个节点
}LTNode;

//双向链表的初始化
/*要改变头节点plist*/
void LTInit(LTNode** pphead);

//双向链表初始化的另一种方式
LTNode* LTInit2();

//双向链表节点的申请
LTNode* LTBuyNode(LTDataType x);

//双向链表的尾插
//我们需要二级指针(LTNode** )的唯一场景是:我们需要修改"头指针"本身的值
void LTPushBack(LTNode* phead, LTDataType);

//双向链表的头插
void LTPushFront(LTNode* phead, LTDataType);

//双向链表的尾删
void LTPopBack(LTNode* phead);

//双向链表的头删
void LTPopFront(LTNode* phead);

//双向链表的头删
void LTPopFront(LTNode* phead);

//判断双向链表是否为空
bool LTEmpty(LTNode* phead);

//双向链表的遍历
void LTPrint(LTNode* phead);

//双向链表的查找
LTNode* LTFind(LTNode* phead, LTDataType x);

//双向链表在pos之前插入节点
void LTInsert(LTNode* pos, LTDataType x);

//双向链表任意位置的删除
void LTErase(LTNode* phead, LTNode* pos);

//双向链表的销毁
void LTDestroy(LTNode** pphead);

test.c

test.c的文件负责对函数功能进行测试

记得包含头文件!!!
#include "ListNode.h"

❓测试尾插和尾删(附带销毁)

c 复制代码
//1.测试尾插
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
printf("插入1、2、3、4后:");
LTPrint(plist);

//2.测试尾删
 LTPopBack(plist);
 printf("尾删一次后:     ");
 LTPrint(plist); // 预期: 1 ->2 ->3 ->

 LTPopBack(plist);
 LTPopBack(plist);
 printf("再尾删两次后:   ");
 LTPrint(plist); // 预期: 1 ->

// 3. 测试双向链表的删除
LTDestroy(&plist);
	printf("销毁链表后:     ");
LTPrint(plist); // 触发 assert(phead)

❓测试头插和头删(附带销毁)

c 复制代码
    // 1. 测试头插
    LTPushFront(plist, 100);
    LTPushFront(plist, 200);
    LTPushFront(plist, 300);
    printf("头插100,200,300后: ");
    LTPrint(plist); // 预期: 300 ->200 ->100 ->

    // 2. 测试头删
    LTPopFront(plist);
    printf("头删一次后:        ");
    LTPrint(plist); // 预期: 200 ->100 ->

    // 3. 测试判空
    LTPopFront(plist);
    LTPopFront(plist);
    if (LTEmpty(plist)) {
        printf("链表当前为空 (Correct)\n");
    }
    else {
        printf("链表判空逻辑错误\n");
    }
		
	  //4. 测试销毁
	  LTDestroy(&plist);
	  printf("销毁链表后:     ");
		LTPrint(plist); // 触发 assert(phead)

❓测试 查找、任意位置插入(LTInsert)、任意位置删除(LTErase)

c 复制代码
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
printf("初始链表: ");
LTPrint(plist);

// 1. 测试查找 + 插入
// 需求:在 2 的前面插入 20
LTNode* pos = LTFind(plist, 2);
if (pos) {
    LTInsert(pos, 20);
    printf("在2前面插入20: ");
    LTPrint(plist); // 预期: 1 ->20 ->2 ->3 ->
}
else {
    printf("未找到节点 2\n");
}

// 2. 测试查找 + 删除
// 需求:删除节点 2
pos = LTFind(plist, 2);
if (pos) {
    LTErase(plist, pos);
    pos = NULL; // 防止非法访问
    printf("删除节点2后:   ");
    LTPrint(plist); // 预期: 1 ->20 ->3 ->
}

❓测试主函数

c 复制代码
int main(){
	//TestList1();
	//TestList2();
	//TestList3();

return 0;
}

三个测试函数不可以一起调用,因为**assert会报错**


希望读者多多三连

给小编一些动力

蟹蟹啦!

相关推荐
2401_841495642 小时前
【LeetCode刷题】缺失的第一个正数
数据结构·python·算法·leetcode·数组·哈希·缺失最小正整数
C++业余爱好者2 小时前
Java 中的数据结构详解及应用场景
java·数据结构·python
ouliten2 小时前
《Linux C编程实战》笔记:mmap
linux·c++·笔记
小尧嵌入式2 小时前
深入理解C/C++指针
java·c语言·开发语言·c++·qt·音视频
ULTRA??2 小时前
字符串处理小写字母转换大写字母
c++·python·rust
fish_xk2 小时前
c++的字符串string
开发语言·c++
拼好饭和她皆失2 小时前
二分答案算法详解:从理论到实践解决最优化问题
数据结构·算法·二分·二分答案
DeltaTime2 小时前
一 图形学概述, 线性代数
c++·图形渲染
月明长歌2 小时前
【码道初阶】Leetcode234进阶版回文链表:牛客一道链表Hard,链表的回文结构——如何用 O(1) 空间“折叠”链表?
数据结构·链表