带头双向循环链表的增删查改
简介
首先我们来看一下带头双向循环链表的结构示意图,在实际内存中并非是这样的结构,画图是为了我们能更好的理解链表
带头双向循环链表的结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。虽然这个结构虽然复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了 。
结构体的创建和函数的声明
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LTNode;
// 创建一个新结点
LTNode* BuyListNode(LTDataType x);
// 初始化链表
LTNode* ListInit();
// 打印链表
void LTPrint(LTNode* phead);
// 尾插
void LTPushBack(LTNode* phead, LTDataType x);
// 尾删
void LTPopBack(LTNode* phead);
// 头插
void LTPushFront(LTNode* phead, LTDataType x);
// 头删
void LTPopFront(LTNode* phead);
// 查找
LTNode* LTFind(LTNode* phead, LTDataType x);
// 在POS之前插入数据
void LTInsert(LTNode* pos, LTDataType x);
// 删除pos位置的数据
void LTErase(LTNode* pos);
// 链表判空
bool LTEmpty(LTNode* phead);
// 链表大小
size_t LTSize(LTNode* phead);
// 链表销毁
void LTDestroy(LTNode* phead);
函数功能的实现
创建一个新结点
创建新结点在初始化和插入数据时都需要使用,选择使用malloc
开辟空间是为了在程序运行整个过程中链表都不会因为出了函数作用域而销毁。
这里我们使用LTDataType
代替int
是方便后续代码的修改,只需在头文件定义中修改数据类型就可实现不同类型的链表。
LTNode* BuyListNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail");
exit(-1);
}
node->data = x;
node->next = NULL;
node->prev = NULL;
return node;
}
初始化链表
初始化时与单链表稍有不同,初始化链表为空时需要创建一个哨兵位的头结点phead
。
头结点的prev
要指向尾结点,尾结点的next
要指向头结点phead
,这样才会形成循环。
但我们在初始化的时候,链表中是没有数据的,那么phead
头结点的prev
和next
应该如何初始化呢?
因此,在初始化链表时,我们需要将头结点phead
中的prev
和next
都指向自己,这样就会形成循环。
LTNode* ListInit()
{
LTNode* phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
打印链表
打印链表时从头结点的下一个结点phead->next
开始向后遍历并打印,直到遍历到头结点处(phead->next == phead
)时便停止遍历和打印。
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
尾插
尾插数据时,首先要寻找链表的尾结点tail
,不论链表是否为空,我们都能通过phead->prev
找到尾结点,然后将新结点插入尾结点之后即可。
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
// 创建节点
LTNode* newnode = BuyListNode(x);
// 寻找尾节点
LTNode* tail = phead->prev;
// 插入数据
tail->next = newnode;
newnode->prev = tail;
phead->prev = newnode;
newnode->next = phead;
}
尾删
尾删时,需要找到两个结点,尾结点tail
和尾结点的前一个结点tailPrev
,将尾结点删除,再将phead->prev
指向tailPrev
,重新构成循环。
因为每一个结点都是
malloc
开辟的,在删除结点时还需要把它释放掉。删除结点时,需要判断是否存在结点。
void LTPopBack(LTNode* phead)
{
assert(phead);
// 判断是否只有一个头结点
assert(phead->next != phead);
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
phead->prev = tailPrev;
tailPrev->next = phead;
free(tail);
}
头插
创建一个新结点newnode
,把它插入phead->next
处。
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
}
头删
头删和尾删类似,需要判断链表是否为空,把要删的结点phead->next
和要删除结点的下一个结点phead->next->next
找到,将头结点指向删除结点的后一个结点,记得free
掉删除的结点,防止内存泄露。
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* cur = phead->next;
LTNode* next = cur->next;
free(cur);
phead->next = next;
next->prev = phead;
}
查找
遍历链表中的数据,如果匹配就返回当前结点的地址,如果没有找到返回NULL
。
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
else
{
cur = cur->next;
}
}
return NULL;
}
在pos位置之前插入数据
在pos位置之前插入数据,找到pos位置之前的结点,创建新的结点插入pos之前。
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* newnode = BuyListNode(x);
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
我们之前写的头插和尾插函数,可以复用LTInsert()
函数。
头插就相当于在头结点的下一个结点phead->next
前插入一个数据。
LTInsert(phead->next, x);
尾插就相当于在头结点phead
前插入一个数据。
LTInsert(phead,x);
删除pos位置的数据
删除pos位置的数据时,先要找到pos位置前后两个结点pos->prev
和pos->next
,前后两个结点链接后free掉pos位置的结点。
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* next = pos->next;
free(pos);
prev->next = next;
next->prev = prev;
}
同样头删和尾删也可以复用LTErase()
函数。
头删就相当于删除头结点的下一个结点phead->next
。
LTErase(phead->next);
尾删就就相当于删除头结点的前一个结点phead->prev
。
LTErase(phead->prev);
链表判空
如果phead->next == phead
,那就说明链表中没有数据。
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
链表大小
遍历链表,统计结点个数,并返回。
size_t LTSize(LTNode* phead)
{
assert(phead);
size_t size = 0;
LTNode* cur = phead->next;
while (cur != phead)
{
size++;
cur = cur->next;
}
return size;
}
链表销毁
遍历销毁链表,这里要注意提前记录删除当前结点的下一个结点,如果直接释放,会导致无法找到后续结点,造成内存泄漏。
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
演示案例
void TestList1()
{
LTNode* phead = ListInit();
// 尾插
LTPushBack(phead, 1);
LTPushBack(phead, 2);
LTPushBack(phead, 3);
LTPushBack(phead, 4);
LTPushBack(phead, 5);
LTPrint(phead);
// 尾删
LTPopBack(phead);
LTPrint(phead);
LTPopBack(phead);
LTPrint(phead);
LTPopBack(phead);
LTPrint(phead);
LTPopBack(phead);
LTPrint(phead);
LTPopBack(phead);
LTPrint(phead);
// 头插
LTPushFront(phead, 1);
LTPushFront(phead, 2);
LTPushFront(phead, 3);
LTPushFront(phead, 4);
LTPushFront(phead, 5);
LTPrint(phead);
// 头删
LTPopFront(phead);
LTPrint(phead);
LTPopFront(phead);
LTPrint(phead);
}
int main()
{
TestList1();
return 0;
}
运行结果: