一 . 链表的分类
链表的结构非常多样,以下情况组合起来就有8种( 2 * 2 * 2 ) 链表结构 :
1.1 单向/双向
1 . 单向 : 只能往一个方向遍历 (仅有一个指针 --> 指向下一个结点的地址), 如下图 : 只能从d1找到d2 , d2 找不到d1
2 . 双向 : 能从两个方向遍历 ( 有指向下一个结点的地址--后继,也有指向上一个结点的地址---前驱) , 如下面的第二幅图 , d2 可以找到d1 , 也可找到d3
1.2 带头/不带头
注意 : 这里的 “带头” 跟前面我们说的 “头结点” 是两个概念!
前面讲单链表的时候,会表述 “头结点”---> 链表首结点(第一个结点),这并不严谨,只是为了更好的去理解 链表首个位置有效 的结点 , 实际上这个表述是错误的因为链表分类中存在 一种带头链表,里面的头结点不存储任何有效元素 , 只是负责占位置(哨兵位)
"哨兵位" --- 作用 : 站在这里“放哨” ,不需要判断链表是否为空!因为在对链表进行插入等操作时,要先判断链表是否为空再执行,这样代码重复率很高,有些冗余。如果带头链表中只有头结点 , 我们称这个链表为空
1.3 循环/不循环
结合以上分析 : 我们可以推出
--------->单链表 : 单向不带头不循环链表
在接下来学习的双链表是:
--------->双链表 : 双向带头循环链表
我们可以这么去记 , 双链表和单链表的各类的类型都相反
虽然有这么多的类型 :
但是常用的还是单链表和双链表:
1 . 单链表 : 结构简单 , 一般不会单独用来存储数据 . 实际上更多的是左右数据结构的子结构 , 如哈希图 , 图的邻接表等 , 另外的这种结构在笔试面试中会出现很多
2 . 双链表 : 结构复杂,一般单独存储数据.实际中使用的链表结构,基本上都是双链表(双向带头循环链表) , 虽说结构复杂 , 但是代码实现后会发现结构会带来很多优势,实现反而简单了!
二 . 双向链表常见接口实现
2.1 双链表的结构
带头链表⾥的头结点,实际为“哨兵位”,哨兵位结点不存储任何有效元素,只是站在这里“放哨的”,在进行插入等操作时 , 无需判断链表是否为空 .
2.2 初始化
两种方式 :
1 . 返回值的方式(返回结点) ---> 主要使用这种方法
2 . 传参数形式
(初始化的时候 , 要保证是循环的 ----> 自己指向自己)
方式一 : 返回值的方式
方式二 : 参数形式
注意 : 初始化时 , 需要形参的改变影响实参 , 所以需要传地址 , 这里需要用到二级指针!
2.3 尾插
注意 :
在进行尾插的时候 , 不需要像单链表一样使用二级指针 , 因为"哨兵位"phead的结点不会改变 , 尾插一个结点是通过访问结点的成员变量来实现的 .
1 . 创建一个新节点 : 向操作系统申请一块结点大小的空间 ---> 存储需要尾插的结点 , 在后续可能还需要再插入结点 , 这里可以先把创建一个结点的代码进行封装(buyNode)
2 . 尾插 : 尾插时 , 先对newnode 指向进行更改 , 这样可以保证原链表的指向不被改变 ,对于尾插的逻辑思考不明白的 , 可以先画图 , 然后思考尾插一个结点时 , 有影响的结点有什么 ? 影响的指向是什么 ?
//尾插
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;
}
//申请一块结点大小的空间 --- 创建新结点
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;
}
2.4 头插
头插需要注意的是 : 往哪里插入结点?
==> 插入到"哨兵位"之后 , 第一个有效结点之前 !
==> 如果插入到"哨兵位"之前 , 就是尾插了!
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.5 打印链表
打印链表意味着我们需要遍历链表 , 涉及遍历就需要思考结束条件 , 在单链表中 , 遍历结束的条件是 pucr == NULL 时终止 , 在双向链表中 , 不存在NULL, 而具有独特标志性的结点 , 就是"哨兵位" -- > 头结点 !!!
所以当 pcur == phead 时候 , 就终止遍历 !
//打印链表
void LTPrint(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d -> ", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
2.6 判断链表是否为空
在双向链表中 , 当只存在一个 " 哨兵位" 的时候 ,判断双向链表为空 , 此时 的head , next 指针都指向"哨兵位" 自己
//判断链表是否为空
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
2.7 尾删
1 . 尾删时 , 先判断链表是否为空 ---> 断言 --> 调用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.8 头删
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.9 查找数据
定义一个指针变量 (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;
}
//查找
LTNode* find = LTFind(plist, 1);
if (find == NULL)
{
printf("没找到!\n");
}
else {
printf("找到了\n");
}
}
2.10 指定位置插入数据
注意 : 在指定位置之前 或者指定位置之后插入数据 , 代码基本是一样的 , 因为双链表是循环的
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.11 指定位置删除数据
1 . 明确要删除的结点所影响的结点 ( pos->prev , pos , pos->next )
2 . 改变结点方向
//删除指定位置的数据
void LTErase(LTNode* pos)
{
assert(pos);
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
}
2.12 销毁链表
链表中的结点是独立存在的 , 所以销毁链表的时候 , 需要一个一个结点的释放空间 , 同时也包括头指针 !
1 . 定义一个指针变量 , 指向头结点的下一个结点 ( pcur = head->next)
2 . 定义一个指针变量 next, 存储pcur的下一个结点 ---> pcur 指向的结点释放完之后 , 继续释放下一个结点
3 . 遍历链表
4 . 释放头结点
这里传的是二级指针 , 因为需要形参的改变影响实参 (把哨兵位也给释放掉 , 并置为NULL)
//销毁链表
void LTDestory(LTNode** pphead)
{
assert(pphead);
LTNode* pcur = (*pphead)->next;
while (pcur != (*pphead))
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(*pphead);
*pphead = NULL;
}
代码写到这里时 , 我们可以发现 , 除了以上的销毁链表的代码 , 其他使用的都是一级指针 , 为了减少我们的记忆成本 , 并保持代码接口一致性 , 我们可以使用一级指针 , 但是 ,一级指针中形参的改变并不影响实参 , 最后头结点的地址并不会改变 , 所以我们还需要额外的把地址置为NULL
//销毁链表 -- 一级指针
void LTDestory(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(phead);
phead = NULL;
}
//销毁链表 -- 一级指针
LTDestory(plist);
plist = NULL;
2.13 总代码
test .c
#include "List.h"
void Test()
{
//初始化 -- 返回值的方式
LTNode* plist = LTInit();
//初始化 -- 传参的方式
//LTNode* plist = NULL;
//LTInit(&plist);
//尾插
//LTPushBack(plist, 1);
//LTPushBack(plist, 2);
//LTPushBack(plist, 3);
//LTPushBack(plist, 4);
//LTPushBack(plist, 5);
//头插
LTPushFront(plist, 1);
LTPushFront(plist, 2);
LTPushFront(plist, 3);
LTPushFront(plist, 4);
LTPrint(plist);
//尾删
//LTPopBack(plist);
//LTPrint(plist);
//头删
//LTPopFront(plist);
//LTPrint(plist);
//查找
LTNode* find = LTFind(plist, 1);
if (find == NULL)
{
printf("没找到!\n");
}
else {
printf("找到了\n");
}
//在指定位置之后插入数据
//LTInsert(find, 99);
//LTPrint(plist);
//删除指定位置的数据
//LTErase(find);
//LTPrint(plist);
//销毁链表 -- 二级指针
//LTDestory(&plist);
//销毁链表 -- 一级指针
LTDestory(plist);
plist = NULL;
}
int main()
{
Test();
return 0;
}
List . c
#include "List.h"
//申请一块结点大小的空间 --- 创建新结点
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;
}
//初始化链表 -- 返回值方式
LTNode* LTInit()
{
//LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
//phead->data = -1;
//phead->next = phead->prev = phead;
LTNode* phead = buyNode(-1);
return phead;
}
//初始化 -- 传参方式
//void LTInit(LTNode** pphead)
//{
// *pphead= (LTNode*)malloc(sizeof(LTNode));
// (*pphead)->data = -1;
// (*pphead)->next = (*pphead)->prev = (*pphead);
//}
//打印链表
void LTPrint(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d -> ", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
//判断链表是否为空
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
//尾插
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;
}
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
LTNode* newnode = buyNode(x);
newnode->next = phead->next;
newnode->prev = phead;
phead->next->prev = newnode;
phead->next = newnode;
}
//尾删
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;
}
//头删
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;
}
//查找
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;
}
//在指定位置之后插入数据
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;
}
//删除指定位置的数据
void LTErase(LTNode* pos)
{
assert(pos);
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
}
//销毁链表 -- 二级指针
//void LTDestory(LTNode** pphead)
//{
// assert(pphead);
// LTNode* pcur = (*pphead)->next;
// while (pcur != (*pphead))
// {
// LTNode* next = pcur->next;
// free(pcur);
// pcur = next;
// }
// free(*pphead);
// *pphead = NULL;
//}
//销毁链表 -- 一级指针
void LTDestory(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(phead);
phead = NULL;
}
List . h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
//定义双向链表(结点)结构
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;
struct ListNode* prev;
struct ListNode* next;
}LTNode;
//初始化双链表
//初始化 -- 返回值的方式
LTNode* LTInit();
//
//初始化 -- 传参方式
//void LTInit(LTNode** pphead);
//判断是否为空
bool LTEmpty(LTNode* phead);
//打印链表
void LTPrint(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);
//查找
LTNode* LTFind(LTNode* phead , LTDataType x);
//在指定位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);
//删除指定位置的数据
void LTErase(LTNode* pos);
//销毁链表
void LTDestory(LTNode** pphead);
三 . 顺序表与链表的分析
注 : 两者并没有谁一定好于谁的说法 ! 不同情形 , 不同需求 , 使用不同的数据结构 , 在后续中我们还会学习更多的数据结构 , 方便我们解决更多的问题 , 敬请期待