双链表(双向)的简介与基本代码实现

单双链表的具体性质

单链表一般特指为单向无哨兵位不循环链表

双链表特指双向有哨兵位循环链表

1.双链表的含义与基本结构

双向链表是一种链式数据结构,它与单向链表最大的不同在于,每个节点不仅包含一个指向后继节点的指针next,还包含一个指向前驱节点的指针prev。

2.代码实现 

2.1 双向链表结构体创建

 由双向链表的结构组成可知我们需要:

1.type类型的变量

2.指向下一个节点的指针next

3.指向前一个节点的指针prev

为了使程序有一定灵活性我们需要:

1.使用#define定义数据类型

为了减少写繁杂较长的类型名我们需要:

1.重命名结构体

 2.2 双向链表的初始化

 分析初始化函数:

我们需要构建的是一个循环的双向带头链表

1.带头:也就是有头结点,头结点不需要存储数据,创建出来即可

2.双向循环:也就是说next和prev指针都要指向头结点本身,以此实现单节点循环

!!!我们每写一个细分功能的函数都必须直接测试一次这个功能是否有效,否则最终合并实现去综合功能的时候会报非常多错,难以精确定位到出错位置

在这里的局部变量窗口我们可以看到a下属的三个结构体成员变量

其中x为任意数,因为我们没有初始化x.

然后next和prev都指向了a,这说明实现成功了

2.3 双向链表的销毁

 

我们采用循环遍历节点的方式依次释放节点内存,来达到销毁双链表的目的

 1.先销毁有效节点(phead->next)

2.最后如果pcur==phead,说明指针又绕回来了,即其他的节点已经销毁完成了。此时我们

就把哨兵位销毁即可

 

此时我们看到头结点还是有效的,next和prev指针都指向自己的地址 

此时经过了销毁函数后,发现phead的所有成员变量都是无效的,说明双链表销毁成功了 

 2.4  尾插数据

 

尾插分析:

1.创建新节点

2.改变原链表中的各相关节点指向,设置新节点指向

这里的难点在于理清需要处理的节点间的关系和设置指向的先后顺序 

这是有多个节点组成的双向链表的情况

 这是只有头结点的特殊情况

接下来我们讲解为什么先设置新节点的指向

因为先设置新节点不会影响到原链表中各节点的指向,有利于后续改变原链表中节点的指向(否则找不到原链表的尾节点) 

在写完多节点尾插情况后,我们发现可以兼容无节点的情况,所以就直接使用多节点尾插的代码即可

 

这里调试一下尾插我们发现,a->6->5->4->3->a,说明双链表的节点间关系没有问题,调试成功。 

2.5  头插数据 

 

这里的关键也是理清需要改变的指针关系,下面我将所有情况分成三类去分析 

第一类是只有头结点的情况

第二类是只有一个节点的情况

第三类是有多个节点的情况

我们以第三种为例分析:

首先设置new_node的next与prev指针,next->old(phead->next) ,prev->phead

然后设置old(phead->next)节点,prev->new_node

最后改变phead节点,next->new_node

写出上面的代码之后,我们不用着急,先套用上面的代码是否可兼容剩下的情况,如果可以就不用再次分情况写代码了

如我们所愿,可以兼容。

观察调试代码,我们发现 a->3->2->1->a,而且我们是先头插一的,此时一在末尾说明头插成功。

2.6 尾删

这里与上面的尾插核心思路一样,要理清指针指向关系以及变化顺序 

这里简单讲讲:

首先,我们需要改变头结点和新的尾结点的指针指向,然后要释放原来的尾结点。

因为新的尾结点需要通过原来的尾结点来访问,所以我们在一开始需要创建一个新的变量去存储新的尾结点的地址,之后就释放原来的尾结点和改变指针指向就可以了。

同样的,情况分三种,并且也是这段代码也是兼容的。

这里原本是a->3->2->1->a的,然后我们尾删了3,所以变成了a->3->2->a.说明尾删的代码调试成功 

2.7 头删

 

与尾删的思路非常类似(这里头结点一般指第一个有效节点,而真正的头节点一般称为哨兵位)

为了可以访问新的头结点,我们先存储它的地址在new_head中。

然后释放原本的头结点 ,改变新的头结点和哨兵位的指向

 

这里看到原本a->3->2->1->a的关系,经过头删后变成了a->2->1->a,说明头删成功了 

2.8 在指定位置之后插入数据 

 

这里的方法与上面讲过的非常类似,只需要把图画出来就可以迅速解决

!!把各种情况列举出来后,先把普遍情况的代码写出来,然后带入其他情况,看看是否可以兼容进入普遍情况,如果不行就再对代码调整。 

 

这里原本是a->3->2->1->a,然后我们在哨兵位a后面插入了数据0,所以变成了a->0->3->2->1->a。这说明插入指定位置成功了。 

2.9 删除pos节点 

 

在操作顺序上,这里我们看起来有个较大的变动,我们以前不把释放放到最后。

事实上,我们释放节点的前提是获得了足够多需要操作的地址之后。

但这里我们需要利用pos节点间接访问所需要的地址,所以放在最后释放。

 2.10 节点查找

  

 

这里我们用了b指针来接受寻找函数的返回值,如果返回值为NULL,说明没找到。返回有效的地址值,说明找到了。这里我们双链表里面没有5,所以返回了NULL,打印没找到,调试成功 

2.11 打印数据 

 

 

3. 代码分享 

list.c 

#include"list.h"
//双向链表的初始化
ltnode* LTInit()
{
	ltnode *a = (ltnode*)malloc(sizeof(ltnode));
	a->next = a;//形成循环链表
	a->prev = a;
	return a;
}
//双向链表的销毁
void LTDesTroy(ltnode* phead)
{
	assert(phead);
	ltnode*pcur = phead->next;
	while (pcur!=phead)
	{
		ltnode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//此时除了phead都被释放了
	free(phead);
	phead = NULL;
}
//打印节点数据
void LTPrint(ltnode* phead)
{
	assert(phead);
	ltnode *pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d ", pcur->x);
		pcur = pcur->next;
	}

}
//尾插
void LTPushBack(ltnode* phead, type x)
{
	assert(phead);//不能插入节点到NULL处
	ltnode *new_node = (ltnode *)malloc(sizeof(ltnode));
	new_node->x = x;
	//先改变新节点的指向
	new_node->next = phead;
	new_node->prev = phead->prev;
	//改变倒数第二个节点的指向
	ltnode *del = phead->prev;//倒数第二个节点
	del->next = new_node;
	//改变头结点的指向
	phead->prev = new_node;

}
//头插
void LTPushFront(ltnode* phead, type x)
{
	assert(phead);
	ltnode *new_node = (ltnode *)malloc(sizeof(ltnode));
	new_node->x = x;
	//先改变新节点指向
	new_node->next = phead->next;
	new_node->prev = phead;
	//改变原本的第一个有效节点指向
     phead->next->prev = new_node;
	//改变头结点指向
	phead->next = new_node;
}
//尾删
void LTPopBack(ltnode* phead)
{
	//保证链表必须是有有效节点的
	assert(phead && phead->next!=phead);
	//多节点情况
	//找到新的尾结点地址
	ltnode *ptail = phead->prev->prev;
	//销毁尾结点
	free(phead->prev);
	//改变新的尾结点地址
	ptail->next = phead;
	//改变头结点指向
	phead->prev = ptail;
}
//头删
void LTPopFront(ltnode* phead)
{
	assert(phead && phead->next!=phead);
	//获取新的头结点地址
	ltnode * new_head = phead->next->next;
	//释放原来的头结点
	free(phead->next);
	//改变新节点指向
	new_head->prev = phead;
	//改变头结点指向
	phead->next = new_head;
}
//在pos位置之后插入数据
void LTInsert(ltnode* pos, type x)
{
	assert(pos);
	ltnode *new_node = (ltnode *)malloc(sizeof(ltnode));
	new_node->x = x;
	//获取new的下一个节点的地址
	ltnode * next = pos->next;
	//设置new节点的指针指向
	new_node->next = next;
	new_node->prev = pos;
	//设置next节点的指针指向
	next->prev = new_node;
	//设置pos位置的指针指向
	pos->next = new_node;

}
//删除pos节点
void LTErase(ltnode* pos)
{
	assert(pos && pos->next != pos);
	//获取pos前的节点
	ltnode *pre = pos->prev;
	//让pre指向pos后的节点
	pre->next = pos->next;
	//让next的prev指向pre
	pos->next->prev = pre;
	//释放pos节点
	free(pos);
	pos = NULL;
}
//寻找节点
ltnode* LTFind(ltnode* phead, type x)
{
	assert(phead);
	//遍历有效节点
	ltnode * pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->x == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}






int main ()
{
	ltnode *a = LTInit();//测试函数功能是否正常
	LTPushFront(a, 1);
	LTPushFront(a, 2);
	LTPushFront(a, 3);
	
	LTPrint(a);
	LTDesTroy(a->next);//测试是否可以顺利删除
	return 0;
}

 list.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//定义双链表的结构体
#define type int
typedef struct listnode
{
	type x;
	struct listnode *next;
	struct listnode *prev;
}ltnode;

//双向链表的初始化
ltnode* LTInit();  
//双向链表的销毁
void LTDesTroy(ltnode* phead);
//打印节点数据
void LTPrint(ltnode* phead);
//尾插
void LTPushBack(ltnode* phead, type x);
//头插
void LTPushFront(ltnode* phead, type x);
//尾删
void LTPopBack(ltnode* phead);
//头删
void LTPopFront(ltnode* phead);
//在pos位置之后插入数据
void LTInsert(ltnode* pos, type x);
//删除pos节点
void LTErase(ltnode* pos);
//寻找数据
ltnode* LTFind(ltnode* phead, type x);
//打印节点数据
void LTPrint(ltnode* phead);

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值