目录
概念与结构
双向带头链表,分开来就是双向、带头、链表
之前了解了单链表,他只能由上一个节点找到下一个节点,不能由这个节点找到上一个,这个就是单向。
双向就是它的每一个节点都有两个指针,分别为前指针与后指针,分别指向上一个和下一个节点。头节点的前指针指向尾节点,尾节点的后指针指向头节点(哨兵位)。
带头就是它默认有一个头,也就是哨兵位,他是不存放有效数据的。
具体的结构如图:
head: 哨兵位
dn:有效节点
OK,话不多说,直接开始写代码。
链表实现
对小编个人来说,感觉比单链表还是要简单一些的。
因为一个一个详细介绍篇幅会太长,除了第一个函数以外,其他的就通过画图的方式来帮助理解代码,因为原理是相似的,所以就不再做过多文字介绍了。
链表头文件
c
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int LTDataType;
typedef struct ListNode
{
LTDataType val;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
LTNode* LTNodeInit();//链表初始化
bool LTEmpty(LTNode* phead);//检查是否只剩哨兵位
void LTNodePushBack(LTNode* phead, LTDataType x);//尾插
void LTNodePrint(LTNode* phead);//打印
void LTPushFront(LTNode* phead, LTDataType x);//头插
void LTPopBack(LTNode* phead);//尾删
void LTNodePopFront(LTNode* phead);//头删
LTNode* LTFind(LTNode* phead, LTDataType x);
//在pos位置之后插入节点
void LTInsert(LTNode* pos, LTDataType x);
//删除指定位置节点
void LTErase(LTNode* pos);
//销毁链表(包含哨兵位)
void LTDesTroy(LTNode** pphead);
void LTDesTroy2(LTNode* phead);//传一级,需要手动将plist置为NULL
链表初始化
初始化就是给我们的结构体赋一个初值,也就是让链表现有一个哨兵位。
这里我们要借助一个函数实现新链表的创建以及赋值
这里哨兵位的存放的是无效地址,我就用-1来代替这个无效数据。
c
LTNode* BuyNode(LTDataType x)//创建新节点
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
assert(newnode);
newnode->val = x;
newnode->next = newnode->prev = newnode;
return newnode;
}
LTNode* LTNodeInit()//为我们的链表创建哨兵位
{
LTNode* phead = BuyNode(-1);
return phead;
}
链表尾插
思路:
对链表尾插需要的数据有新节点、哨兵位的节点地址、原末尾节点的地址。
通过画图来遍历思考
如图,做标记的地方是我们代码要实现修改的地方,首先我们先修改新节点的两个指针,这样不会扰乱原链表。
将新节点的prev指向d3,将新节点的next指向head,形成环。
接下来将d3的next指向新节点,将head的prev指向新节点。
代码:
c
void LTNodePushBack(LTNode* phead,LTDataType x)
{
LTNode* newnode = BuyNode(x);//创建新节点
//新节点指针修改
newnode->next = phead;
newnode->prev = phead->prev;
//修改原末尾节点的next指向
phead->prev->next = newnode;
//修改哨兵位的prev指向
phead->prev = newnode;
}
链表头插
思路:
c
void LTPushFront(LTNode* phead, LTDataType x)
{
LTNode* newnode = BuyNode(x);//创建新链表
//修改新节点
newnode->next = phead->next;
newnode->prev = phead;
//原头节点修改
phead->next->prev = newnode;
//哨兵位修改
phead->next = newnode;
}
链表尾删
思路:
这里主要是修改哨兵位的前指针以及新尾节点的后指针,同时释放原尾节点。
注意:当链表只剩下哨兵位时,是不能再删除的。
代码:
c
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next != phead;//判断下一个节点是不是自己,是自己就返回flase,不是就返回ture
}
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(LTEmpty(phead));//判断是否只剩下哨兵位
//暂存原末尾节点
LTNode* tmp = phead->prev;
phead->prev = phead->prev->prev;
phead->prev->next = phead;
free(tmp);
tmp = NULL;
}
链表头删
思路:
代码:
c
void LTNodePopFront(LTNode* phead)
{
assert(phead);
assert(LTEmpty(phead));//检查链表是否只剩下哨兵位
LTNode* tmp = phead->next;//暂存头节点地址
phead->next = phead->next->next;//让哨兵位指向第二个节点
phead->next->prev = phead;//让第二节点前指针指向哨兵位
free(tmp);//释放头节点
tmp = NULL;
}
寻找链表某个元素
思路:
从头节点也就是哨兵位的下一位开始循环,因为链表是循环的,当遍历到哨兵位时,就代表链表已经便利了一遍了。
代码:
c
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->val == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
在某个位置之后插入节点
以在第二个节点之后插入为例
思路:
听过上面那个代码找到要插入数据的位置。
比如现在链表里面数据有1,2,3,4;
我要在2后面插入数据,就通过寻找元素2获取位置。
这样获取位置后就简单了,我们将该位置假设为哨兵位,是不是就跟头插的思路一样了。
代码:
c
void LTInsert(LTNode* pos, LTDataType x)
{
LTNode* newnode = BuyNode(x);//创建新节点
newnode->next = pos->next;//新节点指针修改
newnode->prev = pos;
pos->next->prev = newnode;//修改指定位置下一个位置的前指针,也就是图例d3
pos->next = newnode;//修改指定位置的后指针
}
删除指定位置节点
还是以删除d2为例
思路:
这个就很简单了,我们通过寻找元素获取地址进行传参,因为这是双向链表,我们可以通过它找到他前后的节点进行修改,修改完后我们直接释放这个节点的空间就行。
代码:
c
void LTErase(LTNode* pos)
{
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
销毁链表
从头节点遍历链表依次释放空间,然后释放哨兵位的空间,将指向哨兵位的指针置为NULL。
传参二级指针
c
void LTDesTroy(LTNode** pphead)
{
assert(pphead&&*pphead);
LTNode* pcur = (*pphead)->next;
while (pcur != *pphead)
{
LTNode* tmp = pcur;
pcur = pcur->next;
free(tmp);
}
free(pcur);//这时pcur存放的就是哨兵位的地址
*pphead = NULL;//这里*pphead就是主程序里存放哨兵位地址的指针
pcur = NULL;
}
一级指针传参
因为一级指针传参只能通过这个指针找到所有的节点释放空间,但是无法对这个一级指针的实参进行修改,所以我们就需要在主程序里面对plist进行置空操作。
c
void LTDesTroy2(LTNode* phead)//传一级,需要手动将plist置为NULL
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* tmp = pcur;
pcur = pcur->next;
free(tmp);
}
free(pcur);
pcur = NULL;
}
打印链表有效数据
思路:
从头节点的位置进行遍历,依次打印有效数据,当指针回到哨兵位就结束循环。
代码:
c
void LTNodePrint(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
printf("plist->");
while (pcur!= phead)
{
printf("%d->", pcur->val);
pcur = pcur->next;
}
printf("plist->...\n");
}
代码测试
c
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include"list.h"
void test1()
{
LTNode* plist=LTNodeInit();
LTNodePrint(plist);
//LTNodePushBack(plist, 1);
LTPushFront(plist, 1);
LTNodePrint(plist);
//LTNodePushBack(plist, 2);
LTPushFront(plist, 2);
LTNodePrint(plist);
//LTNodePushBack(plist, 3);
LTPushFront(plist, 3);
LTNodePrint(plist);
//LTNodePushBack(plist, 4);
LTPushFront(plist, 4);
LTNodePrint(plist);
printf("\n");
LTNodePopFront(plist);
LTPopBack(plist);
LTNodePrint(plist);
LTNodePopFront(plist);
LTPopBack(plist);
LTNodePrint(plist);
LTPopBack(plist);
LTNodePopFront(plist);
LTNodePrint(plist);
LTNodePushBack(plist, 1);
LTNodePushBack(plist, 2);
LTNodePushBack(plist, 3);
LTNodePushBack(plist, 4);
LTNode* ret=LTFind(plist, 3);
if (ret)
{
printf("YES\n");
}
else
{
printf("NO\n");
}
LTInsert(ret, 6);
LTNodePrint(plist);
LTDesTroy2(plist);
plist = NULL;
}
int main()
{
test1();
return 0;
}
结果如图,代码运行正常,plist是创建的哨兵位。
除了这些之外还有其他的一些可实现功能,这里就不再做过多介绍,基本原理都是差不多的。
谢谢各位观看,编写不易,赏赐一个三连吧。