目录
1.双向链表概念
带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都 是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带 来很多优势,实现反而简单了。
2.带头双循环链表结构
我们都知道,单链表每个内存块只存储下一个节点地址,想要找到前一个数据很麻烦,要另记录前一个数据地址,像是尾插更是要遍历一遍链表才能,这样某些场景时间复杂度和空间复杂都不小。
而双向带头循环链表,它包含一个不存储数据只存储头节点地址的哨兵位,还有每个节点会像单链表一样存储下一个节点的地址,并且还会存储上一个节点的地址,能够通过上一个地址而找到上一个节点,这种结构对于数据的增添或删除的实现是非常简单的。
3.双向带头循环链表实现
双向带头循环链表只看介绍和图理解起来可能比较复杂,但实现却很简单:
//头文件
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int LTDatatype;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDatatype data;
}ListNode;
ListNode* BuyListNode(LTDatatype x);
//初始化
ListNode* Init();
//判断是否为空
bool LTEmpty(ListNode* phead);
//尾插
void PushBack(ListNode* phead, LTDatatype x);
//头插
void PushFront(ListNode* phead, LTDatatype x);
//尾删
void PopBack(ListNode* phead);
//头删
void PopFront(ListNode* phead);
//查找数据
ListNode* LTFine(ListNode* phead, LTDatatype x);
//pos前插入数据
void LTInsert(ListNode* pos, LTDatatype x);
//删除pos位置数据
void LTErase(ListNode* pos);
//释放内存
void Destroy(ListNode* phead);
//打印
void LTPrint(ListNode* phead);
哨兵位的创建为了在初始化时不使用二级指针可以在程序内单独创建:
ListNode* plist = Init();
初始化以及判空:
ListNode* Init()
{
ListNode* phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
bool LTEmpty(ListNode* phead)
{
assert(phead);
return phead->next == phead;
}
判空的使用是在删除数据时判断节点是否只剩下哨兵位,防止误删。
创建内存节点:
ListNode* BuyListNode(LTDatatype x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
if (newnode == NULL)
{
perror("malloc fail");
return;
}
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
头尾数据插入:
void PushBack(ListNode* phead,LTDatatype x)
{
assert(phead);
ListNode* newnode = BuyListNode(x);
ListNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
phead->prev = newnode;
newnode->next = phead;
}
void PushFront(ListNode*phead,LTDatatype x)
{
assert(phead);
ListNode* first = phead->next;
ListNode* newnode = BuyListNode(x);
newnode->next = first;
first->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
}
这里双向带头循环的优势就体现出来了,只需要改变四个节点地址的指向就能实现。
头尾数据删除:
void PopBack(ListNode* phead)
{
assert(phead);
//assert(phead->next != phead);
assert(!LTEmpty(phead));
ListNode* tail = phead->prev;
ListNode* prev = tail->prev;
prev->next = phead;
phead->prev = prev;
free(tail);
tail = NULL;
}
void PopFront(ListNode* phead)
{
assert(phead);
//assert(phead->next != phead);
assert(!LTEmpty(phead));
ListNode* first = phead->next;
ListNode* next = first->next;
phead->next = next;
next->prev = phead;
free(first);
first = NULL;
}
因为能轻易找到头尾数据,头尾数据的删除实现也很容易。
数据查找:
ListNode* LTFine(ListNode* phead, LTDatatype x)
{
assert(phead);
ListNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
数据查找返回的是数据节点地址,所以可以直接对找到的数据进行修改。
指定位置前数据插入:
void LTInsert(ListNode* pos, LTDatatype x)
{
ListNode* newnode = BuyListNode(x);
ListNode* prev = pos->prev;
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
拥有前一个节点地址,可以直接插入数据,不需要分情况以及判空,就可以很容易实现。
指定位置数据删除:
void LTErase(ListNode* pos)
{
ListNode* posNext = pos->next;
ListNode* posPrev = pos->prev;
posNext->prev = posPrev;
posPrev->next = posNext;
free(pos);
}
数据打印:
//遍历打印即可
void LTPrint(ListNode* phead)
{
assert(phead);
printf("guard<==>");
ListNode* cur = phead->next;
while (cur != phead)
{
printf("%d<==>", cur->data);
cur = cur->next;
}
printf("\n");
}
释放链表内存:
void Destroy(ListNode* phead)
{
assert(phead);
ListNode* cur = phead->next;
while (cur != phead)
{
ListNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
释放链表后要在程序单独将链表哨兵位制空。
4.链表测试
使用代码运行测试链表各项功能:
void Testlist1()
{
ListNode* plist = Init();
//增添
PushBack(plist, 1);
PushBack(plist, 2);
PushFront(plist, 3);
PushFront(plist, 4);
LTPrint(plist);
//删除
PopBack(plist);
LTPrint(plist);
PopFront(plist);
LTPrint(plist);
//查找
ListNode*pos = LTFind(plist, 3);
if (pos != NULL)
pos->data = 30;
LTPrint(plist);
//插入
LTInsert(pos, 300);
LTPrint(plist);
//删除
pos = LTFind(plist, 1);
if(pos != NULL)
LTErase(pos);
LTPrint(plist);
//释放
Destroy(plist);
plist = NULL;
}
int main()
{
Testlist1();
return 0;
}
运行结果:
结尾
双向带头循环链表使用起来是很简单的(重要的事情说三遍),在另一方面,比如要求用15分钟甚至10分钟大体实现一个链表功能,就可以使用双向带头循环链表来实现。
这时候只需要写出insert和erase这两个函数就可以,其他头尾插入删除都可以通过复用这两个函数,传不同地址来实现。