文章目录
数据结构4:链表(下)
上一章节我们实现了无头单向不循环链表,也了解链表的概念和分类。本章节我们来给链表收尾。
带头双向循环链表
有的人一看这里又是带头又是双向又是循环的,就感觉带头双向循环链表实现和理解起来会比无头单向不循环链表会更难。恰恰相反带头双向循环链表实际上比无头单向不循环链表简单很多,比如:带头双向循环链表实现尾插时不需要找尾,随机插入/删除可以选择删除pos,pos之前和pos之后的位置不像无头单向不循环链表那样删除pos和pos之前的位置需要辅助链表。
除了上述的优势,双向链表也比单向链表更容易维护。
那么让我们一起来看一下双向链表的成员变量吧
c
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LTNode;
带头双向循环链表有三个参数,分别是保存前面链表的prev,保存后面链表的next和数据,那么头呢?
一般头就是申请一个无任何意义的链表结点而这个结点我们通常叫做哨兵位,然后让其他的链表在它的之前或之后进行插入或删除操作。
双向链表的实现
c
#pragma once
#include<stdlib.h>
#include<assert.h>
#include<stdio.h>
#include<stdbool.h>
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LTNode;
// 初始化双向链表
LTNode* LTInit();
// 双向链表销毁
void LTDestroy(LTNode* phead);
// 双向链表打印
void LTPrint(LTNode* phead);
// 双向链表判空
bool LTEmpty(LTNode* phead);
// 双向链表尾插
void LTPushBack(LTNode* phead, LTDataType x);
// 双向链表尾删
void LTPopBack(LTNode* phead);
// 双向链表头插
void LTPushFront(LTNode* phead, LTDataType x);
// 双向链表头删
void LTPopFront(LTNode* phead);
// 查找
LTNode* LTFind(LTNode* phead, LTDataType x);
// pos位置之前插入
void LTInsert(LTNode* pos, LTDataType x);
// pos位置之前删除
void LTErase(LTNode* pos);
初始化
c
// 初始化双向链表
LTNode* LTInit();
我们发现这次实现双向链表时怎么没有扩容函数啊?
因为正常来说扩容函数向这类函数是供给其他函数使用的而非用户那么最好就是不要将这种类型的函数接口暴露出来。
然后这次实现的是一个带头双向循环链表所以初始化的时候就是给用户返回一个哨兵位,所以这里和单链表不一样,无头的链表可以直接插入数据而带头的链表就需要先创建一个哨兵位。
c
// 扩容
LTNode* BuyListNode(LTDataType x)
{
LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));
if (newNode == NULL)
{
perror("malloc fail");
return NULL; // 这里可以直接返回NULL或者直接exit
}
newNode->data = x;
newNode->next = NULL;
newNode->prev = NULL;
return newNode;
}
// 初始化双向链表
LTNode* LTInit()
{
LTNode* phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
因为这里扩容操作插入需要用到所以封装成了一个函数接口。初始化一个哨兵位后续进行插入和删除就好写很多了。
尾插和尾删
c
// 双向链表尾插
void LTPushBack(LTNode* phead, LTDataType x);
// 双向链表尾删
void LTPopBack(LTNode* phead);
这里插入会很好搞,因为不需要找尾了在哨兵位的头节点之前插入即可。
尾删操作呢?
尾删我们就需要单独去判断一下了,为什么要单独判断呢?
因为我们这里定义的哨兵位,这个哨兵位本身就是一个链表结点如果全部删除都话后续进行插入和删除就不知道插入到谁的后面了。

不过这里的phead怎么是一级指针?不应该是二级指针吗?
因为我们这里实现的是带头的链表,如果单链表带头同样是一级指针,那么为什么带头就是一级指针呢?因为哨兵位在申请之后直到销毁之前一直都在那里不需要进行传地址进行捕获它,而不哨兵位的就需要每次都检查地址才行,因为不哨兵位的链表在销毁之前可以为空。
判断是否为空
c
// 双向链表判空
bool LTEmpty(LTNode* phead);
phead->next == phead; 如果相等那么就证明现在只剩下一个哨兵位。prev也可以看个人喜欢那种方式。
c
// 双向链表判空
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
实现了判空操作,现在再来实现尾插和尾删操作。还是和实现单链表那样先画图再实现。

c
// 双向链表尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newNode = BuyListNode(x);
//phead <-> tail <-> newNode <-> phead
LTNode* tail = phead->prev;
tail->next = newNode;
newNode->prev = tail;
newNode->next = phead;
phead->prev = newNode;
}
// 双向链表尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
// phead <-> tail <-> newNode <-> phead
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
phead->prev = tailPrev;
tailPrev->next = phead;
free(tail);
tail = NULL;
}
实现了尾插和尾删,我们就可以进行测试一下功能了。为了方便演示先去实现一个打印函数。
双向链表打印函数
c
// 双向链表打印
void LTPrint(LTNode* phead)
{
assert(phead);
// 这里可以选择打印哨兵也可以选择不打印哨兵
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d-> ", cur->data);
cur = cur->next;
}
printf("\n");
}
测试尾插和尾删功能
c
void test1()
{
LTNode* head = LTInit();
LTPushBack(head, 1);
LTPushBack(head, 2);
LTPushBack(head, 3);
LTPushBack(head, 4);
LTPrint(head);
//先删除4个,然后多删除一个看会不会assert
LTPopBack(head);
LTPopBack(head);
LTPopBack(head);
LTPopBack(head);
LTPopBack(head);
}
int main()
{
test1();
return 0;
}
头插和头删
c
// 双向链表头插
void LTPushFront(LTNode* phead, LTDataType x);
// 双向链表头删
void LTPopFront(LTNode* phead);

c
// 双向链表头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newNode = BuyListNode(x);
// phead <-> newNode <-> first <-> phead
LTNode* first = phead->next;
phead->next = newNode;
newNode->prev = phead;
first->prev = newNode;
newNode->next = first;
}
// 双向链表头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
// phead <-> first <-> node <-> phead
LTNode* first = phead->next;
LTNode* firstNext = first->next;
phead->next = firstNext;
firstNext->prev = phead;
free(first);
first = NULL;
}
测试头插和头删的功能
c
void test2()
{
LTNode* head = LTInit();
LTPushFront(head, 1);
LTPushFront(head, 2);
LTPushFront(head, 3);
LTPushFront(head, 4);
LTPrint(head);
//先删除4个,然后多删除一个看会不会assert
LTPopFront(head);
LTPopFront(head);
LTPopFront(head);
LTPopFront(head);
LTPopFront(head);
}
int main()
{
//test1();
test2();
return 0;
}
随机插入和随机删除
c
// pos位置之前插入
void LTInsert(LTNode* pos, LTDataType x);
// pos位置删除
void LTErase(LTNode* pos);

c
// 在pos位置之前插入
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newNode = BuyListNode(x);
LTNode* prev = pos->prev;
prev->next = newNode;
newNode->prev = prev;
newNode->next = pos;
pos->prev = newNode;
}
// 删除pos位置节点
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
pos = NULL;
}
注:因为LTErase只有一个pos参数这就导致如果pos就是head的话就未定义了,这种写法是默认用户知道将head当pos是错误写法的。或者再给一个参数head就能解决了。
测试insert和erase功能
因为是给pos所以需要搭配find函数进行使用。那么就先实现一下find再来进行测试吧。
双向链表的查找函数
c
// 查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
测试:
c
void test3()
{
LTNode* head = LTInit();
LTInsert(head, 1);
LTNode* pos = LTFind(head, 1);
if (pos == NULL)
{
return;
}
LTInsert(pos, 2);
LTPushBack(head, 3);
pos = LTFind(head, 3);
LTInsert(pos, 4);
LTPrint(head);
pos = LTFind(head, 1);
if (pos)
{
LTErase(pos);
}
LTPrint(head);
LTErase(head->next);
LTPrint(head);
LTErase(head->prev);
LTPrint(head);
LTErase(head->next);
LTPrint(head);
}
int main()
{
//test1();
//test2();
test3();
return 0;
}
完整代码
c
#include "List.h"
// 扩容
LTNode* BuyListNode(LTDataType x)
{
LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));
if (newNode == NULL)
{
perror("malloc fail");
return NULL; // 这里可以直接返回NULL或者直接exit
}
newNode->data = x;
newNode->next = NULL;
newNode->prev = NULL;
return newNode;
}
// 初始化双向链表
LTNode* LTInit()
{
LTNode* phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
// 双向链表判空
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
// 双向链表尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
//LTNode* newNode = BuyListNode(x);
////phead <-> tail <-> newNode <-> phead
//LTNode* tail = phead->prev;
//tail->next = newNode;
//newNode->prev = tail;
//newNode->next = phead;
//phead->prev = newNode;
LTInsert(phead, x);
}
// 双向链表尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
//// phead <-> tail <-> newNode <-> phead
//LTNode* tail = phead->prev;
//LTNode* tailPrev = tail->prev;
//phead->prev = tailPrev;
//tailPrev->next = phead;
//free(tail);
//tail = NULL;
LTErase(phead->prev);
}
// 双向链表头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
//LTNode* newNode = BuyListNode(x);
//// phead <-> newNode <-> first <-> phead
//LTNode* first = phead->next;
//phead->next = newNode;
//newNode->prev = phead;
//first->prev = newNode;
//newNode->next = first;
LTInsert(phead->next, x);
}
// 双向链表头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
//// phead <-> first <-> node <-> phead
//LTNode* first = phead->next;
//LTNode* firstNext = first->next;
//phead->next = firstNext;
//firstNext->prev = phead;
//free(first);
//first = NULL;
LTErase(phead->next);
}
// 在pos位置之前插入
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newNode = BuyListNode(x);
LTNode* prev = pos->prev;
prev->next = newNode;
newNode->prev = prev;
newNode->next = pos;
pos->prev = newNode;
}
// 删除pos位置节点
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
pos = NULL;
}
// 查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
// 双向链表打印
void LTPrint(LTNode* phead)
{
assert(phead);
// 这里可以选择打印哨兵也可以选择不打印哨兵
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
顺序表和链表的区别
| 不同点 | 顺序表 | 链表 |
|---|---|---|
| 存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
| 随机访问 | 支持O(1) | 不支持:O(N) |
| 任意位置插入或者删除元素 | 可能需要搬移元素,效率低O(N) | 只需修改指针指向 |
| 插入 | 动态顺序表,空间不够时需要扩容 | 没有容量的概念 |
| 应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
| 缓存利用率 | 高 | 低 |
完