提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、链表分类
- 二、双向链表是什么?
- 三、功能函数实现
- 1.申请一个节点
- 2.初始化
- 3.尾插
- 4.头插
- 5.尾删
- 6.头删
- 7.在指定位置后插入
- 8.删除指定位置数据
- 9.查找
- 10.销毁
- 四、整体代码
- 1.头文件
- 总结
前言
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。今天我们就先来学习一下链表中的双向链表。
一、链表分类
有头(哨兵位):在链表的最前端有一个节点,这个节点的数据没有意义,这个节点充当链表的头部进行维护。
循环:链表的第一个节点和最后一个节点是否相连。
双向,单向:从一个节点如果可以找到前一个节点和后一个节点就是双向,如果只能找到后一个节点,就是单向。
这三个特征进行组合就可以组合出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);
总结
以上就是作者对单链表的一些理解和介绍,希望看到这篇文章的朋友们可以积极评价,还请一键三连。