单链表
预先准备4个东西
- ListNode.h(声明)
- ListNode.c(接口实现)
- main.c(调用接口)
申请空间
- 所谓巧妇难为无米之炊,这申请空间就是链表的米
LTN* BuySListNode(Value x)//申请一个节点
{
LTN *NewNode=(LTN *)malloc(sizeof(LTN));
NewNode->data=x;
NewNode->next=NULL;
return NewNode;
}
查
- 他是是如何查的是不是和顺序表一样挨个挨个的找或者是别的方法?
LTN* ListNodeFind(LTN **phead,int find)
{
LTN *cur=*phead;
while(cur)
{
if(cur->data==find)
{
return cur;
}
cur=cur->next;
}
return NULL;
}
显而易见他和顺序表一样是挨个挨个找但是区别在于顺序表返回的是下标,而链表返回的是地址
增
尾插
- 为插相比顺序表来说麻烦许多不能向他一样之间下标访问之间插入
- 需要遍历链表,找尾 时间复杂度O(N)
- 那么他是如何遍历的呢??
void ListNodePushBack(LTN **phead,int num)//尾插
{
assert(phead);
LTN*cur=*phead;
if(cur==NULL)
{
cur=BuySListNode(num);
*phead=cur;
}
else
{
while(cur->next)
{
cur=cur->next;
}
LTN* Newhead=BuySListNode(num);
cur->next=Newhead;
}
}
遍历是找最后一个元素的next,应为最后那个元素指向的是NULL
上面还遇到了俩种情况
- 空链表插入
- 非空链表插入
如图所示:
头插
- 头插可谓是最简单了只需要找到头和他next的地址就可以插入
- 他和顺序表的效率相差甚微 复杂度 O(1)
void ListNodePushFront(LTN **phead,int num)//头插
{
assert(phead);
if(ListNodeEmpty(phead)==1)//链表为空
{
*phead=BuySListNode(num);
}
else//不为空
{
LTN *OldHead= *phead;
LTN *newnode=BuySListNode(num);
*phead=newnode;
(*phead)->next=OldHead;
}
}
头插的时候依然是遇到了俩种情况
- 空链表(和尾插一样)
- 和非空(如图所示)
任意位置插
- 和顺序表一样她是基于查找这个接口实现的但相同有不同
为什么相同有不同?
他有什么缺陷?
void ListNotPopInsert(LTN*find,int num)//插入
void ListNotPopInsert(LTN*find,int num)//插入
{
LTN*next=find->next;
LTN *newnode=BuySListNode(num);
find->next=newnode;
newnode->next=next;
}
实现依然是有俩种情况
- 空链表
- 非空链表(如图所示)
- 这样上面的代码其实都可以更改了传头指针和数据就可以头插,为插也同理
缺陷其实很显而易见,他“只能”在这个位置后面插入(
后面双向循环指针可以解决
)
不同的是顺序表前后插入都方便,相同他们都可以直接找到这个位置插入但是链表插入更优秀
判空
- 这一步对于删数据来说至关重要,如果链表为空会造成
越界访问
int ListNodeEmpty(LTN **phead)//判空
{
if(*phead==NULL)
return 1;
else
return 0;
}
删
一定要判断链表是不是为空
,一定要判断链表是不是为空,一定要判断链表是不是为空,重要的事情说三遍
头删
- 逻辑和头插的逻辑是反的不懂就在看下头插,就不过多介绍了
void ListNodePopFront(LTN **phead)//头删
{
if(ListNodeEmpty(phead)==1)//链表为空
{
exit(-1);
}
LTN* OldHead=*phead;
LTN*Newhead=OldHead->next;
free(OldHead);
*phead=Newhead;
}
尾删
- 这里和头插稍微有点细微的区别
- 是啥区别呢?
- 带着这个问题在看代码
void ListNodePopBack(LTN **phead)//尾删
{
assert(phead);
if(ListNodeEmpty(phead)==1)//链表为空
{
exit(-1);
}
LTN *cur=*phead;
while(cur->next->next)
{
cur=cur->next;
}
free(cur->next);
cur->next=NULL;
}
图解
任意位置删
- 这里要注意一下他最后的元素
指向是否为空
- 他的逻辑和任意位置删基本一样
void ListNotPopInsert(LTN*find,int num)//pos后删除
{
LTN *Next=find->next;
if(find->next!=NULL)
{
LTN *Nextnext=Next->next;
free(Next);
find->next=Nextnext;
}
else
{
exit(-1);
}
}
销毁
void ListNodeDestroy(LTN **phead)//销毁
{
assert(phead);
LTN *cur=*phead;
while(cur)
{
LTN *Next=cur->next;
free(cur);
cur=Next;
}
}
带头双向循环链表
- 相比单链表的实现稍微复杂了点,但是他的是链表中最
实用
的,且用的最多
的一种 - 为什么最实用,他和单链表的区别在哪里?
申请空间
- 和单链一样不多讲
DLN* BuySListNode(Value x)
{
DLN *NewNode=(DLN *)malloc(sizeof(DLN));
NewNode->data=x;
NewNode->next=NULL;
NewNode->prev=NULL;
return NewNode;
}
初始化
- 双向循环就像是一个蛹
DLN *Nenode=BuyDListNode(0);
Nenode->data=0;
Nenode->next=Nenode;
Nenode->prev=Nenode;
*phead=Nenode;
如图所示
查
- 其实逻辑和点链表差不多
- 唯一区别就是判断条件因为最后一个元素
指向的是头节点
DLN *DListNodeFind(DLN **phead,int num)//查找
{
assert(*phead);
DLN *cur=*phead;
while(cur->next!=*phead)
{
if(cur->data==num)
{
return cur;
cur=cur->next;
}
}
return NULL;
}
增
尾插
- 其实逻辑和单链表差不多
- 但是找尾变的
容易了
,并且不用担心空链表 - 带来的是代码变得
复杂了
,果然啥事都是一把双刃剑有好有坏
DLN *DListNodePushBack(DLN **phead,int num)//尾插
{
assert(*phead);
DLN *cur=(*phead)->prev;
DLN*newnode= BuyDListNode(num);
cur->next=newnode;
newnode->prev=newnode;
newnode->next=*phead;
(*phead)->prev=newnode;
}
图解
- 插入分为俩种情况
- 有元素(如图所示)
- 没有元素(如图二三步所示)
头插
- 大体逻辑和单链表一样
void DListNodePushFront(DLN **phead,int num)//头插
{
assert(pphead);
DLN *OldHead=(*phead)->next;
DLN *newnode=BuyDListNode(num);
(*phead)->next=newnode;
newnode->prev=(*phead);
newnode->next=OldHead;
OldHead->prev=newnode;
}
- 插入分为俩种情况
- 有元素(如图所示)
- 没有元素(如图一、二、三步)
任意插
- 他逻辑也和单链表一样
- 但是她是pos位头插和尾插都可以!!!!
- 他结合了头插和尾插于一身
void DListNodePushInsert(DLN **pos,int num)//pos位插入
{
assert(pphead);
//pos位尾插
DLN *next= (*pos)->next;
DLN *newnode=BuyDListNode(num);
(*pos)->next=newnode;
newnode->prev=(*pos);
newnode->next->next;
next->prev=newnode;
//pos头插
DLN *prev= (*pos)->prev;
DLN *newnode=BuyDListNode(num);
prev->next=newnode;
newnode->prev=prev;
newnode->next=(*pos);
(*pos)->prev=newnode;
}
删
- 其实逻辑和点链表差不多
尾删
- 逻辑和尾插相反就不多介绍了
- 代码里面有
注释
void DListNodePopBack(DLN **phead)//尾删
{
assert(pphead);
if(ListNodeEmpty(phead)==1)//链表是否空
{
exit(-1);
}
DLN *tail=(*phead)->prev;//尾巴
DLN *newtail=tail->prev;//尾巴前一个
free(tail);//删除尾巴
(*phead)->prev=newtail;//头节点prev指向老尾巴的前一个
newtail->next=(*phead);//老尾巴的前一个成为新的尾巴
}
头删
- 逻辑和头插,尾删相反就不多介绍了
- 代码里面有
注释
void DListNodePophFront(DLN **phead)//头删
{
assert(pphead);
if(ListNodeEmpty(phead)==1)//链表是否为空
{
exit(-1);
}
DLN *OldHead=(*phead)->next;//现在的头
DLN*newhead=OldHead->next;//现在的头后一个节点
free(OldHead);//删除现在的头
(*phead)->next=newhead;
newhead->prev=(*phead);
}
任意删
- 逻辑和头插,尾删相反就不多介绍了
在这里插入代码片void DListNodePopInsert(DLN **pos)//pos位插入
{
assert(pos);
if(ListNodeEmpty(phead)==1)//链表是否为空
{
exit(-1);
}
//pos位后插
DLN *next= (*pos)->next;
DLN *nextnext=next->next;
free(next);
(*pos)->next=nextnext;
nextnext->prev= (*pos);
// pos位前删
DLN *prevprev=prec->prev;
free(prev);
prevprev->next=(*pos);
(*pos)->prev=prevprev;
}
图解以上删除
以上可以发现删除其实是先存要删除的
前面一个节点
or后面一个节点
销毁
void DListNode(DLN **phead)
{
while((*phead)->next==(*phead)->prev)
{
DLN *tail=(*phead)->prev;
DLN *newtail=tail->next;
free(tail);
newtail->next=(*phead);
(*phead)->prev=tail;
}
if((*phead)->next!=NULL)
{
free((*phead)->next);
free(*phead);
}
}
总结
- 单链表的劣势就是
找不到前一个节点
- 而双向带头循环链表优化了这个一点,但是他的
代码量也大大的增加了
- 且双向链表的
链表的销毁
也比单链表麻烦