数据结构--双向链表

一、链表的分类

链表的结构⾮常多样,以下情况组合起来就有8种(2 x 2 x 2)链表结构:

带头:指的是链表中有哨兵位节点,该节点不存储任何有效数据,属于无效节点,通过这个无效节点当头节点 让我们某些方面使用会有一些优势

双向:有两个指针分别指向前一个节点和后一个节点,可以从两个方向遍历,从前往后从后往前。

循环:尾节点不在指向NULL而是指向头节点。

我们实际中最常⽤的两种结构: 单链表 和 双向带头循环链表

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

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

我们上篇博客讲了单链表,它其实就是不带头单向不循环(简称:单链表)

那么本节博客讲双链表,带头双向循环(简称 :双向链表)

二、双向链表的结构

双向链表节点中有两个指针prev和next,分别指向其前驱指针和后继指针。

链表节点的组成:数据+指向下一个节点的指针+指向前一个节点的指针。

注意:这⾥的“带头”跟前⾯我们说的“头节点”是两个概念,实际前⾯的在单链表阶段称呼不严 谨,带头链表⾥的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这⾥“放哨 的” “哨兵位”存在的意义: 遍历循环链表避免死循环。

三、双向链表的基本操作

老规矩依旧三个文件,一个头文件,两个源文件

我们要在双向链表实现以下操作:

1.定义双向链表

2.初始化双向链表

3.尾插

4.头插

5.尾删

6.头删

7.指定位置之后插入数据

8.删除节点

9.打印

10.查找

11.销毁

1.定义双向链表

typedef int LTDataType;
typedef struct ListNode
{
	LTDataType date;
	struct ListNode* next;
	struct ListNode* prev;
}LTNode;

date用来存储数据 ,next指针保存下个节点,prev指针保存前一个节点。


2.各个函数的声明

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType date;
	struct ListNode* next;
	struct ListNode* prev;
}LTNode;

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

//初始化
LTNode* LTInit();

//插入数据之前,链表必须要初始化到只有一个头节点的情况
//不能改变哨兵位的地址,因此一级即可

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

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

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

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

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

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

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

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

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

3.双向链表的的初始化及节点的申请

.c文件中实现(记得包含头文件)

//申请节点
LTNode* LTBuyNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	node->date = x;
	node->next = node->prev = node;
	return node;
}
//初始化
LTNode* LTInit()
{
	LTNode* phead = LTBuyNode(-1);
	return phead;
}

我们哨兵位不需要有效数据所以我们传个-1就可以了。

双向链表不能指向NULL,所以我们需要申请一个节点作为哨兵为,让我们的哨兵位自己指向自己。

可能会有人要疑惑了,为什么不能指向空呢,为什么又要自己指向自己呢?

双向链表为空时,和单链表不一样,创建双向链表时必须要初始化,初始化为一个哨兵位,我们才能往里面插入数据。

如果指向NULL那还怎么循环起来,所以我们让它指向自己,这样就可以循环起来了。


4.尾插

无论是我们的尾插还是头插,我们都不影响哨兵位,头插是在哨兵位后面插入,哨兵位永远不变。

现在我们先画图分析一下尾插如何插入再来写代码 

插入数据肯定是需要申请节点的,由图可见我们申请好了的节点叫newnode。

根据我们上面的图可以看到我们现在要让我们newnode的前驱指针要指向d3,那怎么拿到d3呢,我们哨兵位的前驱指针指向的不就是d3吗?newnode的后继指针要指向下一个节点,下一个节点不就哨兵位吗?直接让newnode的下一个节点指向哨兵即可,接着让我们d3的下一个节点指向我们新申请的尾节点newnode,再让我们哨兵位的前一个指针指向新申请好了的尾节点newnode。

现在来写代码

//尾插
void LTPushuBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	newnode->prev = phead->prev;
	newnode->next = phead;

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

我们传进来的哨兵位要确保有效,不能为空,如果为空就不是一个有效的双向链表,所以我们要加个断言。

注意:这是因为我们的哨兵位不能被删除,节点的地址也不能被改变,所以传一级即可


5.头插

画图分析一下再来写代码

插入数据肯定是需要申请节点的,由图可见我们申请好了的节点叫newnode。

由图可见我们头插就是在哨兵位的前面插入,我们哨兵指向的下一个节点是头节点,现在让我们的newnode的前一个指针指向哨兵位,我们直接指向哨兵位就可以了,然后newnode的下一个节点要指向我们的头节点,头节点就是哨兵位的下一个节点,直接指向即可,接着我们要让d1指向我们新申请好的头节点newnode,最后让我们哨兵位指向新申请好的头节点newnode

我们来写代码:

//头插
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;
}

记得加断言判断双向链表是否有效


6.打印

打印双向链表其实就是通过while循环来进行打印,由图可见我们定义了一个指针pcur来遍历我们的双向链表,那么while循环的结束条件是什么呢?,我们的pcur不能等于哨兵位,前面我们说哨兵位里面的数据是无效的,所以不能等于,那么条件就是如果等于我们的哨兵位就跳出循环。

来写一下代码:

//打印
void LTPrint(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->date);
		pcur = pcur->next;
	}
	printf("\n");
}

7.尾删

先画图分析一下:

根据上面的图可以看到我们通过定义一个指针del来存储d3这个尾节点,然后让我们d2的下一个节点指向我们的哨兵位,通过del指向前一个节点的nex就可以拿到我们d2的下一个节点了,直接指向哨兵位即可,接着让我们哨兵位的前一个指针指向我们d2的节点,通过del的前一个指针就可以拿到d2,最后把尾节点释放了。

现在来写代码

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

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

	free(del);
	del = NULL;
}

8.头删

老规矩先画图分析

由图可见我们先把头节点保存在一个指针del里面,我们让哨兵位的下一个节点不在指向我们的头节点了,而是指向我们d2,通过头节点的下一个节点就可以拿到d2,此时d2的前一个节点不在指向头节点而是指向哨兵位,最后把头节点释放。

来看代码

//头删
void  LTPopFront(LTNode* phead)
{
	assert(phead && phead->next != phead);
	LTNode* del = phead->next;

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

	free(del);
	del = NULL;
}

记得加断言判断链表,链表必须有效且链表不能为空(只有一个哨兵位)


9.查找

//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->date == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

查找函数和打印函数非常相似,都是通过循环进行遍历,循环的判断条件也是一样的。

如果找到了就返回找到的节点,没找到则返回NULL

10.在pos位置之后插入数据

依旧画图分析

申请了一个节点为newnode

上图所示让我们的newnode的下一个节点指向我们d3这个节点,pos的下一个节点就是d3,直接指向即可,然后还要让newnode的前一个指针指向pos,直接指向就可以了,接着让我们pos的下一个节点的前驱指针指向newnode,最后让pos指向申请好的newnode的节点。

来写一下代码

//在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;
}

注意:这里我们传过来的pos要为有效节点,所以要加个断言


11.删除pos节点

依旧画图分析

如图可以看到我们d3的前一个指针不在指向pos而是指向pos的前一个指针,通过我们pos指向的下一个节点的前驱指针就可以找到d3,接着pos的前一个指针的next指针也不在指向pos而是指向pos的下一个节点,最后把pos这个节点释放掉了。

//删除pos节点
void LTErease(LTNode* pos)
{
	//pos理论上来说不能为phead,但是没有参数phead,无法增加校验
	assert(pos);
	pos->next->prev = pos->prev;
	pos->prev->next = pos->next;
	free(pos);
	pos = NULL;
	

}

 注意:这里我们传过来的pos要为有效节点,所以要加个断言


12.销毁

//销毁
LTNode* 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;
}

通过while循环遍历挨个释放节点然后置为NULL,这里要定义一个指针用来保存下一个节点,循环条件也是不等于哨兵位,跳出循环的时候说明已经等于哨兵位了,接着释放哨兵位。  


注意: 

销毁和删除一定要手动置为NULL 

四、双向链表所有的代码 

.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType date;
	struct ListNode* next;
	struct ListNode* prev;
}LTNode;

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

//初始化
LTNode* LTInit();

//插入数据之前,链表必须要初始化到只有一个头节点的情况
//不能改变哨兵位的地址,因此一级即可

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

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

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

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

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

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

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

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

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

.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"
//打印
void LTPrint(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->date);
		pcur = pcur->next;
	}
	printf("\n");
}

//申请节点
LTNode* LTBuyNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	node->date = x;
	node->next = node->prev = node;
	return node;
}
//初始化
LTNode* LTInit()
{
	LTNode* phead = LTBuyNode(-1);
	return phead;
}

//尾插
void LTPushuBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	newnode->prev = phead->prev;
	newnode->next = phead;

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

//头插
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;
}

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

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

	free(del);
	del = NULL;
}
//头删
void  LTPopFront(LTNode* phead)
{
	assert(phead && phead->next != phead);
	LTNode* del = phead->next;

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

	free(del);
	del = NULL;
}

//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->date == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}
//在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;


}

//删除pos节点
void LTErease(LTNode* pos)
{
	//pos理论上来说不能为phead,但是没有参数phead,无法增加校验
	assert(pos);
	pos->next->prev = pos->prev;
	pos->prev->next = pos->next;
	free(pos);
	pos = NULL;
	

}

//销毁
LTNode* 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;
}

测试文件.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"
void ListTest01()
{
	LTNode* plist = LTInit();//初始化
	
	//测试尾插
	LTPushuBack(plist, 1);
	LTPrint(plist);//打印
	LTPushuBack(plist, 2);
	LTPrint(plist);//打印
	LTPushuBack(plist, 3);
	LTPrint(plist);//打印
	LTPushuBack(plist, 4);
	LTPrint(plist);//打印

	//测试头插
	LTPushFront(plist, 9);
	LTPrint(plist);
	LTPushFront(plist, 8);
	LTPrint(plist);

	//测试尾删
	LTPopBack(plist);
	LTPrint(plist);
	LTPopBack(plist);
	LTPrint(plist);
	LTPopBack(plist);
	LTPrint(plist);

	//测试头删
	LTPopFront(plist);
	LTPrint(plist);
	LTPopFront(plist);
	LTPrint(plist);

	//查找
	LTNode* find = LTFind(plist, 4);
	if (find == NULL)
		printf("没找到\n");
	else
		printf("找到了\n");

	//测试指定位置之后插入数据
	LTInsert(find, 9);
	LTPrint(plist);

	//测试删除pos节点
	LTErease(find);
	LTPrint(plist);
	find = NULL;

	//销毁
	LTDesTroy(plist);
	plist = NULL;
}
int main()
{
	ListTest01();
}
  • 28
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值