提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。今天我们就先来学习一下链表中的双向链表。
一、链表分类
有头(哨兵位):在链表的最前端有一个节点,这个节点的数据没有意义,这个节点充当链表的头部进行维护。
循环:链表的第一个节点和最后一个节点是否相连。
双向,单向:从一个节点如果可以找到前一个节点和后一个节点就是双向,如果只能找到后一个节点,就是单向。
这三个特征进行组合就可以组合出8种链表。而今天的双链表就是有头循环双向的链表。博主上一篇的单链表就是无头不循环单向链表。
二、双向链表是什么?
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。
其结构如图
如下是一个节点的定义,也是下文代码实现中的节点定义
typedef int LTDataType;
typedef struct ListNode
{
LTDataType* data;//储存数据
struct ListNode* prev;//指向上一个节点
struct ListNode* next;//指向下一个节点
}LTNode;
三、功能函数实现
1.申请一个节点
代码如下(示例):
LTNode* LTBuyNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc");
exit(1);
}
node->data = x;
node->next = node->prev = node;
return node;
}
在一个节点中需要两个数据,一个是数值,一个是下一节点的地址。所以我们申请新节点时就传入该节点中的数值(参数),之后使用malloc申请空间并判断是否成功,将data赋值,next指针和prev指针都指向自己(因为这是一个循环结构),最后返回节点地址。
2.初始化
代码如下(示例):
LTNode* LTInit()
{
LTNode* phead = LTBuyNode(-1);
return phead;
}
初始化其实就是设置哨兵位,我们暂时赋-1,这里写成多少都可以。最后返回哨兵位地址,以后的增删改查都与他有关。
3.尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->next=phead;
newnode->prev = phead->prev;
phead->prev->next = newnode;
phead->prev = newnode;
}
尾插就是将新节点插在最后,但因为链表循环,所以尾插也就是插在头节点前面,首先创建新节点,再将新节点的next指针指向头节点,其prev指针指向原本头节点前面的节点。完成后将原本头节点前一个节点的节点的next指向新节点,将头节点的prev指向新节点。
4.头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->next = phead->next;
newnode->prev = phead;
phead->next->prev = newnode;
phead->next = newnode;
}
头插的位置如图是head和d1节点中间的位置,其插入逻辑和尾插一样就不再赘述,主要理解头插的位置。
5.尾删
void LTPopBack(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* del = phead->prev;
del->prev->next = phead;
phead->prev = del->prev;
free(del);
del = NULL;
}
尾删实际上就是删除头节点的上一个节点,这里我们定义一个del指针记录删除节点的地址方便使用。首先先将删除节点的前一个节点的next指针指向头节点,再将头节点的prev指针指向新尾节点,这样就摘除了原来的尾节点,最后通过del释放尾节点。
6.头删
void LTPopFront(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* del = phead->next;
del->next->prev = phead;
phead->next = del->next;
free(del);
del = NULL;
}
头删的位置如图是d1节点的位置,其插入逻辑和尾删一样就不再赘述,主要理解头删的位置。
7.在指定位置后插入
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
newnode->prev = pos;
newnode->next = pos->next;
pos->next->prev = newnode;
pos->next = newnode;
}
在指定位置处插入,其逻辑如图所示,因为这一操作需要调整d1,d2,newnode三个节点,所以我们先调整新节点的指针指向,因为对新指针的更改不影响原链表。接着将pos下一个节点prev指向新节点,pos前一个节点next指向新节点。
8.删除指定位置数据
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* del = pos;
del->next->prev = del->prev;
del->prev->next = del->next;
free(del);
del = NULL;
}
删除指定位置的逻辑和插入指定数据与尾删相似,可以类比理解。
9.查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
查找是比较容易的函数,只要遍历整个链表并进行比较即可,但注意循环停止的条件,当pcur遍历一遍链表回到头结点时,循环结束。
10.销毁
void LTDesTroy(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(phead);
phead = NULL;
}
销毁和整体逻辑就是一边遍历,一边一个一个的释放节点,还是注意循环停止条件和避免出现空指针。
四、整体代码
1.头文件
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int LTDataType;
typedef struct ListNode
{
LTDataType* data;
struct ListNode* prev;
struct ListNode* next;
}LTNode;
LTNode* LTInit();
void LTDesTroy(LTNode* phead);
//插入数据之前,链表必须初始化到只有一个头结点的情况
//不改变哨兵位的地址,因此传一级即可
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);
//删除pos节点
void LTErase(LTNode* pos);
LTNode* LTFind(LTNode* phead, LTDataType x);
总结
以上就是作者对单链表的一些理解和介绍,希望看到这篇文章的朋友们可以积极评价,还请一键三连。