数据结构 - > 双链表

一. 双链表的概念

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

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

二. 双链表

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
相关推荐
小江的记录本2 小时前
【分布式】分布式核心组件——分布式限流:固定窗口、滑动窗口、漏桶、令牌桶算法,网关层/服务层限流实现
java·分布式·后端·python·算法·安全·面试
不懂的浪漫2 小时前
一次设备映射缓存设计:用多索引 Map 把高频查询从遍历变成直接命中
java·算法·spring·缓存
apollowing2 小时前
启发式算法WebApp实验室:从搜索策略到群体智能的能力进阶(三十)
算法·启发式算法·web app
qeen872 小时前
【数据结构】队列及其C语言模拟实现
c语言·数据结构·c++·学习·队列
田野追逐星光2 小时前
C++继承 -- 讲解超详细(上)
c++·算法
ZPC82102 小时前
ROS2 共享内存 SHM > UDP 速度
人工智能·算法·计算机视觉·机器人
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题】【Java基础篇】第4题:LinkedList是单向链表还是双向链表
java·开发语言·数据结构·后端·链表·面试·list
三毛的二哥11 小时前
BEV:典型BEV算法总结
人工智能·算法·计算机视觉·3d
南宫萧幕11 小时前
自控PID+MATLAB仿真+混动P0/P1/P2/P3/P4构型
算法·机器学习·matlab·simulink·控制·pid