目录
继上文之后的单链表学习,最常用的还是双链表中的循环双链表,看起来挺复杂,其实理解之后比单链表省事很多。然后都是基于对单链表的理解去学习,所以篇幅会比较少。
单链表的缺点
由于单链表是只有尾指针tail,所以不论带头还是不带头,它找尾巴很麻烦,需要遍历链表,时间复杂度为,对于单链表,尾插尾删修改链表都比较麻烦,所以对于这些操作,更为方便的其实是双链表。
双链表
带头双向循环链表,简称双链表,结构上相较于单链表比较复杂,在实际应用中,多为带头双向循环链表,但是使用起来真的会比单链表方便很多,因为它有头指针和尾指针双向,在实现链表的增删查改都会十分便利。
那么什么是双链表呢,来一副图说明就是,应该是十分清晰的。从这张结构图可以看出,除了具备基本的单链表特点,它还有一个箭头指向它的前节点,尾节点d4的尾指针指向头节点head,head的前驱指针指向尾节点d4,形成循环。
带头双向循环链表
双链表的实现
双链表该如何实现呢,同样是增删查改,只需要在单链表的基础上加一个前驱指针就行了。
单链表是这样定义的
cpp
typedef int Sltdatatype;
typedef struct SListnode
{
Sltdatatype data;
struct SListnode* next;
}SLTnode;
那么双链表定义就是这样的
双链表定义
cpp
typedef int List;
typedef struct SListnode
{
Sltdatatype data;
struct ListNode* next;
struct ListNode*prev;
}LTNode;
然后在实现双链表的功能之前,需要强调的一点是,双链表不需要用到二级指针,用一级就可以了
why?重点注意
- 为什么不需要二级指针呢,双链表不是改变头指针本身,只是改变结构体内指针变量 ,而改变结构体变量,用结构体指针就可以了。我们在之前写单链表的时候,在头插头删的时候,都是要改变头指针指向,是头指针本身地址,空链表也要申请malloc一块内存空间,指针去指向它,指针本身被修改了,并且,尾删删到最后一个节点,尾插时,链表为NULL的情况下,等于头插,都要改变头节点的指针地址。
- 而双链表一般都是有头节点,头指针在初始化后永远指向头节点,本身内容不会发生改变,(插入的时候改变的是头节点里的指针变量,并非头节点本身)所以只要改变结构体变量,并不涉及指针本身的修改。
- 然后就是第二个注意事项:单链表的初始化不需要单独函数封装,而双链表最好时封装一下:
因为单链表结构简单,最开始初始化的时候只需要把链表置空就好,然后尾插都是动态申请一个空间,然后在申请额外节点的函数内初始化了节点。双链表结构比较复杂,需要给头节点申请并初始化,然后前后指针都指向本身。后续插入不改变头节点本身地址(一直指向自己)只是改变头节点里的指针变量的指向。
单链表的节点申请
其实第一个问题用不用二级指针可以由第二个问题解释,用不用就看头指针内容是否改变。两个问题可以一起思考,一起解决。这个关键之处明白了,剩下的就不是事情了。
先附上双链表的各种场景下总思路图
那有了单链表的基础,就不用再逐一解释了,直接上代码
双链表头文件
cpp
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
//双链表定义
typedef struct ListNode{
int data;
struct ListNode* prev;
struct ListNode* next;
}LNode;
void Listinit(LNode**phead);
void ListDestory(LNode*phead);
void ListPrint(LNode*phead);
bool IfEmptyNode(LNode* phead);
void ListPushBack(LNode* phead,int x );
void ListPushFront(LNode*phead,int x );
void ListPopBack(LNode* phead);
void ListPopFront(LNode* phead);
void InsertPos(LNode*pos,int x);
void ErasePos(LNode* pos);
LNode* LTFind(LNode* phead, int x);
还是初始化,(这里要封装一下),销毁,打印,判空,头插尾插,头删尾删,还有插入,删除,这两个可以运用在其他四个函数功能中。头插就是在头节点后插入,尾插就是在头节点前插入,head的前一个节点是尾节点。头删就是头节点后面删除,尾删就是头节点前删除。这些应该不难理解。只要记住这句话
双向带头节点,或者说带哨兵卫的循环链表,head之前是尾,head后是第一个数据,尾节点后面是head。
附上主功能函数代码
cpp
#include"List.h"
LNode* BuyNode(int x)
{
LNode* newnode = (LNode*)malloc(sizeof(LNode));
if (newnode==NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
void Listinit(LNode** phead)
{
LNode*pphead = BuyNode(-1);
if (pphead==NULL)
{
return ;
}
pphead->next = pphead;
pphead->prev = pphead;
*phead = pphead;
}
void ListDestory(LNode* phead)
{
assert(phead);
LNode* cur = phead->next;
while (cur)
{
LNode* next = cur->next;
free(cur);
cur =next;
}
free(phead);
phead = NULL;
}
bool IfEmptyNode(LNode*phead)
{
assert(phead);
LNode* cur = phead;
if (cur->next == cur)
{
return true;
}
else
return false;
}
void ListPrint(LNode* phead)
{
assert(phead);
printf(" <= phead =>");
LNode* cur = phead->next;
while (cur!=phead)
{
printf("<=%d=>",cur->data);
cur = cur->next;
}
printf("\n");
}
//尾插
void ListPushBack(LNode* phead,int x)
{
assert(phead);
//防止有带头空链表
//LNode* node = BuyNode(x);
//LNode* tail = phead->prev;
//tail->next = node;
//node->prev = tail;
//node->next = phead;
//phead->prev = node;
//哨兵卫前面插入。等于尾插,因为head的前一个节点是尾节点
InsertPos(phead,x);
}
//头插
void ListPushFront(LNode* phead, int x)
{
assert(phead);
/*LNode* node = BuyNode(x);
node->next = phead->next;
phead->next->prev = node;
phead->next = node;
node->prev = phead;*/
//哨兵节点的后面插入,就是在第一个节点前插入
InsertPos(phead->next,x);
}
//尾删
void ListPopBack(LNode* phead)
{
assert(phead);
//防止有空链表,要判断是否为空
assert(!IfEmptyNode(phead));
/*LNode* del = phead->prev;
phead->prev = del->prev;
del->prev->next = phead;
free(del);
del = NULL;*/
ErasePos(phead->prev);
}
void ListPopFront(LNode* phead)
{
assert(phead);
//防止有空链表,要判断是否为空
assert(!IfEmptyNode(phead));
/*LNode* del = phead->next;
phead->next = del->next;
del->next->prev = phead;
free(del);
del = NULL;*/
ErasePos(phead->next);
}
//查找
LNode* LTFind(LNode* phead,int x)
{
assert(phead);
LNode* cur = phead->next;
while (cur!=phead)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
//pos位置前插入,作为接口调用
void InsertPos(LNode* pos, int x)
{
assert(pos);
LNode* pprev = pos->prev;
LNode* node = BuyNode(x);
node->next = pos;
pos->prev = node;
pprev->next = node;
node->prev = pprev;
}
//pos位置删除,可以作为特殊位置删除的接口调用
void ErasePos(LNode* pos)
{
assert(pos);
LNode* pprev = pos->prev;
LNode* pnext = pos->next;
pprev->next = pos->next;
pnext->prev = pprev;
free(pos);
pos = NULL;
}
测试代码就不展示了,博主自己测试过所有函数,是没有什么遗漏的点的。双链表就这些基本操作。理解了单链表,和那两个关键之处,其实就没什么特别难理解的点了。
效率分析:
双链表在某些操作上效率比单链表效率更高,整体上还是要考虑时间和空间复杂度。
1.操作效率优势:双链表在插入操作时间复杂度为
,单链表为
,支持双向遍历,灵活高。
2.空间上相对不足:额外存储前驱指针,内存占用增大
建议在需要频繁插入/删除的场景运用双链表,而在只需要单向操作用单链表(入栈,队列问题)
博主已经在慢慢学习C++了,后续还会不断输出新内容的。