目录
一.何为双向循环链表?
在学习双向带头循环链表之前,我们一定都学习过最基础的单链表。单链表是一种线性的数据结构,由若干个动态内存分配的结点相互链接而形成,其中每一个结点拥有一个指向下一个结点的指针。因此,通过头节点进行顺序访问,我们可以遍历这个单链表获取数据。单链表图示如下:
然而,由于结点只存有下一个结点的指针,使得我们在需要对单链表进行尾部操作(如尾插,尾删)时,需要先遍历单链表找到尾部,其时间复杂度为O(N)。那么有没有一个更好的数据结构能帮我们提高效率呢? 双向带头循环链表由此而来。
那么首先介绍一下何为双向带头循环链表。其中'双向','带头','循环'均为链表的特性。
1.何为'双向'?
与之相对的概念便是'单向','单向'指的是对于每个结点,其内部有一个next指针指向下一个结点。单链表便指的是单向链表。
而双向链表,其内部有prev和next两个指针,分别指向上一个结点和下一个结点,因此支持向上访问结点,也更加灵活。
2.何为'带头'?
一般来说,我们需要对链表进行管理,方便对其进行增删查改各种操作。这里便存在两种管理方式:1.不带头,用头指针进行管理 2.带头,即用不存放数据的一个'空结点'进行管理,此节点又称为'哨兵位结点'。
带头链表具有的优势在单链表中体现为尾部操作(即尾插,尾删)时更加简单,不用区分插入或删除时上一个元素是头指针还是结点。而在双向循环链表中,其更是极大的方便了增删查改的操作,后面我们将进行介绍。
3.何为'循环'?
循环最大的特点便是'头尾相连',即尾结点的下一个结点不指向NULL,而是指向头部结点。形成一个环状结构。此概念较为简单,此处不多赘述。
明白了上面的概念之后,我们就应该了解了双向带头循环链表的结构,图示如下:
二.如何实现双向带头循环链表?
1.基本结构-结点的创建
// 带头+双向+循环链表
typedef int LTDataType;
typedef struct ListNode
{
LTDataType _data;
struct ListNode* _next;
struct ListNode* _prev;
}ListNode;
LTDataType为存放数据类型,_prev指向前一个结点,而_next指向后一个结点,和前面我们认知的结构完全一样。
2.创建哨兵位结点
由于我们需要用哨兵位结点对链表进行管理,因此需要先创建此节点。
ListNode* ListCreate()
{
ListNode* head = (ListNode*)malloc(sizeof(ListNode));
if (head == NULL)
{
printf("create error\n");
exit(-1);
}
head->_prev = head;
head->_next = head;
return head;
}
实现非常简单,动态开辟一个结点,进行初始化后作为函数的返回值返回即可。并命名哨兵位为head-头结点。
3.链表的增删查改
和单链表的实现一样,首先需要实现一个函数创建新结点,其实现与哨兵位结点的创建相似,只是初始化时需要将数据赋值。
ListNode* CreateNewNode(LTDataType x)
{
ListNode* ret = (ListNode*)malloc(sizeof(ListNode));
if (ret == NULL)
{
printf("malloc error\n");
exit(-1);
}
ret->_data = x;
ret->_prev = NULL;
ret->_next = NULL;
return ret;
}
接下来便是增删查改操作的实现,我们需要实现以下功能:
// 双向链表销毁
void ListDestory(ListNode* pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);
和单链表相比,双向带头循环链表插入和删除的操作甚至更为简单,下面给出思路:
假设插入的位置为p2之前,即已知p2地址(特别的,头插为head->next,尾插为head)
1.创建新结点。 2.定义变量存储p1(即为p2->prev)和p2的地址。 3.链接newnode和p1,p2。
所有的插入删除都可以这样操作,此处不多赘述,下面直接给出代码以供参考。
1.头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListNode* newNode = CreateNewNode(x);
//创建成功,开始插入新结点
ListNode* next = pHead->_next;
newNode->_prev = pHead;
pHead->_next = newNode;
newNode->_next = next;
next->_prev = newNode;
}
2.尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListNode* newNode = CreateNewNode(x);
ListNode* tail = pHead->_prev;
newNode->_prev = tail;
tail->_next = newNode;
newNode->_next = pHead;
pHead->_prev = newNode;
}
3.头删
void ListPopFront(ListNode* pHead)
{
assert(pHead);
assert(pHead->_next != pHead);
ListNode* front = pHead->_next;
ListNode* next = front->_next;
free(front);
front = NULL;
pHead->_next = next;
next->_prev = pHead;
ListErase(pHead->_next);
}
4.尾删
void ListPopBack(ListNode* pHead)
{
assert(pHead);
assert(pHead->_next != pHead);
ListNode* tail = pHead->_prev;
ListNode* prev = tail->_prev;
free(tail);
tail = NULL;
pHead->_prev = prev;
prev->_next = pHead;
ListErase(pHead->_prev);
}
在实现指定位置插入和删除之前,为了获取位置,我们先要实现Find函数来查找对应数据的结点位置。实现如下:
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListNode* cur = pHead->_next;
while (cur != pHead)
{
if (cur->_data == x)
return cur;
cur = cur->_next;
}
return NULL;
}
若找到返回对应结点地址,否则返回NULL。此处只需注意遍历的条件:哨兵位结点下一个结点为第一个有效数据结点,从此处开始遍历,直到变量再次指向哨兵位结点时跳出循环。后面链表的打印和销毁遍历时同理。
5.查找
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListNode* cur = pHead->_next;
while (cur != pHead)
{
if (cur->_data == x)
return cur;
cur = cur->_next;
}
return NULL;
}
6.指定位置前插入
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* newNode = CreateNewNode(x);
ListNode* prev = pos->_prev;
newNode->_next = pos;
pos->_prev = newNode;
newNode->_prev = prev;
prev->_next = newNode;
}
7.指定位置删除
void ListErase(ListNode* pos)
{
assert(pos);
ListNode* prev = pos->_prev;
ListNode* next = pos->_next;
free(pos);
pos = NULL;
prev->_next = next;
next->_prev = prev;
}
8.链表打印
void ListPrint(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->_next;
while (cur != pHead)
{
printf("%d ", cur->_data);
cur = cur->_next;
}
printf("\n");
}
9.链表销毁
void ListDestory(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->_next;
while (cur != pHead)
{
ListNode* next = cur->_next;
free(cur);
cur = next;
}
free(pHead);
}
由于哨兵位结点head作为循环结束的标志,因此先不进行销毁,从下一个结点开始遍历结点并销毁,最后再释放head结点。双向带头循环链表操作由此完成。
只要明白结构和大体思路,并对单链表有一定了解,便能轻松的实现双向带头循环链表,操作非常简单,大家也可以动手尝试。