1 带头双向循环链表的概念
带头双向循环链表,是众多链表结构中最复杂的一种结构。顾名思义,带头就是指带有头结点;双向表示链表可以从左往右走,也可以从右往左走;循环则是字面意思,及此类链表可以构成一个环。
其逻辑结构如下:
对于带头双向循环链表,每个节点有两个指针,分别指向当前节点的上一个和下一个结点,我们将其定义为prev和next。
头结点的prev指向尾结点,尾结点的next指向头结点,这样做就形成了一个循环,我们可以很快地通过指针找到尾结点,链表的插入删除会更加快捷。
由于链表中始终有一个结点(头结点),因此在对链表插入删除的过程中,无需再像单链表一样分多种情况考虑。
一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。尽管带头双向循环链表看上去,结构比常见的无头单向非循环链表要复杂,但是使用代码实现以后会发现结构会带来很多优势,接口实现和维护反而会变得更简单。
2 链表实现
2.1 结点的结构
对于带头双向循环链表,每个节点都应该有两个指针以及存放数据的部分。这里我们假设节点当中只存储一个数据。
typedef int LTDatatype;
typedef struct ListNode
{
LTDatatype data;
struct ListNode* prev;
struct ListNode* next;
}LTNode;
2.2 新结点的创建
由于每一次添加新节点都需要开辟新的空间,于是我们编写一个创建新结点的函数:
LTNode* BuyLTNode(LTDatatype x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->prev = NULL;
newnode->next = NULL;
return newnode;
}
2.3 链表初始化
由于该链表是带头的,因此最好是将链表初始化一下,将最初的结点置为头结点。
LTNode* LTInit()
{
LTNode* phead = BuyLTNode(-1);
phead->prev = phead;
phead->next = phead;
return phead;
}
这里需要注意,在链表中只有头结点单个结点时,为了符合双向循环的条件,需要将头结点的next和prev都指向自身,此时头结点既是头,也是尾。
一般对于有头结点的链表,头结点最好不要用来存放有效数据。这里的-1并不是有效数据,只是为了方便创建结点,没有实际意义。
2.4 链表尾插
回想一下单链表的尾插,我们需要循环遍历找到原链表的尾结点后才能执行插入操作,而这时带头双向循环链表的优势就体现出来了,由于头结点中存放了尾结点的地址,就可以很快速的找到尾结点。
同时,尾插的代码量是非常小的,没有多余的判断,只需要改变四个指针。
void LTPushBack(LTNode* phead, LTDatatype x)
{
assert(phead);
LTNode* newnode = BuyLTNode(x);
LTNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
phead->prev = newnode;
newnode->next = phead;
}
2.5 链表头插
对于头插也是同理,只需要改变四个指针。、
void LTPushFront(LTNode* phead, LTDatatype x)
{
assert(phead);
LTNode* newnode = BuyLTNode(x);
LTNode* cur = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = cur;
cur->prev = newnode;
}
2.6 判断链表是否为空
当链表中只有头结点一个节点时,即链表当中没有存储有效数据,那么我们认为这种链表是空链表。
当链表为空以后,如果我们进行删除操作,显然是不符合逻辑,会产生bug的。因此,可以设计一个判断链表为空的函数,在后续配合删除函数使用。
我们可以用布尔值来编写这个函数。
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
这里有一个巧妙地设计,当return中的条件成立的时候,链表中只有头结点一个节点,所以是空间表,返回true;反之就返回false。
这样的设计避免了一部分代码量,同时也容易理解。
2.7 链表尾删
我们先通过头结点的prev找到尾结点,然后通过尾结点的prev找到尾结点的上一个结点(即将成为新的尾结点),将该节点直接与头结点建立循环联系,最后再将原来的尾结点free掉即可。
同时,这里可以增加一句断言,与上面编写的判断链表为空的函数配合使用,避免对空链表进行删除操作引起程序bug。
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTNode* tail = phead->prev;
LTNode* first = tail->prev;
first->next = phead;
phead->prev = first;
free(tail);
}
2.8 链表头删
这里的逻辑跟单链表的头删比较类似,先将头结点与待删结点的下一个结点建立联系,再free掉待删节点。
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTNode* first=phead->next;
LTNode* second = first->next;
phead->next = second;
second->prev = phead;
free(first);
}
2.9 链表元素查找
查找操作与单链表也非常类似,通过循环遍历来查找匹配的结点。
由于循环链表的特性,如果整个链表完全遍历一遍后回到头结点,就说明链表中不存在想要查找的结点。
查找到后返回该节点的地址。
LTNode* LTFind(LTNode* phead, LTDatatype x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
2.10 任意位置插入
任意位置的插入函数,一般是要与查找函数配合使用的。这是因为想要在任意位置处插入一个元素,首先需要有一个确切的位置。
一般是通过查找函数先找到想要插入的位置,再使用插入函数。
void LTInsert(LTNode* pos, LTDatatype x)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* newnode = BuyLTNode(x);
newnode->next = pos;
newnode->prev = prev;
prev->next = newnode;
pos->prev = newnode;
}
对于带头双向循环链表的任意位置插入,一般是插入到指定位置pos之前。
2.11 任意位置删除
这里指的删除,是直接删除掉指定位置pos处的结点,具体逻辑也跟头删尾删类似。
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* posPrev = pos->prev;
LTNode* posNext = pos->next;
posPrev->next = posNext;
posNext->prev = posPrev;
free(pos);
}
2.12 对于头尾插入、删除的改良
有了任意位置的插入、删除函数,那么对于头尾结点这样特殊位置的插入、删除,是否也可以使用任意位置插入删除函数来进行操作呢?
答案是可以。
既然可以,那我们就可以将这个函数复用以下,减少头尾插入、删除函数的代码量。
头插:
void LTPushFront(LTNode* phead, LTDatatype x)
{
assert(phead);
LTInsert(phead->next, x);
}
尾插:
void LTPushBack(LTNode* phead, LTDatatype x)
{
assert(phead);
LTInsert(phead, x);
}
尾插是在指定位置pos之前的位置插入,对于phead, 它之前的位置就是原来的尾结点,因此这里将phead作为参数。
头删:
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTErase(phead->next);
}
尾删:
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTErase(phead->prev);
}
2.13 链表的销毁
我认为销毁操作和初始化操作应该是所有链表当中操作最简单,最容易理解的操作,就是将所开辟的空间依次释放掉即可。
void LTDestory(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
3 源码
3.1 List.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <assert.h>
#include <stdbool.h>
typedef int LTDatatype;
typedef struct ListNode
{
LTDatatype data;
struct ListNode* prev;
struct ListNode* next;
}LTNode;
LTNode* LTInit();
void LTPrint(LTNode* phead);
void LTPushBack(LTNode* phead, LTDatatype x);
void LTPushFront(LTNode* phead, LTDatatype x);
void LTPopFront(LTNode* phead);
void LTPopBack(LTNode* phead);
LTNode* LTFind(LTNode* phead);
//在pos之前插入
void LTInsert(LTNode* pos, LTDatatype x);
void LTErase(LTNode* pos);
void LTDestory(LTNode* phead);
3.2 List.c
#include "List.h"
LTNode* BuyLTNode(LTDatatype x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->prev = NULL;
newnode->next = NULL;
return newnode;
}
void LTPrint(LTNode* phead)
{
assert(phead);
printf("guard <==> ");
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d <==> ", cur->data);
cur = cur->next;
}
printf("\n");
}
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
LTNode* LTInit()
{
LTNode* phead = BuyLTNode(-1);
phead->prev = phead;
phead->next = phead;
return phead;
}
void LTPushBack(LTNode* phead, LTDatatype x)
{
assert(phead);
/*LTNode* newnode = BuyLTNode(x);
LTNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
phead->prev = newnode;
newnode->next = phead;*/
LTInsert(phead, x);
}
void LTPushFront(LTNode* phead, LTDatatype x)
{
assert(phead);
/*LTNode* newnode = BuyLTNode(x);
LTNode* cur = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = cur;
cur->prev = newnode;*/
LTInsert(phead->next, x);
}
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
/*LTNode* first=phead->next;
LTNode* second = first->next;
phead->next = second;
second->prev = phead;
free(first);*/
LTErase(phead->next);
}
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
/*LTNode* tail = phead->prev;
LTNode* first = tail->prev;
first->next = phead;
phead->prev = first;
free(tail);*/
LTErase(phead->prev);
}
LTNode* LTFind(LTNode* phead, LTDatatype x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
void LTInsert(LTNode* pos, LTDatatype x)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* newnode = BuyLTNode(x);
newnode->next = pos;
newnode->prev = prev;
prev->next = newnode;
pos->prev = newnode;
}
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* posPrev = pos->prev;
LTNode* posNext = pos->next;
posPrev->next = posNext;
posNext->prev = posPrev;
free(pos);
}
void LTDestory(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}