链表的分类
单向或双向
单向的只有next,只能从头遍历到尾;
双向的既有next 也有prve,对于双向的我们既可以从头遍历,也可以从尾遍历到头
带头或不带头
我们前面学的单链表是不带头的 ,前面提到的头节点只是为了让我们好理解,才这样做的;
真正的带头链表也叫头节点,又称哨兵位,是用来占位子的,
因此,在带头链表中,除了头节点,其它节点都存储有效的数据。
循环和不循环
尾节点的next指针不为空的是循环链表,为空的则为不循环链表
总共22 2种(8种)。
单链表的全称:单向不带头不循环链表
双向链表全称:双向带头循环链表
链表的种类虽然很多,我们学习以上这两种即可,其余的我们可以通过举一反三自己就能实现。
双向链表的理解
双向链表的结构虽然比单向链表的结构复的多,但是接口的实现要比单向链表简单
双向链表中的哨兵位的prev节点和尾节点的next节点是相互指的,因为双向链表是循环链表
双向链表的结构是由:数据 + 指向后一个节点的指针 + 指向前一个节点的指针 组成的
双向链表的各种功能的实现:
双向链表的定义
核心代码:
c
//双向链表的定义
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
////使用这种方式也可以重命名:
//typedef struct ListNode LTNode;
双向链表向内存申请新节点
c
LTNode* LTBuyNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));//记得判断是否申请失败
if (newnode == NULL)
{
perror("malloc fail");
exit(1);//等价于return 1;
}
newnode->data = x;
newnode->next = newnode->prev = newnode;//双向链表为循环链表,自己指向自己
return newnode;
}
双向链表的初始化
c
//双向链表的初始化
//链表中只有一个头节点的情况下,才叫双向链表为空;若连头节点都没有,那它是单链表
void LTInit(LTNode** pphead)//头节点发生改变,传的时二级指针
{
//创建一个新节点作为头节点, -1代表头节点的无效值
*pphead = LTBuyNode(-1);
}
尾插
尾插思路图
核心代码:
c
//第一个参数传一级还是二级,看的是pphead指向的节点是否会发生变化
// 如果发生变化,那pphead的改变需要影响到实参,需要传二级
// 如果不发生变化,那pphead不会影响实参,传一级
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);//穿的变量是有效的,哨兵位不能为空,所以这里不能为空
//1.创建新节点
LTNode* newnode = LTBuyNode(x);
//2.找到受到影响的节点,与新节点进行尾插
//phead phead->prev newnode 先改变newnode的指向,再去改变其他的
newnode->next = phead;
newnode->prev = phead->prev;
////方法1:
//phead->prev->next = newnode;
//phead->prev = newnode;
//方法2:用中间变量,实现
LTNode* Tail = phead->prev;
Tail->next = newnode;
phead->prev = newnode;
}
头插
头插思路图:
核心代码:
c
void LTPushFrant(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);//和尾插步骤差不多,不懂得,可以去看看尾插的注释
//phead phead->next newnode
newnode->next = phead->next;
newnode->prev = phead;
////方法1:
//phead->next->prev = newnode;
//phead->next = newnode;
//方法2:
LTNode* tmp = phead->next;
phead->next = newnode;
tmp->prev = newnode;
}
判断双向链表是否为空
核心代码:
c
bool LTEmpty(LTNode* phead)//用来判断链表是否为空的
{
assert(phead);//双向链表的头节点不能为空
return phead->next == NULL;//如果下一个节点为空,则说明链表为空,条件成立,返回true,反之返回false
}
尾删
尾删思路图
核心代码:
c
void LTPopBack(LTNode* phead)
{
//0.通过断言判断,传的参数是否为空,双向链表的头节点不能为空;其次双向链表不能为空,若为空头删谁去呀
assert(phead);
assert(!LTEmpty(phead));//若LTEmpty返回的是true,通过 ! 转成false,断言报错,反之正常运行
//1.找到删除的节点 和删除该节点后受到影响的节点并用临时变量保存,先将受到影响的节点的指向修改,最后删除要删除的节点
LTNode* del = phead->prev;
LTNode* prev = del->prev;
prev->next = phead;
phead->prev = prev;
free(del);
del = NULL;
}
头删
头删思路图
核心代码:
c
void LTPopFrant(LTNode* phead)
{
//头删这里的思路和前面尾删的思路是大差不差的,可以去看上面的尾删的注释
assert(phead);
assert(!LTEmpty(phead));
LTNode* del = phead->next;
phead->next = del->next;
del->next->prev = phead;
free(del);
del = NULL;
}
查找
核心代码:
c
LTNode* LTFind(LTNode* phead,LTDataType x)
{
assert(phead);
LTNode* pcur = phead->next;//由于这里是双向链表,头节点的下一个节点是有效的节点,所以我们是从第一个有效的节点开始遍历的
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
//没找到继续往后找
pcur = pcur->next;
}
//出了链表还没找到,那就是没找到了
return NULL;
}
//有了查找这个功能,我们就可以在任意位置之后\之前插入删除数据了,我们这里先实现在位置之后的,之前的后续会实现
再指定位置之后插入
思路图:
核心代码:
c
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);//pos位置不能传空
//1.创建新节点
LTNode* newnode = LTBuyNode(x);
//2.找到受到影响的节点,与新节点进行尾插
//pos newnode pos->next 先改变newnode的指向,再去改变其他的
newnode->next = pos->next;
newnode->prev = pos;
//方法1:
pos->next->prev = newnode;
pos->next = newnode;
////方法2:
//LTNode* tmp = pos->next;
//pos->next = newnode;
//tmp->prev = newnode;
}
在指定位置之前插入节点
思路图:
核心代码:
c
void LTInBefor(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
newnode->next = pos;
newnode->prev = pos->prev;
pos->prev->next = newnode;
pos->prev = newnode;
}
删除指定位置节点
思路图:
核心代码:
c
void LTErase(LTNode* pos)
{
assert(pos);
//pos->prev pos pos->next --- 先将删除pos后受到影响的两个节点的指向进行修改,再将pos节点进行删除
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
双向链表销毁
思路图:
核心代码:
c
void LTDestroy(LTNode** pphead)
//销毁传的是二级指针,因为最后我们要将头节点也要进行销毁
//创建指针遍历pcur指向*pphead的下一个指针,进行依次销毁之前,先对pcur的下一个指针用Next进行保存,
// 方便后续继续往后销毁,再进行销毁操作,因为我们传的是二级指针,所以我们最后要对*pphead进行手动销毁
{
LTNode* pcur = (*pphead)->next;//这里的-> 权限比*高,这里要加上括号
while (pcur != *pphead)
{
LTNode* Next = pcur->next;
free(pcur);
pcur = Next;
}
*pphead = NULL;
pcur = NULL;
}
保持接口的一致性后代码优化
为了保持接口的一致性,将初始化和销毁优化接口都为一级指针(接口:指的是实现功能的方法)
以下是优化后的核心代码:
销毁优化:
c
void LTDestroy2(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* Next = pcur->next;
free(pcur);
pcur = Next;
}
phead = pcur = NULL;
}
初始化优化:
c
LTNode* LTInit2()
{
LTNode* phead = LTBuyNode(-1);
return phead;
}
顺序表和链表分析

没有绝对的谁好,只是应用的场景不同,存在即合理!