在了解了(无头不循环)单链表的基础操作之后,我们将进一步了解带头循环双向链表。
目录
首先,我们对链表的八种结构做一个简单的了解:
链表在结构上具有三种属性:是否有哨兵位(不存储有效数据)、是否循环、双向/单向。
将这三种属性进行组合,就可以得到2^3(也就是8)种链表结构。
之前,我们了解的是最简单的一种结构,这一次,我们将了解最复杂的一种结构。
1.1 创建链表节点
typedef int DLTDataType;
typedef struct DListNode {
struct DListNode* next;
//对于双向结构需要定义指针prev,指向前一个节点
struct DListNode* prev;
DLTDataType data;
}DLTNode;
DLTNode* BuyListNode(DLTDataType x) {
DLTNode* node = (DLTNode*)malloc(sizeof(DLTNode));
if (node == NULL) {
perror("malloc fail");
return NULL;
}
node->next = NULL;
node->prev = NULL;
node->data = x;
return node;
}
1.2 对链表进行初始化
根据之前对于(无头不循环)单链表的了解,我们知道该种结构的链表并没有进行初始化,但是对于带头双向循环链表需要进行初始化,这是为什么呢?
如下图所示:单链表的初始状态只需要一个节点存在即可,但是带头双向循环链表却需要两个指向自己的指针维护链表结构的完整性。
DLTNode* DLTInit() {
DLTNode* phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
1.3 进行打印
void DLTPrint(DLTNode* phead) {
assert(phead);
printf("<-head->");
DLTNode* cur = phead->next;
while (cur != phead) {
printf("%d<->", cur->data);
cur = cur->next;
}
}
1.4 插入函数
进行尾插
void DLTPushBack(DLTNode* phead, DLTDataType x) {
assert(phead);
DLTNode* newnode = BuyListNode(x);
//找尾:循环的特点——尾是头节点的前一个节点
DLTNode* tail = phead->prev;
//目前的节点:phead tail newnode
tail->next = newnode;
newnode->prev=tail;
newnode->next = phead;
phead->prev = newnode;
}
进行头插
void DLTPushFront(DLTNode* phead, DLTDataType x) {
assert(phead);
DLTNode* newnode = BuyListNode(x);
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
}
在pos位置前插入
void DLTInsert(DLTNode* pos, DLTDataType x) {
assert(pos);
//找到pos位置的前一个
DLTNode* Prev = pos->prev;
DLTNode* newnode = BuyListNode(x);
//开始链接
Prev->next = newnode;
newnode->next = Prev;
newnode->next = pos;
pos->prev = newnode;
}
1.5 删除函数
进行尾删
首先,需要判断链表是否为空,如果为空,则无法进行删除。
bool DLTEmpty(DLTNode* phead) {
assert(phead);
return phead->next == phead;
}
尾删
void DLTPopBack(DLTNode* phead) {
assert(phead);
assert(DLTEmpty(phead));
//找尾
DLTNode* tail = phead->prev;
//找尾的前一个结点
DLTNode* tailPrev = tail->prev;
tailPrev->next = phead;
phead->prev = tailPrev;
free(tail);
tail = NULL;
}
进行头删
此处需要注意:头删删除的是哨兵位(phead)指向的下一个节点,而非哨兵位,否则链表的结构将被破坏。
void DLTPopFront(DLTNode* phead) {
assert(phead);
assert(!DLTEmpty(phead));
DLTNode* Next = phead->next;
DLTNode* pNext = Next->next;
phead->next = pNext;
pNext->prev = phead;
free(Next);
Next = NULL;
}
删除pos节点
void DLTErase(DLTNode* pos) {
assert(pos);
DLTNode* Prev = pos->prev;
DLTNode* Next = pos->next;
//开始链接
Prev->next = Next;
Next->prev = Prev;
free(pos);
pos = NULL;
}
1.6 代码测试
头文件(DList.h)
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef int DLTDataType;
typedef struct DListNode {
struct DListNode* next;
struct DListNode* prev;
DLTDataType data;
}DLTNode;
//初始化
DLTNode* DLTInit();
//创建新节点
DLTNode* BuyListNode(DLTDataType x);
//打印
void DLTPrint(DLTNode* phead);
//尾插
void DLTPushBack(DLTNode* phead, DLTDataType x);
//头插
void DLTPushFront(DLTNode* phead, DLTDataType x);
//pos位置插入
void DLTInsert(DLTNode* pos, DLTDataType x);
//判断链表是否为空
bool DLTEmpty(DLTNode* phead);
//尾删
void DLTPopBack(DLTNode* phead);
//头删
void DLTPopFront(DLTNode* phead);
//pos位置删除
void DLTErase(DLTNode* pos);
源文件1(Dlist.c)
#include"DList.h"
DLTNode* BuyListNode(DLTDataType x) {
DLTNode* node = (DLTNode*)malloc(sizeof(DLTNode));
if (node == NULL) {
perror("malloc fail");
return NULL;
}
node->next = NULL;
node->prev = NULL;
node->data = x;
return node;
}
DLTNode* DLTInit() {
DLTNode* phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
void DLTPrint(DLTNode* phead) {
assert(phead);
printf("<-head->");
DLTNode* cur = phead->next;
while (cur != phead) {
printf("%d<->", cur->data);
cur = cur->next;
}
printf("\n");
}
void DLTPushBack(DLTNode* phead, DLTDataType x) {
assert(phead);
DLTNode* newnode = BuyListNode(x);
//找尾:循环的特点——尾是头节点的前一个节点
DLTNode* tail = phead->prev;
//目前的节点:phead tail newnode
tail->next = newnode;
newnode->prev=tail;
newnode->next = phead;
phead->prev = newnode;
}
bool DLTEmpty(DLTNode* phead) {
assert(phead);
return phead->next == phead;
}
//尾删
void DLTPopBack(DLTNode* phead) {
assert(phead);
assert(!DLTEmpty(phead));
//找尾
DLTNode* tail = phead->prev;
//找尾的前一个结点
DLTNode* tailPrev = tail->prev;
tailPrev->next = phead;
phead->prev = tailPrev;
free(tail);
tail = NULL;
}
//删除
void DLTErase(DLTNode* pos) {
assert(pos);
DLTNode* Prev = pos->prev;
DLTNode* Next = pos->next;
//开始链接
Prev->next = Next;
Next->prev = Prev;
free(pos);
pos = NULL;
}
//头删
void DLTPopFront(DLTNode* phead) {
assert(phead);
assert(!DLTEmpty(phead));
DLTNode* Next = phead->next;
DLTNode* pNext = Next->next;
phead->next = pNext;
pNext->prev = phead;
free(Next);
Next = NULL;
}
void DLTPushFront(DLTNode* phead, DLTDataType x) {
assert(phead);
DLTNode* newnode = BuyListNode(x);
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
}
void DLTInsert(DLTNode* pos, DLTDataType x) {
assert(pos);
//找到pos位置的前一个
DLTNode* Prev = pos->prev;
DLTNode* newnode = BuyListNode(x);
//开始链接
Prev->next = newnode;
newnode->next = Prev;
newnode->next = pos;
pos->prev = newnode;
}
源文件2(Test.c)
#include"DList.h"
int main() {
DLTNode* plist = DLTInit();
//可以使用任何在Dlist.h中定义的函数
DLTPushBack(plist, 1);
DLTPushBack(plist, 2);
DLTPushBack(plist, 6);
DLTPushBack(plist, 9);
DLTPrint(plist);
DLTErase(plist->next);//实际是头删
//等同于DLTPopFront(plist);
DLTPrint(plist);
return 0;
}
小结:
尽管带头双向循环链表结构相对于单向链表更加复杂,但是它的操作却更加简便,因为该链表结构完整,毫无死角,可以非常轻松的找到对应位置的节点并进行操作。
本篇博客是博主学习带头双向循环链表结构后的感悟和笔记,如有疑问或不足之处,请在评论区留言,期待大家通过交流更加深入地理解链表,谢谢!