双向链表专题

双向链表专题

1.双向链表的概念和结构

前面我们知道我们学习的单链表是单向不带头不循环链表

而今天我们学习的双向链表恰恰相反——双向带头循环链表

我们来看看双向链表的节点定义:

struct ListNode
{
	int data;
	srtuct ListNode* next; // 指向下一个节点
	struct ListNode* prev; // 指向前一个节点
}

注意:

我们之前学习的单链表在空的时候就是一个NULL什么都没有

但是双向链表中并不是这样的,双向链表为空的时候,是存在一个哨兵位(也就是头节点)的 ,而这个哨兵位的next指针指向NULL

2.双向链表的实现

#pragma once
#define  _CRT_SECURE_NO_WARNINGS 1
# include<stdio.h>
# include<assert.h>
# include<stdlib.h>

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


// 声明双向链表的函数(方法)

// 双向链表的初始化
void LTInit(LTNode** pphead);
// 初始化的第二个思路
LTNode* LTInit2();


// 双向链表的打印
void LTPrint(LTNode* phead);

//双向链表的尾插
void LTPushBack(LTNode* phead, LTDataType x); // 如果传二级指针 但是不去修改哨兵位的位置 也是ok的
// 为什么这里传的是phead的一级指针呢 因为我们传进去的是哨兵位,不管是尾删还是头删我们都不改变哨兵位节点的位置

// 双向链表的头插
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);

双向链表的初始化:

初始化的代码:

// 申请节点
LTNode* LTBuyNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc");
		exit(1);
	}

	node->data = x;
	// 我们知道双向链表是一个带头双向循环链表  
	// 这里对next和prev指针的初始化不能置为空  要让其循环起来 也就是让这两个指针指向自己就行了
	node->next = node->prev = node;

	return node;
}

// 双向链表的初始化
void LTInit(LTNode** pphead)
{
	// 先给双向链表申请一个 哨兵位(头节点)
	*pphead = LTBuyNode(-1);// 这个-1没有意义 只是因为必须传个值给哨兵位而已
}
// 初始化的第二个思路
LTNode* LTInit2()
{
	LTNode* phead = LTBuyNode(-1); // 手动创建一个哨兵位 
	return phead;
}

调试代码:

void Test01()
{
	// 测试双向链表的初始化
	LTNode* plist = NULL;
	LTInit(&plist);
}


int main()
{
	Test01();
	return 0;
}

下面是调试的监视窗口 可以看到代码没有问题

双向链表的打印:

我们要先思考:

  1. 双向链表是一个带头双向循环链表,要考虑哨兵位的存在
  2. 遍历链表的时候循环什么时候停止

我们来看看代码如何是实现的:

// 双向链表的打印
void LTPrint(LTNode* phead)
{
	LTNode* pcur = phead->next; // 让pcur指向第一个有效节点
	if (pcur == phead)
	{
		printf("该双向链表为空,无法打印!\n");
		return;
	}
	// 遍历双向链表 找到每一个节点  
	while (pcur != phead)// 判断pcur是否是哨兵位 是的话就退出循环
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	// 走到这里已经打印完毕了
	printf("\n"); 
}

测试代码:

LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPrint(plist);// 1->2->

双向链表的尾插:

//双向链表的尾插
void LTPushBack(LTNode* pphead, LTDataType x);

我们先来思考一下

为什么传入的是一级指针呢?

因为不管是头删还是尾删 ,头插还是尾插 我们都不需要去改变哨兵位的位置

因此传入一级指针 就ok了

尾插的思路:

  1. 申请一个要尾插的节点newnode 存入用户输入的数据
  2. 将newnode的prev指针指向原链表的尾节点 phead->prev
  3. 将newnode的next指针指向哨兵位 newnode->next = phead
  4. 将原链表的尾节点的next指针指向newnode ,也就是phead->prev->next=newnode
  5. 将哨兵位的prev指针指向newnode 也就是phead->prev = newnode

来看一下尾插的代码如何实现:

// 双向链表的尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead); // 哨兵位不能为空

	LTNode* newnode = LTBuyNode(x); // 申请插入的节点
	// 开始尾插
	newnode->next = phead; // 让新节点的next指针指向哨兵位
	newnode->prev = phead->prev; // 让新节点的prev指针指向原链表的尾节点 (phead->prev)就是尾节点,

	phead->prev->next = newnode;// 让原链表的尾节点的next指针指向newnode
	phead->prev = newnode;// 让哨兵位的prev指针指向newnode  
	//要记住这个代码的顺序和上面的顺序不能调换,不然phead->prev被提前修改 就会出现错误
}

尾插的测试代码:

// 测试尾插
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPrint(plist);// 1->2->

双向链表的头插:

我们知道在哨兵位之前插入元素其实就是尾插

头插是插入到第一个有效节点和哨兵位之间

头插的思路:

  1. 申请一个newnode作为要插入的节点
  2. 让newnode的next指针指向原链表的第一个节点 也就是newnode->next = phead->next
  3. 让newnode的prev指针指向哨兵位,也就是newnode->prev = phead
  4. 再让原链表的第一个节点的prev指针指向newnode,也就是phead->next->prev = newnode
  5. 再让哨兵位的next指针指向newnode,也就是phead->next = newnode
  6. 注意了第五步的顺序和第四部不能调换

我们来看一下头插实现的代码:

// 双向链表的头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* newnode = LTBuyNode(x);
	// 开始头插
	// 先改变不会被受影响的newnode节点 如果先改变哨兵位 可能导致无法找到第一个节点之类的问题
	newnode->next = phead->next; // 让newnode的next指针指向原链表的第一个节点
	newnode->prev = phead;// 让newnode的prev指针指向哨兵位

	phead->next->prev = newnode;// 让第一个节点的prev指针指向newnode
	phead->next = newnode; // 让哨兵位的next指针指向newnode

	 如果一定要调换上面两句代码的顺序 也可以这样改
	//phead->next = newnode;
	//newnode->next->prev = newnode;
}

测试代码:

	// 测试头插
	LTPushFront(plist, 2);
	LTPushFront(plist, 1);
	LTPrint(plist);

双向链表的尾删:

先思考尾删的时候收到影响的节点和节点存储的指针有哪些

我们来看代码的实现:

// 双向链表的尾删
void LTPopBack(LTNode* phead)
{
	// phead不能为NULL  也就是必须是一个有效的双向链表
	assert(phead);
	// 并且这个双向链表不能为空
	assert(phead->next != phead);// phead不指向自己则不是空

	LTNode* del = phead->prev; // 让del保存尾节点
	// 先让该改变的指针改变,再去尾删
	del->prev->next = phead;// 让尾节点的前一个节点的next指针指向哨兵位
	phead->prev = del->prev;// 让哨兵位的prev指针指向尾节点的前一个节点
	// 尾删
	free(del);
	del = NULL;
}

测试代码:

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

双向链表的头删:

头删的思路:

  1. 首先用del记下要删除的节点phead->next
  2. 再让del的下一个节点的prev指针指向哨兵位
  3. 让哨兵位的next指针指向del的下一个节点

来看代码的实现:

// 双向链表的头删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);// 双向链表不能为空

	LTNode* del = phead->next;
	//头删 
	del->next->prev = phead; // 让del的下一个指针的prev指针指向哨兵位
	phead->next = del->next;// 让哨兵位的next指针指向del的下一个节点

	free(del);
	del = NULL;
}

测试代码:

	// 测试头删
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPopFront(plist);
	LTPopFront(plist);
	LTPopFront(plist);
	LTPrint(plist);

双向链表的查找:

查找的代码:

// 双向链表的查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* pcur = phead->next;//  pcur指向的是第一个节点
	// 遍历双向链表
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	// 走到这里已经遍历了所有节点了
	return NULL;
}

测试代码:

	// 测试查找
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTNode* ret = LTFind(plist, 1);
	if (ret == NULL)
	{
		printf("找不到\n");
	}
	else
	{
		printf("找到了\n");
	}

在链表指定位置pos之后插入数据:

思路:

而之前大同小异

但是要注意 当pos是尾节点的时候 相当于尾插,但是我们不能直接调用尾插函数,因为我们尾插函数的形参是一级指针 我们传入是相当于传值调用,无法改变实参的值。

因此只能重新编写尾插函数

我们来看看代码如何实现的:

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

	LTNode* newnode = LTBuyNode(x);
	//  我们要把pos  newnode  pos->next 链接起来
	newnode->next = pos->next;
	newnode->prev = pos;

	pos->next->prev = newnode;
	pos->next = newnode;
	// 不管是常规情况还是 pos是尾节点  上面的代码都能完成
}

测试代码:

	// 测试 在pos位置之后插入数据
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTNode* ret = LTFind(plist, 3);
	LTInsert(plist, 5);// 头插
	LTInsert(ret, 5);// 尾插
	LTPrint(plist);// 5->1->2->3->5->

在链表pos指定位置删除:

思路:

和之前也是大同小异

我们直接来看代码:

// 删除pos位置数据
void LTErase(LTNode* pos) 
// 理论上是要传入二级指针的  不然我们在函数内部的修改pos为NULL 是无法传回给实参的 但是为了接口一致性 我们用一级指针作为形参
{
	assert(pos);// pos不能为空 没有数据怎么删除
	// 理论上来说 还要加一个pos != phead 的校验  但是没有phead参数  其实就是pos不能是哨兵位

	// 删除
	// pos->prev  pos  pos->next  删除pos  让前一个节点和后一个节点相连
	pos->next->prev = pos->prev;
	pos->prev->next = pos->next;
	
	free(pos);
	pos = NULL;
}

测试代码:

	// 测试 删除pos位置的数据
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTNode* ret = LTFind(plist, 1);
	LTErase(ret);// 删除完之后ret变成了野指针
	ret = NULL;  // 因为我们传入的是ret一级指针,在函数内部对pos进行NULL置空不会影响到ret 因此需要手动置空才能实现置空
	LTPrint(plist);// 2->3->

链表的销毁:

思路:

  1. 让pcur去指向第一个节点,next记录下pcur的下一个节点
  2. 删除pcur 让pcur移动到next
  3. 循环往复
  4. 直至pcur == phead 也就是移动到哨兵位了
  5. 这个时候删除哨兵位

我们来看看代码是如何实现的:

// 链表的销毁
void LTDestroy(LTNode* phead)// 为了保持接口一致性才设置的一级指针
{
	assert(phead);
	
	// 销毁
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	// 走到这里pcur指向哨兵位 
	free(phead);
	phead = NULL; // 由于传入的是一级指针,这里的NULL无法让实参置为NULL
	// 因此外边需要手动 将实参置为NULL  这是为了接口一致性所造成的缺陷
}

测试代码:

	// 测试链表的销毁
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTDestroy(plist); 
	// 由于是一级指针的实参 这里是传值调用 函数里对phead置为NULL 这边plist是不会置为NULL的 需要手动NULL
	plist = NULL;

注意了!!!:

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

  • 35
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值