【数据结构】双向链表

2023-10-29

前面我们已经学完了单向链表,知道了单向链表如何进行增删查改等基本功能,而今天,我们将要学习双向链表。

目录

1.链表的分类

2.双向链表定义

3.双向链表接口的实现

所有接口函数一览

创建返回链表头节点

初始化链表

双向链表打印

双向链表尾插

双向链表尾删

双向链表头插

双向链表头删

双向链表在pos的前面进行插入

双向链表删除pos位置的节点

求双向链表长度

双向链表销毁

4.完整代码


回忆上次学习的单向链表结构图:

 下面还要详细的介绍一下各种链表示意图,上期遗漏了这部分内容

1.链表的分类

1. 单向或者双向

 2. 带头或者不带头

 3. 循环或者非循环

 虽然有这么多的链表的结构,但是我们实际中最常用还是以下两种结构:

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

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

由于上期我们已经学完了无头单向非循环链表,因此这期我们来学习带头双向循环链表

2.双向链表定义

和单向链表一样,双向链表每个节点都是由结构体组成的数据类型,但是双向链表结构体中增加了一个指向前一个节点的指针。

typedef int LTDataType; //方便修改数据类型

typedef struct ListNode
{
	struct ListNode* next;//指向下一个节点的指针
	struct ListNode* prev;//指向前一个节点的指针
	LTDataType data; //存储的数据
}LTNode;

我们今天要将的是带哨兵卫头节点的双向循环链表,它的头节点不存储有效数据,头节点中的prev指针指向最后一个节点

 当链表为空时,指针的指向关系图:

3.双向链表接口的实现

所有接口函数一览

下面是双向链表中经常要用到的一些接口函数:

//初始化
LTNode* ListInit();
//打印
void printList(LTNode* phead);
//尾插
void ListPushBack(LTNode* phead, LTDataType x);
//头插
void ListPushFront(LTNode* phead, LTDataType x);
//尾删
void ListPopBack(LTNode* phead);
//头删
void ListPopFront(LTNode* phead);
//判断链表是否为空
bool ListEmpty(LTNode* phead);
//在pos位置前插入x
void ListInsert(LTNode* pos, LTDataType x);
//删除pos位置节点
void ListErase(LTNode* pos);
//求链表长度
int ListSize(LTNode* phead);
//销毁
void ListDestory(LTNode* phead);

创建返回链表头节点

函数使用malloc动态分配了一个 LTNode类型的内存空间,用node进行接收。同时要检查内存分配是否成功,将新节点中的data赋值为传入的数据x,将新节点的nextprev指针都设置为NULL,最后返回新节点的地址。

//创建新节点
LTNode* BuyListNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	node->data = x;
	node->next = NULL;
	node->prev = NULL;
	return node;
}

初始化链表

初始化链表就是给链表创建一个头节点,这里需要使用到上面介绍完的创建新节点BuyListNode函数,因为头节点不存储有效数据,所以我们将data赋值为-1,同时头节点中的next和prev都指向自己,最后返回头节点的地址。

LTNode* ListInit()
{
	LTNode* phead = BuyListNode(-1);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

双向链表打印

双向链表的打印和单向链表类似,注意循环的截止条件不是cur为空,而当当cur重新指向头节点的时候停止循环。因为最后一个节点的next指针指向的是头节点。

//打印
void printList(LTNode* phead)
{
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

 

双向链表尾插

普通的方法是创建一个新的节点插入到链表的尾部,当然还有更为简单的方法,就是调用一下我们后面要讲到的 ListInsert 函数,这个函数可以在指定位置前插入一个节点并存储有效数据。如果这个指定位置是头节点的位置,那么头节点的前一个位置就是链表最后一个节点所在的位置。

下面介绍一下普通的方法吧:

 将最后一个节点命名为tail,然后根据上图修改一下phead tail newNode之间的指向关系即可

//尾插
void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);

	/*
		//可以直接调用void ListInsert来实现
		ListInsert(phead , x);
		//代码复用
	*/

	LTNode* newNode = BuyListNode(x);
	LTNode* tail = phead->prev;
	newNode->next = phead;
	newNode->prev = tail;
	tail->next = newNode;
	phead->prev = newNode;
}

 

双向链表尾删

这里也可以使用两种方法来实现,普通方法是找到最后一个节点,修改指向关系,再释放掉最后一个节点的空间。简单的方法是调用一个可以删除指定位置节点的函数ListErase,将最后一个节点的位置传入到这个函数就可以进行尾删,(最后一个节点的位置是phead->prev)这个函数下面也将会给大家介绍到。

普通方法:

 将最后一个节点命名为tail,倒数第二个节点命名为tailPrev,按上图修改它们的指向关系即可。

为了防止出现尾删空链表的情况出现,我们需要使用一个ListEmpty函数来判断链表是否为空,同时使用assert断言,一旦链表为空,用户仍然使用尾删程序就会报错。头删同理如此。

//尾删
void ListPopBack(LTNode* phead)
{
	assert(phead);
	assert(ListEmpty(phead));

	/*
		//可以复用ListErase函数来实现后面的代码
		ListErase(phead->prev);
	*/
	

	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;
	phead->prev = tailPrev;
	tailPrev->next = phead;

	free(tail);
}

双向链表头插

这里也可使用两种方法,我们介绍一下普通的方法:

 将头节点的后一个节点命名为pheadNext,然后将newNode插入到pheadpheadNext之间即可。

//头插
void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	/*
		//可以直接调用void ListInsert来实现
		ListInsert(phead->next , x);
		//代码复用
	*/

	LTNode* newNode = BuyListNode(x);
	LTNode* pheadNext = phead->next;
	phead->next = newNode;
	newNode->prev = phead;
	newNode->next = pheadNext;
	pheadNext->prev = newNode;
}

双向链表头删

这里也可使用两种方法,我们介绍一下普通的方法:

//头删
void ListPopFront(LTNode* phead)
{
	assert(phead);
	assert(ListEmpty(phead));

	/*
		//可以复用ListErase函数来实现后面的代码
		ListErase(phead->next);
	*/

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

	free(next);
}

双向链表在pos的前面进行插入

直接将pos的前一个节点命名为prev,再将newNode插入。

//在pos位置前插入x
void ListInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* prev = pos->prev;
	LTNode* newNode = BuyListNode(x);

	prev->next = newNode;
	newNode->prev = prev;
	newNode->next = pos;
	pos->prev = newNode;
}

双向链表删除pos位置的节点

将pos前一个节点命名为prev,后一个节点命名为next,这样命名方便我们修改指针的指向。然后按照上图修改指针即可。

//删除pos位置节点
void ListErase(LTNode* pos)
{
	assert(pos);
	LTNode* prev = pos->prev;
	LTNode* next = pos->next;

	prev->next = next;
	next->prev = prev;
	free(pos);
}

求双向链表长度

这里使用的方法是遍历整个链表。

//求链表长度
int ListSize(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	int size = 0;
	while (cur != phead)
	{
		++size;
		cur = cur->next;
	}
	return size;
}

双向链表销毁

这一步可以复用删除pos位置节点的函数ListErase,我们只要将链表的每一个节点地址都传入到这个函数,就可销毁链表。当然,最后也要将头节点给释放销毁。

//销毁
void ListDestory(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		ListErase(cur);
		cur = next;
	}
	free(phead);
}

4.顺序表和链表的区别

不同点 顺序表 链表
存储空间上 物理上一定连续 逻辑上连续,但物理上不一定连 续
随机访问 支持O(1) 不支持:O(N)
任意位置插入或者删除元 素 可能需要搬移元素,效率低O(N) 只需修改指针指向
插入 动态顺序表,空间不够时需要扩 容 没有容量的概念
应用场景 元素高效存储+频繁访问 任意位置插入和删除频繁
缓存利用率

顺序表优点:下标随机访问,cpu高速缓存命中率高

顺序表缺点:头部或者中间插入删除效率低,扩容有一定程度性能消耗,可能存在一定程度空间浪费。

链表优点:任意位置插入删除O(1)复杂度,按需申请释放。

链表缺点:不支持下标随机访问。

5.完整代码

头文件: 存放函数声明部分

#pragma once

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

typedef int LTDataType; //方便修改数据类型

typedef struct ListNode
{
	struct ListNode* next;//指向下一个节点的指针
	struct ListNode* prev;//指向前一个节点的指针
	LTDataType data; //存储的数据
}LTNode;
 
//初始化
LTNode* ListInit();
//打印
void printList(LTNode* phead);
//尾插
void ListPushBack(LTNode* phead, LTDataType x);
//头插
void ListPushFront(LTNode* phead, LTDataType x);
//尾删
void ListPopBack(LTNode* phead);
//头删
void ListPopFront(LTNode* phead);
//判断链表是否为空
bool ListEmpty(LTNode* phead);
//在pos位置前插入x
void ListInsert(LTNode* pos, LTDataType x);
//删除pos位置节点
void ListErase(LTNode* pos);
//求链表长度
int ListSize(LTNode* phead);
//销毁
void ListDestory(LTNode* phead);

List.c文件  存放函数定义

#define _CRT_SECURE_NO_WARNINGS 1

#include"List.h"

//创建新节点
LTNode* BuyListNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	node->data = x;
	node->next = NULL;
	node->prev = NULL;
	return node;
}

//初始化
LTNode* ListInit()
{
	LTNode* phead = BuyListNode(-1);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

//判断链表是否为空
bool ListEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next != phead;
}

//打印
void printList(LTNode* phead)
{
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

//尾插
void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);

	/*
		//可以直接调用void ListInsert来实现
		ListInsert(phead , x);
		//代码复用
	*/

	LTNode* newNode = BuyListNode(x);
	LTNode* tail = phead->prev;
	newNode->next = phead;
	newNode->prev = tail;
	tail->next = newNode;
	phead->prev = newNode;
}

//头插
void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	/*
		//可以直接调用void ListInsert来实现
		ListInsert(phead->next , x);
		//代码复用
	*/

	LTNode* newNode = BuyListNode(x);
	LTNode* pheadNext = phead->next;
	phead->next = newNode;
	newNode->prev = phead;
	newNode->next = pheadNext;
	pheadNext->prev = newNode;
}

//尾删
void ListPopBack(LTNode* phead)
{
	assert(phead);
	assert(ListEmpty(phead));

	/*
		//可以复用ListErase函数来实现后面的代码
		ListErase(phead->prev);
	*/
	

	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;
	phead->prev = tailPrev;
	tailPrev->next = phead;

	free(tail);
}

//头删
void ListPopFront(LTNode* phead)
{
	assert(phead);
	assert(ListEmpty(phead));

	/*
		//可以复用ListErase函数来实现后面的代码
		ListErase(phead->next);
	*/

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

	free(next);
}

//在pos位置前插入x
void ListInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* prev = pos->prev;
	LTNode* newNode = BuyListNode(x);

	prev->next = newNode;
	newNode->prev = prev;
	newNode->next = pos;
	pos->prev = newNode;
}

//删除pos位置节点
void ListErase(LTNode* pos)
{
	assert(pos);
	LTNode* prev = pos->prev;
	LTNode* next = pos->next;

	prev->next = next;
	next->prev = prev;
	free(pos);
}

//求链表长度
int ListSize(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	int size = 0;
	while (cur != phead)
	{
		++size;
		cur = cur->next;
	}
	return size;
}
	
//销毁
void ListDestory(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		ListErase(cur);
		cur = next;
	}
	free(phead);
}

test.c文件   用于测试接口函数正确性

#define _CRT_SECURE_NO_WARNINGS 1

#include"List.h"

//尾插测试
void ListTest1()
{
	LTNode* plist = ListInit();
	
	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);
	ListPushBack(plist, 5);

	printList(plist);

}

//头插测试
void ListTest2()
{
	LTNode* plist = ListInit();

	ListPushFront(plist, 1);
	ListPushFront(plist, 2);
	ListPushFront(plist, 3);
	ListPushFront(plist, 4);
	ListPushFront(plist, 5);

	printList(plist);
	 
}

//尾删测试
void ListTest3()
{
	LTNode* plist = ListInit();

	ListPushFront(plist, 1);
	ListPushFront(plist, 2);
	ListPushFront(plist, 3);
	ListPushFront(plist, 4);
	ListPushFront(plist, 5);
	printList(plist);

	ListPopBack(plist);
	ListPopBack(plist);
	ListPopBack(plist);
	ListPopBack(plist);

	printList(plist);
}

//头删测试
void ListTest4()
{
	LTNode* plist = ListInit();

	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);
	ListPushBack(plist, 5);
	printList(plist);

	ListPopFront(plist);
	ListPopFront(plist);
	ListPopFront(plist);
	ListPopFront(plist);
	
	printList(plist);
}


void ListTest5()
{
	LTNode* plist = ListInit();

	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);
	ListPushBack(plist, 5);
	printList(plist);

	ListDestory(plist);

	//printList(plist);
}

int main()
{
	//ListTest1();
	ListTest1();

	return 0;
}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【数据结构】双向链表 的相关文章

  • git回滚reset到指定分支

    在git中我们经常会遇到提交代码之后需要进行回滚的操作 可以通过git reset 命令进行回滚 首先找到需要回滚到的提交的commit id 然后通过 git reset hard 老的commit id 然后更新当前分支到最新提交 gi

随机推荐

  • 16 单台与多台机器配置https证书、全站https(以discuzx为例)

    HTTPS 1 HTTPS基本概述 为什么需要使用HTTPS 因为HTTP不安全 当我们使用http网站时 会遭到劫持和篡改 如果采用https协议 那么数据在传输过程中是加密的 所以黑客无法窃取或者篡改数据报文信息 同时也避免网站传输时信
  • angularJS1笔记-(1)-多控制器

    前端写好 div div div div
  • ubuntu安装lxml

    ubuntu安装lxml 可以参考一下 先执行 sudo apt get install libxml2 dev libxslt dev python dev 然后执行 sudo easy install lxml
  • 用户态、内核态的基本概念及切换方式

    用户态 内核态 一 用户态 内核态的基本概念 二 用户态 内核态的切换方式 一 用户态 内核态的基本概念 用户态 用户态运行的进程可以直接读取用户程序的数据 内核态 内核态运行的进程或程序几乎可以访问计算机的任何资源 不受限制 两者最重要的
  • MySQL8.0.15重置密码 windows10 64位 (忘记密码或者无法登录)

    经过多次试验最终 重置密码的步骤如下 1 以管理员身份 打开命令窗口cmd 输入命令 net stop mysql 停止MySQL服务 2 开启跳过密码验证登录的MySQL服务 输入命令 mysqld console skip grant
  • Linux Ubuntu 设置脚本开机启动

    主要参考下面这个博客 ubuntu18开机启动脚本 但是要注意 有的ubuntu里面并不存在这个目录 在一开始的 vim etc systemd system rc local service 这一步就会失败 比如我的系统 最后我使用fin
  • runtime engine VM的一些随想

    这篇文章还是我在写作的新书 新时期的Node js 入门的一部分 一些比喻 我们可以通过一些现实的比喻来理解接下来要讲述的概念 苏联是社会主义的一种运行时 这大概是我这辈子能想到的最贴切的比喻了 笑 社会主义只是一种思想 可以看做是一门编程
  • 控制流图怎么画

    一 什么是控制流图 控制流图 Control Flow Graph CFG 也叫控制流程图 是一个过程或程序的抽象表现 是用在编译器中的一个抽象数据结构 由编译器在内部维护 代表了一个程序执行过程中会遍历到的所有路径 它用图的形式表示一个过
  • FPGA实现图像二值形态学滤波——腐蚀膨胀

    一 二值图像 二值图像 Binary Image 是指图像上的每一个像素只有两种可能的取值或灰度等级状态 简言之 在图像中灰度等级只有两种0或255 黑或白 二 形态学 形态学 即数学形态学 Mathematical Morphology
  • 解决 required a single bean, but 2 were found的spring注入bean错误

    背景介绍 个人定义了一个interface 为了抽象与规范使用泛型进行约束 名字举例为 ITestService java public interface ITestService
  • 希沃展台如何使用_气化街小学开展希沃触摸一体机使用方法培训

    为进一步推进气化街小学信息化教学 帮助教师熟悉希沃教学触摸一体机设备的使用功能 掌握希沃教学触摸式一体机的基本操作技巧 充分发挥触摸一体机的教学辅助作用 5月29日上午10点 万柏林区气化街小学组织一 二 三年级全体任课老师 在王学光老师的
  • 老生常谈session,cookie的区别,安全性

    老生常谈session cookie的区别 安全性 张映 发表于 2010 07 25 分类目录 php 一 为什么session cookie经常会有人提到 做web开发的人基本上都会用session和cookie 但是仅仅只是会用 并不
  • 《五分钟科普ChatGPT》系列专栏---介绍 ChatGPT未来的发展方向

    VI ChatGPT未来的发展方向 聊天机器人技术的未来发展方向包括以下几个方面 6 1 强化学习和自主学习能力 强化学习是一种让机器代理通过与环境的交互学习并改进自身策略的方法 ChatGPT未来可能会融合强化学习技术 使其能够从与用户的
  • 开放定址法(线性探测),拉链法 -Hash算法

    总结 哈希别名为 Hash 或者 散列表 开放定址法是为了解决hash值碰撞后的处理 哈希表查找 杂凑法 http c biancheng net cpp html 1031 html 查找 http blog csdn net yang
  • 如何在Android Studio中添加RecyclerView-v7支持包

    一直知道RecyclerView可以代替ListView GridView使用 听说功能很强大 但还没有去学习过 今天想学习 没想到还没开始便撞墙了 输入Recycler 只有这两个东西 没有提示RecyclerView 说明支持包中没有
  • git命令合并分支代码

    合并步骤 例 dev分支合并到master分支 1 git checkout master 进入要合并的分支 2 git pull 拉取最新代码 3 git branch a 查看所有分支是否都pull下来了 4 git merge dev
  • Linux性能监控工具sysstat的cron文件

    简单来讲sysstat就是检测系统性能的工具 安装 yum install sysstat 查看生成的相关文件 rpm ql sysstat etc cron d sysstat etc sysconfig sysstat etc sysc
  • 如何使用Linux Top命令

    Linux中的top命令允许您监视当前正在运行的进程及其使用的系统资源 作为系统管理员 它可能是工具箱中最有用的工具 特别是如果您知道如何使用它的话 所有Linux发行版都预装了top实用程序 通过这个交互式命令 您可以自定义如何浏览进程列
  • node 版本管理工具 nvm,node版本升级、降级

    不同项目需要的 nodejs 版本不一致 需要在电脑上安装多个 node 版本 此时知道有一个 nvm 版本管理工具就非常必要了 NVM 下载安装 nvm 安装地址 https github com coreybutler nvm wind
  • 【数据结构】双向链表

    前面我们已经学完了单向链表 知道了单向链表如何进行增删查改等基本功能 而今天 我们将要学习双向链表 目录 1 链表的分类 2 双向链表定义 3 双向链表接口的实现 所有接口函数一览 创建返回链表头节点 初始化链表 双向链表打印 双向链表尾插