C++实现带头双向循环链表
前言
链表按照是否带头, 是否循环 , 单向双向共有八种组合
- 不带头单向不循环链表
- 不带头单向循环链表
- 不带头双向不循环链表
- 不带头双向循环链表
- 不带头单向不循环链表
- 不带头单向循环链表
- 不带头双向不循环链表
- 不带头双向循环链表
本文将对带头双向循环链表的概念, 实现 , 增删查改等进行介绍
一 . 结构体的定义
- 我们知道, 链表由一个或多个节点组成 。
- 每个节点又分为数据域和指针域
- 循环链表的指针域需要两个指针, 一个用于指向上一个节点, 另一个用于指向下一个节点
- 是否带头指的是 , 该链表是否有 哨兵头节点
- 哨兵头节点不存储具体的数据, 只是起到一个监视的作用
根据以上信息来对节点进行定义
typedef int LTDataType; // 该示例链表所存储数据为 int 类型
typedef struct ListNode
{
LTDataType data; // 数据域, 变量data 存放数据
ListNode* prev; // 指针域, 存放上一节点的地址
ListNode* next; // 指针域, 存放下一节点的地址
}ListNode; // 结构体别名
二 . 链表的初始化
在我们定义结构体之后, 要对链表进行初始化。
- 实际上是声明一个头指针, 以及构建一个哨兵头节点
- 头指针指向该哨兵头节点
// 根据指定值, 创建节点
ListNode* BuyLTNode(LTDataType val)
{
ListNode* newNode = new ListNode;
if (!newNode)
{
perror("new is failed!");
exit(-1);
}
newNode->data = val;
newNode->next = NULL;
newNode->prev = NULL;
return newNode;
}
// 链表初始化 , 设置哨兵头节点
ListNode* LTInit()
{
ListNode* phead = BuyLTNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
ListNode* phead = LTInit(); // 使得头指针指向哨兵头节点
三 . 链表的增删改查
1 . 向链表中插入新节点
1) 头插法
// 链表头插
void LTPushFront(ListNode* phead, LTDataType val)
{
if (!phead) // 判断头指针是否为空, 即链表是否被初始化
{
perror("phead is NULL");
exit(-1);
}
ListNode* newNode = BuyLTNode(val);
ListNode* cur = phead->next;
phead->next = newNode;
newNode->prev = phead;
newNode->next = cur;
cur->prev = newNode;
}
2) 尾插法
// 链表尾插
void LTPushBack(ListNode* phead, LTDataType val)
{
if (!phead) // 判断头指针是否为空, 即链表是否被初始化
{
perror("phead is NULL");
exit(-1);
}
ListNode* newNode = BuyLTNode(val);
ListNode* tail = phead->prev;
tail->next = newNode;
newNode->prev = tail;
newNode->next = phead;
phead->prev = newNode;
}
3) 指定位置前插入
// 链表 指定位置插入
void LTInsert(ListNode* pos, LTDataType val)
{ // 这里的pos是指向节点的指针, 我们要插入的节点, 是插入到pos指向的节点之前的位置。
// 在这里可以增加一步实现, 判断pos是否等于phead , 这就需要传入第二个参数 phead
if (!pos)
{
perror("pos is NULL");
exit(-1);
}
ListNode* prev = pos->prev;
ListNode* next = pos; // 这一步也可以不创建临时变量, 直接使用pos就可以 , 不会修改pos指向 创建临时变量只是为了名字上统一
ListNode* newNode = BuyLTNode(val);
prev->next = newNode;
newNode->prev = prev;
newNode->next = next;
next->prev = newNode;
}
2 . 删除链表中的已有节点
1) 头删
// 链表头删
void LTPopFront(ListNode* phead)
{ // 还需要判断phead是否为空
ListNode* cur = phead->next;
if (cur == phead)
{
perror("链表中已无节点, 无法继续删除");
exit(-1);
}
phead->next = cur->next;
cur->next->prev = phead;
delete cur;
cur = NULL;
}
2) 尾删
// 链表尾删
void LTPopBack(ListNode* phead)
{ // 还需要判断 phead是否为空
ListNode* tail = phead->prev;
if (tail == phead) // 这里判断是否为空表
{
perror("链表中已无节点, 无法继续删除");
exit(-1);
}
phead->prev = tail->prev;
tail->prev->next = phead;
delete tail;
tail = NULL;
}
3) 删除指定位置的节点
// 链表 指定位置删除
void LTErase(ListNode* pos)
{ // 在这里可以增加一步实现, 判断pos是否等于phead , 这就需要传入第二个参数 phead
if (!pos)
{
perror("pos is NULL");
exit(-1);
}
ListNode* prev = pos->prev;
ListNode* next = pos->next;
prev->next = next;
next->prev = prev;
delete pos;
}
3 . 链表的修改
1) 修改链表中指定节点的值
链表不同于顺序表 , 知道节点的位置, 修改其data值十分简单。
// 修改链表中指定节点的data值
void LTModify(ListNode* pos, LTDataType val)
{ // 在这里可以增加一步实现, 判断pos是否等于phead , 这就需要传入第二个参数 phead
if (!pos)
{
perror("pos is NULL");
exit(-1);
}
pos->data = val;
}
4 . 链表的查找
1) 查询链表中是否存在数据域为指定值的节点
// 链表 查找指定值
ListNode* LTFind(ListNode* phead, LTDataType val)
{ // 判断 phead是否为空 , 也就是是否存在哨兵头节点
ListNode* cur = phead->next;
while (cur != phead) // 还需要判断链表是否为空
{
if (cur->data == val)
return cur;
cur = cur->next;
}
return NULL;
}
2) 查询链表中节点的个数
这里所查询的节点的个数指除哨兵头节点之外的节点的个数。
即 数据域正常存储数据的节点的个数
// 链表 返回链表中有效节点(数据节点)的个数
int LTSize(ListNode* phead)
{
// 判断 phead是否为空 , 也就是是否存在哨兵节点(链表是否已初始化)
int size = 0;
ListNode* cur = phead->next;
while (cur!= phead)
{
cur = cur->next;
++size;
}
return size;
}
四 . 打印链表各个节点的数据域
// 链表打印
void LTPrint(ListNode* phead)
{
if (!phead)
{
perror("phead is NULL");
exit(-1);
}
ListNode* cur = phead->next;
if (cur == phead)
{
cout << "空表" << endl;
return;
}
cout << "phead <=> ";
while (cur!=phead)
{
cout << cur->data << " <=> ";
cur = cur->next;
}
}
五 . 链表的销毁
// 链表销毁
ListNode* LTDestroy(ListNode* phead)
{ // 该函数可以定义为无返回值。
ListNode* cur = phead->next;
ListNode* prev = NULL;
while (cur != phead)
{
prev = cur;
cur = cur->next;
delete prev;
prev = NULL;
}
delete phead; // 哨兵头节点也需要释放其内存
return NULL;
}
phead = LTDestroy(phead);
六 . 链表的优缺点
- 优点 :
- 任意位置元素的插入删除效率高
- 按需申请或释放内存, 不存在空间的浪费
- 缺点 :
- 存储的数据不支持随机访问(不可使用下标进行访问)
- 物理上存储空间是随机的,不连续。 CPU高速缓存命中率低
-(也就是 存储不连续,导致缓存利用率低)
七 . 顺序表与链表的区别
不同点 | 顺序表 | 链表 |
---|---|---|
存储空间 | 物理逻辑双连续 | 逻辑上连续,但物理上不一定连续 |
随机访问 | 支持(可下标访问): O(1) | 不支持(遍历访问):O(N) |
元素的插入和删除 | 可能需要搬移元素,效率低O(N) | 只需修改指针指向 |
插入 | 动态顺序表,空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
缓存利用率 | 高 | 低 |
相同点 :
- 都是线性结构, 线性存储数据, 结构简单, 都可被称为线性表
- 线性表的顺序存储 – 顺序表
- 线性表的链式存储 – 链表