初阶数据结构
第一章 时间复杂度和空间复杂度
第二章 动态顺序表的实现
第三章 单向链表的讲解与实现
第四章 带头双向链表的讲解与实现
文章目录
- 初阶数据结构
- 前言
- 一、什么是头节点(哨兵位)
- 二、双向链表结点的定义
- 三、接口函数的实现
- 1、初始化
- 2、尾插
- 3、头插
- 4、尾删
- 5、头删
- 6、打印
- 7、查找
- 8、随机插入
- 9、随机删除
- 10、销毁
前言
上一章节中,我们学习了无头指针的单向链表的逻辑和代码实现,但是我们发现这种单向结构在使用的时候是存在很大的弊端的,比如说我们无法通过中间的某个节点直接找到他前一个节点,只能从头开始遍历。这是非常低效地一种方式。为了解决链表的单向性,我们本章节将介绍一个双向链表。
同时,由于上一章节中,我们的链表是不带头节点的,即没有哨兵位。因此,我们在实现接口函数的时候大概率要单独讨论链表为空的情况。那么本章节将介绍一种带头的链表,从而统一各种情况下的处理方式。
综上,我们就引出了我们今天的主题:带头双向链表
一、什么是头节点(哨兵位)
在上述的双向链表中,在第一个节点的前面多出了一个节点,这个节点的数据域是没有用处的。但是头节点的指针域具有很重要的作用,它的指向后方节点的指针域记录了第一个节点的地址。指向前方节点的指针域记录了最后一个节点的地址。
那么这样设置一个头节点有什么作用呢?
最大的作用就是让链表中的节点个数始终不为0,什么意思呢?我们看下方图示:
我们发现,即使链表为空,头节点依旧存在,其妙处将在下面实现接口函数的时候体现出来。
二、双向链表结点的定义
typedef int ElementType;
typedef struct DListNode
{
struct DListNode* next;
struct DlistNode* prev;
ElementType data;
}DLTnode;
三、接口函数的实现
1、初始化
初始化,即开辟一个头节点,然后让这个头节点的前后指针域都指向自己。
DLTnode* DListInit()
{
DLTnode* phead = (DLTnode*)malloc(sizeof(DLTnode));
phead->next = phead;
phead->prev = phead;
return phead;
}
2、尾插
尾插的逻辑如下图所示:
void DListPushBack(DLTnode* phead, ElementType dat)
{
assert(phead != NULL);
DLTnode* newnode = (DLTnode*)malloc(sizeof(DLTnode));
newnode->data = dat;
DLTnode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
我们发现上述代码并没有单独讨论空链表的情况,这就是头节点的好处,之所以不用讨论就是因为节点的个数不可能为0,最少也包括一个头节点。
3、头插
void DListPushFront(DLTnode* phead, ElementType dat)
{
assert(phead != NULL);
DLTnode* newnode = (DLTnode*)malloc(sizeof(DLTnode));
newnode->data = dat;
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
}
4、尾删
void DListPopBack(DLTnode* phead)
{
assert(phead != NULL);
assert(phead->next!=phead);//防止删掉头节点
DLTnode* tail = phead->prev;
DLTnode*tailprev=tail->prev;
tailprev->next = phead;
phead->prev = tailprev;
free(tail);
tail = NULL;
}
5、头删
void DListPopFront(DLTnode* phead)
{
assert(phead!=NULL);
assert(phead->next!=phead);//防止删掉头节点
DLTnode* First = phead->next;
DLTnode* second = phead->next->next;
phead->next = second;
second->prev = phead;
free(First);
First = NULL;
}
6、打印
打印的逻辑非常简单,就是从头遍历一遍即可,但是需要注意的是,这是一个循环链表,如果我们不加限制条件的话,他会一直循环下去。所以,我们这里需要加上判断条件。
void DListPrint(DLTnode* phead)
{
assert(phead != NULL);
DLTnode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
7、查找
逻辑同查找一致。
DLTnode* DListFind(DLTnode* phead, ElementType dat)
{
assert(phead!=NULL);
DLTnode* cur = phead->next;
while (cur != phead)
{
if (cur->data == dat)
{
return cur;
}
else
{
cur = cur->next;
}
}
return NULL;
}
8、随机插入
void DListInsert(DLTnode*pos,ElementType dat)
{
DLTnode* newnode = (DLTnode*)malloc(sizeof(DLTnode));
newnode->data = dat;
DLTnode* posprev = pos->prev;
posprev->next = newnode;
newnode->prev = posprev;
newnode->next = pos;
pos->prev = newnode;
}
9、随机删除
void DListErase(DLTnode*pos)
{
DLTnode*posprev = pos->prev;
DLTnode* posnex = pos->next;
posprev->next = posnex;
posnex->prev = posprev;
free(pos);
pos = NULL;
}
10、销毁
销毁的逻辑和单链表一样,逐个遍历,逐个销毁,但是注意野指针问题!!
void DListDestory(DLTnode** pphead)
{
DLTnode* cur = (*pphead)->next;
while (cur != *pphead)
{
DLTnode* curnex = cur->next;
free(cur);
cur = curnex;
}
free(*pphead);
*pphead = NULL;
cur = NULL;
}