- 带头双向链表在单链表的基础上,每个结点增加了指向上一个结点的指针,这样在任意位置插入删除时,不需要去遍历链表,找上一个结点,增加了效率
- 同时由于它有一个带哨兵位的头节点,不需要为头指针的改变而单独判断
带头双向链表的结构:
结构定义:
typedef int DListDataType;
typedef struct DListNode
{
DListDataType val;
struct DListNode* next;//下一个结点的指针
struct DListNode* prev;//上一个结点的指针
}DListNode;
大致接口:
//打印
void DListPrint(DListNode* phead);
//初始化
DListNode* DListInit();
//销毁
void DListDestroy(DListNode* phead);
//头插
void DListPushFront(DListNode* phead, DListDataType x);
//尾插
void DListPushBack(DListNode* phead, DListDataType x);
//头删
void DListPopFront(DListNode* phead);
//尾删
void DListPopBack(DListNode* phead);
//查找
DListNode* DListFind(DListNode* phead, DListDataType x);
//pos位置前插入
void DListInsert(DListNode* pos, DListDataType x);
//pos位置删除
void DListErase(DListNode* pos);
- 在初始化中,我们需要建立一个不存储有效数据的头节点,该结点的next和prev指针都指向自己,并返回头节点
后面的插入数据都需要建立新结点,那么干脆将建立新结点分装成一个函数,返回新结点的地址
DListNode* CreateNewNode(DListDataType x)
{
DListNode* newNode = (DListNode*)malloc(sizeof(DListNode));
if (newNode == NULL)
{
perroe("CreateNewNode:malloc fail");
exit(-1);
}
newNode->val = x;
newNode->next = NULL;
newNode->prev = NULL;
return newNode;
}
//初始化
DListNode* DListInit()
{
DListNode* phead = CreateNewNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
头插:
- 哨兵位节点的下一个结点才是头结点
//头插
void DListPushFront(DListNode* phead, DListDataType x)
{
assert(phead);
DListNode* newNode = CreateNewNode(x);
DListNode* head = phead->next;
newNode->next = head;
head->prev = newNode;
phead->next = newNode;
newNode->prev = phead;
}
可以看到,该代码跟单链表的头插相比,简洁了很多,并且该代码适用于空结点的情况
打印函数:
//打印
void DListPrint(DListNode* phead)
{
assert(phead);
DListNode* cur = phead->next;
while (cur != phead)
{
printf("%d<=>", cur->val);
cur = cur->next;
}
}
- 注意:此时cur停止的条件不再是空了,而是等于哨兵位结点
尾插:
//尾插
void DListPushBack(DListNode* phead, DListDataType x)
{
assert(phead);
DListNode* newNode = CreateNewNode(x);
DListNode* tail = phead->prev;
tail->next = newNode;
newNode->prev = tail;
newNode->next = phead;
phead->prev = newNode;
}
该代码同样适用于空结点的情况
头删:
- 空结点时,不能进行删除,哨兵位结点的next指向自己时表示空结点
//头删
void DListPopFront(DListNode* phead)
{
assert(phead);
assert(phead->next != phead);
DListNode* cur = phead->next;
phead->next = cur->next;
cur->next->prev = phead;
free(cur);
}
尾删:
- 为空结点时不能删除
//尾删
void DListPopBack(DListNode* phead)
{
assert(phead);
assert(phead->next != phead);
DListNode* tail = phead->prev;
tail->prev->next = phead;
phead->prev = tail->prev;
free(tail);
}
查找:
- 如果找到了返回结点的地址,找不到返回空
- cur结束的条件是等于哨兵位结点
//查找
DListNode* DListFind(DListNode* phead, DListDataType x)
{
assert(phead);
DListNode* cur = phead->next;
while (cur != phead)
{
if (cur->val == x)
return cur;
cur = cur->next;
}
return NULL;
}
在pos前插入:
//pos位置前插入
void DListInsert(DListNode* pos, DListDataType x)
{
assert(pos);
DListNode* newNode = CreateNewNode(x);
pos->prev->next = newNode;
newNode->prev = pos->prev;
newNode->next = pos;
pos->prev = newNode;
}
- 如果pos等于哨兵位结点,相当于尾插
- 如果pos等于哨兵位结点的下一个结点,相当于头插
因此,前面的头插和尾插函数可以用该函数替换
//头插
void DListPushFront(DListNode* phead, DListDataType x)
{
assert(phead);
DListInsert(phead->next, x);
}
//尾插
void DListPushBack(DListNode* phead, DListDataType x)
{
assert(phead);
DListInsert(phead, x);
}
删除pos位置:
//pos位置删除
void DListErase(DListNode* pos)
{
assert(pos);
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
}
- 如果pos为尾指针,相当于尾删
- 如果pos为哨兵位结点的下一个结点,相当于头删
因此,前面的头删和尾删可以用该函数替换
//头删
void DListPopFront(DListNode* phead)
{
assert(phead);
assert(phead->next != phead);
DListErase(phead->next);
}
//尾删
void DListPopBack(DListNode* phead)
{
assert(phead);
assert(phead->next != phead);
DListErase(phead->prev);
}
未来面试官可以让你在10分钟内写出一个双向循环链表,你会想:怎么可能在10分钟内写出一个双向循环链表出来呢?
实际上,我们只要实现pos位置前插入,pos位置删除两个接口,其他的接口就游刃而解了
还有最后的销毁:
- 先释放掉链表结点,最后释放掉哨兵位结点
- 由于哨兵位结点已经释放掉,在销毁函数中应当将指针置空,但因为函数的参数是一级指针,即使置空也不会影响到外面的指针,所以销毁后应当手动置空,避免野指针
//销毁
void DListDestroy(DListNode* phead)
{
assert(phead);
DListNode* cur = phead->next;
while (cur != phead)
{
DListNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
//测试函数
void TestDList()
{
DListNode* dl = DListInit();
DListPushFront(dl, 1);
DListPushFront(dl, 2);
DListPushFront(dl, 3);
DListPrint(dl);
DListPushBack(dl, 6);
DListPushBack(dl, 7);
DListPrint(dl);
DListPopFront(dl);
DListPrint(dl);
DListPopBack(dl);
DListPrint(dl);
DListNode* pos = DListFind(dl, 2);
if (pos != NULL)
printf("找到了\n");
else
printf("找不到\n");
pos = DListFind(dl, 6);
DListInsert(pos, 11);
DListPrint(dl);
pos = DListFind(dl, 2);
DListErase(pos);
DListPrint(dl);
DListDestroy(dl);
dl = NULL;
}
顺序表和链表的对比:
顺序表的优势:
- 支持下标随机访问,便于排序数据
- 内存命中率高(额外的知识)
顺序表的劣势:
- 只适合头插和尾插;在其他位置插入删除都要挪动数据,效率低
- 每次都是2倍扩容,而实际上我们不确定要存多少数据,可能会存在空间的浪费
链表(双向循环)的优势:
- 在任意位置插入删除时间复杂度都是O(1),效率高
- 按需开辟空间,不会造成空间的浪费
链表的劣势:
- 不支持下标随机访问,不方便排序数据
- 容易产生内存碎片
总的来说,顺序表和链表是互补的关系,没有哪一种数据结构能满足我们所有的需要,只有不同的场景应用不同的数据结构!
最后,有需要本篇文章源码的小伙伴可以去我的Gitee自行查看!DList/DList · baiyahua/LeetCode - 码云 - 开源中国 (gitee.com)