根据上一道面试题的讲解,我们也在其中探讨了一下双向链表和单向链表的区别,并且通过随机链表找到了,如何复制链表的内在逻辑。如果有感到有点遗忘的小伙伴可以点击下面:
那么我们今天就是链表的收尾了再附带一些存储器的知识内容。
目录
1.链表的分类
首先链表的结构非常多样,以下情况组合起来就有8种( 2 * 2 * 2 ) 链表结构 :

1.1单向/双向
1 . 单向 : 只能往一个方向遍历 (仅有一个指针 --> 指向下一个结点的地址), 如下图 : 只能从d1找到d2 , d2 找不到d1
2 . 双向 : 能从两个方向遍历 ( 有指向下一个结点的地址--后继,也有指向上一个结点的地址---前驱) , 如下面的第二幅图 , d2 可以找到d1 , 也可找到d3

1.2 带头/不带头
注意 : 这里的 “带头” 跟前面我们说的 “头结点” 是两个概念!
前面讲单链表的时候,会表述 “头结点”---> 链表首结点(第一个结点),这并不严谨,只是为了更好的去理解 链表首个位置有效 的结点 , 实际上这个表述是错误的
因为链表分类中存在 一种带头链表,里面的头结点不存储任何有效元素 , 只是负责占位置(哨兵位)
"哨兵位" --- 作用 : 站在这里“放哨” ,不需要判断链表是否为空!因为在对链表进行插入等操作时,要先判断链表是否为空再执行,这样代码重复率很高,有些冗余。
如果带头链表中只有头结点 , 我们称这个链表为空

1.3 循环/不循环

结合以上分析 : 我们可以推出
--------->单链表 : 单向不带头不循环链表
在接下来学习的双链表是:
--------->双链表 : 双向带头循环链表
我们可以这么去记 , 双链表和单链表的各类的类型都相反
虽然有这么多的类型 ,但是常用的还是单链表和双链表
1 . 单链表 : 结构简单 , 一般不会单独用来存储数据 . 实际上更多的是左右数据结构的子结构 , 如哈希图 , 图的邻接表等 , 另外的这种结构在笔试面试中会出现很多
2 . 双链表 : 结构复杂,一般单独存储数据.实际中使用的链表结构,基本上都是双链表(双向带头循环链表) , 虽说结构复杂 , 但是代码实现后会发现结构会带来很多优势,实现反而简单了!
2. 双向链表常见接口实现
2.1初始化
两种方式 : 1 . 返回值的方式(返回结点) -->主要使用这种方法
2 . 传参数形式(初始化的时候 , 要保证是循环的 ----> 自己指向自己)

所以这里我们推荐用返回值的方式来返回,这样不容易错。
但是还是讲一下方式二 : 参数形式
注意 : 初始化时 , 需要形参的改变影响实参 , 所以需要传地址 , 这里需要用到二级指针!

2.2尾插
注意 : 在进行尾插的时候 , 不需要像单链表一样使用二级指针 , 因为"哨兵位"phead的结点不会改变 , 尾插一个结点是通过访问结点的成员变量来实现的 .
1 . 创建一个新节点 : 向操作系统申请一块结点大小的空间 ---> 存储需要尾插的结点 , 在后续可能还需要再插入结点 , 这里可以先把创建一个结点的代码进行封装(buyNode)。
2 . 尾插 : 尾插时 , 先对newnode 指向进行更改 , 这样可以保证原链表的指向不被改变 ,思考不明白的 , 可以先画图 , 然后思考尾插一个结点时 , 有影响的结点有什么 ? 影响的指向是什么 ?

//申请一块结点大小的空间 --- 创建新结点
LTNode* buyNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail!");
return 1;
}
node->data = x;
node->next = node->prev = node;
}
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = buyNode(x);
//phead phead->prev newnode
newnode->prev = phead->prev;
newnode->next = phead;
phead->prev->next = newnode;
phead->prev = newnode;
}
2.3头插
头插需要注意的是 : 往哪里插入结点?
==> 插入到"哨兵位"之后 , 第一个有效结点之前 !
==> 如果插入到"哨兵位"之前 , 就是尾插了!

void LTPushFront(LTNode* phead, LTDataType x)
{
LTNode* newnode = buyNode(x);
newnode->next = phead->next;
newnode->prev = phead;
phead->next->prev = newnode;
phead->next = newnode;
}
2.4尾删
1 . 尾删时 , 先判断链表是否为空 ---> 断言(arrest) --> 调用LTEmpty 函数
2 . 明确删除尾结点所影响的结点
3 . 改变有影响结点的指向
4 . 删除尾结点 (free)

//尾删
void LTPopBack(LTNode* phead)
{
assert(!LTEmpty(phead));
LTNode* del = phead->prev;
//phead del->prv del
del->prev->next = phead;
phead->prev = del->prev;
//删掉尾结点
free(del);
del = NULL;
}
2.5头删
1 . 头删时 , 先判断链表是否为空 ---> 断言 --> 调用LTEmpty 函数
2 . 明确删除头结点所影响的结点( head del del->next )
3 . 改变结点的指向
4 . 删除头结点( free)

//头删
void LTPopFront(LTNode* phead)
{
assert(!LTEmpty(phead));
LTNode* del = phead->next;
//phead del del->next
del->next->prev = phead;
phead->next = del->next;
free(del);
del = NULL;
}
2.6查找
定义一个指针变量 (pcur) , 指向链表中第一个有效的结点( head->next) , 开始遍历 , 如果找到了 , 返回该结点 , 没找到 , 返回NULL
//查找
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;
}
2.7在指定位置插入元素
注意 : 在指定位置之前 或者指定位置之后插入数据 , 代码基本是一样的 , 因为双链表是循环的
1 . 向操作系统申请一块结点的空间
2 . 找到在指定位置插入数据所影响的结点
3 . 改变结点的方向

//在指定位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = buyNode(x);
newnode->prev = pos;
newnode->next = pos->next;
pos->next->prev = newnode;
pos->next = newnode;
}
2.8指定位置删除数据

//删除指定位置的数据
void LTErase(LTNode* pos)
{
assert(pos);
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
}
再把这张图放一下,这张图我在单链表的结尾也放过。

如果你觉得对你有帮助,可以点赞关注加收藏,感谢您的阅读,我们下一篇文章再见。
一步步来,总会学会的,首先要懂思路,才能有东西写。
1万+

被折叠的 条评论
为什么被折叠?



