文章目录
前言
该篇文章是继上一篇文章数据结构之单链表如果没有看过前一篇文章建议先把那篇文章看了再看这里,不然可能看不懂,还有就是,上一篇说的单向不带头节点,所以我在上一篇提到的头节点指的是第一个节点***,而我在***这篇文章中提到的所有头节点都是指的标兵位(也有叫哨兵位的),和上一篇中说的不是一个东西,希望大家别搞混了。
数据结构——单链表
单向不带头非循环的优缺点
优点:
不需要向顺序表一样,频繁扩容,而且不会浪费空间,不管是头插还是中间插入,特别方面,不需要移动元素,直接插入即可
缺点:
在尾插的时候需要遍历链表,找到最后一个节点才能插入,这就使得尾插效率比较低
通过单链表的不足引入双向带头循环链表
对于单向链表,尾插效率低,在这里引入带头循环链表,头节点存放两个指针,一个指针指向第一个节点,一个指针指向最后一个节点,这样便形成一个环,想找到最后一个节点只需要一行代码,这样大大的提高了尾插的效率
双向带头循环链表的实现
节点定义
typedef int anytype;
typedef struct STL_list{
anytype data;
struct STL_list* prev; //用于指向前一个节点
struct STL_list* next; //用于指向后一个节点
}list;
链表初始化
因为这是带头的链表,所以在还没有任何数据插入时这个头节点就要创建好,要插入数据就往这个头节点后面插,就算清空整个链表也不能删除这个头节点,只有在销毁这个链表的时候才能把这个头节点删除
又因为这是一个双向循环链表,所以头节点的prev指针要指向最后一个节点,最后一个节点的next指针要指向头节点,当链表为空的时候,头节点还拥有其他两个身份,他既是第一个节点又是最后一个节点,所以初始化的时候我们让头节点的next和prev都指向自己
list* List_Init() { //链表初始化成功返回指向头节点的指针
list* plist = (list*)malloc(sizeof(list));
if (plist != NULL) {
plist->data = 0; //头节点的data可以不使用他,也可以使用他记录一些特殊标志位什么的
plist->next = plist;
plist->prev = plist;
}
return plist;
}
main掉用示例
#<include>
typedef struct STL_list{
anytype data;
struct STL_list* prev; //用于指向前一个节点
struct STL_list* next; //用于指向后一个节点
}list;
int main(){
list* phead = List_Init();
/*调用初始化链表的函数,接收指向头节点的指针,初始化完成,链表就创建
成功了,只是目前链表为空,拿到头节点的指针之后可以做任何想做的事了,头插,尾插,头删,尾删等等*/
push_front(phead,1);
push_front(phead,2);
push_front(phead,3);
return 0;
}
创建节点
不管在任何位置插入节点都要先申请节点,有节点才能插入,所以为了提高代码的复用和减少代码量,我们把创建节点单独封装成一个函数
list* Add_newspace(anytype x) {
list* newspace = (list*)malloc(sizeof(list));
if (newspace != NULL) {
newspace->data = x;
newspace->next = NULL;
newspace->prev = NULL;
return newspace;
}
return NULL;
}
打印链表
因为在初始化的时候我们就让链表的头节点的prev和next节点指向自己了,所以判断这个链表是否为空只需要判断头节点的prev和next指针是不是指向自己就可以了,如果链表不为空,循环打印,为空直接返回
void list_printf(list* phead) {
list* cur = phead->next;
if (phead->next == phead) {
printf("链表为空!\n");
return;
}
else {
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
}
尾插节点
示意图
尾插原理:我们要找到最后一个节点,插入流程分五步
在这里不用考虑当链表为空的情况,因为我们在初始化的时候就让头节点(哨兵位)的prev和next指针都指向了自己,已经形成了一个循环,当链表为空时,头节点不仅只是哨兵位,同时也是第一个节点,又是最后一个节点
如果不明白在纸上画一画就明白了,学数据结构一定要多画!
1.找到最后一个节点保存到cur
2.让最后一个节点的next指针指向即将插入的新节点
3.让新节点的prev指针指向链表的最后一个节点
4.让头节点(哨兵位)的prev指针指向新节点
5.让新节点的next指针指向头节点(哨兵位)
void push_back(list* phead, anytype x) {
list* newspace = Add_newspace(x); //创建节点
list* cur = phead->prev; //得到最后一个节点对应第一步
cur->next = newspace; //对应插入流程第二步
newspace->prev = cur; //对应插入流程第三步
phead->prev = newspace; //对应插入流程第四步
newspace->next = phead; //对应插入流程第五步
}
头插节点
头插原理:要先找到第一个节点,当然这个非常简单,头节点的next指针指向的就是第一个节点,这里同上,不用考虑链表为空的情况
流程分五步:
第一:找到第一个节点,保存到cur中
第二:先让第一个节点指向新节点
第三:再让新节点的prev指针指向头节点
第四:让新节点的next指针指向第一个节点
第五:让第一个节点的prev指向新节点
void push_front(list* phead, anytype x) {
list* newspace = Add_newspace(x);
list* cur = phead->next;
phead->next = newspace;
newspace->prev = phead;
newspace->next = cur;
cur->prev = newspace;
}
尾删节点
尾删原理:先用头节点的prev指针找到最后一个节点,再通过最后一个节点找到最后一个节点的前一个节点,然后把最后一个节点的前一个节点与头节点连接起来即可,大致分为以下几步:
1: 找到尾节点并保存起来(这里我保存到了cur中)
2.判断链表是否为空,如果链表为空直接退出
3.把尾节点的前一个节点保存起来(这里我保存到了prev中)
4.让最后一个的前一个节点的next指针指向头节点
5.让头节点的prev指针指向最后一个节点的前一个节点
6.删除最后一个节点
//以下步骤依次对应上面6步
void pop_back(list* phead) {
list* cur = phead->prev;
if (cur == phead) {
printf("链表为空!");
return;
}
else {
list* prev = cur->prev;
prev->next = phead;
phead->prev = prev;
free(cur);
cur = NULL;
}
}
头删节点
头删和尾删原理一样,只是这里我们找到的是第一个节点和第一个节点后面的那个节点,然后把头节点和第一个节点后面的那个节点连接起来,最后把头节点删除即可,如果不理解可以再倒回去看看尾删
void pop_front(list* phead) {
list* cur = phead->next;
if (cur == phead){
printf("链表为空!");
return;
}
else {
list* next = cur->next;
phead->next = next;
next->prev = phead;
free(cur);
cur = NULL;
}
}
查找指定节点
原理:找到了就返回指定节点的指针,找不到就返回空
老规矩还是要先判断链表是否为空,如果是空直接返回,否则就循环对比,找到指定节点直接返回该节点指针,若链表不为空,循环也没找到,说明该节点不存在,返回空
list* find(list* phead, anytype x) {
list* cur = phead->next;
if (cur == phead) { //判断链表是否为空
return NULL;
}
else {
while (cur != phead) { //循环对比
if (cur->data == x) {
return cur; //找到了立即退出
}
cur = cur->next;
}
}
return NULL; //链表不为空,循环也没找到,返回NULL
}
在指定节点的下一个位置添加节点
重点: 调用这个函数之前一定要先调用上面那个find函数,再把find函数的返回值传到这个函数中
原理:要添加新节点第一步先申请空间,存放节点
然后判断传进来的指针是否为空,如果为空直接返回
然后找到指定节点的下一个节点,把这个新节点放到这两个节点中间连接起来即可
(若看不懂代码,可以倒回去看看头插)
void Insert(list* pos, anytype x) {
list* newspace = Add_newspace(x);
if (pos == NULL) { //判断传进来的指针是否有效
printf("pos为空!\n");
}
else { //插入新节点
list* cur = pos->next;
pos->next = newspace;
newspace->prev = pos;
newspace->next = cur;
cur->prev = newspace;
}
}
删除指定节点
和上面一样,先用find函数找到你要删的函数,然后再把find函数的返回值传进来,然后找到这个节点的前一个和后一个节点,把他的前一个和后一个节点连接,最后再把这个节点删除掉,别忘了把指针置空,否则可能出现野指针
void Erase(list* pos) {
if (pos == NULL) {
printf("pos为空!\n"); //判断传进来的指针是否有效
}
else { //连接要删除节点的前一个节点和后一个节点,再删除该节点
list* next = pos->next;
list* prev = pos->prev;
next->prev = prev;
prev->next = next;
free(pos);
pos = NULL;
}
}
销毁链表
这个就比较简单粗暴了,循环从最后一个节点删,一直到把头节点都删掉为止
void Destroy(list ** phead) {
while (*phead != NULL) { //只要指向头节点的指针没有被置空说明链表还在,要继续删
list* cur = (*phead)->next; //找到最后一个节点
if (cur == *phead) { /*如果最后一个节点就是头节点,说明链表为空了,把头节点删除,
指向头节点的指针置空,该函数执行完毕*/
(*phead)->next = NULL;
(*phead)->prev = NULL;
free((*phead));
(*phead) = NULL;
}
else { //链表不为空,删除最后一个节点
list* next = cur->next;
(*phead)->next = next;
next->prev = (*phead);
free(cur);
cur = NULL;
}
}
}
完整代码
#include<stdio.h>
#include<stdbool.h>
#include<stdlib.h>
typedef int anytype;
typedef struct STL_list {
anytype data;
struct STL_list* prev;
struct STL_list* next;
}list;
list* List_Init();
void list_printf(list* phead);
list* Add_newspace(anytype x);
void push_back(list* phead, anytype x);
void push_front(list* phead, anytype x);
void pop_back(list* phead);
void pop_front(list* phead);
list* find(list* phead, anytype x);
void Insert(list* pos, anytype x);
void Erase(list* pos);
void Destroy(list** phead);
list* List_Init() {
list* plist = (list*)malloc(sizeof(list));
if (plist != NULL) {
plist->next = plist;
plist->prev = plist;
}
return plist;
}
void list_printf(list* phead) {
list* cur = phead->next;
if (phead->next == phead) {
printf("链表为空!\n");
return;
}
else {
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
}
list* Add_newspace(anytype x) {
list* newspace = (list*)malloc(sizeof(list));
if (newspace != NULL) {
newspace->data = x;
newspace->next = NULL;
newspace->prev = NULL;
return newspace;
}
return NULL;
}
void push_back(list* phead, anytype x) {
list* newspace = Add_newspace(x);
list* cur = phead->prev;
newspace->prev = cur;
cur->next = newspace;
newspace->next = phead;
phead->prev = newspace;
}
void push_front(list* phead, anytype x) {
list* newspace = Add_newspace(x);
list* cur = phead->next;
phead->next = newspace;
newspace->prev = phead;
newspace->next = cur;
cur->prev = newspace;
}
void pop_back(list* phead) {
list* cur = phead->prev;
if (cur == phead) {
printf("链表为空!");
return;
}
else {
list* prev = cur->prev;
prev->next = phead;
phead->prev = prev;
free(cur);
cur = NULL;
}
}
void pop_front(list* phead) {
list* cur = phead->next;
if (cur == phead) {
printf("链表为空!");
return;
}
else {
list* next = cur->next;
phead->next = next;
next->prev = phead;
free(cur);
cur = NULL;
}
}
list* find(list* phead, anytype x) {
list* cur = phead->next;
if (cur == phead) {
return NULL;
}
else {
while (cur != phead) {
if (cur->data == x) {
return cur;
}
cur = cur->next;
}
}
return NULL;
}
void Insert(list* pos, anytype x) {
list* newspace = Add_newspace(x);
if (pos == NULL) {
printf("pos为空!\n");
}
else {
list* cur = pos->next;
pos->next = newspace;
newspace->prev = pos;
newspace->next = cur;
cur->prev = newspace;
}
}
void Erase(list* pos) {
if (pos == NULL) {
printf("pos为空!\n");
}
else {
list* next = pos->next;
list* prev = pos->prev;
next->prev = prev;
prev->next = next;
free(pos);
pos = NULL;
}
}
void Destroy(list** phead) {
while (*phead != NULL) {
list* cur = (*phead)->next;
if (cur == *phead) {
(*phead)->next = NULL;
(*phead)->prev = NULL;
free((*phead));
(*phead) = NULL;
}
else {
list* next = cur->next;
(*phead)->next = next;
next->prev = (*phead);
free(cur);
cur = NULL;
}
}
}
int main()
{
list* phead = List_Init();
for (int i = 0; i < 10; i++) {
push_back(phead,i);
}
list_printf(phead);
return 0;
}
总结
到这里链表就差不多干完了,我主要讲了单向不循环不带头和双向循环带头,只要把这两种弄明白了,剩下的单向循环,单向带头,双向不循环,双向不带头等,就不是什么大问题了,相信家人们肯定能自己弄出来。
最后再申明一下,虽然我一直在拿各种数据结构做对比,但是并不是说那一种最好,我这样做只是为了方便记忆,还是那句话,没有最好的,只有最合适的,要根据实际场景选择合适的数据结构。