【数据结构】——3.2双链表
一、双链表的概念及结构
1. 双链表的概念
? 双向带头循环链表:结构最复杂,但是操作最方便,使用两个指针分别存储前后结点的位置,并且尾结点的下一位又回到了头结点。是实际开发中所使用的链表
2. 双链表结构
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
} ListNode;
二、双链表的实现
1. 初始化
- 初始化时不传参数,直接将链表头指针返回更简便
- 初始化时要创建头结点,并将链表头结点的
prev 和next 指针指向自己,构成循环
ListNode* ListInit(void)
{
ListNode* guard = (ListNode*)malloc(sizeof(ListNode));
if (guard == NULL)
{
perror("malloc fail\n");
exit(-1);
}
guard->next = guard;
guard->prev = guard;
return guard;
}
2. 动态申请结点
? 动态申请结点时,要将前后指针初始化为NULL,值由参数传递
ListNode* BuyListNode(LTDataType x)
{
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
if (node == NULL)
{
perror("malloc fail\n");
exit(-1);
}
node->next = NULL;
node->prev = NULL;
node->data = x;
return node;
}
3. 遍历打印双链表
? 判断链表遍历结束的条件不再是尾结点的next为NULL了,而是尾结点的next是头指针
void ListPrint(ListNode* plist)
{
assert(plist != NULL);
ListNode* cur = plist->next;
while (cur != plist)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
4. 判断双链表是否为空
? 判断链表没有元素的条件是头结点的next指向自己
_Bool ListEmpty(ListNode* plist)
{
assert(plist != NULL);
return plist->next == plist;
}
5. 获取双链表结点个数
- 使用无符号整型存储链表结点个数
- 遍历双链表,记录结点个数
size_t ListSize(ListNode* plist)
{
assert(plist != NULL);
size_t count = 0;
ListNode* cur = plist->next;
while (cur != plist)
{
count++;
cur = cur->next;
}
return count;
}
6.插入数据
6.1 尾插
- 链表尾插要先改变新结点的两个指针,再改变前结点的
next 指针和后结点的prev 指针 - 可以声明指针变量记录尾结点位置,再进行插入(这里没有这样做)
void ListPushBack(ListNode* plist, LTDataType x)
{
assert(plist != NULL);
ListNode* newnode = BuyListNode(x);
newnode->next = plist;
newnode->prev = plist->prev;
plist->prev->next = newnode;
plist->prev = newnode;
}
6.2 头插
? 头插和尾插差不多,只是在头结点的后边插入结点
void ListPushFront(ListNode* plist, LTDataType x)
{
assert(plist != NULL);
ListNode* newnode = BuyListNode(x);
newnode->next = plist->next;
newnode->prev = plist;
plist->next->prev = newnode;
plist->next = newnode;
}
6.3 插入
- 插入函数默认插入到
pos 结点之前,插入到pos 后的函数大同小异,不做实现 - 相对于单链表来说不用遍历查找
pos 之前的结点,非常方便且高效
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos != NULL);
ListNode* newnode = BuyListNode(x);
newnode->next = pos;
newnode->prev = pos->prev;
pos->prev->next = newnode;
pos->prev = newnode;
}
7. 删除数据
7.1 尾删
- 双向链表尾删时不用找尾,直接删除头结点前的结点就可以
- 删除之前要先对链表判空,若为空可以进行断言处理(这里不做处理)
void ListPopBack(ListNode* plist)
{
assert(plist != NULL);
if (ListEmpty(plist))
{
return;
}
ListNode* node = plist->prev;
node->prev->next = plist;
plist->prev = node->prev;
free(node);
}
7.2 头删
? 跟尾删差不多,只是删除结点为第一个结点
void ListPopFront(ListNode* plist)
{
assert(plist != NULL);
if (ListEmpty(plist))
{
return;
}
ListNode* node = plist->next;
plist->next = node->next;
node->next->prev = plist;
free(node);
}
7.3 删除
? 删除指针指向的结点,调整前后指针指向,然后直接删除即可
void ListErase(ListNode* pos)
{
assert(pos != NULL);
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
}
8. 查找数据
- 查找就是直接遍历,比较值,并返回结点的地址
- 查找有修改功能,调用者可以根据结点的指针修改内容
ListNode* ListFind(ListNode* plist, LTDataType x)
{
assert(plist != NULL);
ListNode* cur = plist->next;
while (cur != plist)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
9. 销毁双链表
- 顺序遍历链表,依次释放结点,最后释放头结点
- 销毁时只销毁内存,不将链表头指针置空,要由调用者完成。(带置空的销毁函数参数要传二级指针)
void ListDetroy(ListNode* plist)
{
assert(plist != NULL);
ListNode* cur = plist->next;
ListNode* next = NULL;
while (cur != plist)
{
next = cur->next;
free(cur);
cur = next;
}
free(cur);
}
四、双链表的相关问题
1. 实现双链表的相关问题
1.1 初始化问题
带头结点的双向循环链表需要初始化,申请头结点空间,并将前后指针指向自己形成循环
1.2 二级指针问题
- 带头结点的链表无需使用二级指针传参,头指针一定指向头结点
- 头指针一定要断言,头结点一定存在
1.3 头结点问题
- 头结点的数据域是不使用的,不能用来存储结点个数,数据域的大小有限,结点个数多时就会发生溢出现象
- 头结点的出现方便插入和删除时不用单独判断第一个结点操作时而改变头指针
五、双链表的特点
1. 双链表的优点
- 任意位置插入删除效率高
- 按需申请释放,减少空间浪费
2. 双链表的缺点
- 不支持随机访问
3. 顺序表与双链表的比较
不同点 | 顺序表 | 链表 |
---|
存储空间上 | 物理上一定连续 | 逻辑上连续,物理上不一定连续 | 随机访问 | 支持
O
(
1
)
O(1)
O(1) | 不支持
O
(
n
)
O(n)
O(n) | 任意位置插入删除 | 需要移动元素
O
(
n
)
O(n)
O(n) | 只用修改指针方向
O
(
1
)
O(1)
O(1) | 插入 | 空间不够时需要扩容 | 没有容量概念 | 应用场景 | 元素高效存储和频繁访问 | 任意位置插入和删除 | 空间利用率 | 高 | 低 |
六、完整代码
代码保存在gitee中:点击完整代码参考
|