前言:我们已经学习并知道了单链表的实现,链表的实现方式不只是单链表一种,今天我们就来介绍一种新的结构,就是双链表结构,本质上是将节点间进行双向链接,从而使一些操作更加容易实现。
目录
1.双向链表的简介
2.双向链表的实现
带头节点的哨兵位的创建和初始化
为什么要使用哨兵位?
节点的头、尾插和头、尾删
插入和删除函数
完整程序代码及测试代码
List.h
List.cpp
Test.cpp
3.金句频道
1.双向链表的简介
双向链表(Doubly Linked List)是一种常见的线性数据结构,它相比于单向链表多了一个指针,可以支持双向遍历。
每个节点在双向链表中都有两个指针,一个指向前一个节点,一个指向后一个节点,因此可以从任意一个节点开始,依次遍历前一个节点和后一个节点。双向链表可以对数据进行插入和删除操作,这些操作相比较单向链表来说更加方便和高效。
双向链表的特点包括:
1. 每个节点都有两个指针,一个指向前一个节点,一个指向后一个节点。
2. 可以从任意一个节点开始,依次遍历前一个节点和后一个节点。
3. 可以对数据进行插入和删除操作,保证了操作的高效性和方便性。
4. 相对于单向链表来说,占用更多的存储空间,因为需要多一个指针来指向前一个节点。
需要注意的是,当插入或删除一个节点时,需要同时修改其前后两个节点的指针,才能保证链表结构的完整性。
2.双向链表的实现
带头节点的哨兵位的创建和初始化
为什么要使用哨兵位?
链表中哨兵位(Sentinel)是一种特殊的节点,位于链表头部或尾部,不存储任何实际的数据。引入哨兵位的主要目的是为了简化链表的操作,提高代码的可读性和可维护性。
使用哨兵位的优点主要包括:
1. 简化代码逻辑:引入哨兵位后,链表的头尾节点都指向哨兵位而不是NULL,这样在任何情况下,我们都不需要对头尾节点进行特判,也不需要用二级指针进行传址了(链表中引入二级指针的目的,其实就是为了在头结点为空的情况下,节点还能正常创建并使用,说白了就是为了“对NULL指针进行解引用操作”)。这样能够简化代码逻辑,减少代码复杂度,提高代码可读性和可维护性。
2. 避免空指针异常:使用哨兵位可以避免在链表为空或链表操作时出现空指针异常。如果没有哨兵位,我们需要在头尾节点为空时进行特判,否则可能会导致程序崩溃。
LTNode* LTInit()
{
Listnode head = (Listnode)malloc(sizeof(LTNode));
assert(head);//断言,防止开辟不成功
head->data = -1;
//初始让头结点的前驱和后继都指向自己,这样可以便与判断链表是否为空并且与尾结点指向哨兵位的操作上统一
head->next = head->prev = head;
return head;
}
节点的头、尾插和头、尾删
这些操作基本与单链表的操作大同小异,只是多了一个前驱结点的操作,这里需要注意以下几点:
1.注意对可能为NULL的指针进行断言避免出现野指针和堆错误问题;
2.对不再使用的空间进行及时的释放,避免内存泄漏。
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = new LTNode(x);
LTNode* first = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = first;
first->prev = newnode;
//LTInsert(phead->next, x);
}
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
Listnode newnode = new LTNode(x);
//这里最好将两个及以上解引用的操作分开
LTNode *first = phead->prev;
first->next = newnode;
newnode->next = phead;
newnode->prev = first;
phead->prev = newnode;
//或者可以直接调用插入函数
//LTInsert(phead, x);
}
//头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTNode* p = phead->next;
LTNode* ppnext = p->next;
free(p);
phead->next=ppnext;
ppnext->prev=phead;
//LTErase(phead->next);
}
//尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
free(tail);
tailPrev->next = phead;
phead->prev = tailPrev;
//LTErase(phead->prev);
}
细心地朋友可能已经注意到了,这些不同的函数内部都有相同的LTInsert或LTErase函数,这不是偶然,我们将不同方式插入或删除的函数进行对比,就会发现,它们的代码具有一定的逻辑重复性,存在逻辑上互通的部分,我们可以将这个部分归纳为一个函数,像头插和尾插,我们可以写一个在任意节点前或后插入的函数,再将不同的节点需要的参数分别传入,就能实现不同的功能,尾删和头删也是同理,这样就大大简化了代码,减少了使用指针出错的可能性。
插入和删除函数
// 在pos之前插入
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);//断言,判断pos是否合法
Listnode newnode = new LTNode(x);
Listnode pre = pos->prev;
pre->next = newnode;
newnode->next = pos;
newnode->prev = pre;
pos->prev = newnode;
}
// 删除pos位置的值
void LTErase(LTNode* pos)
{
assert(pos);
Listnode front = pos->prev;
Listnode behind = pos->next;
front->next = behind;
behind->prev = front;
free(pos);
pos = NULL;
}
完整程序代码及测试代码
List.h
#pragma once
#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;
//C++构造函数//可不写
ListNode(LTDataType d = 0, ListNode* pr = NULL, ListNode* ne = NULL)
{
data = d, prev = pr, next = ne;
}
}LTNode,*Listnode;
//创建并初始化头结点
LTNode* LTInit();
//打印带头结点的单链表
void LTPrint(LTNode* phead);
//判断链表是否为空
bool LTEmpty(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);
//查找值为x的节点
LTNode* LTFind(LTNode* phead, LTDataType x);
// 在pos之前插入
void LTInsert(LTNode* pos, LTDataType x);
// 删除pos位置的值
void LTErase(LTNode* pos);
//销毁链表
void LTDestroy(LTNode* phead);
List.cpp
#include <bits/stdc++.h>
#include "List.h"
using namespace std;
LTNode* LTInit()
{
Listnode head = (Listnode)malloc(sizeof(LTNode));
assert(head);//断言,防止开辟不成功
head->data = -1;
//初始让头结点的前驱和后继都指向自己
head->next = head->prev = head;
return head;
}
void LTPrint(LTNode* phead)
{
assert(phead);
printf("guard");
Listnode p = phead->next;
while (p!= phead)
{
printf("->%d", p->data);
p = p->next;
}
printf("\n");
}
// 在pos之前插入
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);//断言,判断pos是否合法
Listnode newnode = new LTNode(x);
Listnode pre = pos->prev;
pre->next = newnode;
newnode->next = pos;
newnode->prev = pre;
pos->prev = newnode;
}
bool LTEmpty(LTNode* phead)
{
return phead->next == phead;
}
void LTPushBack(LTNode* phead, LTDataType x)
{
Listnode newnode = new LTNode(x);
//这里最好将两个及以上解引用的操作分开
LTNode *first = phead->prev;
first->next = newnode;
newnode->next = phead;
newnode->prev = first;
phead->prev = newnode;
//或者可以直接调用插入函数
//LTInsert(phead, x);
}
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = new LTNode(x);
LTNode* first = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = first;
first->prev = newnode;
//LTInsert(phead->next, x);
}
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
free(tail);
tailPrev->next = phead;
phead->prev = tailPrev;
//LTErase(phead->prev);
}
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTNode* p = phead->next;
LTNode* ppnext = p->next;
free(p);
phead->next=ppnext;
ppnext->prev=phead;
//LTErase(phead->next);
}
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
Listnode p = phead->next;
while (p!= phead)
{
if (p->data == x)
return p;
p = p->next;
}
return NULL;
}
// 删除pos位置的值
void LTErase(LTNode* pos)
{
assert(pos);
Listnode front = pos->prev;
Listnode behind = pos->next;
front->next = behind;
behind->prev = front;
free(pos);
pos = NULL;
}
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = (phead)->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
phead = NULL;//此处传的是一级指针,在外部函数内修改没有作用,应该传二级指针或者在主函数内进行销毁,这里我们选择在主函数内单独进行置空
}
Test.cpp
#include <bits/stdc++.h>
#include "List.h"
using namespace std;
void TestList1()
{
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPrint(plist);
LTPopBack(plist);
LTPrint(plist);
LTPopBack(plist);
LTPrint(plist);
LTPopBack(plist);
LTPrint(plist);
LTPopBack(plist);
LTPrint(plist);
//LTPopBack(plist);
//LTPrint(plist);
LTDestroy(plist);
plist = NULL;
}
void TestList2()
{
LTNode* plist = LTInit();
LTPushFront(plist, 1);
LTPushFront(plist, 2);
LTPushFront(plist, 3);
LTPushFront(plist, 4);
LTPrint(plist);
LTPopFront(plist);
LTPrint(plist);
LTPopFront(plist);
LTPrint(plist);
LTPopFront(plist);
LTPrint(plist);
LTPopFront(plist);
LTPrint(plist);
/*LTPopFront(plist);
LTPrint(plist);*/
LTDestroy(plist);
plist = NULL;
}
void TestList3()
{
LTNode* plist = LTInit();
LTPushFront(plist, 1);
LTPushFront(plist, 2);
LTPushFront(plist, 3);
LTPushFront(plist, 4);
LTPrint(plist);
LTNode* pos = LTFind(plist, 3);
if (pos)
{
LTInsert(pos, 30);
}
LTPrint(plist);
LTDestroy(plist);
plist = NULL;
}
int main()
{
TestList1();
TestList2();
TestList3();
return 0;
}
3.金句频道
最近看到这样一句话:“当我真正开始爱自己,我睡的越来越早,也越来越喜欢锻炼。我不在纠结和焦虑,变得自信满满,去追求有意义的人和事,并为之燃烧自己的热情。我发现,人生才刚刚开始.”