之前已经介绍过数据结构链表中的单链表,现在我们一起来学习双链表。
那什么又是双链表呢?
在学习双链表之前我们来看看链表的分类。
链表的结构非常多样,以下情况组合起来就有8种(2x2x2)链表结构:
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构: 单链表 和双向带头循环链表。
注意:这里的“带头”跟前面我们说的“头节点”是两个概念,实际前面的在单链表阶段称呼不严谨,但是为了同学们更好的理解就直接称为单链表的头节点。
带头链表里的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这里“放哨的”。
“哨兵位”存在的意义:遍历循环链表避免死循环。
我们在上面的图中看见,每个节点有三部分内容:第一部分就是保存的数据data
,第二部分就是保存下一个节点地址的nex
t指针,
第三部分就是保存前一个节点地址的prev
指针。
在初始状态时,双向链表为空,这里的空指的是只有一个哨兵位,而哨兵位节点是不能被操作的,即哨兵位不能被改变。
要用C语言先定义一个包含哨兵位的双向循环链表。我们发现此时哨兵位节点的下一个节点指向的还是哨兵位,哨兵位的前一个指针指向的还是哨兵位。
typedef struct ListNode {
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
向之前的单链表一样,用双向循环链表来实现增删改查。下面我们一起来学习一下吧。
同之前的单链表一样我们用三个文件来实现,
List.h
用来实现函数声明,List.c
用来实现相关函数,test.c
用来测试代码。
而在实现双向循环链表初始化时不需要传入参数,调用该方法之后给我们返回一个头结点。
为了更加方面知道哨兵位的位置,在初始化时给它的数据赋值为-1。
我们实现节点有三部分内容:数据 前驱指针 后继指针。
而在上面我们就提到哨兵位节点的下一个节点指向的还是哨兵位,哨兵位的前一个指针指向的还是哨兵位。
LTNode* LTInit()
{
LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
if (phead == NULL) {
perror("malloc error");
return;
}
phead->data = -1;
phead->next = phead->prev = phead;
return phead;
}
尾插就是在节点之后插入新节点,值得注意的是,在双向循环链表中要实现尾插就是在哨兵位的前面插入节点。
因为涉及到新节点的申请,那先讲讲如何申请一个新的节点。
我们用malloc
向内存申请一个大小为sizeof(LTNode)
的空间,用来存放我们所要插入的数据x。而它的node->next
和node->prev
在插入前都置为NULL
。
LTNode* ListBuyNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
node->data = x;
node->next = node->prev = NULL;
return node;
}
在进行尾插时要先处理插入节点node的前驱和后继指针,让node->prev = phead->prev
,和node->next = phead
;再处理phead->prev
(之前的尾结点) 和 phead
,让phead->prev->next = node
,和phead->prev = node
。
下面我们用代码来实现一下:
void LTPushBack(LTNode* phead, LTDataType x) {
assert(phead);
LTNode* node = ListBuyNode(x);
node->prev = phead->prev;
node->next = phead;
phead->prev->next = node;
phead->prev = node;
}
同尾插一样,再插入之前要先申请一个节点用来存放新插入的数据,就不再多赘述。
头插要处理node
的prev
和next
,让node->prev = phead
,和node->next = phead->next
;再处理phead
和 phead->next
,让phead->next->prev = node
,和phead->next = node
。
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* node = ListBuyNode(x);
node->prev = phead;
node->next = phead->next;
phead->next->prev = node;
phead->next = node;
}
我们用del
来表示要删除的节点,要实现尾巴删除,就要先确定删除节点的位置,而我们知道这是在双向循环链表中,哨兵位的前一个节点就是del
,所以就先定义del = phead->prev
。
先处理del
的prev
节点,让它的下一个节点指向哨兵位del->prev->next = phead
,再处理phead
,让它的前一个节点指向del的前一个节点就是phead->prev = del->prev
。
最后再释放del
,将del
置为NULL
。
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* del = phead->prev;
del->prev->next = phead;
phead->prev = del->prev;
free(del);
del = NULL;
}
同尾删一样,也用del
来表示要删除的节点。
也是要先找到del的位置,而它的位置就是哨兵位的下一个节点,也就是phead->nex
t。
然后同样先处理del
的下一个节点,让它的prev
指向哨兵位,就是del->next->prev = phead
;再处理哨兵位的下一个节点,让它等于del的下一个节点,就是phead->next = del->next
。
同上面语言先把del
释放,再置为NULL
。
void LTPopFront(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* del = phead->next;
//phead del->next
del->next->prev = phead;
phead->next = del->next;
free(del);
del = NULL;
}
要在指定位置之后插入数据,我们要先找到这个位置,用pos
来记录这个地址。
而插入的数据的节点我们同样用node
来记录。
首先处理node
的prev
和next
,让node
的下一个节点指向pos
的下一个节点,就是node->next = pos->next
;然后让node
的prev
指向pos
,就是node->prev = pos
。
再处理pos
的next
和 pos->next
的prev
,让pos
的next
指向node
,就是pos->next = node
;在让pos->next
的prev
指向node
,也就是node->next->prev = node
。
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* node = ListBuyNode(x);
node->next = pos->next;
node->prev = pos;
pos->next = node;
node->next->prev = node;
}
同样我们要先找到这个位置,用pos
来记录这个地址。
先处理pos->next
的prev,让pos->next->prev = pos->prev
;
再处理pos->prev
的next
,让pos->prev->next = pos->nex
t;
最后同样是释放pos
,将pos
置为NULL
。
void LTErase(LTNode* pos)
{
assert(pos);
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
定义一个cur
指针用来存除了哨兵位以后的位置,让它初始化为cur = phead->next
;
使用while
循环时,再定义一个指针next
,让它始终为next = cur->next
,这样释放cur
以后,cur
就能继续向后遍历。
而除了循环之后,哨兵位还没有释放,此时我们需要在循环外释放哨兵位,然后将它置为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;
}
#pragma once
#include
#include
#include
#include
//定义双向链表节点的结构
typedef int LTDataType;
typedef struct ListNode {
LTDataType data;//val == data
struct ListNode* next;
struct ListNode* prev;
}LTNode;
void LTPrint(LTNode* phead);
//链表的初始化以及销毁
//void LTInit(LTNode** pphead);//前提:我们要传入一个头结点
LTNode* LTInit();//不需要传入参数,调用该方法之后给我们返回一个头结点
//实际这里更倾向于传递一级指针,因为要保持接口一致性
void LTDestroy(LTNode* phead);
//void LTDestroy(LTNode** pphead);
//在双向链表中不会改变哨兵位,所以这里都可以传一级指针
//插入删除操作
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);
void LTErase(LTNode* pos);
LTNode* LTFind(LTNode* phead, LTDataType x);
#include"List.h"
//前提:我们要传入一个头结点
//void LTInit(LTNode** pphead) {
// *pphead = (LTNode*)malloc(sizeof(LTNode));
// if (*pphead == NULL) {
// perror("malloc error");
// return;
// }
// //节点有三部分内容:数据 前驱指针 后继指针
// (*pphead)->data = -1;//哨兵位
// (*pphead)->next = (*pphead)->prev = *pphead;
//}
//不需要传入参数,调用该方法之后给我们返回一个头结点
LTNode* LTInit() {
LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
if (phead == NULL) {
perror("malloc error");
return;
}
phead->data = -1;
phead->next = phead->prev = phead;
return phead;
}
LTNode* ListBuyNode(LTDataType x) {
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
node->data = x;
node->next = node->prev = NULL;
return node;
}
//插入操作
void LTPushBack(LTNode* phead, LTDataType x) {
assert(phead);
LTNode* node = ListBuyNode(x);
//先处理新节点node的前驱和后继指针
node->prev = phead->prev;
node->next = phead;
//再处理phead->prev(之前的尾结点) 和 phead
phead->prev->next = node;
phead->prev = node;
}
void LTPrint(LTNode* phead) {
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("\n");
}
void LTPushFront(LTNode* phead, LTDataType x) {
assert(phead);
LTNode* node = ListBuyNode(x);
//node节点的 prev next
node->prev = phead;
node->next = phead->next;
//处理phead phead->next
phead->next->prev = node;
phead->next = node;
}
void LTPopBack(LTNode* phead) {
assert(phead);
//链表不能为空链表:链表中只有一个哨兵位节点
assert(phead->next != phead);
LTNode* del = phead->prev;
//先处理del的prev节点
del->prev->next = phead;
//处理phead
phead->prev = del->prev;
free(del);
del = NULL;
}
void LTPopFront(LTNode* phead) {
assert(phead && phead->next != phead);
LTNode* del = phead->next;
//phead del->next
del->next->prev = phead;
phead->next = del->next;
free(del);
del = NULL;
}
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x) {
assert(pos);
LTNode* node = ListBuyNode(x);
//node的prev 和 next
node->next = pos->next;
node->prev = pos;
//pos的next 和 pos->next的prev
pos->next = node;
node->next->prev = node;
}
void LTErase(LTNode* pos) {
assert(pos);
//pos->prev:next pos pos->next:prev
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
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 LTDestroy(LTNode* phead) {
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
//除了循环之后,哨兵位还没有释放
free(phead);
phead = NULL;
}
//void LTDestroy(LTNode** pphead) {
// assert(pphead && *pphead);
// LTNode* cur = (*pphead)->next;
// while ( cur!=*pphead )
// {
// LTNode* next = cur->next;
// free(cur);
// cur = next;
// }
// free(*pphead);
// *pphead = NULL;
//}
#include"List.h"
void ListTest() {
//LTNode* plist = NULL;
//LTInit(&plist);
LTNode* plist = LTInit();
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPrint(plist);// 1 2 3 4
//LTPushFront(plist, 5);
//LTPushFront(plist, 6);
//LTPushFront(plist, 7);
//LTPrint(plist);//7 6 5 1 2 3 4
//LTPopBack(plist);
//LTPopBack(plist);
//LTPopBack(plist);
//LTPopBack(plist);
//LTPopBack(plist);
//LTPrint(plist);
/*LTPopFront(plist);
LTPopFront(plist);
LTPopFront(plist);
LTPopFront(plist);
LTPopFront(plist);
LTPrint(plist);*/
测试指定位置之后插入
//LTNode* find = LTFind(plist, 1);
///*LTInsert(find, 11);*/
//LTErase(find);
//LTPrint(plist);
LTDestroy(&plist);
/*LTDestroy(plist);*/
//传一级指针的时候需要手动将plist置为空
plist = NULL;
LTPrint(plist);
}
int main()
{
ListTest();
return 0;
}