<数据结构>NO4.带头双向循环链表


在这里插入图片描述

前言

链表的实现有多种,带头、不带头的,单向、双向的,非循环、循环的,前面我们已经实现了不带头单向非循环链表,这次我们实现带头双线循环链表(这个结构巧妙的设计造成了很容易实现)
关于单链表可以看这篇文章syseptembera的个人博客:单链表

依然分DobuleLinkList.h,DoubleLinkList.c,test.c三个文件实现

头文件放类型定义,函数声明。
源文件存放函数实现
主函数编写测试用例

1. 头文件

既然双链表是数据结构的一种,那么对它的操作无非是增、删、查、改等,而双链表的节点由3部分组成:数据域指向前一个结点的指针域指向后一个节点的指针域

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int DataType;
typedef struct DLTNode
{
	DataType val;
	struct DLTNode* next;
	struct DLTNode* prev;
}DLTNode;
//新增节点
extern DLTNode* BuyOneNode(DataType x);
//创建哨兵节点
extern DLTNode* DLTCreat(void);
//打印链表
extern void DLTPrint(DLTNode* phead);
//判断链表是否为空
extern bool DLTEmpty(DLTNode* phead);
//头插
extern void DLTPushFront(DLTNode* phead, DataType x);
//尾插
extern void DLTPushBack(DLTNode* phead, DataType x);
//头删
extern void DLTPopFront(DLTNode* phead);
//尾删
extern void DLTPopBack(DLTNode* phead);
//查找
extern DLTNode* DLTFind(DLTNode* phead, DataType x);//返回指针方便后续插入删除等操作
//pos前插入
extern void DLTInsert(DLTNode* pos, DataType x);
//删除pos处节点
extern void DLTErase(DLTNode* pos);
//销毁(传一级指针不能置空实参)
extern void DLTDestory(DLTNode* phead);

为什么所有的参数传递的是一级指针?
在无头单链表的实现中,我们插入删除等操作有可能改变实参的值,所以需要传递二级指针,但是对于有头双链表而言,我们的传递的实参phead是指向头的(头是哨兵节点,不属于链表有效节点),所以对有头双链表进行插入删除等操作,不存在改变实参,所以不需要传递二级指针.

为什么查找函数返回的是地址?
和单链表类似,原因之一是我们可以通过该返回值直接修改该节点的数据,原因之二是可以通过返回的地址进行后续插入删除等操作而不需要再传递哨兵节点地址给插入删除函数。


2. 函数实现

1)创建哨兵位节点

和之前实现的单链表不同点之一是这里实现的双链表是带哨兵节点的,所以我们需要先创建一个哨兵节点,哨兵节点满足的条件是next指向自己,pre也指向自己,这样一个节点也是循环节点,数值域可以是任意值(这里定义为-1)。

//创建哨兵位节点
DLTNode* DLTCreat()
{
	DLTNode* head = (DLTNode*)malloc(sizeof(DLTNode));
	assert(head);
	//哨兵位节点next,prev指向自己
	head->next = head;
	head->prev = head;
	head->val = -1;
	return head;
}

2)新增一个节点

每次插入时都需要新增一个节点,所以直接将新增节点封装为一个函数减少代码冗余

//新增节点
DLTNode* BuyOneNode(DataType x)
{
	DLTNode* newnode = (DLTNode*)malloc(sizeof(DLTNode));
	assert(newnode);//检查是否成功创建
	newnode->val = x;
	newnode->next = newnode->prev = NULL;
	assert(newnode);
	return newnode;
}

3)打印链表

总体来说和单链表类似,但是需要注意的有2点:
❗单链表是无哨兵位节点的,传递的实参是指向第一个有效节点的,双链表是有头节点的,传递的实参是指向哨兵节点的,因此开始打印时需要从实参的后一个节点开始打印
❗单链表是不循环的,打印到NULL节点停止,双链表是循环的,没有空节点,打印到哨兵节点停止。

//打印链表
void DLTPrint(DLTNode* phead)
{
	assert(phead);
	DLTNode* cur = phead->next;//从哨兵节点后一个节点开始打印
	printf("guard<==>");
	while (cur != phead)//遍历到哨兵节点
	{
		printf("%d<==>", cur->val);
		cur = cur->next;
	}
	puts("");
}

4)头插

在这里插入图片描述
方便后续操作,我们保存头节点的地址

//头插
void DLTPushFront(DLTNode* phead, DataType x)
{
	assert(phead);
	DLTNode* first = phead->next;
	DLTNode* newnode = BuyOneNode(x);
	newnode->next = first;
	newnode->prev = phead;
	phead->next = newnode;
	first->prev = newnode;
}

注意:这段代码当链表为空时仍然成立,链表为空时只有哨兵节点,所以哨兵节点就是first

5)尾插

在这里插入图片描述

方便后续操作,我们保留tail的地址

void DLTPushBack(DLTNode* phead, DataType x)
{
	assert(phead);
	DLTNode* tail = phead->prev;
	DLTNode* newnode = BuyOneNode(x);
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}

注意:尾插也适用于只存在哨兵节点的情况,哨兵节点就是tail

6)头删

在这里插入图片描述
方便后续操作,头删保存second的节点
头删需要判断链表是否为空(只有哨兵一个节点)

//判断链表是否为空
bool DLTEmpty(DLTNode* phead)
{
	return phead == phead->next;
}
void DLTPopFront(DLTNode* phead)
{
	assert(phead);
	assert(!DLTEmpty(phead));//链表为空不可以删除

	DLTNode* first = phead->next;
	DLTNode* second = first->next;
	phead->next = second;
	second->prev = phead;
	free(first);
}

注意:头删可以用于只存在一个节点的情况,即哨兵节点就是second

7)尾删

在这里插入图片描述
为了方便操作,保留倒数第2个节点tailPrev
当链表为空时不能尾删

//尾删
void DLTPopBack(DLTNode* phead)
{
	assert(phead);
	assert(!DLTEmpty(phead));//链表为空不可以删除

	DLTNode* tail = phead->prev;
	DLTNode* tailPrev = tail->prev;
	phead->prev = tailPrev;
	tailPrev->next = phead;
	free(tail);
}

注意:尾删可以用于只存在一个节点的情况,即哨兵节点就是tailPrev

8)查找

//查找
DLTNode* DLTFind(DLTNode* phead, DataType x)
{
	assert(phead);
	DLTNode* cur = phead->next;//从哨兵节点下一个节点开始寻找
	while (cur != phead)//遍历到哨兵节点
	{
		if (cur->val == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
}

9)pos前插入

在这里插入图片描述
方便操作,我们将pos指向节点的前一个结点posPrev保存下来

//pos前插入
void DLTInsert(DLTNode* pos, DataType x)
{
	assert(pos);
	DLTNode* posPrev = pos->prev;
	DLTNode* newnode = BuyOneNode(x);
	newnode->prev = posPrev;
	posPrev->next = newnode;
	newnode->next = pos;
	pos->prev = newnode;
}

头插和尾插可以直接复用该代码
头插时pos是第一个有效节点的地址DLTInsert(phead->next, x);
尾插时pos是哨兵节点的地址DLTInsert(phead, x);

10)删除pos处节点

在这里插入图片描述
方便后续操作,我们保留posPrev和posNext节点

//删除pos处节点
void DLTErase(DLTNode* pos)
{
	DLTNode* posNext = pos->next;
	DLTNode* posPrev = pos->prev;
	posPrev->next = posNext;
	posNext->prev = posPrev;
	free(pos);
}

头删和尾删可以直接复用该代码
头删时pos是第一个有效节点的地址DLTErase(phead->next, x);
尾删时pos是尾节点的地址DLTErase(phead->prev, x);

11)销毁

在这里插入图片描述

方便后续操作,每次保存下一个结点的地址

//销毁(传一级指针不能置空实参)
void DLTDestory(DLTNode* phead)
{
	assert(phead);
	DLTNode* cur = phead->next;//从第一个节点开始
	while (cur != phead)
	{
		DLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);//销毁哨兵节点
}

3. 测试用例

在主函数中写出对应的测试函数测试双链表功能是否正常

#define _CRT_SECURE_NO_WARNINGS 1
#include "DoubleLinkList.h"
//测试头插
void test1()
{
	DLTNode* plist = DLTCreat();
	DLTPushFront(plist, 1);
	DLTPushFront(plist, 2);
	DLTPushFront(plist, 3);
	DLTPrint(plist);
}

//测试尾插
void test2()
{
	DLTNode* plist = DLTCreat();
	DLTPushBack(plist, 1);
	DLTPushBack(plist, 2);
	DLTPushBack(plist, 3);
	DLTPrint(plist);
}

//测试头删
void test3()
{
	DLTNode* plist = DLTCreat();
	DLTPushBack(plist, 1);
	DLTPushBack(plist, 2);
	DLTPushBack(plist, 3);
	DLTPopFront(plist);
	DLTPopFront(plist);
	DLTPrint(plist);
}

//测试尾删
void test4()
{
	DLTNode* plist = DLTCreat();
	DLTPushBack(plist, 1);
	DLTPushBack(plist, 2);
	DLTPushBack(plist, 3);
	DLTPopBack(plist);
	DLTPopBack(plist);
	DLTPrint(plist);
}

//测试查找
void test5()
{
	DLTNode* plist = DLTCreat();
	DLTPushBack(plist, 1);
	DLTPushBack(plist, 2);
	DLTPushBack(plist, 3);
	DLTPushBack(plist, 4);
	DLTNode* pos = DLTFind(plist, 4);
	//找到了
	if (pos)
		pos->val *= 10;
	DLTPrint(plist);
}

//测试pos前插入
void test6()
{
	DLTNode* plist = DLTCreat();
	DLTPushBack(plist, 1);
	DLTPushBack(plist, 2);
	DLTPushBack(plist, 3);
	DLTPushBack(plist, 4);
	DLTInsert( DLTFind(plist, 1), 11);
	DLTInsert(DLTFind(plist, 4), 11);
	DLTPrint(plist);
}

//测试删除pos处节点
void test7()
{
	DLTNode* plist = DLTCreat();
	DLTPushBack(plist, 1);
	DLTPushBack(plist, 2);
	DLTPushBack(plist, 3);
	DLTPushBack(plist, 4);
	DLTErase(DLTFind(plist, 1));
	DLTErase(DLTFind(plist, 4));
	DLTPrint(plist);
}

//测试销毁
void test8()
{
	DLTNode* plist = DLTCreat();
	DLTPushBack(plist, 1);
	DLTPushBack(plist, 2);
	DLTPushBack(plist, 3);
	DLTPushBack(plist, 4);
	DLTDestory(plist);
}
int main()
{
	test1();
	test2();
	test3();
	test4();
	test5();
	test6();
	test7();
	test8();
	return 0;
}

在这里插入图片描述


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值