数据结构 - > 双链表

一. 双链表的概念

双链表一般指的是双向循环带头结点的链表,是一种链式存储结构,它是单链表的升级版,和单链表的最大区别是:每个节点不仅能指向后继节点,还能指向前驱节点,可以双向遍历。

这里的"带头"跟平常我们说的"头节点"是两个概念,实际在单链表中的称呼不严谨。带头链表里的"头节点",实际为**"哨兵位"** ,"哨兵位"节点不存储任何有效元素 ,只是站在这里"放哨"的"哨兵位"存在的意义:遍历循环链表避免死循环

二. 双链表

1. 单链表实现的功能(List.h 声明函数)

cpp 复制代码
#pragma once

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

typedef int LTDataType;

// 定义双向链表节点的结构
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}LTNode;


// 声明双向链表中提供的方法

// 初始化
LTNode* LTInit();

// 打印
void LTPrint(LTNode* phead);

// 尾插
void LTPushBack(LTNode* phead, LTDataType x);

// 申请节点
LTNode* LTBuyNode(LTDataType x);

// 头插
void LTPushFront(LTNode* phead,LTDataType x);

// 尾删
void LTPopBack(LTNode* phead);

// 头删
void LTPopFront(LTNode* phead);

// 查找
LTNode* LTFind(LTNode* phead, LTDataType x);

// 在 pos 位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);

// 删除 pos 节点
void LTErase(LTNode* pos);

// 销毁
void LTDesTroy(LTNode* phead);

**说明:**双链表是带头链表,即它的头节点(哨兵位)是不可变的,它的作用是站岗放哨,为后续的有效节点标明位置,因此,与单链表不同的是,双链表在函数传参的过程中传的是一级指针,不再是二级指针(保证哨兵节点不可改变)

2. 函数实现(LIst.c)

1> 打印

cpp 复制代码
// 打印
void LTPrint(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

2> 申请节点

cpp 复制代码
// 申请节点
LTNode* LTBuyNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		exit(1);
	}
	node->data = x;
	node->next = node->prev = node;

	return node;
}

3> 初始化

cpp 复制代码
// 初始化
LTNode* LTInit()
{
	LTNode* phead = LTBuyNode(-1);
	return phead;
}

单链表为空的意思是就是空链表,链表内无存在任何节点,而双链表为空时,此时链表中只剩下一个头节点(哨兵位),无有效节点,则带表双链表为空。

因此,在插入数据之前,双链表必须初始化到只有一个头节点的情况。

那么,是否可以将哨兵节点的 prev 指针和 next 指针初始化为 NULL ? 不可以

双链表是循环链表,首尾相连,因此,链表的 prev 指针和 next 指针应该在初始化时指向哨兵节点本身。

4> 尾插

cpp 复制代码
// 尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);

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

	// 这两行代码不能交换位置
	phead->prev->next = newnode;
	phead->prev = newnode;
}

这两行代码不能交换位置:如图,phead->prev 指的是 d3 ,这里是要让 d3 指向新的节点(newnode),再让头节点前驱指向新的节点,形成新的循环。

如果代码位置交换,会找不到 d3。

5> 头插

cpp 复制代码
// 头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);

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

	// 这两行代码不能交换位置
	phead->next->prev = newnode;
	phead->next = newnode;
}

要注意,我们这里所说的头插是指头插到第一个有效节点的前面,即头节点(哨兵位)的后面。

这两行代码不能交换位置:先改后面节点的指向,再改前面头节点的指向,因为头节点一改,就再也找不到原来的第一个节点(d1)。

6> 尾删

cpp 复制代码
// 尾删
void LTPopBack(LTNode* phead)
{
	// 链表必须有效且链表不能为空(只有一个哨兵位)
	assert(phead && phead->next != phead);

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

	// 删除 del 节点
	free(del);
	del = NULL;
}

删除的条件:链表有效且链表不能为空(只有一个哨兵位)

注意:在操作之前要将原来的尾节点(del=phead->prev) 保存下来,防止后续的改变指针指向而找不到原尾节点的位置了。

7> 头删

cpp 复制代码
// 头删
void LTPopFront(LTNode* phead)
{
	assert(phead && phead->next != phead);

	LTNode* del = phead->next;

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

	// 删除 del 节点
	free(del);
	del = NULL;
}

删除的条件:链表有效且链表不能为空(只有一个哨兵位)

头删是指删除链表的第一个有效节点,使第二个有效节点成为新的一个有效节点。

8> 查找

cpp 复制代码
// 查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	// 没有找到
	return NULL;
}

9> 在 pos 位置之后插入数据

cpp 复制代码
// 在 pos 位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* newnode = LTBuyNode(x);
	newnode->next = pos->next;
	newnode->prev = pos;
	
	pos->next->prev = newnode;
	pos->next = newnode;
}

10> 删除 pos 节点

cpp 复制代码
// 删除 pos 节点
void LTErase(LTNode* pos)
{
	assert(pos);
	pos->next->prev = pos->prev;
	pos->prev->next = pos->next;
}

11> 销毁

cpp 复制代码
// 销毁
void LTDesTroy(LTNode* phead)
{
	assert(phead);

	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	
	// 此时 pcur 指向phead, 而 phead 还没有被销毁
	free(phead);
	phead = NULL;
}

注意:LTErase 和 LTDestroy 函数参数理论上要传二级指针,因为我们需要让形参的改变影响到实参,但为了保持接口一致性传一级指针。传一级指针存在的问题:当形参 phead 置为 NULL后,实参 plist 不会被修改为 NULL ,因此需要调用方法后手动将实参置为 NULL。

3. 代码演示(test.c)

cpp 复制代码
#include"List.h"

void ListTest01()
{
	LTNode* plist = LTInit();

	LTPushBack(plist, 1);
	LTPrint(plist);
	LTPushBack(plist, 2);
	LTPrint(plist);
	LTPushBack(plist, 3);
	LTPrint(plist);
	LTPushBack(plist, 4);
	LTPrint(plist);


	LTPushFront(plist, 5);
	LTPrint(plist);
	LTPushFront(plist, 6);
	LTPrint(plist);
	LTPushFront(plist, 7);
	LTPrint(plist);


	LTPopBack(plist);
	LTPrint(plist);
	LTPopBack(plist);
	LTPrint(plist);


	LTPopFront(plist);
	LTPrint(plist);
	LTPopFront(plist);
	LTPrint(plist);


	LTNode* find = LTFind(plist, 5);
	if (find == NULL)
	{
		printf("没有找到\n");
	}
	else
	{
		printf("找到了\n");
	}


	LTInsert(find, 66);
	LTPrint(plist);


	LTErase(find);
	LTPrint(plist);


	LTDesTroy(plist);
}

int main()
{
	ListTest01();

	return 0;
}v
相关推荐
ID_1800790547314 分钟前
淘宝商品详情数据接口深度解析:架构、鉴权、数据结构与实战
数据结构·架构
散峰而望41 分钟前
【算法练习】算法练习精选:陶陶摘苹果(基础+升级)、Music Notes、字串变换,你能AC几道?
数据结构·c++·算法·leetcode·贪心算法·github·动态规划
暗夜猎手-大魔王1 小时前
转载--Hermes Agent 04 | Agent 主循环:一次对话背后发生了什么
人工智能·python·算法
羊羊一洋1 小时前
GCC __attribute__ 完全指南:从入门到实战
c语言·stm32
手写码匠1 小时前
华为云Flexus+DeepSeek征文|基于华为云Flexus X实例 + Dify + DeepSeek 构建企业级智能知识库问答系统实战
人工智能·深度学习·算法·aigc
凤凰院凶涛QAQ1 小时前
《Java版数据结构 & 集合类剖析》集合框架的封装设计与顺序表:“从 Iterable 到 ArrayList:集合框架的‘职业树“
java·开发语言·数据结构
吴可可1231 小时前
Win7上开发CAD2004自定义实体全解析
c++·算法
YXXY3131 小时前
二叉树中的深搜算法介绍
算法
zz34572981132 小时前
C语言中字符串常量存储位置
c语言·开发语言·算法·青少年编程
noipp2 小时前
推荐题目:洛谷 P16510 [GKS 2015 #C] gRanks
java·c语言·开发语言·c++·python·算法