数据结构链表,看这两篇就足够了(上集,动图版)

Posted ^jhao^

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构链表,看这两篇就足够了(上集,动图版)相关的知识,希望对你有一定的参考价值。

文章篇幅较长,如有需要请先收藏❤❤❤

前言

链表的概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表的结构也有8种之多,分别是单向或者多向,带头或者不带头,
循环或者非循环,这里我们重点讲的是单链表的结构和带头循环双向链表的结构,会了这两种,其他的其实也只是小菜一碟!

学习原因:单链表作为图的邻接表,哈希桶等结构的子结构,而带头循环双向链表,这个结构虽然复杂,但是实现后在很多方便都很便利,我们下面会叙述。这篇文章主要介绍这两种结构,下一篇会对链表的难点进行详细叙述。



一、单链表

单链表对于中间/头部插入删除比较遍历,而且每次增容都是开一个空间,现开现用,不会浪费内存空间但是在能用链表和顺序表的结构,我们优先还是使用顺序表的结构,因为顺序表随机访问率比较高

1.单链表的结构

typedef int SLTDateType;
typedef struct SListNode
{
	SLTDateType data;
	struct SListNode* next;
}SListNode;

这里的SLTDateType表示链表当中存储的数据类型,在代码多的时候,我们修改这里就可以实现通用了,结构体当中存放的三个元素,struct SListNode* next表示下个元素的地址,data存放的就是当前元素的值

2.单链表的基本接口

// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x);
// 单链表打印
void SListPrint(SListNode* plist);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x);
// 单链表在pos位置之后插入x
// 分析思考为什么不在pos位置之前插入?
//答案,要遍历一遍找到前一个,效率太低了
void SListInsertAfter(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之后的值
// 分析思考为什么不删除pos位置?
//答案:删除pos位置也要有pos位置的前一个
void SListEraseAfter(SListNode* pos);
// 单链表的销毁
void SListDestory(SListNode** plist);

1.我们先讲讲单链表的头插

一开始我们在test函数可以创建一个SListNode* sl =NULL,表示有个单链表的sl的指针指向NULL,那我们就可以开始插入,这里先讲讲头插单链表的好处得益于相比顺序表而言头部的插入不用移动数据!!
动图:图中的pphead为二级指针

从图就可以看出,当我们没有结点的时候,3这个结点的出现就要成为我们链表的头,而newnode的出现便要将头指向我们的2结点,这里有个重要的地方:test函数在调用我们的SListPushFront头插需要二级指针,为什么呢?
打个比方,我们大家都有写过两个数的交换,想要交换两个数就要传这两个数的地址,而我们现在从函数内部改变外部指针的指向,也就是改变它的值,所以这时我们也需要传指针的地址!!!!

SListNode* BuySListNode(SLTDateType x)
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if (newnode == NULL)
	{
		printf("申请失败\\n");
		exit(-1);
	}

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

	return newnode;
}

因为这个创建结点的逻辑很多地方都用,我们单独弄成接口,让其他地方复用

void SListPushFront(SListNode** pplist, SLTDateType x)
{
	//头插在这里会改变头节点的指向,要外面拿到也要二级指针
	assert(pplist);
	if (*pplist == NULL)//这里也要考虑这个问题(其实也可以不考虑),空链表就要给他创建
	{
		SListNode* newnode = BuySListNode(x);
		*pplist = newnode;
	}
	else
	{
		SListNode* newnode = BuySListNode(x);
		newnode->next = *pplist;
		*pplist = newnode;
	}
	//这个地方也可以合并成一个逻辑,将else当中的逻辑拿来用就可以了。如下:
	/*assert(pplist);
	SListNode* newnode = BuySListNode(x);
	newnode->next = *pplist;
	*pplist = newnode;*/
}

这里对代码进行解释,assert断言pplist是因为如果我们这里如果传过来的指针的地址是NULL的话,就报错,因为即使指针是NULL,但他的地址也一定不会是NULL,再看if (*pplist == NULL),代码内部用到了解引用,所以也一定不能是NULL指针,断言方便我们后续查错


2.单链表的尾插

动图:图中的pphead为二级指针

尾插的逻辑相对头插来说只有一个地方需要注意,就是当我们SListNode* sl =NULL的时候我们需要通过二级指针(同理上面)来改变外面的指向,sl当有已经指向有效结点的时候,我们只需要去遍历找到尾,在尾部插入

void SListPushBack(SListNode** pplist, SLTDateType x)
{
	assert(pplist);//检测指针的地址是否为空
	//情况一就是传过来的是NULL,这时我们要改变他的头,并且外面要拿到,所以这个地方只能用二级指针,用返回的方式也可以,但是要用外面的头指针接受
	if (*pplist == NULL)
	{
		SListNode *newnode= BuySListNode(x);
		*pplist = newnode;
	}
	else
	{
		SListNode* cur = *pplist;
		while (cur->next)
		{
			cur = cur->next;
		}
		SListNode* newnode = BuySListNode(x);
		cur->next = newnode;
	}
}

其中的if逻辑就是要改变外面指针的指向,else就是在尾部上的插入

3.单链表的头删

单链表的头删需要注意什么呢,当只有一个元素的时候,我们应当也把传进来的指针置成NULL,所以这里也不得不传二级指针,当然用返回值的方式也可以解决,但是这里使用二级指针更加恰当,在能够完善我们的功能的情况下,我们才继续追求接口的一致性
动图:

头删根据动画也可以看出头删每次都要跟新头节点,并且当只剩下一个节点的时候要将外面的头节点也置成NULL,所以我们这里也要用二级指针传参

void SListPopFront(SListNode** pplist)
{
	assert(pplist);
	assert(*pplist);
	SListNode* cur = (*pplist)->next;//保留下一个结点
	free(*pplist);
	*pplist = NULL;
	*pplist = cur;
}

实现起来也相对容易,只需要每次保存下一个节点的地址,然后迭代往后面走就可以了

4.单链表的尾删

尾删的时候每次都要找到尾结点和尾结点的前一个指针,所以当我们只有一个节点的时候就可以free掉头结点,把指针也置成NULL就可以了,所以这里也要用二级指针,当没有节点的时候我们就用assert报错
动图:

void SListPopBack(SListNode** pplist)
{
	assert(pplist);
	assert(*pplist);
	//删除的时候若是空链表,我们这边就报错

	//尾删的时候有可能删除头结点,所以用二级指针
	SListNode* cur = *pplist;
	SListNode* prev = NULL;
	while (cur->next)
	{
		 prev = cur;
		cur = cur->next;
	}
	//情况一:删除的头节点
	if (cur == *pplist)
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		//在这里要注意还要处理prev的next指针!!
		free(cur);
		cur = NULL;
		prev->next = NULL;
	}
}

代码的逻辑也是先去判断是否传进来的是空指针,并且链表为NULL的时候我们也直接报错,因为在c++list库当中也是这样子处理的,我们这里也借鉴学习。

5.单链表的查找

单链表的查找其实就是去遍历一遍链表,找到就返回结点的地址

SListNode* SListFind(SListNode* plist, SLTDateType x)
{
	//这里不用断言是因为plist可以是NULL指针,是外面头节点的一个临时拷贝
	SListNode* cur = plist;
	while (cur->next)
	{
		if (cur->data == x)
			return cur;

		cur = cur->next;
	}
	return NULL;
}

即找的到返回结点地址,找不到返回NULL
打印链表也是相同逻辑,当我们要修改单链表,拿到结点就可以直接操作了!!!

void SListPrint(SListNode* plist)
{
	SListNode* cur = plist;
	while (cur)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("NULL\\n");
}

6.单链表的删除

单链表的删除通常配合单链表的查找进行使用,并且通常是删除该节点的下一个结点,因为要删除当前结点需要找到上一个节点的位置,这样就需要在遍历一次,时间复杂度就高了,要删除当前位置我们会使用双向链表的结构,下文会讲到带头循环的双向链表结构!

void SListEraseAfter(SListNode* pos)
{
	assert(pos);
	assert(pos->next);
	SListNode* nextnext = pos->next->next;
	SListNode* next = pos->next;

	//free(pos->next);
	//pos->next = null;这种写法是错误的,next并没有置成NULL
	free(next);
	next = NULL;

	pos->next = nextnext;
}

这里的保存下下个结点,删除该节点的下一个结点,并且将当前结点与下下个结点相连,并且只有一个结点的时候我们就报错,动图如下:

6.单链表所有代码链接

单链表


一、带头循环双向链表

1.结构

// 带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType _data;
	struct ListNode* _next;
	struct ListNode* _prev;
}ListNode;

对于带头循环双向链表,有个指针指向后一个结点,一个指向前一个结点,还有一个是存放的值

常见接口:

void ListInit(ListNode** ppHead);
// 创建返回链表的头结点.
ListNode* ListCreate(LTDataType x);
// 双向链表销毁
void ListDestory(ListNode* pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);
bool isEmpty(ListNode* pos);

2.带头循环双向链表的基本接口

1.带头循环双向链表的初始化

初始化时我们应该要创一个结点,让节点的前指针和后指针都指向自己

void ListInit(ListNode** ppHead)
{
	assert(ppHead);
	assert(*ppHead);
	//这里malloc会返回一个堆上的地址,所以要用二级指针接受
	
	ListNode*newnode = ListCreate(-1);
	if (newnode)
	{
		*ppHead = newnode;
		(*ppHead)->_data = -1;
		(*ppHead)->_next = *ppHead;
		(*ppHead)->_prev = *ppHead;
	}
	else
		printf("初始化失败\\n");
	
}

2.带头循环双向链表的查找

这个逻辑跟单链表的查找一摸一样,就是要控制结束的标志我们用cur表示第一个结点,cur == phead的时候我们就完成了遍历,同理打印链表

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;
}
void ListPrint(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->_next;
	while (cur != pHead)
	{
		printf("%d ", cur->_data);
		cur = cur->_next;
	}
	printf("\\n");

}

3.带头循环双向链表的任意位置插入

我们在很多地方都会用到创建一个结点的逻辑,所以我们把他独立出来

ListNode* ListCreate(LTDataType x)//创建一个结点
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	newnode->_data = x;
	newnode->_next = newnode->_prev = NULL;
	return newnode;
}


与单链表不同,带头循环双向链表是一种使用起来非常遍历的结构,找到我们要插入的pos位置,记录上一个结点(相比单链表不需要遍历一遍链表)
就可以进行链接

void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);

	ListNode* prev = pos->_prev;
	ListNode* newnode = ListCreate(x);
	prev->_next = newnode;
	newnode->_prev = prev;
	pos->_prev = newnode;
	newnode->_next = pos;

}

4.带头循环双向链表的头插,尾插

这些逻辑都可以复用ListInsert的逻辑

void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListInsert(pHead, x);
}
void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListInsert(pHead->_next, x);
}

5.带头循环双向链表的任意位置删除

删除数据时我们需要判断是否有有效数据,如果没有的话我们直接报错处理,代码和图结合观看

void ListErase(ListNode* pos)
{
	assert(pos);
	assert(!isEmpty(pos));
	ListNode* prev = pos->_prev;
	ListNode* next = pos->_next;
	free(pos);
	pos = NULL;
	prev->_next = next;
	next->_prev = prev;
}

6.带头循环双向链表的头删,尾删

我们这里就可以复用ListErase的逻辑了

void ListPopFront(ListNode* pHead)
{
	ListErase(pHead->_next);
}
void ListPopBack(ListNode* pHead)
{
	ListErase(pHead->_prev);
}

简简单单的两段代码,这就是这种结构的好处,很多地方都能实现复用!!

7.带头循环双向链表的全部代码

带头循环双向链表


总结

到此,我们就完成了链表的两种实现方式,下一个博客就讲讲链表的带环和循环队列的实现,看到这里不妨给个一键三连把!!!

以上是关于数据结构链表,看这两篇就足够了(上集,动图版)的主要内容,如果未能解决你的问题,请参考以下文章

Linux文本处理三剑客之一——awk详解——awk看这两篇就够啦~PS:文末有练习,来练练手吧

webpack入门,看这篇就足够了!

Flink面试,看这篇就足够了

Java多线程超级详解(看这篇就足够了)

深入理解JVM虚拟机13:JVM面试题,看这篇就足够了(87题详解)

JVM面试题,看这篇就足够了(87题详解)