既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
这其中,我们的接口实现主要研究的是函数实现文件 List.c中的内容,对 test.c 文件中的内容分不关心。
2.接口实现(本文重点):
这里是本文重点中的重点,即 List.c 文件中的接口具体实现:
Ⅰ.双向链表初始化:
- 双向链表初始化:
- 动态申请首节点,并使两个指向直接前置节点与直接后继节点的指针均指向自己,形成循环结构。
- 最后返回初始化完成的头节点。
LNode* LInit() { //哨兵位头节点: LNode* phead = (LNode*)molloc(sizeof(LNode)); phead->next = phead; phead->prev = phead; return phead; }
Ⅱ.打印双向链表:
- 执行操作前需对传入指针进行非空判断,防止对空指针进行操作。
- 双向链表的打印方式与单链表相似,采用方式均为由头节点开始,通过节点指针指向寻找下一节点的方式循环进行遍历打印。
- 不同点是,由于双向循环链表首尾相连,形成闭环,因此终止循环打印条件将不再是执行至空指针而是执行回到头节点(即完成整个循环)。
void LPrint(LNode* phead) { if (phead == NULL) { return; } LNode* cur = phead->next; while (cur != phead) { printf("%d ", cur->data); cur = cur->next; } printf("NULL\n"); }
Ⅲ.申请新节点:
- 新节点的申请与单链表基本相同,动态申请新节点后,再对新节点进行操作。
- 不同点是双向链表多出一个指向直接前驱节点的指针,因此该指针也需要在使用前进行置空操作,防止造成野指针错误。
LNode* BuyListNode(LDataType x) { LNode* newnode = (LNode*)malloc(sizeof(LNode)); newnode->data = x; newnode->next = NULL; newnode->prev = NULL; return newnode; }
Ⅳ.双向链表尾插:
- 执行操作前应当进行非空判断,防止传入空指针。
- 在进行尾插操作前应当首先找到尾节点,采用的方式是,通过双向循环链表中头节点的前驱指针指向来找到尾节点。
- 接着动态申请新节点。
- 最后执行尾插操作。首先使前面找到的尾节点的后继指针指向新节点,并使新节点的前驱指针也指向尾节点;接着使头节点的前驱指针指向新节点,并使新节点的后继指针指向头节点。
void LPushBack(LNode* phead, LDataType x) { if (phead == NULL) { return; } LNode* tail = phead->prev; //找到尾节点 LNode* newnode = BuyListNode(x); //新尾互指: tail->next = newnode; newnode->prev = tail; //新头互指: phead->prev = newnode; newnode->next = phead; }
- 测试尾插接口功能实现:
Ⅴ.双向链表尾删:
- 在双向链表进行尾删时,不仅要防止传入空指针,同时也要注意避免链表为空(只含有哨兵节点)的情况发生。
- 首先找到尾节点及尾节点的前驱节点,接着使该前驱节点与头节点跳过尾节点互指,最后释放原尾节点并置空即可。
void LPopBack(LNode* phead) { if ((phead == NULL) || (phead->next == phead)) //排除为空的情况 { return; } LNode* tail = phead->prev; //找到尾 LNode* tailprev = tail->prev; //找到尾的前驱 tailprev->next = phead; //尾前驱节点的后继指针指向头 phead->prev = tail->prev; //头的前驱指针指向尾的前驱 free(tail); tail = NULL; }
- 测试尾删接口功能实现:
Ⅵ.双向链表头插:
- 在执行操作前应当对传入指针进行判断,防止传入空指针。
- 在改变指向前,应当首先保存哨兵节点的后继节点,因为当我们插入新节点后链表结构将会发生改变,再想要找到该节点将变得麻烦。
- 进行插入操作时,使哨兵节点的后继指针指向新节点,再使新节点的前驱指针指向哨兵节点,接着使用同样的操作使新节点与哨兵节点的原后继节点互指。
void LPushFront(LNode* phead, LDataType x) { if (phead == NULL) { return; } LNode* newnode = BuyListNode(x); LNode* next = phead->next; //保存哨兵节点的后继节点 phead->next = newnode; //哨兵点的后继指针指向新节点 newnode->prev = phead; //新节点的前驱指针指向哨兵节点 newnode->next = next; //新节点的后继指针指向哨兵节点的原后继节点 next->prev = newnode; //哨兵节点的原后继节点前驱指针指向新节点 }
- 测试头插接口功能实现:
Ⅶ.双向链表头删:
- 首先**进行非空判断,并排除链表为空(只含有哨兵节点)**的情况。
- 开始进行头删操作前找到并保存头节点,防止改变指向后链表结构发生变化难以找到原本的头节点,以便于最后进行释放。
- 并且应当找到并保存原头节点的后继节点,防止防止改变指向后链表结构发生变化而难以找到。
- 具体操作便是使哨兵节点与头节点的后继节点跳过头节点互指,再将要删除的头节点释放并置空即可。
void LPopFront(LNode* phead) { if ((phead == NULL) || (phead->next == phead)) //排除为空的情况 { return; } LNode* next = phead->next; //保存头节点,便于释放 LNode* nextNext = next->next; //保存头节点的后继节点 phead->next = nextNext; nextNext->prev = phead; free(next); next = NULL; }
- 测试头删接口功能实现:
Ⅷ.双向链表查找:
- 执行操作前需进行非空判断,防止对空指针进行操作。
- 查找逻辑很简单,遍历整个链表,直至链表完整循环一遍,若比对存在匹配元素返回该节点,否则返回空。
LNode* LFind(LNode* phead, LDataType x) { if (phead == NULL) { return; } LNode* cur = phead->next; while (cur != phead) { if (cur->data == x) { return cur; } else { cur = cur->next; } } return NULL; }
- 测试查找接口功能实现:
Ⅸ.双向链表给定节点前插:
- 执行操作前需进行非空判断,防止对空指针进行操作。
- 执行逻辑很简单,将数据存入通过动态申请的来的新节点中后,找到目标节点,使目标节点的前驱节点与新节点互指,再使新节点与目标节点互指接即可。
void LInsert(LNode* pos, LDataType x) { if (pos == NULL) { return; } LNode* posPrev = pos->prev; LNode* newnode = BuyListNode(x); posPrev->next = newnode; newnode->prev = posPrev; newnode->next = pos; pos->prev = newnode; }
- 测试前插接口功能实现:
Ⅹ.双向链表给定节点后插:
- 执行操作前需进行非空判断,防止对空指针进行操作。
- 执行逻辑与前插高度类似,不同的是使目标节点的后继节点与新节点互指,再使新节点与目标节点互指。
void LInsertBack(LNode* pos, LDataType x) { if (pos == NULL) { return; } LNode* posPrev = pos->next; LNode* newnode = BuyListNode(x); posPrev->prev = newnode; newnode->next = posPrev; newnode->prev = pos; pos->next = newnode; }
- 测试后插接口功能实现:
ⅩⅠ.双向链表删除给定节点:
- 执行操作前需进行非空判断,防止操作空指针。
void LErase(LNode* pos) { if (pos == NULL) { return; } LNode* posPrev = pos->prev; LNode* posNext = pos->next; posPrev->next = posNext; posNext->prev = posPrev; free(pos); pos = NULL; }
- 测试删除接口功能实现:
ⅩⅡ.双向链表销毁:
- 若哨兵节点为空,即表示链表内没有有效数据节点,则无需进行销毁、释放与置空操作。
- 含有有效节点则遍历所有节点,将每一个节点均进行释放,特别注意所有数据节点释放完毕之后,不要忘记释放哨兵节点。
void LDestroy(LNode* phead) { if(phead==NULL) { return; } LNode* cur = phead->next; while (cur != phead); { LNode* next = cur->next; free(cur); cur = next; } free(phead); phead = NULL; }
🍄三、完整接口实现代码🍄:
1.List.h:
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//自定数据类型LDataType:
typedef int LDataType;
//双向链表节点结构:
typedef struct ListNode
{
LDataType data;
struct LNode* next;
struct LNode* prev;
}LNode;
LNode* LInit(); //初始化双向循环链表
void LPrint(LNode* phead); //打印双向循环链表
LNode* BuyListNode(LDataType x);//双向循环链表新节点申请
void LPushBack(LNode* phead, LDataType x); //双向循环链表尾插
void LPopBack(LNode* phead); //双向循环链表尾删
void LPushFront(LNode* phead, LDataType x); //双向循环链表头插
void LPopFront(LNode* phead); //双向循环链表头删
LNode* LFind(LNode* phead, LDataType x); //双向循环链表查找
void LInsertFront(LNode* pos, LDataType x); //双向循环链表给定节点前插
void LInsertBack(LNode* pos, LDataType x); //双向循环链表给定节点后插
void LErase(LNode* pos); //双向循环链表给定节点删除
void LDestroy(LNode* phead); //双向循环链表的销毁
2.List.c:
#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
//初始化双向循环链表初始化:
LNode* LInit()
{
//哨兵位头节点:
LNode* phead = (LNode*)malloc(sizeof(LNode));
phead->next = phead;
phead->prev = phead;
return phead;
}
//打印双向循环链表
void LPrint(LNode* phead)
{
if (phead == NULL)
{
return;
}
LNode* cur = phead->next;
while (cur != phead)
{
printf("%d -> ", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
//双向循环链表申请新节点:
LNode* BuyListNode(LDataType x)
{
LNode* newnode = (LNode*)malloc(sizeof(LNode));
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
//双向循环链表尾插:
void LPushBack(LNode* phead, LDataType x)
{
if (phead == NULL)
{
return;
}
LNode* tail = phead->prev; //找到尾节点
LNode* newnode = BuyListNode(x);
//新尾互指:
tail->next = newnode;
newnode->prev = tail;
//新头互指:
phead->prev = newnode;
newnode->next = phead;
}
//双向循环链表尾删:
void LPopBack(LNode* phead)
{
if ((phead == NULL) || (phead->next == phead)) //排除为空的情况
{
return;
}
LNode* tail = phead->prev; //找到尾
LNode* tailprev = tail->prev; //找到尾的前驱
tailprev->next = phead; //尾前驱节点的后继指针指向头
phead->prev = tail->prev; //头的前驱指针指向尾的前驱
free(tail);
tail = NULL;
}
//双向循环链表头插:
void LPushFront(LNode* phead, LDataType x)
{
if (phead == NULL)
{
return;
}
LNode* newnode = BuyListNode(x);
LNode* next = phead->next; //保存哨兵节点的后继节点
phead->next = newnode; //哨兵点的后继指针指向新节点
newnode->prev = phead; //新节点的前驱指针指向哨兵节点
newnode->next = next; //新节点的后继指针指向哨兵节点的原后继节点
next->prev = newnode; //哨兵节点的原后继节点前驱指针指向新节点
}
//双向循环链表头删:
void LPopFront(LNode* phead)
{
if ((phead == NULL) || (phead->next == phead)) //排除为空的情况
{
return;
}
LNode* next = phead->next; //保存头节点,便于释放
LNode* nextNext = next->next; //保存头节点的后继节点
phead->next = nextNext;
nextNext->prev = phead;
free(next);
next = NULL;
}
//双向循环链表查找:
LNode* LFind(LNode* phead, LDataType x)
{
if (phead == NULL)
{
return;
}
LNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
else
{
cur = cur->next;
}
}
return NULL;
}
//双向循环链表给定节点前插:
void LInsert(LNode* pos, LDataType x)
{
if (pos == NULL)
{
return;
}
LNode* posPrev = pos->prev;
LNode* newnode = BuyListNode(x);
posPrev->next = newnode;
newnode->prev = posPrev;


**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
cur->next;
}
}
return NULL;
}
//双向循环链表给定节点前插:
void LInsert(LNode* pos, LDataType x)
{
if (pos == NULL)
{
return;
}
LNode* posPrev = pos->prev;
LNode* newnode = BuyListNode(x);
posPrev->next = newnode;
newnode->prev = posPrev;
[外链图片转存中...(img-glviqYmk-1715716232082)]
[外链图片转存中...(img-ZKt6SGwv-1715716232082)]
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**