目录
一、前言
上一篇文章我们详解了单向链表,现在我们进行对双向链表的学习
点击进入单项链表的学习:单项链表
二、双向带头循环链表
1.结构
带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
与单项链表不同的是,双向循环链表的每个结点都增加了一个Prev指针,指向了上一个结点,实现了链表的双向访问。
带头的意思是增加了一个头部head结点,此结点不存放数据仅仅表示头部。
2.代码实现
跟单项链表一样,我们同样需要实现增、删、查、改。所以我们要构建以下函数接口:
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
}ListNode;
// 创建返回链表的头结点.
ListNode* ListBuyNode(LTDataType x);
//链表初始化
ListNode* ListInit();
// 双向链表销毁
void ListDestory(ListNode* pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);
下面我带大家一一实现以上接口:
创建返回链表的头结点:ListBuyNode
第一步:在内存中malloc一片空间
第二步:将申请的空间的date赋值,并将next与prev置为空指针
这样我们就得到了一个单独的链表结点
ListNode* ListBuyNode(LTDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
if (newnode == NULL)
{
perror("malloc error");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
链表初始化:ListInit
第一步:创建一个头结点
第二步:将这个头节点以循环的结构链接起来
ListNode* ListInit()
{
ListNode* phead = ListBuyNode(0);
phead->next = phead;
phead->prev = phead;
return phead;
}
双向链表打印:ListPrint
第一步:找到头节点的下一个结点
第二步:依次向后遍历,直到cur != pHead结束
void ListPrint(ListNode* pHead)
{
assert(pHead);
printf("pHead<=>");
ListNode* cur = pHead->next;
while (cur != pHead)
{
printf("%d<=>", cur->data);
cur = cur->next;
}
printf("pHead\n");
}
双向链表尾插:ListPushBack
第一步:找尾tail = pHead->prev,因为是双向循环结构,找尾只需要找通过头结点的上一个即可
第二步:将tail与newhead进行链接,newhead与pHead进行链接即可
void ListPushBack(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListNode* tail = pHead->prev;
ListNode* newhead = ListBuyNode(x);
newhead->next = pHead;
pHead->prev = newhead;
tail->next = newhead;
newhead->prev = tail;
}
双向链表尾删:ListPopBack
第一步:找出倒数第二个结点tail
第二步:释放掉倒数第一个结点
第三步:链接tail与pHead
void ListPopBack(ListNode* pHead)
{
assert(pHead);
assert(pHead->next != pHead);
ListNode* tail = pHead->prev->prev;
free(tail->next);
tail->next = pHead;
pHead->prev = tail;
}
三、链表与顺序表的区别
不同点 | 顺序表 | 链表 |
---|---|---|
存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
随机访问 | 支持O(1) | 不支持:O(N) |
任意位置插入或者删除元素 | 可能需要搬移元素,效率低O(N) | 只需修改指针指向 |
插入 | 动态顺序表,空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
缓存利用率 | 高 | 低 |
总结
双向链表剩下的接口与上文非常相似,本文不再赘述,有兴趣的请移步小编的个人博客:https://github.com/wuming12138/C_CODE