链表分类及双向链表的实现
链表的分类

一共有22 2=8种链表
next:指向下一个结点(后继结点)
prev:指向前一个结点(前驱结点)
头结点不存储数据,作为"哨兵位"。
(单链表的第一个结点不能叫头结点!因为其存储了数据!)
不循环链表的尾结点next为空,循环链表的尾结点next不为空
- 虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:单链表(单向不带头不循环链表)和双向链表(双向带头循环链表)
- 单链表:结构简单,⼀般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。
- 双向链表:结构最复杂,⼀般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。
双向链表
结点结构
c
struct ListNode
{
int data;
struct ListNode* next;
struct ListNode* prev;
}
链表结构
双向带头循环链表:
特点:
- 头结点的prev指向尾结点,尾结点的next指向头结点。表达为:
ptail=phead->prev
phead=ptail->next
- 循环链表的特性:如果遍历条件是
pcur!=NULL
,链表将无线循环遍历下去。 - phead(即双向链表头结点的地址)永远不会发生改变。(但是头结点中的成员是可以改变的!)
初始化
双向链表为空的情况下只有一个哨兵位,且next和prev指针都指向自己。
(如果连哨兵位都没有的话,这就是单链表而不是双向链表)
c
//形成一个新的结点,频繁使用,独立出来
LTNode* buyNode(LTDataType x) {
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL) {
perror("malloc fail!");
exit(1);
}
node->data = x;
node->next = node->prev = node;
return node;
}
/双向链表的初始化法1
void LTInit01(LTNode** pphead) {
//只有传地址才能真正改变phead!
*pphead = buyNode(-1);//随便给一个data就行了,因为头结点的数据是不会用到了
}
//双向链表的初始化法2
LTNode* LTInit02() {
LTNode* phead = buyNode(-1);
return phead;
}
遍历/打印
双向链表是循环链表,所以循环条件不能再是pcur!=NULL
c
//打印双向链表
void LTPrint(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead) {
printf("%d -> ", pcur->data);
pcur = pcur->next;
}
}
尾插
添加元素时,需要先分情况为链表为空和链表非空,因为有可能操作是不同的。(但是尾插的情况下操作相同就可以合并)
- 修改顺序?------修改指针指向时,先修改对原链表无影响的指针
- newnode的prev、next可以先定
- 再修改尾结点的next和头结点的prev
c
//尾插
//传入的phead结点不会发生改变,参数就只传一级指针足够
//如果传入的phead指针需要发生改变(LTInit01),参数就需要传二级
void LTPushBack(LTNode* phead, LTDataType x) {
//如果传入的phead为空,则传入的根本不是双向链表,直接断言报错
assert(phead);
LTNode* newnode = buyNode(x);
newnode->next = phead;
LTNode* ptail = phead->prev;
newnode->prev = ptail;
ptail->next = newnode;
phead->prev = newnode;
}
头插
头插 是指插入一个结点作为链表的第一个有效节点
当然不能插在phead前面!因为我们知道phead->prev其实是尾结点。
- 与尾插相同,修改指针指向时,先修改对原链表无影响的指针
c
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
LTNode* newnode = buyNode(x);
newnode->prev = phead;
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
}
尾删
进行删除前一定要先探空------防止导致头结点被删除导致链表销毁等异常问题
//尾删
c
void LTPopBack(LTNode* phead)
{
assert(!LTEmpty(phead));
LTNode* del = phead->prev;//把尾结点(即要删除的结点)单独写出来避免代码难读
del->prev->next = phead;
phead->prev = del->prev;
free(del);
}
头删
c
void LTPushFront(LTNode* phead, LTDataType x)
{
LTNode* newnode = buyNode(x);
newnode->prev = phead;
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
}
查找
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;
}
在pos位置之后插入
c
//在pos位置之后插入
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;
}
删除pos位置的结点
c
//删除pos位置的结点
void LTErase(LTNode* pos, LTDataType x)
{
assert(pos);
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
销毁双向链表
c
//销毁01 因为需要释放头结点,所以需要传二级指针
void LTDesTroy(LTNode** pphead)
{
LTNode* pcur = (*pphead)->next;
while (pcur != *pphead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(*pphead);
*pphead = NULL;
}
c
//销毁02 为保持接口一致性,建议统一参数指针都为一级
//但是需要手动把phead实参置空,否则本函数里只能改变形参
void LTDesTroy(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(phead);
}
顺序表与链表的分析(线性表)
