一. 链表的分类
链表的结构非常多样,按照以下的组合起来就有8种:

(1)带头或者不带头:

带头链表中的"头结点",不存储任何有效的数据,只是用来占位的,我们称之为"哨兵位"。
在前面的文章中,有时候表述"头结点",但实际上单链表中把第一个节点称为"头结点"这种说法是错误的。
(2)单向或者双向:


(3)循环或者不循环


虽然这么多的链表结构,但是我们最常用的有两种:单链表(不带头单向不循环链表)和双链表(带头双向循环链表)。
二. 双向链表
2.1概念与结构

双向链表由一个一个的节点组成,这里的节点包括3个部分。
cpp
struct ListNode{
int data;
struct ListNode* prev;
struct ListNode* next;
};
注:当双向链表为空 时,这里表示双链表中只有一个哨兵位,且它的前驱指针和后继指针都指向自身。图示如下:

2.2 双向链表的初始化
cpp
#pragma once
#include <stdio.h>
#include <stdlib.h>
typedef int LTDataType;
typedef struct ListNode {
LTDataType data;
LTNode* prev;
LTNode* next;
}LTNode;
//1.初始化
void LTInit(LTNode** phead);
cpp
#include "List.h"
void LTInit(LTNode** pphead)
{
*pphead = (LTNode*)malloc(sizeof(LTNode));
if (*pphead == NULL)
{
perror("malloc fail!");
exit(1);
}
(*pphead)->data = -1;//哨兵位节点不存储任何有效数据,-1为无效
(*pphead)->next = (*pphead)->prev = NULL;
}
现在,我们已经有了一个空的双向链表,接下来,我们就要对其"增删改查"了。注意:在双向链表中,增删改查都不会改变哨兵位节点。
2.3 尾插
cpp
LTNode* LTBuyNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
while (newnode == NULL)
{
perror("malloc fail!");
exit(1);
}
newnode->prev = newnode->next = newnode;
newnode->data = x;
return newnode;
}
cpp
//2.尾插
LTNode* pushback(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->prev = phead->prev;
newnode->next = phead;
phead->prev->next = newnode;
phead->prev = newnode;
}
注:在尾插时,我们要特别注意几个指针的顺序,应该先处理newnode的前驱指针和后继指针,再处理原来双链表中尾节点的后继指针,使其指向newnode,最后处理头节点的前驱指针,使其指向最后一个节点(此时也就是newnode)。(此处一定要注意顺序,如果错乱,就有可能找不到原来的头节点)。
2.4 头插
头插时,节点是插在哨兵位的前面,还是插在哨兵位和下一个节点的中间?显然,答案是后者。因为哨兵位不存储有效数据,并不可以算作是双链表的第一个节点。
cpp
//3.头插
LTNode* pushfront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->next = phead->next;
newnode->prev = phead;
phead->next->prev = newnode;
phead->next = newnode;
}
2.5 尾删

cpp
//4.判断链表是否为空
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
//5.尾删
void popback(LTNode* phead)
{
if (!LTEmpty(phead))
{
LTNode* del = phead->prev;
del->prev->next = phead;
phead->prev = del->prev;
free(del);
del = NULL;
}
}
2.6 打印双链表
cpp
//6.打印双链表
void LTPrint(LTNode* phead)
{
LTNode* pcur = phead;
while (pcur != phead)
{
printf("%d-> ", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
2.7 头删
cpp
//7.头删
LTNode* popfront(LTNode* phead)
{
assert(!LTEmpty(phead));
LTNode* del = phead->next;
del->next->prev = phead;
phead->next = del->next;
free(del);
del = NULL;
}
2.8 查找
cpp
//8.查找
LTNode* find(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.9 在指定位置之后插入数据
cpp
//9.在pos位置之后插入数据
void LTInit(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
newnode->prev = pos;
newnode->next = pos->next;
pos->next->prev = newnode;
pos->next = newnode;
}
2.10 删除指定位置的节点
cpp
//10.删除pos位置的节点
void erase(LTNode* pos)
{
assert(pos);
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
pos = NULL;
}
2.11 销毁双链表
cpp
//11.销毁双链表
void LTDestory(LTNode** pphead)
{
LTNode* pcur = (*pphead)->next;
while (pcur != *pphead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
//销毁头节点
free(*pphead);
*pphead = NULL;
}
三. 代码改进
由上述的代码可以看出,除了初始化和释放形参是二级指针,其余功能实现形参都是一级指针,那我们为了保持接口一致性,能不能初始化和释放这两个功能也弄成一级指针呢?具体方法如下:
3.1 初始化
cpp
//1.初始化
LTNode* LTInit()
{
LTNode* phead = LTBuyNode(-1);
return phead;
}
3.2 销毁
cpp
void destory(LTNode* phead)
{
LTNode* pcur = (phead)->next;
while (pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
//销毁头节点
free(phead);
phead = NULL;
}
但是,这种写法需要自己手动将实参置为空。
四. 顺序表与链表分析
|--------------|----------------------|------------------------|
| 不同点 | 顺序表 | 链表(单链表) |
| 存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
| 随机访问 | 支持O(1) | 不支持: |
| 任意位置插入或者删除元素 | 可能需要搬移元素,效率低O(N) | 只需修改指针指向 |
| 插入 | 动态顺序表,空间不够时需要扩容和空间浪费 | 没有容量的概念,按需申请释放,不存在空间浪费 |
| 应用场景 | 元素高效存储+频繁访问 | 任意位置高效插入和删除 |
以上就是今天的内容,到目前为止,顺序表和链表就告一段落啦~喜欢的朋友们可以一键三连哦~