文章目录
双向链表-----------(c语言)
前言:
也是有一段时间没有更新博客了,如今来填填坑,今天要讲的内容是双向循环链表。顾名思义,就是有两个方向的链表,并且双向循环,具体是怎么一回事呢。请看下方
有两个指针,一个是prev,一个是next。下面介绍它的食用方法
双向循环链表的创建
1.创建
参照上图,在结构体ListNode中创建两个指针。
typedef struct ListNode {
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
}LNode;
2.初始化
此处用malloc一个新节点,此处形参的改变不会影响实参,因此后面插入链表时会造成链表指针错误,可以用二级指针或者返回值的方式解决,这里我选择用返回值的方式,返回新节点的地址。
//创建节点
LNode* BuyListNode(LTDataType x) {
LNode* node = (LNode*)malloc(sizeof(LNode));
node->data = x;
node->next = NULL;
node->prev = NULL;
return node;
}
为了简化代码,并且方便对双向链表的操作,这里我选择使用带哨兵位的结构,当然如果你已经对链表掌握到如火纯情的地步了,可以使用不带哨兵位的结构(要避免空指针还有边界问题)
// 创建返回链表的头结点.
LNode* ListCreate() {
//创建哨兵节点
//哨兵节点不存储有效数据
LNode* phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
//返回phead的地址
return phead;
}
ok,到这里双向链表的带头哨兵结构也是创建起来了。
双向循环链表的增删查改
1.插入方式:
1.头插
1.观察双向链表的结构,插入时要修改四个指针,因此我们要格外小心,不要牛头不对马嘴
2.由于双向链表优秀的结构,修改这个结构体时,可以不需要二级指针来操作。借助哨兵节点完成即可
3.四个指针的足以给一些对指针操作不熟悉的人来说是噩梦,因此这里建议跟博主一样,创建三个指针来表达清楚,这样子无论先是那一步,都清晰明了。
4.插入前要检查头节点是否为空
// 双向链表头插
void ListPushFront(LNode* pHead, LTDataType x) {
assert(pHead);
LNode* newnode = BuyListNode(x);
LNode* first = pHead->next;
//phead newnode first
pHead->next = newnode;
newnode->prev = pHead;
//如果没有节点,下面两条语句相当于第一个插入的节点的地址不变
newnode->next = first;
first->prev = newnode;
//代码复用,在哨兵节点的下一个节点之前插入,即是头插
ListInsert(pHead->next, x);
}
图解
只有一个哨兵位节点时插入非常easy,最后把他们整理一下就变成下面这样
当继续插入节点时,也用上面一样反复循环,这里不做过多探讨,给个高清大图,让你们自己试试看(bushi:是博主懒得画)
2.尾插
1.按照链表的经典插入,尾插首先要找尾巴,那么如何找到尾巴呢,观察双向链表的结构可以得出,prev指针指向的地方就是尾巴
2.同样思考只有一个哨兵结点跟有多个尾插是如何操作的
3.尾插前也要检查头结点是否为空
// 双向链表尾插
void ListPushBack(LNode* pHead, LTDataType x) {
//断言
assert(pHead);
LNode* tali = pHead->prev;
//新结点
LNode* newnode = BuyListNode(x);
//修改四个指针,newnode先链接
newnode->prev = tali;//此处不是pHead,因为是尾插,要从尾结点插入
tali->next = newnode;
newnode->next = pHead;
pHead->prev = newnode;
//pHead的perv就是尾结点,也是在尾结点后面插入
//ListInsert(pHead, x);
}
图解
依旧跟头插一样,继续插入节点时也是类似的效果,自己逝逝看吧(也是懒得画)
2.删除方式
1.同样,删除前也要检查是否为空(都是空了还删什么)
2.与单链表的删除很类似,头删也要考虑到链表丢失的问题,因此这里推荐用一个指针保存删除节点的下一个节点位置来操作(注:对链表结构了解到炉火纯青的,可以按照自己的想法写)
1.头删
// 双向链表头删
void ListPopFront(LNode* pHead) {
assert(pHead);
LNode* cur = pHead->next;//从哨兵节点后面开始删除
LNode* cur_next = pHead->next->next;//保存要删除节点的下一个节点的位置,防止链表丢失
pHead->next = cur_next;
cur_next->prev = pHead;
free(cur);
}
图解
1.这里可以把cur的next跟prev置空,但是没必要
2.当只剩下一个有效节点时,这个代码也能完美解决,不信你自己逝逝看(因为是双向循环链表,也是懒得画)
2.尾删
1.同样,尾删之前进行检查,这里还要在判断哨兵节点后面是否还有节点
2.跟尾插一样,设置一个尾指针,这里建议在设置一个尾结点前一个节点的指针,让操作变得更简单,否则操作不当也会导致链表的指针发生不可描述的错误
3.前面已经提到过,尾结点就是哨兵节点的前指针指向的位置
// 双向链表尾删
void ListPopBack(LNode* pHead) {
assert(pHead);
assert(pHead->next != pHead);//判断哨兵结点后面是否有结点
LNode* tail = pHead->prev;
LNode* tailprev = tail->prev;
free(tail);
tailprev->next = pHead;
pHead->prev = tailprev;
}
图解
同样,这段代码对于双向循环链表的结构来说,还是很好解决了只剩下一个有效节点如何删除
3.其他操作
1.打印双向链表
1.与单链表不同,双向循环链表遍历时的停止条件略有差异,单链表一般是以NULL作为结束条件,而双向循环链表是以pHead作为停止条件,至于为什么,动动你的脑袋瓜想一想
2.同样也要检查链表是否为空
3.从有效节点开始打印
// 双向链表打印
void ListPrint(LNode* pHead) {
//断言
assert(pHead);
LNode* cur = pHead->next;
printf("pHead=>");
while (cur != pHead)
{
printf("%d<=>", cur->data);
cur = cur->next;
}
printf("\n");
}
写完这个后,我们来打印一下头插、尾插、头删、尾删的结果
当然并不是写到这里才开始打印插入跟删除的结果,每写一个函数就要测试
运行结果
头插:
尾插:
头删:
void test4() {
//头插
LNode* head = ListCreate();
ListPushFront(head, 1);
ListPushFront(head, 2);
ListPushFront(head, 3);
ListPushFront(head, 4);
ListPushFront(head, 5);
//头删
ListPopFront(head);
ListPrint(head);
ListPopFront(head);
ListPrint(head);
ListPopFront(head);
ListPrint(head);
ListPopFront(head);
ListPrint(head);
ListPopFront(head);
ListPrint(head);
}
尾删:
void test5() {
//尾插
LNode* head = ListCreate();
ListPushBack(head, 1);
ListPushBack(head, 2);
ListPushBack(head, 3);
ListPushBack(head, 4);
ListPushBack(head, 5);
ListPrint(head);
//尾删
ListPopBack(head);
ListPrint(head);
ListPopBack(head);
ListPrint(head);
ListPopBack(head);
ListPrint(head);
ListPopBack(head);
ListPrint(head);
ListPopBack(head);
ListPrint(head);
}
2.查找
顾名思义,就是查单链表的数据,并且返回这个链表所在的地址
// 双向链表查找
LNode* ListFind(LNode* pHead, LTDataType x) {
assert(pHead);
LNode* pos = pHead->next;
while (pos != pHead) {
if (pos->data == x) {
return pos;
}
else {
pos = pos->next;
}
}
}
运行结果
void test7() {
//头插
LNode* head = ListCreate();
ListPushFront(head, 1);
ListPushFront(head, 2);
ListPushFront(head, 3);
ListPushFront(head, 4);
ListPushFront(head, 5);
//在pos位置插入
ListPrint(head);
printf("%p\n", ListFind(head, 3));//输出3的地址
}
这里查找一下3所在节点的地址
用调试窗口找到3的地址,与输出的一致,说明代码写对了
3.在pos位置前插入
1.从头插跟尾插可以看出来,在pos位置插入简直易如反掌
// 双向链表在pos的前面进行插入
void ListInsert(LNode* pos, LTDataType x) {
LNode* posprev = pos->prev;
LNode* newnode = BuyListNode(x);
newnode->next = pos;
pos->prev = newnode;
posprev->next = newnode;
newnode->prev = posprev;
}
图解
运行结果
例如在4的前面插入20,首先要用ListFind找到4的地址,在上面已经介绍了Listfind的写法,这里直接用就行了
void test6() {
//头插
LNode* head = ListCreate();
ListPushFront(head, 1);
ListPushFront(head, 2);
ListPushFront(head, 3);
ListPushFront(head, 4);
ListPushFront(head, 5);
ListPrint(head);
//在pos位置插入
ListInsert(ListFind(head, 4), 20);
ListPrint(head);
}
可以看见成功在4的前面插入20
4.删除pos位置的节点
1.删除前也要检查pos位置是否为空
2.由于双向链表指针过多,我们可以在创建多个指针来方便我们识别并且操作,避免不必要的错误
3.同头删跟尾删类似
// 双向链表删除pos位置的节点
void ListErase(LNode* pos) {
assert(pos);
LNode* posprev = pos->prev;
LNode* posnext = pos->next;
posprev->next = pos->next;
posnext->prev = posprev;
free(pos);
}
运行结果
删除4这个节点
void test8() {
//头插
LNode* head = ListCreate();
ListPushFront(head, 1);
ListPushFront(head, 2);
ListPushFront(head, 3);
ListPushFront(head, 4);
ListPushFront(head, 5);
ListPrint(head);
//在pos位置插入
ListErase(ListFind(head, 4));
ListPrint(head);
}
非常成功
5.双向链表的销毁
本质上就是遍历链表一个个删除节点,删除时要注意保存下一个节点地址,这里博主用pnext来保存下一个节点的位置防止链表丢失
// 双向链表销毁
void ListDestory(LNode* pHead) {
assert(pHead);
LNode* p = pHead->next;
free(pHead);
pHead->next = NULL;
pHead->prev = NULL;
while (p != pHead) {
LNode* pnext = p->next;
free(p);
p->next = NULL;
p->prev = NULL;
p = pnext;
}
printf("SUCCEESFUL DELETE!\n");
}
运行结果
可以看见链表已经被销毁,再次打印时就已经没有任何数据了
void test9() {
//头插
LNode* head = ListCreate();
ListPushFront(head, 1);
ListPushFront(head, 2);
ListPushFront(head, 3);
ListPushFront(head, 4);
ListPushFront(head, 5);
ListPrint(head);
//销毁
ListDestory(head);
ListPrint(head);
}
4.代码优化
细心的同学已经注意到,在头插跟尾插这里,有被注释掉的代码,在写出函数ListInsert跟ListErase时,头插、尾插、头删、尾删就可以复用这两个函数来实现,这里我只演示了头插跟尾插
//代码复用,在哨兵节点的下一个节点之前插入,即是头插
ListInsert(pHead->next, x);
//pHead的perv就是尾结点,也是在尾结点后面插入
ListInsert(pHead, x);
头删跟尾删可以自己尝试
源代码
DList.h
#pragma once
#pragma warning (disable:4996)
#include<stdio.h>
#include<malloc.h>
#include<assert.h>
typedef int LTDataType;
typedef struct ListNode {
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
}LNode;
//创建节点
LNode* BuyListNode(LTDataType x);
// 创建返回链表的头结点.
LNode* ListCreate();
// 双向链表销毁
void ListDestory(LNode* pHead);
// 双向链表打印
void ListPrint(LNode* pHead);
// 双向链表尾插
void ListPushBack(LNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(LNode* pHead);
// 双向链表头插
void ListPushFront(LNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(LNode* pHead);
// 双向链表查找
LNode* ListFind(LNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(LNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(LNode* pos);
DList.c
#include "DList.h"
//创建节点
LNode* BuyListNode(LTDataType x) {
LNode* node = (LNode*)malloc(sizeof(LNode));
node->data = x;
node->next = NULL;
node->prev = NULL;
return node;
}
// 创建返回链表的头结点.
LNode* ListCreate() {
//创建哨兵节点
//哨兵节点不存储有效数据
LNode* phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
//返回phead的地址
return phead;
}
// 双向链表尾插
void ListPushBack(LNode* pHead, LTDataType x) {
//断言
assert(pHead);
//LNode* tali = pHead->prev;
新结点
//LNode* newnode = BuyListNode(x);
修改四个指针,newnode先链接
//newnode->prev = tali;//此处不是pHead,因为是尾插,要从尾结点插入
//tali->next = newnode;
//newnode->next = pHead;
//pHead->prev = newnode;
//pHead的perv就是尾结点,也是在尾结点后面插入
ListInsert(pHead, x);
}
// 双向链表头插
void ListPushFront(LNode* pHead, LTDataType x) {
assert(pHead);
//LNode* newnode = BuyListNode(x);
//LNode* first = pHead->next;
phead newnode first
//pHead->next = newnode;
//newnode->prev = pHead;
如果没有节点,下面两条语句相当于第一个插入的节点的地址不变
//newnode->next = first;
//first->prev = newnode;
//代码复用,在哨兵节点的下一个节点之前插入,即是头插
ListInsert(pHead->next, x);
}
// 双向链表头删
void ListPopFront(LNode* pHead) {
assert(pHead);
LNode* cur = pHead->next;
LNode* cur_next = pHead->next->next;
pHead->next = cur_next;
cur_next->prev = pHead;
free(cur);
}
// 双向链表尾删
void ListPopBack(LNode* pHead) {
assert(pHead);
assert(pHead->next != pHead);//判断哨兵结点后面是否有结点
LNode* tail = pHead->prev;
LNode* tailprev = tail->prev;
free(tail);
tailprev->next = pHead;
pHead->prev = tailprev;
}
// 双向链表打印
void ListPrint(LNode* pHead) {
//断言
assert(pHead);
LNode* cur = pHead->next;
printf("pHead=>");
while (cur != pHead)
{
printf("%d<=>", cur->data);
cur = cur->next;
}
printf("\n");
}
// 双向链表查找
LNode* ListFind(LNode* pHead, LTDataType x) {
assert(pHead);
LNode* pos = pHead->next;
while (pos != pHead) {
if (pos->data == x) {
return pos;
}
else {
pos = pos->next;
}
}
}
// 双向链表在pos的前面进行插入
void ListInsert(LNode* pos, LTDataType x) {
LNode* posprev = pos->prev;
LNode* newnode = BuyListNode(x);
newnode->next = pos;
pos->prev = newnode;
posprev->next = newnode;
newnode->prev = posprev;
}
// 双向链表删除pos位置的节点
void ListErase(LNode* pos) {
assert(pos);
LNode* posprev = pos->prev;
LNode* posnext = pos->next;
posprev->next = pos->next;
posnext->prev = posprev;
free(pos);
}
// 双向链表销毁
void ListDestory(LNode* pHead) {
assert(pHead);
LNode* p = pHead->next;
free(pHead);
pHead->next = NULL;
pHead->prev = NULL;
while (p != pHead) {
LNode* pnext = p->next;
free(p);
p->next = NULL;
p->prev = NULL;
p = pnext;
}
printf("SUCCEESFUL DELETE!\n");
}
test.c
#include "DList.h"
void test1() {
//头插
LNode* head = ListCreate();
ListPushFront(head, 1);
ListPushFront(head, 2);
ListPushFront(head, 3);
ListPushFront(head, 4);
ListPushFront(head, 5);
//头删
ListPrint(head);
}
void test2() {
//尾插
LNode* head = ListCreate();
ListPushBack(head, 1);
ListPushBack(head, 2);
ListPushBack(head, 3);
ListPushBack(head, 4);
ListPushBack(head, 5);
ListPrint(head);
}
void test3() {
LNode* head = ListCreate();
ListPushFront(head, 1);
ListPushFront(head, 2);
ListPushFront(head, 3);
ListPushFront(head, 4);
ListPushFront(head, 5);
ListPrint(head);
LNode* pos = ListFind(head, 2);
ListInsert(pos, 10);
ListPrint(head);
LNode* pos1 = ListFind(head, 5);
ListErase(pos1);
ListPrint(head);
ListDestory(head);
ListPrint(head);
}
void test4() {
//头插
LNode* head = ListCreate();
ListPushFront(head, 1);
ListPushFront(head, 2);
ListPushFront(head, 3);
ListPushFront(head, 4);
ListPushFront(head, 5);
//头删
ListPopFront(head);
ListPrint(head);
ListPopFront(head);
ListPrint(head);
ListPopFront(head);
ListPrint(head);
ListPopFront(head);
ListPrint(head);
ListPopFront(head);
ListPrint(head);
}
void test5() {
//尾插
LNode* head = ListCreate();
ListPushBack(head, 1);
ListPushBack(head, 2);
ListPushBack(head, 3);
ListPushBack(head, 4);
ListPushBack(head, 5);
ListPrint(head);
//尾删
ListPopBack(head);
ListPrint(head);
ListPopBack(head);
ListPrint(head);
ListPopBack(head);
ListPrint(head);
ListPopBack(head);
ListPrint(head);
ListPopBack(head);
ListPrint(head);
}
void test6() {
//头插
LNode* head = ListCreate();
ListPushFront(head, 1);
ListPushFront(head, 2);
ListPushFront(head, 3);
ListPushFront(head, 4);
ListPushFront(head, 5);
ListPrint(head);
//在pos位置插入
ListInsert(ListFind(head, 4), 20);
ListPrint(head);
}
void test7() {
//头插
LNode* head = ListCreate();
ListPushFront(head, 1);
ListPushFront(head, 2);
ListPushFront(head, 3);
ListPushFront(head, 4);
ListPushFront(head, 5);
//在pos位置插入
ListPrint(head);
printf("%p\n", ListFind(head, 3));
}
void test8() {
//头插
LNode* head = ListCreate();
ListPushFront(head, 1);
ListPushFront(head, 2);
ListPushFront(head, 3);
ListPushFront(head, 4);
ListPushFront(head, 5);
ListPrint(head);
//在pos位置插入
ListErase(ListFind(head, 4));
ListPrint(head);
}
void test9() {
//头插
LNode* head = ListCreate();
ListPushFront(head, 1);
ListPushFront(head, 2);
ListPushFront(head, 3);
ListPushFront(head, 4);
ListPushFront(head, 5);
ListPrint(head);
//销毁
ListDestory(head);
ListPrint(head);
}
int main() {
test9();
return 0;
}
总结
基本实现双向循环链表的增删查改,并且还完成了部分代码的优化复用,减少代码量,测试过程出现的诸多问题可以自己解决
就这样,开溜,如果大佬们发现有什么bug,可以私信找我