一、带头双向循环链表的含义
带头双向循环链表是一种数据结构,它结合了双向链表和循环链表的特点,链表包含一个哨兵位(或称为头节点),这个节点不存储实际的数据,主要用于简化对链表的插入和删除操作,因为在进行这些操作时,不需要特别处理头节点。与单向链表不同,每个节点包含两个指针,一个指向下一个节点(next),另一个指向前一个节点(prev),这使得数据可以从两个方向进行遍历。与单向链表不同,每个节点包含两个指针,一个指向下一个节点(next),另一个指向前一个节点(prev),这使得数据可以从两个方向进行遍历。
二、带头双向循环链表的构成
typedef int LTDataType;//将变量类型重定义,方便更改数据类型
typedef struct ListNode {
LTDataType val;//数据域
struct ListNode* prev;//指向下一个节点
struct ListNode* next;//指向下一个节点
}ListNode;//将带头双向循环链表重命名方便后面使用
三、带头双向循环链表的接口功能实现
1、带头双向循环链表的初始化
将链表进行初始化,防止出现野指针之类的情况。
ListNode* ListCreate() {
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));//使用malloc创建一个头结点
if (newnode == NULL) {
perror("malloc fail");
exit(-1);
}
newnode->next = newnode;//将头结点的下一个指向本身
newnode->prev = newnode;//将头结点的上一个指向本身
newnode->val = 0;//初始化头结点中的数据初始化为任何值都行,头结点的数据域不存储任何数据,所以该值不会被访问
return newnode;
}
2、带头双向循环链表的打印输出接口。
该接口用于调试观察,向链表中插入的值正确与否,实现了一个从前向后遍历访问,一个从后往前遍历访问。
void ListPrint1(ListNode* pHead) {//--------->从前往后
assert(pHead);
ListNode* cur = pHead->next;//将头结点的下一个赋值给中间变量cur,头结点不被访问
while (cur != pHead) {//使用头结点来作为循环结束条件,当cur到达头结点时就意味着已经遍历一遍了
printf(" %d ", cur->val);//打印输出
cur = cur->next;//切换节点
}
printf("\n");//换行符
}
void ListPrint2(ListNode* pHead) {//---------->从后往前
assert(pHead);
ListNode* cur = pHead->prev;//将头结点的上一个赋值给中间变量cur,头结点不能被访问
while (cur != pHead) {使用头结点来作为循环结束条件,当cur到达头结点时就意味着已经遍历一遍了
printf(" %d ", cur->val);//打印输出
cur = cur->prev;//切换节点
}
printf("\n");//换行符
}
3、创建带头双向循环链表节点
创建链表的节点在头插、尾插等几个接口都会使用到,所以我把他封装成一个公共函数。
ListNode* CreateNode(LTDataType x) {
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));//创建链表节点
if (newnode == NULL) {
perror("malloc fail");//如果链表节点创建失败,对失败原因进行打印
exit(-1);
}
newnode->next =NULL;//将新节点的next和prev暂时置空,
newnode->prev = NULL;
newnode->val = x;//赋值给新节点
return newnode;//返回新节点
}
4、带头双向循环链表尾插
链表尾插时注意先后顺序,防止链接错误。
void ListPushBack(ListNode* pHead, LTDataType x) {
assert(pHead);
ListNode* newnode = CreateNode(x);//创建节点
pHead->prev->next = newnode;//将头结点的prev的next连接到新结点
newnode->prev = pHead->prev;//将新节点的prev连接到头结点的以前的prev
pHead->prev = newnode;//将头结点的prev更新为新节点
newnode->next = pHead;//将新节点的next链接到头结点
//ListInsert(pHead,x);//这时将在任意位置插入接口实现后,可以将上面的步骤之间简化为调用该函数
}
5、带头双向循环链表尾删
注意这里释放tail的时机,否则可能造成释放错误
void ListPopBack(ListNode* pHead) {
assert(pHead);//防止头结点为空
assert(pHead->next != pHead);
ListNode* tail = pHead->prev;//定义中间变量指向尾节点
ListNode* tailprev = tail->prev;//定义变量指向尾节点的上一个
free(tail);//将尾节点释放
tailprev->next = pHead;将尾节点链接到头结点
pHead->prev = tailprev;
//ListErase(pHead->prev);//待实现任意位置删除元素方可使用
//
}
6、带头双向循环链表头插
void ListPushFront(ListNode* pHead, LTDataType x) {
assert(pHead);
ListNode* newnode = CreateNode(x);
pHead->next->prev = newnode;
newnode->next = pHead->next;
pHead->next = newnode;
newnode->prev = pHead;
//ListInsert(pHead->next, x);
}
7、带头双向循环链表头删
void ListPopFront(ListNode* pHead) {
assert(pHead);
ListNode* next = pHead->next;
ListNode* nnext = next->next;
free(next);
nnext->prev = pHead;
pHead->next = nnext;
//ListErase(pHead->next);
}
8、带头双向循环链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x) {
assert(pHead);
assert(pHead->next);
ListNode* cur = pHead;
while (cur != NULL) {
if (cur->val == x)//将链表的值和需要找到的值做比较
return cur;//成功返回节点,错误返回空
cur = cur->next;
}
return NULL;
}
9、带头双向循环链表任意位置插入
void ListInsert(ListNode* pos, LTDataType x) {
assert(pos);
ListNode* newnode = CreateNode(x);
ListNode* prev = pos->prev;
prev->next = newnode;
newnode->prev = prev;
pos->prev = newnode;
newnode->next = pos;
}
9、带头双向循环链表任意位置删除
void ListErase(ListNode* pos) {
assert(pos->next);
assert(pos);
ListNode* prev = pos->prev;
ListNode* next = pos->next;
free(pos);
prev->next = next;
next->prev = prev;
}
四、带头双向循环链表的优缺点
优点包括:
- 任意位置插入删除效率高:由于双向链表的设计,可以在任何位置进行插入或删除操作,且时间复杂度为O(1),这意味着不需要移动大量数据,操作速度快。
- 空间利用率高:可以按需申请释放空间,不会造成空间浪费,有效利用了存储空间。
- 可以双向寻找:由于存在前驱与后继的关系,可以双向寻找数据,这在某些应用场景中可能非常有用
缺点包括:
- 不支持随机访问:与顺序表相比,双向链表不支持通过下标直接访问元素,这限制了某些算法的应用,如排序和二分查找等。
- 需要额外的空间存储指针:除了存储数据外,还需要额外的空间来存储指向前驱和后继的指针,这会增加一定的内存开销。
综上所述,带头双向循环链表在需要频繁进行插入和删除操作,且对空间利用率有较高要求的场景中表现优异。然而,由于其不支持随机访问的特性,对于需要快速访问或修改大量数据的位置的应用,可能不是最佳选择。