目录
双链表定义
初始化
创建节点
尾插
编辑
尾删
头插
头删
打印
查找
pos插入
头插复用
尾插复用
pos删除
头删复用
尾删复用
判空
size
销毁
完整代码
前面我们学习了单链表,但按照带头不带头(哨兵)和循环不循环我们可以有四种单链表,双链表也是如此,我们最常用到的是无头单向非循环链表和带头双向循环链表,今天我们给大家讲解这种双链表的相关知识,让大家对链表有一个更深层次的理解。
双链表定义
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* prev;
struct ListNode* next;
LTDataType data;
}LTNode;
初始化
思考一下,双链表的初始化该怎么定义?
我们不妨对比一下单链表和顺序表的初始化,顺序表是一块连续储存结构,他需要通过结构体使size=0完成初始化(capacity可以分配初始空间),而单链表是一个一个节点通过指针连结起来的,在创建新节点后,我们就直接将它的指针置空,可以说基本不需要写一个初始化函数。
而带头循环双链表不同,它的初始化要先创建出哨兵节点,为了构成循环,它的两个指针必须指向自己。
创建节点
首先我们写一个创建节点函数:
LTNode* BuyListNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));//开空间
if (node == NULL)
{
perror("malloc fail");
exit(-1);
}
LTNode* next = nullptr;
LTNode* prev = nullptr;
node->data = x;
return node;
}
因为改变了指针,我们用返回值接收。
接着让两个结构体两个成员指针指向自身(哨兵节点),完成初始化。
LTNode* LTInit()
{
LTNode* phead = BuyListNode(-1);//数据无意义
phead->next = phead;
phead->prev = phead;
return phead;
}
因为初始化只需一次,我们这里也用一级指针返回接收,当然,传递指针地址改变指针也行。
尾插
尾插的实现很简单,不需要像单链表一样遍历找尾,直接通过头部的prev节点就能进行尾的插入。而且后面的增删改操作都不需要传二级指针(头节点不变)。
void LTPushBack(LTNode* phead, LTDataType x)//尾插
{
assert(phead);//避免人为传错
LTNode* newnode = BuyListNode(x);
LTNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
刚才我们将头节点自己指向自己的目的现在大家应该能体会到了。
不要去看代码,要自己去想这个图,这样你会觉得连结的过程十分丝滑。这里为什么没有将新节点指针置空呢,因为双向节点的每一个指针都指向前/后的对象,所以不存在指向空指针问题。
尾删
尾删类似于尾插,同样妙的一点是,删除操作也仅仅是改变指针指向并释放空间,并不复杂。
只有一个节点:
当删除到只剩一个节点时,tail的前一个节点正好就是头节点,此时删除后就又恢复到了初始状态。
void LTPopBack(LTNode* phead)//尾删
{
assert(phead);
assert(phead->next != phead);//防止删掉自己
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
phead->prev = tailPrev;
tailPrev->next = phead;
free(tail);
}
头插
头插需要注意要保存下一个节点的地址或者先将newnode连结下一个节点再用phead连结newnode,避免找不到后面的节点。
void LTPushFront(LTNode* phead, LTDataType x)//头插
{
assert(phead);
ListNode* newnode = BuyListNode(x);
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
//顺序无关
/*LTNode* first = phead->next;
phead->next = newnode;
newnode->next = first;
newnode->prev = phead;
first->prev = newnode;*/
}
头删
注意判断是否可能删除自身的情况。
void LTPopFront(LTNode* phead)//头删
{
assert(phead);
assert(phead->next != phead);//空
LTNode* first = phead->next;
LTNode* second = first->next;
free(first);
second->prev = phead;
phead->next = second;
}
打印
我们先来实现一下打印函数并测试一下我们之前的代码。打印可以倒着打印或者正向打印,这里我们实现正向打印。注意头部是不能打印的。
void LTPrint(LTNode* phead)//打印
{
assert(phead);
LTNode* cur = phead->next;//正向
while (cur != phead)//结束条件
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
尾插尾删:
头插头删:
查找
查找是从头节点的下一个开始。找到数据后可以通过返回的指针对数据进行修改。
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 nullptr;//找不到返回空
}
测试:
pos插入
与单链表不同的是,这里不需要传二级头指针就能实现插入,且不用循环找它的前一个节点(Prev指针)。这也体现了这种链表的优势,
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* newnode = BuyListNode(x);
// prev newnode pos
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
头插复用
void LTPushFront(LTNode* phead, LTDataType x)//头插
{
assert(phead);
LTInsert(phead->next, x);
}
尾插复用
尾插复用一般人认为是在最后一个节点复用,实际上我们传递的是头节点。因为我们的插入位置是pos的前一个节点,如果传递最后一个节点就插入到它前面去了,而传递phead,通过它的prev节点就能找到最后一个节点并插入,请注意这一点。
void LTPushBack(LTNode* phead, LTDataType x)//尾插
{
assert(phead);
LTInsert(phead,x);
}
pos删除
删除只需找到pos位置的前后节点然后就可以删除啦。在复用尾删头删的时候也是直接传要删除的节点位置就行了。
void LTErase(LTNode* pos)//pos删除
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* next = pos->next;
free(pos);
prev->next = next;
next->prev = prev;
}
头删复用
void LTPopFront(LTNode* phead)//头删
{
assert(phead);
assert(phead->next != phead);//空
LTErase(phead->next);
}
尾删复用
void LTPopBack(LTNode* phead)//尾删
{
assert(phead);
assert(phead->next != phead);
LTErase(phead->prev);
}
测试:
判空
bool LTEmpty(LTNode* phead)//判空
{
return phead->next == phead;
}
size
这个接口一般不常用,遍历一遍就能得到长度。大家想想可不可以用哨兵位记录它的长度呢?
size_t LTSize(LTNode* phead)
{
assert(phead);
size_t size = 0;
LTNode* cur = phead->next;
while (cur != phead)
{
++size;
cur = cur->next;
}
return size;
}
销毁
和单链表销毁的流程一样,注意最后头节点也要释放掉。
和单链表一样,带头双向链表也要传递二级指针才能使指针正确置空(顺序表是直接将成员指针置空)。为了保持一级指针一致性,我们也可以在上一层栈帧最后进行置空操作。
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
测试: 文章来源:https://www.toymoban.com/news/detail-520218.html
文章来源地址https://www.toymoban.com/news/detail-520218.html
完整代码
//List.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>//判空
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* prev;
struct ListNode* next;
LTDataType data;
}LTNode;
LTNode* BuyListNode(LTDataType x);//创建节点
LTNode* LTInit();//初始化
void LTPushBack(LTNode* phead, LTDataType x);//尾插
void LTPopBack(LTNode* phead);//尾删
void LTPushFront(LTNode* phead, LTDataType x);//头插
void LTPrint(LTNode* phead);//打印
void LTPopFront(LTNode* phead);//头删
LTNode* LTFind(LTNode* phead, LTDataType x);//查找
void LTInsert(LTNode* pos, LTDataType x);//pos插入
void LTErase(LTNode* pos);//pos删除
bool LTEmpty(LTNode* phead);//判空
size_t LTSize(LTNode* phead);
void LTDestroy(LTNode* phead);//销毁
//List.cpp
#include"List.h"
LTNode* BuyListNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));//开空间
if (node == NULL)
{
perror("malloc fail");
exit(-1);
}
LTNode* next = nullptr;
LTNode* prev = nullptr;
node->data = x;
return node;
}
LTNode* LTInit()
{
LTNode* phead = BuyListNode(-1);//数据无意义
phead->next = phead;
phead->prev = phead;
return phead;
}
void LTPushBack(LTNode* phead, LTDataType x)//尾插
{
assert(phead);//可以不断言
LTNode* newnode = BuyListNode(x);
LTNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
//LTInsert(phead, x);
}
void LTPopBack(LTNode* phead)//尾删
{
assert(phead);
assert(phead->next != phead);//防止删掉自己
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
phead->prev = tailPrev;
tailPrev->next = phead;
free(tail);
LTErase(phead->prev);
}
void LTPushFront(LTNode* phead, LTDataType x)//头插
{
assert(phead);
ListNode* newnode = BuyListNode(x);
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
//顺序无关
/*LTNode* first = phead->next;
phead->next = newnode;
newnode->next = first;
newnode->prev = phead;
first->prev = newnode;*/
//LTInsert(phead->next, x);
}
void LTPopFront(LTNode* phead)//头删
{
assert(phead);
assert(phead->next != phead);//空
LTNode* first = phead->next;
LTNode* second = first->next;
free(first);
second->prev = phead;
phead->next = second;
//LTErase(phead->next);
}
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 nullptr;//找不到返回空
}
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* newnode = BuyListNode(x);
// prev newnode pos
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
void LTErase(LTNode* pos)//pos删除
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* next = pos->next;
free(pos);
prev->next = next;
next->prev = prev;
}
void LTPrint(LTNode* phead)//打印
{
assert(phead);
LTNode* cur = phead->next;//正向
while (cur != phead)//结束条件
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
bool LTEmpty(LTNode* phead)//判空
{
return phead->next == phead;
}
size_t LTSize(LTNode* phead)
{
assert(phead);
size_t size = 0;
LTNode* cur = phead->next;
while (cur != phead)
{
++size;
cur = cur->next;
}
return size;
}
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
到了这里,关于[数据结构初阶]双链表的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!