数据结构之带头双向循环链表

目录

链表的分类

带头双向循环链表的实现

带头双向循环链表的结构

带头双向循环链表的结构示意图

空链表结构示意图

单结点链表结构示意图

多结点链表结构示意图

链表创建结点

双向链表初始化

销毁双向链表

打印双向链表

双向链表尾插

尾插函数测试

双向链表头插

头插函数测试

双向链表尾删

尾删函数测试

双向链表头删

头删函数测试

双向链表查找

双向链表pos位置前插

插入函数测试

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

删除函数测试

[利用 ListInsert()函数改造头插尾插函数](#利用 ListInsert()函数改造头插尾插函数)

尾插函数改造版本

头插函数改造版本

[利用ListEarse()函数改造头删 尾删函数](#利用ListEarse()函数改造头删 尾删函数)

头删函数改造版本

尾删函数改造版本

计算双向链表长度


链表的分类

  • 单向/双向

单向列表:每一个结点结构中只保存下一结点的地址,所以很难从后一结点找到前一节点;

双向列表:每一个结点结构中不仅保存下一结点的地址,还保存上一节点的地址;方便寻找前一节点和后一节点;

  • 带头/不带头

带头:在头结点之前有一个哨兵位结点,哨兵位的数据域不存储有效数据,指针域指向头结点;

不带头:没有哨兵位结点,尾插尾删考虑头结点情况;

  • 循环/非循环

循环:头结点与尾结点相连;

非循环:头结点与尾结点不相连;

上述情况相互组合,共有8种情况, 实际中使用的链表数据结构,都是带头双向循环链表,带头双向循环链表虽然结构复杂,但是其结构具有很多优势,实现反而简单;

带头双向循环链表的实现

带头双向循环链表的结构

cpp 复制代码
typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* prev;//前址域-存放前一个结点的地址
	LTDataType data;//数据域
	struct ListNode* next;//后址域-存放后一个结点的地址
}ListNode;

逻辑图:

物理图:

带头双向循环链表的结构示意图

空链表结构示意图

由图可知,head->prev=head; head->next=head;

单结点链表结构示意图

由图可知:

head->next=FirstNode;

head->prev=FirstNode;

FirstNode->prev=head;

FirstNode->next=head;

多结点链表结构示意图

由图可知:

head->next=firstnode;

head->prev=tail;

tail->next=head;

firstnode->prev=head;

链表创建结点

cpp 复制代码
//创建链表结点,返回链表结点地址
ListNode* BuyListNode(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc failed:");
		exit(-1);
	}

	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;

	return newnode;
}

双向链表初始化

注:函数调用时得到动态开辟的链表空间起始地址的两种方案如下

方案一: 当传参时为链表结点的地址,函数的形参设计为二级指针,只有通过传址调用,可以将动态开辟的链表的起始地址带出函数;

方案二: 设计函数的返回类型为结点指针,返回动态开辟的链表结点指针,如此可以得到链表空间的起始地址;

cpp 复制代码
//初始化链表(空链表)
ListNode* ListInit()
{
	//创建哨兵位结点
	ListNode* head = BuyListNode(0);//0不是有效数据

	//初始化哨兵位结点的指针域
	head->next = head;
	head->prev = head;

	return head;
}

销毁双向链表

  • 循环遍历释放结点,包含哨兵位结点;
  • 释放前保存下一结点地址,避免地址丢失;
cpp 复制代码
//销毁链表,包含哨兵位结点
void DestoryList(ListNode* phead)
{
	assert(phead);

	//创建寻址指针
	ListNode* cur = phead;
	//断开循环链表
	phead->prev->next = NULL;
	while (cur != NULL)
	{
		//记录下一结点地址
		ListNode* next = cur->next;
        //释放当前结点
		free(cur);
		//寻找下一节点
		cur = next;
	}
	return;
}

打印双向链表

  • 循环遍历链表打印数据,不显示哨兵位结点的数据域;
  • 以哨兵位头结点作为结束标志;
cpp 复制代码
void PrintList(ListNode* phead)
{
	assert(phead != NULL);

	ListNode* cur = phead->next;

	printf("phead<==>");
	while (cur != phead)
	{
		printf("%d<==>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

双向链表尾插

  • 尾插先找尾,哨兵位的前址域即为尾结点即tail=head->prev;
  • 当链表为空时,连接的逻辑关系相同(创建三个指针变量,按照新结点的前址域指向谁,谁指向新结点,新结点的后址域指向谁,谁指向新结点进行连接);
cpp 复制代码
void ListPushBack(ListNode* phead, LTDataType x)
{
	assert(phead);
    
	//寻找尾结点
	ListNode* tail = phead->prev;
    //创建新结点
	ListNode* newnode = BuyListNode(x);

   //尾插
	newnode->prev = tail;
	tail->next = newnode;

	newnode->next = phead;
	phead->prev = newnode;
}
尾插函数测试
cpp 复制代码
void Test1()
{
	ListNode* plist=ListInit();
	
	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);
	ListPushBack(plist, 5);
	PrintList(plist);
}
int main()
{
	Test1();
	return 0;
}

运行结果:

双向链表头插

  • 头插前先保存哨兵位结点的下一节点即原先真正的首节点;
  • 按照按照新结点的前址域指向谁,谁指向新结点,新结点的后址域指向谁,谁指向新结点进行连接从而实现头插,链表为空时,头插逻辑仍然相同;
cpp 复制代码
//链表头插
void ListPushFront(ListNode* phead, LTDataType x)
{
	assert(phead);

	//保存原先的首节点
	ListNode* firstnode = phead->next;
	//创建新结点
	ListNode* newnode = BuyListNode(x);

	//头插
	newnode->prev = phead;
	phead->next = newnode;

	newnode->next = firstnode;
	firstnode->prev = newnode;
}
头插函数测试
cpp 复制代码
void Test2()
{
	ListNode* plist = ListInit();
	ListPushFront(plist, 10);
	ListPushFront(plist, 20);
	ListPushFront(plist, 30);
	ListPushFront(plist, 40);
	ListPushFront(plist, 50);
	PrintList(plist);

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

运行结果:

双向链表尾删

  • 链表中只剩哨兵位结点,此时链表为空,不再进行尾删;
  • 尾删前记录前一节点的地址,方便修改逻辑关系;
cpp 复制代码
//链表尾删
void ListPopBack(ListNode* phead)
{
	assert(phead);

	//链表中只剩哨兵位的情况
	assert(phead->next != phead);

	//查找尾结点
	ListNode* tail = phead->prev;
	//保存尾结点的上一节点
	ListNode* tailprev = tail->prev;

   //尾删
	free(tail);
   //建立链接关系
	tailprev->next = phead;
	phead->prev = tailprev;

}
尾删函数测试
cpp 复制代码
void Test3()
{
	ListNode* plist = ListInit();

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

	ListPopBack(plist);
	PrintList(plist);

	ListPopBack(plist);
	PrintList(plist);

	ListPopBack(plist);
	PrintList(plist);

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

运行结果:

双向链表头删

  • 链表中只剩哨兵位结点,此时链表为空,不再进行头删;
  • 头删前记录下一节点的地址,方便修改逻辑关系;
cpp 复制代码
//链表头删
void ListPopFront(ListNode* phead)
{
	assert(phead);
	//只剩哨兵位,不再头删
	assert(phead->next != phead);

	//保存原先的首节点
	ListNode* head = phead->next;

	//保存首结点的下一节点
	ListNode* headnext = phead->next->next;

	//头删
	free(head);
	//建立链接关系
	headnext->prev = phead;
	phead->next = headnext;

}
头删函数测试
cpp 复制代码
void Test4()
{
	ListNode* plist = ListInit();

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

	ListPopFront(plist);
	PrintList(plist);

	ListPopFront(plist);
	PrintList(plist);

	ListPopFront(plist);
	PrintList(plist);
}
int main()
{
	Test4();
	return 0;
}

运行结果:

双向链表查找

  • 循环遍历链表,从首节点开始遍历,以哨兵位头结点作为结束标志;
  • 根据数据域进行查找,找到返回数据域的结点地址,找不到返回空指针;
cpp 复制代码
ListNode* ListFind(ListNode* phead, LTDataType x)
{
	assert(phead);

	//创建遍历指针
	ListNode* cur = phead->next;

	//遍历链表
	while (cur != phead)
	{
		if ((cur->data) == x)
		{
			//找到返回下标
			return cur;
		}
		cur = cur->next;
	}
	//没找到返回空指针
	return NULL;
}

双向链表pos位置前插

  • 前插时保存pos位置的前一个节点,方便修改逻辑关系;
  • 按照按照新结点的前址域指向谁,谁指向新结点,新结点的后址域指向谁,谁指向新结点进行链接;
cpp 复制代码
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos != NULL);

	//创建新结点
	ListNode* newnode = BuyListNode(x);
	//保存pos位置的前一个结点
	ListNode* posprev = pos->prev;

	//前插
	newnode->prev = posprev;
	posprev->next = newnode;
	newnode->next = pos;
	pos->prev = newnode;
}
插入函数测试
cpp 复制代码
void Test5()
{
	ListNode* plist = ListInit();
	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);
	ListPushBack(plist, 5);

	int x = 0;
	printf("请输入查找的数值:");
	scanf("%d", &x);
	ListNode* pos = ListFind(plist, x);
	if (pos == NULL)
	{
		printf("要查找的值不存在\n");
		return;
	}
	//在查找到数值前插入100
	ListInsert(pos, 100);
	PrintList(plist);

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

运行结果:

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

  • 链表删除pos位置处的结点前先保存前结点和后结点的地址,方便处理链接关系;
cpp 复制代码
//双向链表删除pos位置
void ListEarse(ListNode* pos)
{
	assert(pos);

	//保存pos位置处的前一个和后一个结点;
	ListNode* posprev = pos->prev;
	ListNode* posnext = pos->next;
	//删除pos位置结点
	free(pos);

	//建立前后节点的链接关系
	posprev->next = posnext;
	posnext->prev = posprev;

}
删除函数测试
cpp 复制代码
void Test6()
{
	ListNode* plist = ListInit();
	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);
	ListPushBack(plist, 5);
	PrintList(plist);

	int x = 0;
	printf("请输入删除的数值:");
	scanf("%d", &x);
	ListNode* pos = ListFind(plist, x);
	if (pos == NULL)
	{
		printf("要删除的值不存在\n");
		return;
	}
	
	ListEarse(pos);
	PrintList(plist);
}
int main()
{
	Test6();
	return 0;
}

运行结果:

利用 ListInsert()函数改造头插尾插函数

尾插函数改造版本
cpp 复制代码
void Listpushback(ListNode* phead, LTDataType x)
{
	assert(phead);
	ListInsert(phead, x);
}
头插函数改造版本
cpp 复制代码
void Listpushfront(ListNode* phead, LTDataType x)
{
	assert(phead);
	ListInsert(phead->next, x);
}

利用ListEarse()函数改造头删 尾删函数

头删函数改造版本
cpp 复制代码
void Listpopfront(ListNode* phead)
{
	assert(phead);
	//只剩哨兵位,不再头删
	assert(phead->next != phead);

	ListEarse(phead->next);
}
尾删函数改造版本
cpp 复制代码
void Listpopback(ListNode* phead)
{
	assert(phead);

	//链表中只剩哨兵位的情况
	assert(phead->next != phead);

	ListEarse(phead->prev);
}

计算双向链表长度

cpp 复制代码
int ListLength(ListNode* phead)
{
	assert(phead);

	int size = 0;
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		++size;
		cur = cur->next;
	}
	return size;
}
相关推荐
sjsjs114 分钟前
【数据结构-扫描线】力扣57. 插入区间
数据结构·算法·leetcode
xiaozhang_749338015 分钟前
数据结构day2
数据结构
王哈哈嘻嘻噜噜6 分钟前
数据结构中线性表的定义和特点
数据结构·算法
一杯茶一道题25 分钟前
LeetCode 260. 只出现一次的数字 III
算法·leetcode
MogulNemenis26 分钟前
力扣415周赛
java·数据结构·算法·leetcode
Rense130 分钟前
常用的基于无线射频( UWB)室内定位技术的原理与算法
算法
zzhnwpu30 分钟前
代码随想录算法训练营第三七天| 动态规划:完全背包理论基础 518.零钱兑换II 377. 组合总和 Ⅳ 322. 零钱兑换
算法·leetcode·动态规划
奇点 ♡1 小时前
【线程】线程的控制
linux·运维·c语言·开发语言·c++·visual studio code
一道秘制的小菜1 小时前
C++第十一节课 new和delete
开发语言·数据结构·c++·学习·算法
学不会lostfound1 小时前
一、机器学习算法与实践_03概率论与贝叶斯算法笔记
算法·机器学习·概率论·高斯贝叶斯