数据结构链表,看这两篇就足够了(上集,动图版)
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:文末有练习,来练练手吧