双链表:比单链表更高效的增删查改

目录

单链表的缺点

双链表

双链表的实现

双链表定义

why?重点注意

双链表头文件

附上主功能函数代码

效率分析:


继上文之后的单链表学习,最常用的还是双链表中的循环双链表,看起来挺复杂,其实理解之后比单链表省事很多。然后都是基于对单链表的理解去学习,所以篇幅会比较少。

单链表的缺点

由于单链表是只有尾指针tail,所以不论带头还是不带头,它找尾巴很麻烦,需要遍历链表,时间复杂度为,对于单链表,尾插尾删修改链表都比较麻烦,所以对于这些操作,更为方便的其实是双链表。

双链表

带头双向循环链表,简称双链表,结构上相较于单链表比较复杂,在实际应用中,多为带头双向循环链表,但是使用起来真的会比单链表方便很多,因为它有头指针和尾指针双向,在实现链表的增删查改都会十分便利。

那么什么是双链表呢,来一副图说明就是,应该是十分清晰的。从这张结构图可以看出,除了具备基本的单链表特点,它还有一个箭头指向它的前节点,尾节点d4的尾指针指向头节点head,head的前驱指针指向尾节点d4,形成循环。
带头双向循环链表

双链表的实现

双链表该如何实现呢,同样是增删查改,只需要在单链表的基础上加一个前驱指针就行了。

单链表是这样定义的

cpp 复制代码
typedef int Sltdatatype;
typedef struct SListnode
{
	Sltdatatype data;
	struct SListnode* next;
}SLTnode;

那么双链表定义就是这样的

双链表定义

cpp 复制代码
typedef int List;
typedef struct SListnode
{
	Sltdatatype data;
	struct ListNode* next;
    struct ListNode*prev;
}LTNode;

然后在实现双链表的功能之前,需要强调的一点是,双链表不需要用到二级指针,用一级就可以了

why?重点注意


  1. 为什么不需要二级指针呢,双链表不是改变头指针本身,只是改变结构体内指针变量 ,而改变结构体变量,用结构体指针就可以了。我们在之前写单链表的时候,在头插头删的时候,都是要改变头指针指向,是头指针本身地址,空链表也要申请malloc一块内存空间,指针去指向它,指针本身被修改了,并且,尾删删到最后一个节点,尾插时,链表为NULL的情况下,等于头插,都要改变头节点的指针地址。
  2. 而双链表一般都是有头节点,头指针在初始化后永远指向头节点,本身内容不会发生改变,(插入的时候改变的是头节点里的指针变量,并非头节点本身)所以只要改变结构体变量,并不涉及指针本身的修改。
  3. 然后就是第二个注意事项:单链表的初始化不需要单独函数封装,而双链表最好时封装一下:

因为单链表结构简单,最开始初始化的时候只需要把链表置空就好,然后尾插都是动态申请一个空间,然后在申请额外节点的函数内初始化了节点。双链表结构比较复杂,需要给头节点申请并初始化,然后前后指针都指向本身。后续插入不改变头节点本身地址(一直指向自己)只是改变头节点里的指针变量的指向。
单链表的节点申请

其实第一个问题用不用二级指针可以由第二个问题解释,用不用就看头指针内容是否改变。两个问题可以一起思考,一起解决。这个关键之处明白了,剩下的就不是事情了。

先附上双链表的各种场景下总思路图

那有了单链表的基础,就不用再逐一解释了,直接上代码

双链表头文件

cpp 复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
//双链表定义
typedef struct ListNode{
	int data;
	struct ListNode* prev;
	struct ListNode* next;

}LNode;
void Listinit(LNode**phead);
void ListDestory(LNode*phead);
void ListPrint(LNode*phead);
bool IfEmptyNode(LNode* phead);
void ListPushBack(LNode* phead,int x );
void ListPushFront(LNode*phead,int x );
void ListPopBack(LNode* phead);
void ListPopFront(LNode* phead);
void InsertPos(LNode*pos,int x);
void ErasePos(LNode* pos);
LNode* LTFind(LNode* phead, int x);

还是初始化,(这里要封装一下),销毁,打印,判空,头插尾插,头删尾删,还有插入,删除,这两个可以运用在其他四个函数功能中。头插就是在头节点后插入,尾插就是在头节点前插入,head的前一个节点是尾节点。头删就是头节点后面删除,尾删就是头节点前删除。这些应该不难理解。只要记住这句话

双向带头节点,或者说带哨兵卫的循环链表,head之前是尾,head后是第一个数据,尾节点后面是head。

附上主功能函数代码

cpp 复制代码
#include"List.h"

LNode* BuyNode(int x)
{
	LNode* newnode = (LNode*)malloc(sizeof(LNode));
	if (newnode==NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}
void Listinit(LNode** phead)
{
	LNode*pphead = BuyNode(-1);
	if (pphead==NULL)
	{
		return ;
	}
	pphead->next = pphead;
	pphead->prev = pphead;
	*phead = pphead;
}

void ListDestory(LNode* phead)
{
	assert(phead);
	LNode* cur = phead->next;
	while (cur)
	{
		LNode* next = cur->next;
		
		free(cur);
		cur =next;
	}
	free(phead);
	phead = NULL;
}


bool IfEmptyNode(LNode*phead)
{
	assert(phead);
	LNode* cur = phead;
	if (cur->next == cur)
	{
		return true;
	}
	else
		return false;

}
void ListPrint(LNode* phead)
{
	assert(phead);
	printf(" <= phead =>");
	LNode* cur = phead->next;
	while (cur!=phead)
	{
		printf("<=%d=>",cur->data);
		cur = cur->next;
	}
	printf("\n");
}
//尾插
void ListPushBack(LNode* phead,int x)
{
	assert(phead);
	//防止有带头空链表
	//LNode* node = BuyNode(x); 
	//LNode* tail = phead->prev;
	//tail->next = node;
	//node->prev = tail;
	//node->next = phead;
	//phead->prev = node;

	//哨兵卫前面插入。等于尾插,因为head的前一个节点是尾节点
	InsertPos(phead,x);

}
//头插
void ListPushFront(LNode* phead, int x)
{
	assert(phead);
	/*LNode* node = BuyNode(x);
	node->next = phead->next;
	phead->next->prev = node;
	phead->next = node;
	node->prev = phead;*/
	//哨兵节点的后面插入,就是在第一个节点前插入
	InsertPos(phead->next,x);
}


//尾删
void ListPopBack(LNode* phead)
{
	assert(phead);
	//防止有空链表,要判断是否为空
	assert(!IfEmptyNode(phead));
	/*LNode* del = phead->prev;
	phead->prev = del->prev;
	del->prev->next = phead;
	free(del);
	del = NULL;*/
	ErasePos(phead->prev);
}

void ListPopFront(LNode* phead)
{
	assert(phead);
	//防止有空链表,要判断是否为空
	assert(!IfEmptyNode(phead));
	/*LNode* del = phead->next;
	phead->next = del->next;
	del->next->prev = phead;
	free(del);
	del = NULL;*/
	ErasePos(phead->next);
}

//查找
LNode* LTFind(LNode* phead,int x)
{
	assert(phead);
	
	LNode* cur = phead->next;
	while (cur!=phead)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
			
	}
return NULL;
}
//pos位置前插入,作为接口调用
void InsertPos(LNode* pos, int x)
{
	assert(pos);
	LNode* pprev = pos->prev;
	LNode* node = BuyNode(x);
	node->next = pos;
	pos->prev = node;
	pprev->next = node;
	node->prev = pprev;

}


//pos位置删除,可以作为特殊位置删除的接口调用
void ErasePos(LNode* pos)
{
	assert(pos);
	LNode* pprev = pos->prev;
	LNode* pnext = pos->next;
	pprev->next = pos->next;
	pnext->prev = pprev;
	free(pos);
	pos = NULL;
}

测试代码就不展示了,博主自己测试过所有函数,是没有什么遗漏的点的。双链表就这些基本操作。理解了单链表,和那两个关键之处,其实就没什么特别难理解的点了。

效率分析:


双链表在某些操作上效率比单链表效率更高,整体上还是要考虑时间和空间复杂度。

1.操作效率优势:双链表在插入操作时间复杂度为,单链表为,支持双向遍历,灵活高。
2.空间上相对不足:额外存储前驱指针,内存占用增大

建议在需要频繁插入/删除的场景运用双链表,而在只需要单向操作用单链表(入栈,队列问题)

博主已经在慢慢学习C++了,后续还会不断输出新内容的。

相关推荐
xie_pin_an2 小时前
从二叉搜索树到哈希表:四种常用数据结构的原理与实现
java·数据结构
C++实习生2 小时前
Visual Studio Express 2015 for Windows Desktop 中文学习版
windows·express·visual studio
栈与堆4 小时前
LeetCode 21 - 合并两个有序链表
java·数据结构·python·算法·leetcode·链表·rust
viqjeee4 小时前
ALSA驱动开发流程
数据结构·驱动开发·b树
XH华5 小时前
数据结构第九章:树的学习(上)
数据结构·学习
我是大咖5 小时前
二维数组与数组指针
java·数据结构·算法
灏瀚星空6 小时前
基于 Python 与 GitHub,打造个人专属本地化思维导图工具全流程方案(上)
开发语言·人工智能·经验分享·笔记·python·个人开发·visual studio
爱编码的傅同学7 小时前
【今日算法】Leetcode 581.最短无序连续子数组 和 42.接雨水
数据结构·算法·leetcode
wm10437 小时前
代码随想录第四天
数据结构·链表