链表类型概述
链表作为基础数据结构,根据不同的特性组合可以分为多种类型。主要分类维度包括:
- 单向 vs 双向:节点指针指向单一方向还是可以双向访问
- 循环 vs 非循环:尾节点是否指向头节点形成闭环
- 带头节点 vs 不带头节点:是否存在不存储数据的哨兵节点
我们之前学习的单链表属于单向不循环无头节点 链表,而本文将实现功能更完善的双向循环带头节点链表。
1. 链表结构与初始化
节点结构定义
c
arduino
typedef int LTDataType;
typedef struct ListNode {
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
} ListNode;
每个节点包含数据域和两个指针域,分别指向前驱和后继节点。
链表初始化
c
ini
// 创建新节点
ListNode* CreateNode(LTDataType x) {
ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
assert(newNode);
newNode->data = x;
newNode->next = newNode;
newNode->prev = newNode;
return newNode;
}
// 初始化双向循环链表
ListNode* LTInit() {
ListNode* pHead = CreateNode(-1); // 创建哨兵节点
return pHead;
}
哨兵节点特点:
- 数据域通常存储无效值(如-1)
- 不参与实际数据存储
- 永远存在于链表中,避免空链表特殊情况
2. 链表基本操作
打印链表
c
scss
void LTPrint(ListNode* pHead) {
assert(pHead);
ListNode* pCur = pHead->next;
printf("链表内容: ");
while (pCur != pHead) {
printf("%d -> ", pCur->data);
pCur = pCur->next;
}
printf("HEAD\n");
}
实现要点:
- 从哨兵节点的下一个节点开始遍历
- 遇到哨兵节点时停止,避免无限循环
尾插操作
c
ini
void LTPushBack(ListNode* pHead, LTDataType x) {
assert(pHead);
ListNode* newNode = CreateNode(x);
// 新节点与链表建立连接
newNode->prev = pHead->prev;
newNode->next = pHead;
// 链表与新节点建立连接
pHead->prev->next = newNode;
pHead->prev = newNode;
}
操作流程:
- 创建新节点
- 新节点前驱指向原尾节点,后继指向头节点
- 原尾节点后继指向新节点
- 头节点前驱指向新节点
头插操作
c
ini
void LTPushFront(ListNode* pHead, LTDataType x) {
assert(pHead);
ListNode* newNode = CreateNode(x);
// 新节点与链表建立连接
newNode->next = pHead->next;
newNode->prev = pHead;
// 链表与新节点建立连接
pHead->next->prev = newNode;
pHead->next = newNode;
}
3. 链表删除操作
尾删操作
c
ini
void LTPopBack(ListNode* pHead) {
assert(pHead && pHead->next != pHead); // 确保链表不为空
ListNode* delNode = pHead->prev;
// 重新链接节点
delNode->prev->next = pHead;
pHead->prev = delNode->prev;
// 释放内存
free(delNode);
delNode = NULL;
}
头删操作
c
ini
void LTPopFront(ListNode* pHead) {
assert(pHead && pHead->next != pHead); // 确保链表不为空
ListNode* delNode = pHead->next;
// 重新链接节点
pHead->next = delNode->next;
delNode->next->prev = pHead;
// 释放内存
free(delNode);
delNode = NULL;
}
4. 指定位置操作
查找节点
c
ini
ListNode* LTFind(ListNode* pHead, LTDataType x) {
assert(pHead);
ListNode* pCur = pHead->next;
while (pCur != pHead) {
if (pCur->data == x) {
return pCur; // 返回找到的节点指针
}
pCur = pCur->next;
}
return NULL; // 未找到返回空指针
}
在指定位置后插入
c
ini
void LTInsert(ListNode* pos, LTDataType x) {
assert(pos);
ListNode* newNode = CreateNode(x);
// 新节点与前后节点建立连接
newNode->next = pos->next;
newNode->prev = pos;
// 前后节点与新节点建立连接
pos->next->prev = newNode;
pos->next = newNode;
}
删除指定位置节点
c
perl
void LTErase(ListNode* pos) {
assert(pos);
// 前后节点直接建立连接,跳过当前节点
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
// 释放内存
free(pos);
pos = NULL;
}
5. 完整测试示例
c
scss
#include "List.h"
void TestList() {
// 初始化链表
ListNode* plist = LTInit();
// 测试尾插
printf("=== 尾插测试 ===\n");
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPrint(plist); // 输出: 1 -> 2 -> 3 -> HEAD
// 测试头插
printf("\n=== 头插测试 ===\n");
LTPushFront(plist, 0);
LTPushFront(plist, -1);
LTPrint(plist); // 输出: -1 -> 0 -> 1 -> 2 -> 3 -> HEAD
// 测试查找和插入
printf("\n=== 查找插入测试 ===\n");
ListNode* pos = LTFind(plist, 1);
if (pos != NULL) {
LTInsert(pos, 99);
LTPrint(plist); // 在1后面插入99
}
// 测试删除
printf("\n=== 删除测试 ===\n");
LTErase(pos); // 删除节点1
LTPrint(plist);
// 测试头删尾删
printf("\n=== 头尾删除测试 ===\n");
LTPopFront(plist);
LTPopBack(plist);
LTPrint(plist);
}
int main() {
TestList();
return 0;
}
双向循环带头链表的优势
- 统一的空链表处理:哨兵节点确保链表永不为空,简化代码逻辑
- 高效的双向遍历:支持从前向后和从后向前遍历
- 循环特性:尾节点直接连接头节点,形成闭环
- 操作一致性:所有插入删除操作遵循相同模式,不需要特殊处理边界情况
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 初始化 | O(1) | O(1) |
| 插入(头/尾/指定位置) | O(1) | O(1) |
| 删除(头/尾/指定位置) | O(1) | O(1) |
| 查找 | O(n) | O(1) |
| 遍历 | O(n) | O(1) |
这种链表结构在实际应用中非常常见,特别是在需要频繁在两端进行插