一.双向链表的分类
1不带头双向链表
2.带头双向链表
二.带头双向循环链表VS无头单向非循环链表
1.带头双向链表
1.1优点
- 简化边界条件处理:由于存在哑节点(哨兵节点),它本身不存储有效数据,但在链表中扮演了重要角色,特别是在插入和删除操作时,可以极大地简化对头部和尾部边界条件的处理。这使得代码更加简洁和统一。
- 提高代码可读性:通过哑节点,可以使得插入和删除操作更加直观,提高了代码的可读性和可维护性。
- 减少空链表判断:在带头双向链表中,由于哑节点的存在,当链表为空时,哑节点的
prev
和next
都指向自己,这样可以减少空链表的判断逻辑。
1.2缺点
- 轻微的空间开销:相比不带头双向链表,带头双向链表需要额外维护一个哑节点,这在一定程度上增加了空间开销。然而,这种开销通常是可以接受的,因为哑节点所占用的空间相对较小。
- 实现复杂度:虽然哑节点简化了插入和删除操作,但在实现时需要注意哑节点的正确处理,包括在初始化、插入和删除操作中对哑节点的维护。这可能会增加一定的实现复杂度。
2.无头单向非循环链表
2.1优点
- 节省空间:相比带头双向链表,不带头双向链表不需要维护哑节点,因此可以节省一定的空间开销。这对于空间敏感的应用场景来说是一个优势。
- 实现简单:不带头双向链表的实现相对简单,不需要处理哑节点的相关逻辑。这使得初学者更容易理解和掌握双向链表的基本概念和操作。
2.2缺点
- 边界条件处理复杂:在不带头双向链表中,对头部和尾部的插入和删除操作需要特殊处理,因为头部和尾部没有哑节点来简化边界条件的处理。这可能会增加代码的复杂度和出错的可能性。
- 空链表判断繁琐:在不带头双向链表中,当链表为空时,需要特别注意头部指针和尾部指针的指向问题。这可能会使得空链表的判断逻辑变得繁琐和复杂。
三.双向链表的代码实现
在双向链表的代码块中都有相关的注释。
首先先看一下双向链表的插入及删除过程
3.1有关双向链表的头文件
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int LTDataType;
typedef struct ListNode
{
LTDataType* prev;
LTDataType* next;
LTDataType* data;
}LTNode;
//链表初始化
void LTInit(LTNode** phead);
//链表的销毁
void LTDestroy(LTNode* phead);
//链表在某个位置前插入
void LTInsert(LTNode* pos, LTDataType x);
//void LTEarse(LTNode* pos);
//链表的删除
void LTEarse(LTNode* phead,LTDataType x);
//链表的查找
LTNode* LTFind(LTNode* phead, LTDataType x);
//链表的打印
void LTPrintf(LTNode* phead);
//链表的尾插
void LTPushBack(LTNode* pos, LTDataType x);
//链表的尾删
void LTPopback(LTNode* phead);
//链表的头插
void LTPushFront(LTNode* phead, LTDataType x);
//链表的头删
void LTPopFront(LTNode* phead);
3.2有关双向链表代码的.c文件
#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
//通过插入的x创建一个newnodde
LTNode* BuyListNode(LTDataType x)
{
LTNode* phead = (LTNode*)malloc(sizeof(LTNode));//为x扩容空间
if (phead == NULL)//判断是否扩容成功
{
perror("malloc");
return NULL;
}
phead->prev = NULL;
phead->next = NULL;
phead->data = x;
return phead;
}
void LTInit(LTNode** pphead)
{
*pphead = BuyListNode(-1);//头指针,哨兵位结点
(*pphead)->prev = *pphead;//哨兵位结点的prev指向自己
(*pphead)->next = *pphead;//next也指向自己
}
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* tmp = cur->next;//保存要销毁的下一个结点,若不保存当该结点free释放之后,cur->next将属于野指针
free(cur);
cur = tmp;
}
}
void LTInsert(LTNode* pos,LTDataType x)
{
assert(pos);
LTNode* newnode = BuyListNode(x);//为插入的x开辟空间
LTNode* pre = pos->prev;//保存要删除的上一个结点
pos->prev = newnode;
newnode->next = pos;
pre->next = newnode;
newnode->prev = pre;
}
void LTEarse(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)//哨兵位不能被删除,所以从哨兵位的下一个结点开始寻找
{
if (cur->data == x)//找到该结点
{
LTNode* pre = cur->prev;
LTNode* tail = cur->next;
pre->next = tail;
tail->prev = pre;
free(cur);
return;
}
cur = cur->next;
}
}
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 LTPrintf(LTNode* phead)
{
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("\n");
}
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
LTNode* tail = phead->prev;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
tail->next = newnode;
}
void LTPopback(LTNode* phead)
{
assert(phead);
if (phead->next == phead)
return;
LTNode* tail = phead->prev;
LTNode* ptail = tail->prev;
ptail->next = phead;
phead->next = ptail;
free(tail);
}
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
LTNode* first = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = first;
first->prev = newnode;
//另一种不需要保存phead->next的解法
/*newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
newnode->prev = phead;*/
}
void LTPopFront(LTNode* phead)
{
assert(phead);
if (phead->next == phead)
return;
LTNode* tail = phead->next;
LTNode* Ntail = tail->next;
phead->next = Ntail;
Ntail->prev = phead;
free(tail);
}
结尾
如果有什么建议和疑问,或是有什么错误,希望大家可以在评论区提一下。
希望大家以后也能和我一起进步!!
如果这篇文章对你有用的话,希望能给我一个小小的赞!