初阶数据结构:链表(二)

目录

一、前言

二、带头双向循环链表

1.带头双向循环链表的结构

(1)什么是带头?什么是带头?)

(2)什么是双向呢?

(3)那什么是循环呢?

2.带头双向循环链表的实现

(1)节点结构

(2)创建链表的头节点,也即哨兵节点

(3)创建其他节点和链表打印

(4)链表尾插和尾删功能的实现

(5)链表的头插和头删

(6)链表的查找

(7)双链表在pos位置前后插入和删除pos位置功能


一、前言

在上一篇博客中,我们实现的是单链表。我们知道链表有8种结构,由单向和双向、有头和无头、循环和非循环组合而成。单链表就是无头单向非循环链表。它是一种结构简单的链表,通常不会用来单独作为存储数据用,实际中更多的是作为其它数据结构的子结构存在,如哈希桶、图的邻接等等。单链表虽然在头插、头删方面很方便,但在尾插和尾删又比不过顺序表。那么有没有一种链表,在头插头删和尾插尾删上都很方便呢?

当然有,那就是链表中的"王者"------带头双向循环链表。它的结构非常复杂,但效率极高。让我们来看看带头双向循环链表的结构吧!

二、带头双向循环链表

1.带头双向循环链表的结构

(1)什么是带头?

指链表中没有存储数据的节点时,头指针仍然指向一个节点,这个节点不存储数据,只起到站位的作用,其后才是链表的实际数据节点,因此它也被成为哨兵节点。

它的作用是什么呢?在单链表中,我们在改变头指针的链接对象时,需要使用二级指针。有了哨兵节点,我们就不需要二级指针。在执行插入、删除等操作时,也不需要对链表是否为空或是否为最后一个节点进行特殊判断,从而使代码更加简洁和统一,也让我们不至于被绕晕。

(2)什么是双向呢?

看图就可以明白。

节点结构体指针域中有两个指针。一个指向上一个节点,一个指向下一个节点。如此就可以由一个链表中的一个节点位置得到所有节点的位置。

(3)那什么是循环呢?

循环链表 则是将尾节点的后继指针指向头结点,而头结点的前驱指针指向尾节点,从而形成一个闭环。这样的设计使得从链表的任何一个节点开始都可以很方便地遍历整个链表,无论是向前还是向后。也即:

循环链表的哨兵节点的头指针指向尾节点,尾节点的next指针指向哨兵节点。

不要看带头双向循环链表的结构很复杂,就认为它的实现也很难,正因为结构如此,它的实现也避开了需多难题。相比于单链表的实现,它反而简单。

2.带头双向循环链表的实现

(1)节点结构

为了实现双向,那么作为节点的结构体就需要有两个指针和存储数据的位置。代码如下:

cpp 复制代码
typedef int type;
typedef struct ListNde
{
	struct ListNde* next;//指向下一个节点
	struct ListNde* head;//指向上一个节点
	type data;//存储数据
}ListNode;

(2)创建链表的头节点,也即哨兵节点

先看代码:

cpp 复制代码
ListNode* ListCreate()
{
	//动态申请一个结构体空间
	ListNode* head = (ListNode*)malloc(sizeof(ListNode));
	if (head == NULL)
	{
		perror("ListCreate::malloc");
		return NULL;
	}
	//使节点不存储数据
	head->data = NULL;
	//因为作为头节点存在,因此在没有其他节点时,需要让
	//前指针和后指针都指向自己
	head->head = head;
	head->next = head;
	//如果不返回,那就需要传二级指针来使头指针和哨兵节点链接
	return head;
}

为什么哨兵节点的前指针和后指针都要指向自己。看图:

(3)创建其他节点和链表打印

创建其他节点和之前单链表创建节点一样,代码如下:

cpp 复制代码
ListNode* BuyListNode()
{
	ListNode* ptr = (ListNode*)malloc(sizeof(ListNode));
	if (ptr == NULL)
	{
		perror("BuyListNode::malloc");
		return NULL;
	}
	ptr->head = NULL;
	ptr->next = NULL;
	return ptr;
}

链表打印和单链表打印大至一样,循环打印即可,但与单链表打印不同的是,它需要有一个循环终止条件,单纯的不为空可不行,带头双向循环链表可没有为空。而且因为哨兵节点没有存储数据,因此要避免打印哨兵节点。代码如下:

cpp 复制代码
void ListPrint(ListNode* plist)
{
	//不打印哨兵节点,哨兵节点不存储任何数据
	ListNode* ptr = plist->next;
	//以这个代表哨兵节点
	printf("<head>");
	//循环打印
	while (ptr)
	{
		printf("<%d>", ptr->data);
		//因为带头双向循环链表尾节点和哨兵节点是相链接的
		//所以需要一个条件来作为循环的终止条件
		if (ptr->next == plist)
		{
			//ptr指向尾节点时,ptr->next指向哨兵节点,退出循环
			break;
		}
		ptr = ptr->next;
	}
}

(4)链表尾插和尾删功能的实现

先看图:

我们想要使新节点和链表链接最好带顺序的去进行指针的互换,不然容易漏掉,或者换错。我们先让尾节点的后指针指向新节点,新节点的前指针指向尾节点,新节点的后指针指向哨兵节点,哨兵节点的前指针指向新节点,这样按顺序来,既不容易漏掉,也具有逻辑美,更容易理解。代码如下

cpp 复制代码
void ListPushBack(ListNode* plist)
{
	//断言判断plist不为空,因为有哨兵节点存在,那么plist
	//传过来的必定不为空
	assert(plist);
	ListNode* ptr = plist;
	//由ptr->head得到尾节点
	ListNode* tail = ptr->head;
	//申请一个新的节点
	ListNode* NewNode = BuyListNode();
	printf("请输入要尾插的数字\n");
	scanf("%d", &NewNode->data);
	//尾插:尾节点的next指向新节点
	tail->next = NewNode;
	//新节点的前指针指向尾节点
	NewNode->head = tail;
	//新节点的后指针指向哨兵节点
	NewNode->next = ptr;
	//哨兵节点的前指针指向新节点
	ptr->head = NewNode;
}

链表的尾删也很简单,只需按上述步骤逆着来然后释放要删除的空间就行了。代码如下:

cpp 复制代码
void ListPopBack(ListNode* plist)
{
	assert(plist);
	ListNode* ptr = plist;
	ListNode* tail = ptr->head;
	ListNode* tail1 = tail->head;
	tail1->next = ptr;
	ptr->head = tail1;
	free(tail);
	printf("删除成功\n");
}

(5)链表的头插和头删

链表的头插和头删和链表的尾插尾删差不多,只不过这里的头插和头删是在哨兵节点的下一个节点,不是让你删或者插在哨兵节点后面。并且,在所有带头链表的删除功能里一定不能删除哨兵节点。那会出现野指针的,程序运行也会不安全。代码如下:

cpp 复制代码
// 双向链表头插
void ListPushFront(ListNode* plist)
{
	assert(plist);
	ListNode* ptr = plist;
	ListNode* newnode = BuyListNode();
	ListNode* ptrnext = ptr->next;
	ptr->next = newnode;
	newnode->head = ptr;
	newnode->next = ptrnext;
	ptrnext->head = newnode;
	printf("请输入头插数字\n");
	scanf("%d", &newnode->data);
	printf("插入成功\n");
}
// 双向链表头删
void ListPopFront(ListNode* plist)
{
	assert(plist);
	ListNode* ptr = plist;
	ListNode* ptrnext2= ptr->next->next;
	free(ptr->next);
	ptr->next = ptrnext2;
	ptrnext2->head = ptr;
	printf("删除成功\n");
}

(6)链表的查找

cpp 复制代码
ListNode* ListFind(ListNode* plist)
{
	assert(plist);
	int a;
	printf("请输入要查找的数字\n");
	scanf("%d", &a);
	ListNode* ptr = plist->next;
	while (ptr)
	{
		//查找到直接返回
		if (ptr->data == a)
		{
			printf("查找成功\n");
			return ptr;
		}
		//一定要有这个条件,防止查找不到陷入死循环
		if (ptr->next == plist)
		{
			printf("查找失败,未找到\n");
			return NULL;
		}
		ptr = ptr->next;
	}
}

(7)双链表在pos位置前后插入和删除pos位置功能

这些和头插头删没有多大区别,且双链表在pos位置前插入比单链表简单。代码如下:

cpp 复制代码
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos)
{
	assert(pos);
	ListNode* front = pos->head;
	ListNode* ptr = pos;
	ListNode* newnode = BuyListNode();
	front->next = newnode;
	newnode->head = front;
	newnode->next = ptr;
	ptr->head = newnode;
	printf("请输入要在pos前插入的数\n");
	scanf("%d", &newnode->data);
	printf("插入成功\n");
}
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
	assert(pos);
	ListNode* front = pos->head;
	ListNode* posnext = pos->next;
	free(pos);
	front->next = posnext;
	posnext->head = front;
	printf("删除成功\n");
}
// 双向链表在pos位置之后插入
void ListInsertAfter(ListNode* pos)
{
	assert(pos);
	ListNode* posnext = pos->next;
	ListNode* newnode = BuyListNode();
	ListNode* ptr = pos;
	ptr->next = newnode;
	newnode->head = ptr;
	newnode->next = posnext;
	posnext->head = newnode;
	printf("请输入要插入的数字\n");
	scanf("%d", &newnode->data);
}

这样一个带头双向循环链表也就完成了。单链表和带头双向循环链表虽然是两个极端,但当我们可以自主实现后,其他的链表结构我们也可以信手拈来。

全部代码如下:

listnode.h:

cpp 复制代码
#pragma once
#pragma warning(disable : 4996)
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int type;
typedef struct ListNde
{
	struct ListNde* next;//指向下一个节点
	struct ListNde* head;//指向上一个节点
	type data;//存储数据
}ListNode;
// 创建链表的头节点,也即哨兵节点.
ListNode* ListCreate();
//创建其他节点
ListNode* BuyListNode();
// 双向链表销毁
void ListDestory(ListNode* plist);
// 双向链表打印
void ListPrint(ListNode* plist);
// 双向链表尾插
void ListPushBack(ListNode* plist);
// 双向链表尾删
void ListPopBack(ListNode* plist);
// 双向链表头插
void ListPushFront(ListNode* plist);
// 双向链表头删
void ListPopFront(ListNode* plist);
// 双向链表查找
ListNode* ListFind(ListNode* plist);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);
// 双向链表在pos位置之后插入
void ListInsertAfter(ListNode* pos);

Flistnode.c:

cpp 复制代码
#include"listnode.h"
ListNode* ListCreate()
{
	//动态申请一个结构体空间
	ListNode* head = (ListNode*)malloc(sizeof(ListNode));
	if (head == NULL)
	{
		perror("ListCreate::malloc");
		return NULL;
	}
	//使节点不存储数据
	head->data = NULL;
	//因为作为头节点存在,因此在没有其他节点时,需要让
	//前指针和后指针都指向自己
	head->head = head;
	head->next = head;
	//如果不返回,那就需要传二级指针来使头指针和哨兵节点链接
	return head;
}

ListNode* BuyListNode()
{
	ListNode* ptr = (ListNode*)malloc(sizeof(ListNode));
	if (ptr == NULL)
	{
		perror("BuyListNode::malloc");
		return NULL;
	}
	ptr->head = NULL;
	ptr->next = NULL;
	return ptr;
}
void ListDestory(ListNode* plist)
{
	//assert(plist);
	ListNode* ptr = plist->next;
	while (ptr)
	{
		ListNode* ptrnext = ptr->next;
		free(ptr);
		if (ptrnext == plist)
		{
			break;
		}
		ptr = ptrnext;
	}
	free(plist);
	printf("销毁成功\n");
}
void ListPrint(ListNode* plist)
{
	//不打印哨兵节点,哨兵节点不存储任何数据
	ListNode* ptr = plist->next;
	//以这个代表哨兵节点
	printf("<head>");
	//循环打印
	while (ptr)
	{
		printf("<%d>", ptr->data);
		//因为带头双向循环链表尾节点和哨兵节点是相链接的
		//所以需要一个条件来作为循环的终止条件
		if (ptr->next == plist)
		{
			//ptr指向尾节点时,ptr->next指向哨兵节点,退出循环
			break;
		}
		ptr = ptr->next;
	}
}

void ListPushBack(ListNode* plist)
{
	//断言判断plist不为空,因为有哨兵节点存在,那么plist
	//传过来的必定不为空
	assert(plist);
	ListNode* ptr = plist;
	//由ptr->head得到尾节点
	ListNode* tail = ptr->head;
	//申请一个新的节点
	ListNode* NewNode = BuyListNode();
	printf("请输入要尾插的数字\n");
	scanf("%d", &NewNode->data);
	//尾插:尾节点的next指向新节点
	tail->next = NewNode;
	//新节点的前指针指向尾节点
	NewNode->head = tail;
	//新节点的后指针指向哨兵节点
	NewNode->next = ptr;
	//哨兵节点的前指针指向新节点
	ptr->head = NewNode;
}

void ListPopBack(ListNode* plist)
{
	assert(plist);
	ListNode* ptr = plist;
	ListNode* tail = ptr->head;
	ListNode* tail1 = tail->head;
	tail1->next = ptr;
	ptr->head = tail1;
	free(tail);
	printf("删除成功\n");
}
// 双向链表头插
void ListPushFront(ListNode* plist)
{
	assert(plist);
	ListNode* ptr = plist;
	ListNode* newnode = BuyListNode();
	ListNode* ptrnext = ptr->next;
	ptr->next = newnode;
	newnode->head = ptr;
	newnode->next = ptrnext;
	ptrnext->head = newnode;
	printf("请输入头插数字\n");
	scanf("%d", &newnode->data);
	printf("插入成功\n");
}
// 双向链表头删
void ListPopFront(ListNode* plist)
{
	assert(plist);
	ListNode* ptr = plist;
	ListNode* ptrnext2= ptr->next->next;
	free(ptr->next);
	ptr->next = ptrnext2;
	ptrnext2->head = ptr;
	printf("删除成功\n");
}

ListNode* ListFind(ListNode* plist)
{
	assert(plist);
	int a;
	printf("请输入要查找的数字\n");
	scanf("%d", &a);
	ListNode* ptr = plist->next;
	while (ptr)
	{
		//查找到直接返回
		if (ptr->data == a)
		{
			printf("查找成功\n");
			return ptr;
		}
		//一定要有这个条件,防止查找不到陷入死循环
		if (ptr->next == plist)
		{
			printf("查找失败,未找到\n");
			return NULL;
		}
		ptr = ptr->next;
	}
}
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos)
{
	assert(pos);
	ListNode* front = pos->head;
	ListNode* ptr = pos;
	ListNode* newnode = BuyListNode();
	front->next = newnode;
	newnode->head = front;
	newnode->next = ptr;
	ptr->head = newnode;
	printf("请输入要在pos前插入的数\n");
	scanf("%d", &newnode->data);
	printf("插入成功\n");
}
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
	assert(pos);
	ListNode* front = pos->head;
	ListNode* posnext = pos->next;
	free(pos);
	front->next = posnext;
	posnext->head = front;
	printf("删除成功\n");
}
// 双向链表在pos位置之后插入
void ListInsertAfter(ListNode* pos)
{
	assert(pos);
	ListNode* posnext = pos->next;
	ListNode* newnode = BuyListNode();
	ListNode* ptr = pos;
	ptr->next = newnode;
	newnode->head = ptr;
	newnode->next = posnext;
	posnext->head = newnode;
	printf("请输入要插入的数字\n");
	scanf("%d", &newnode->data);
}

listnode.c:

cpp 复制代码
#include"listnode.h"
ListNode* pplist = NULL;
int main()
{
	int a;
	pplist = ListCreate();
	ListNode* pos = NULL;
	do
	{
		printf("请输入数字");
		scanf("%d", &a);
		switch (a)
		{
		case 1:
			// 双向链表打印
			ListPrint(pplist);
			break;
		case 2:
			// 双向链表尾插
			ListPushBack(pplist);
			break;
		case 3:
			// 双向链表的头插
			ListPushFront(pplist);
			break;
		case 4:
			// 双向链表的尾删
			ListPopBack(pplist);
			break;
		case 5:
			// 双向链表头删
			ListPopFront(pplist);
			break;
		case 6:
			// 双向链表查找
			pos = ListFind(pplist);
			break;
		case 7:
			// 双向链表在pos位置之后插入x
			ListInsertAfter(pos);
			break;
		case 8:
			// 在pos的前面插入
			ListInsert(pos);
			break;
		case 9:
			// 删除pos位置
			ListErase(pos);
			break;
		case 0:
			//链表的销毁
			ListDestory(pplist);
			printf("退出\n");
			break;
		default:
			printf("输入数字错误,请重新输入\n");
			break;
		}
	} while (a);
     return 0;
}

至此,链表就完了,大家可以去力扣或者牛客上找一些链表的题做一做来巩固一下。提个建议,如果在链表中,或者说在整个数据结构中,画图永远是你最好的伙伴。

相关推荐
火车驶向云外.111 小时前
计数排序算法
数据结构·算法·排序算法
兑生3 小时前
力扣面试150 快乐数 循环链表找环 链表抽象 哈希
leetcode·链表·面试
思逻辑维6 小时前
强大到工业层面的软件
数据结构·sql·sqlite·json
所以遗憾是什么呢?7 小时前
【题解】Codeforces Round 996 C.The Trail D.Scarecrow
数据结构·算法·贪心算法
qystca7 小时前
【16届蓝桥杯寒假刷题营】第2期DAY4
数据结构·c++·算法·蓝桥杯·哈希
Xzh04239 小时前
c语言网 1127 尼科彻斯定理
数据结构·c++·算法
gentle_ice9 小时前
leetcode——删除链表的倒数第N个节点(java)
java·leetcode·链表
这是我5810 小时前
链表的介绍
数据结构·c++·其他·链表·visual studio·介绍·图文结合
艺杯羹11 小时前
C语言二级题解:查找字母以及其他字符个数、数字字符串转双精度值、二维数组上下三角区域数据对调
c语言·开发语言·数据结构
励志成为美貌才华为一体的女子11 小时前
python算法和数据结构刷题[1]:数组、矩阵、字符串
数据结构