【数据结构】一篇博客带你实现双向带头循环链表!!!(零基础小白也可以看懂)

news2024/11/17 21:25:23

目录

0.前言

1. 简述双向带头链表

2.双向带头循环链表的实现

2.1 设计双向带头循环链表结构体

2.2双向带头循环链表的初始化

2.3双向带头循环链表的尾插

2.4双向带头循环链表的尾删

2.5双向带头循环链表的头插

2.6双向带头循环链表的头删

 2.7双向带头循环链表的插入

2.8双向带头循环链表的删除

2.9双向带头循环链表的查找

2.10双向带头循环链表的打印

2.11双向带头循环链表的销毁

3.顺序表和链表对比

3.1 阴阳双生子

3.2 缓存命中率

3.2.1 缓存命中率基础知识

3.2.2 小例子说明CPU与各级存储

3.2.3 顺序表和链表的缓存命中率


0.前言

本文所有代码以及图片文件资源都已上传gitee:

3双向链表实现 · onlookerzy123456qwq/data_structure_practice_primer - 码云 - 开源中国 (gitee.com)https://gitee.com/onlookerzy123456qwq/data_structure_practice_primer/tree/master/3%E5%8F%8C%E5%90%91%E9%93%BE%E8%A1%A8%E5%AE%9E%E7%8E%B0

1. 简述双向带头链表

我们之前实现的是结构最为简单的无头单向非循环链表,这是博客链接:(11条消息) 一只脚踏入数据结构的大门,如何用C语言实现一个单链表(超超超详解,我的灵魂受到了升华)_yuyulovespicy的博客-CSDN博客https://blog.csdn.net/qq_63992711/article/details/128240283?spm=1001.2014.3001.5501

今天我们来实现结构相对复杂,但是实现起来却比较简单带头双向循环链表

 前面我们说过最朴素的无头单向非循环链表找尾必须一直遍历一遍链表,而双向循环链表下,循环决定了头结点和尾节点之间存在链接关系双向决定了头结点和尾节点之间的链接关系是双向的,即不仅尾节点可以通过next找到头结点,而且头结点也可以通过prev找到尾节点。这样可以大大提高链表找尾的效率,同样我们的尾插/尾删效率大大提高了!

带头双向循环链表,带头的意思是,链表有一个哨兵位的头结点,即我们初始化链表的时候,链表就一直自带一个哨兵位的头结点,此时与不带头的链表相比,不带头的链表为空时,链表plist头就是NULL;而如果是带头的链表,那为空的时候,即还没有有效数据插入之前代表此时链表plist为,是该链表只有一个哨兵位头结点

可以看到由于哨兵位头结点的存在,我们链表的头节点的位置是一直不会改变的,即永远是哨兵位头结点的指针,所以插入/删除的过程中我们就不用更新plist头节点指针的位置了!

不带头的链表,头结点就是第一个有效节点的指针,为NULL的时候进行首次插入,这时候就必须分类讨论,如果plist为NULL,我们需要更新plist头结点的位置,更不必说头插每次插入之后都必须更新头结点plist的指针。同时还有尾删,头删,这些接口的实现就非常方便了,就省去了我们分类讨论以及更新头结点的麻烦

上面的好处我们可以在实现的时候得到体现:下面我们对双向带头循环链表进行实现。

2.双向带头循环链表的实现

2.1 设计双向带头循环链表结构体

首先我们要实现出链表的基础组成元素----节点双向带头循环链表的节点ListNode,里面应该存储的成员变量,首先是存储的数据data,然后双向就需要我们存储一个ListNode* next和ListNode* prevnext指向当前节点的下一个节点,prev指向当前节点的前一个节点

//范式类型LDataType
typedef int LDataType;

//双向链表节点
typedef struct ListNode
{
	LDataType _data;
	struct ListNode* _prev;//需要带struct,因为typedef只有在这个结构体定义之后才生效
	struct ListNode* _next;
}ListNode;

然后代表一个链表的话,其实我们只要找到首节点的指针就可以找到该链表的所有节点,所以首节点的指针就代表整个链表。而对于此双向带头循环链表,知道哨兵位头结点的指针就可以代表这个链表实体。

2.2双向带头循环链表的初始化

我们初始化接口所需做的,就是为该链表创建哨兵位的头结点,然后处理哨兵位头结点的链接关系,即哨兵位头结点的next和prev都指向自己

 然而我们一开始在main函数中,进行初始化的场景,是我们首先定义一个头结点指针为空,即ListNode* plist = NULL;然后调用初始化函数。在双向带头循环链表的初始化中,我们当然首先创建一个哨兵位的头节点,然后在函数中使得plist头结点指针修改为哨兵位头结点的指针。

int main()
{
    //创建一个节点
    ListNode* plist = NULL
    //然后使用初始化函数,对该链表进行初始化
}

所以我们在调用完这个初始化函数之后,我们就需要改变我们plist指针这个指针实体的值。改变plist实体将之赋值为哨兵位头结点的值,就需要传入plist的地址,即ListNode** pplist,所以我们可以将接口设计成这样子:

//链表初始化函数
void ListInit(ListNode** pplist);
int main()
{
    //创建一个链表
    ListNode* plist = NULL
    //然后使用初始化函数,对该链表进行初始化
    ListInit(&plist);
}

这里我们不用传二级指针法,而是使用返回值法完成对链表的初始化,采用这种方法我们可以返回创建的哨兵位头结点的指针,直接用外部的plist去接收,即可完成对链表的初始化,此时我们也就不用传二级指针作为参数了!实现如下:

//初始化plist==NULL,在初始化时需要给plist实体赋值为哨兵位头结点的指针
//可以选用传入外部plist地址,即二级指针初始化方法;我们下面选用的是返回值初始化的方法。
ListNode* ListInit()
{
	//创建哨兵位头结点
	ListNode* phead = (ListNode*)malloc(sizeof(ListNode));
	//双向循环链表中,只有一个节点时,该节点链接关系首尾相接
	phead->_next = phead;
	phead->_prev = phead;
	return phead;
}

应用代码如下:

ListNode* ListInit();
int main()
{
    //创建一个链表
    ListNode* plist = NULL
    //然后使用初始化函数,对该链表进行初始化
    plist = ListInit();
}

2.3双向带头循环链表的尾插

自从有了哨兵位头结点,就再也不用传二级指针了,因为我们的头结点指针在插入/删除之后不会被改变,而永远是哨兵位头结点的指针

然后我们分析一下为什么双向带头循环链表的尾插是非常非常优秀的:

1.不用传二级指针了,因为哨兵位头结点指针不会变。

2.双向循环链表的特性,可以使得phead->_prev可以直接找到tail尾,使效率达到O(1)。

3.单链表单独为NULL的情况,也不用单独拿出来讨论了,因为始终有有一个头结点。

然后我们作为接口的设计者,肯定要讨论一些非法情况:预防phead传入空,一定必须是有带哨兵位头结点的!

下面是尾插的实现:我们通过phead->_prev找到尾节点tail,然后在tail后面链接上创建的newnode节点,然后注意这里我们要处理的链接关系有很多,比如tail的next需要修改为newnode,newnode的prev指向tail,newnode的next指向phead,phead的prev指向newnode

所以代码的实现就是这样的:

//封装创建一个双向节点的接口 --方便后续插入
ListNode* BuyListNode(LDataType x);
void ListPushBack(ListNode* phead, LDataType x)
{
	//直接采用复用法,在phead节点之前插入,就是尾插
	//ListInsert(phead, x);
	
	//防止无头链表NULL
	assert(phead);
	//创建节点
	ListNode* newnode = BuyListNode(x);
	//找尾
	ListNode* tail = phead->_prev;
	//插入链接
	tail->_next = newnode;
	phead->_prev = newnode;
	newnode->_next = phead;
	newnode->_prev = tail;
}
ListNode* BuyListNode(LDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	//初始化创建的节点
	newnode->_prev = newnode->_next = NULL;
	newnode->_data = x;
	return newnode;
}

2.4双向带头循环链表的尾删

思路是类似的,通过phead->_prev找到尾节点tail进行释放删除,当然删除之后我们还是需要处理好phead和tail->_prev之间的链接关系

当然当链表为空的时候,即没有有效节点只有一个哨兵位头节点的时候是禁止删除的。

void ListPopBack(ListNode* phead)
{
	//直接采用复用法:Erase最后一个节点,即phead的前节点
	//ListErase(phead->_prev);
	
	//防止非法情况:无头空链表
	assert(phead);
	//防止非法情况:空链表,无有效节点
	assert(phead->_next != phead);
	//记录节点
	ListNode* tail = phead->_prev;
	ListNode* tail_prv = tail->_prev;
	free(tail);
	tail_prv->_next = phead;
	phead->_prev = tail_prv;
}

2.5双向带头循环链表的头插

带头链表头插的位置,应该是phead哨兵位头结点的后面next,因为我们要保持哨兵位头结点的位置恒定头插在这里的意义作为有效节点的头部,而不是整个链表的头部

void ListPushFront(ListNode* phead, LDataType x)
{
	//防止非法情况:无头空链表
	assert(phead);
	//创建新节点
	ListNode* newnode = BuyListNode(x);
	//插入
	ListNode* phead_next = phead->_next;
	phead->_next = newnode;
	newnode->_prev = phead;
	newnode->_next = phead_next;
	phead_next->_prev = newnode;
}

2.6双向带头循环链表的头删

我们这里还是在找到有效节点中的第一个节点,即phead->next,然后释放删除处理好链接关系,但是我们在空链表的时候,即没有有效节点只有一个哨兵位头结点的时候是禁止删除的!

void ListPopFront(ListNode* phead)
{
	//防止非法情况:无头空链表
	assert(phead);
	//防止非法情况:空链表,无有效节点
	assert(phead->_next != phead);
	
	ListNode* erase_pos = phead->_next;
	ListNode* erase_next = erase_pos -> _next;
	free(erase_pos);
	phead->_next = erase_next;
	erase_next->_prev = phead;
}

 2.7双向带头循环链表的插入

双向带头循环链表的插入是在指定节点位置的前面位置插入。这个效率也是O(1),因为我们可以直接通过pos->_prev找到前一个节点,以及pos->_next找到后一个节点,可以直接处理好相应的链接关系。

//核心接口:Insert插入
void ListInsert(ListNode* pos, LDataType x)
{
	//非法情况,无头链表
	assert(pos);
	//记录节点&&创建节点
	ListNode* pos_prv = pos->_prev;
	ListNode* newnode = BuyListNode(x);
	//处理链接关系
	pos_prv->_next = newnode;
	pos->_prev = newnode;
	newnode->_prev = pos_prv;
	newnode->_next = pos;
}

2.8双向带头循环链表的删除

对指定位置的节点pos进行删除释放,效率也可以提升到O(1),同样也是因为我们可以通过pos->_prev找到前一个节点,以及pos->_next找到后一个节点,可以直接处理好相应的链接关系。

void ListErase(ListNode* pos)
{
	//非法情况:无头链表
	assert(pos);
	//非法情况:空链表(只有头)
	assert(pos->_next != pos);
	//记录节点
	ListNode* pos_prv = pos->_prev;
	ListNode* pos_next = pos->_next;
	free(pos);
	pos = NULL;
	pos_prv->_next = pos_next;
	pos_next->_prev = pos_prv;
}

2.9双向带头循环链表的查找

给我们一个值,然后我们根据这个值找到链表中对应的节点位置。思路很简单当然是暴力遍历查找整个链表的有效节点。当然这里我们还是要预防无头节点即NULL链表的传入,同时对于带头双向循环链表,我们遍历这个链表候,遍历的终止条件应该是cur!=phead,而不是cur!=NULL。

ListNode* ListFind(ListNode* phead, LDataType x)
{
	assert(phead);
	//需要注意有效数据的节点,存储在哨兵头结点之后
	ListNode* cur = phead->_next;
	while (cur != phead)
	{
		if (cur->_data == x)
		{
			return cur;
		}
		cur = cur->_next;
	}
	return NULL;
}

2.10双向带头循环链表的打印

仿效双向带头循环链表的查找,也是遍历整个链表的有效节点,对之进行打印。

void ListPrint(ListNode* phead)
{
	assert(phead);
	//需要注意有效数据的节点,存储在哨兵头结点之后
	ListNode* cur = phead->_next;
	while (cur != phead)
	{
		printf("%d->", cur->_data);
		cur = cur->_next;
	}
	printf("\n");
}

2.11双向带头循环链表的销毁

防止堆区空间的泄漏,我们当然需要在最后对整个链表的空间进行销毁。即依次对每一个节点进行释放。

void ListDestroy(ListNode* phead)
{
	//从头结点phead开始释放链表中的每一个节点
	ListNode* cur = phead->_next;
	while (cur != phead)
	{
		ListNode* cur_next = cur->_next;
		free(cur);
		cur = cur_next;
	}
	free(phead);
	phead = NULL;
	//我们传入的外部的头结点指针的拷贝(一级指针),所以Destroy后在外部需置空,否则出现野指针问题
}

3.顺序表和链表对比

3.1 阴阳双生子

顺序表和链表,这两种数据结构其实可以说是代表两种存储方式的双生子你的优势就是我的劣势,相反,你的劣势就是我的优势。所以这两种结构各有优势,很难说谁更优,严格来说,他们是相辅相成的两种结构。

分析顺序表的优点与缺点,在这篇博客中我做了细致的分析​​​​​​1.3为什么要有链表(vs顺序表)​​​​​​

下面我们简单总结:
顺序表的缺点:

1. 开辟物理空间必须是连续的,空间不够了需要增容,而增容尤其是异地扩容,效率消耗很大

2. 为了防止频繁的增容,顺序表在设计时,空间不够时一般按照当前容量的倍数去扩容,用不完的空间存在一定的空间浪费

3. 由于数据在空间上是连续存储的,我们进行头部或中间位置的插入或删除时,需要挪动数据,效率为O(n),效率不高。

相对应的链表的优点

链表的数据是不连续存储的,数据通过一个个节点相互链接存储,所使用的空间在内存中也是分散申请使用的,由此衍生出链表的优点。

1. 链表每次所需申请的空间只需要一个节点,不需要申请连续物理空间不会出现异地增容申请空间的效率高

2. 链表可以按需申请空间,存多少数据用多少节点空间,不会造成空间浪费

3. 链表在进行任意位置的插入或删除的时候,不需要挪动数据效率为O(1)

顺序表的优点

1.每个数据元素都有对应连续下标支持随机访问,需要随机访问结构支持的算法,如快排等可以很好的适用。

2.CPU高速缓存命中率更高。(这个我们后面细致讲解)

相对应的链表的缺点
1.链表不支持随机访问不能用下标去直接访问)。这也就意味着:如快排,如二分查找等算法在链表结构上就不再适用了。

2.链表节点不是通过下标访问,而是需要通过指针相链接,所以链表节点不仅需要存储数据,还要存储链接指针,这有一定消耗

3.CPU高速缓存命中率更低

3.2 缓存命中率

3.2.1 缓存命中率基础知识

上面我们讲顺序表的CPU高速缓存命中率更高,而链表的CPU高速缓存命中率更低,在讲缓存命中率前,我们先解释计算机的存储体系结构

计算机中的存储分为带电存储和不带电存储两种,如图,从磁盘和内存为界,即L0--L4为带电存储,L5--L6为不带电存储。也就是说,电脑的主存(即内存),电脑的三级缓存寄存器等,都是在有电的情况下才能进行存储,一旦断电都将丢失。而如本地磁盘,网盘等,这些存储是没电也能存

然后是各级存储成本大小+存储大小+存储效率的问题:

我们看,自上到下,依次是寄存器,三级缓存,主存(内存),本地磁盘,网盘,这些存储结构。这个顺序也代表着,越在上层的存储结构,离着CPU越近,例如寄存器就在CPU内部,CPU紧挨着三级缓存,再远是内存,再再再离着CPU远的是磁盘。

你可以这样想,离着CPU越近,CPU在计算的时候,就越容易被CPU获取到数据,即存储效率就越高,从这个角度说,寄存器存储效率最高,三级缓存次之,再慢一点的是内存,最慢的是本地磁盘,网盘。

有的人问:寄存器的存储效率最高,那为什么不把电脑中的所有存储都用寄存器呢,这样CPU取数据的时间不久很少,可以大大提高运算效率了吗?这当然是不可以的,那是因为越上层的存储结构,这些硬件的成本就越高,比如制造寄存器以及三级缓存的成本是最高,其次是内存,再便宜的是磁盘。我们看国内的电脑/手机厂商,都是8+256,16+512,设备当中,硬盘的大小远高于内存的大小,这就是制作成本的问题。成本越高的东西,我们就少用一点,成本越低的东西,我们就多用一点。

所以自上到下,成本就越低,同时在计算机中使用的空间就越多。自下到上,成本就越高,在计算机中的空间就相对更少。

3.2.2 小例子说明CPU与各级存储

我们从一个小例子出发,讲解CPU是如何运行的:

1//比如a,b变量,一开始当然都是存储在内存中的栈区/堆区中的。

2//然后我们CPU要计算a+b,首先要获取a,b变量

3//如果计算大小的话,我们当然要在CPU上,而在CPU上跑,需要把要计算的数据传入到离CPU近的地方存储 ,方便CPU计算的时候取到数据。  

【3.5】//离CPU越近,CPU可以取到要计算的数据的效率就越高

4//如果变量a,b所占内存小一点点,就直接存储到寄存器上;如果a,b变量很大,寄存器放不下,那就要首先借助存储到空间更大的高速缓存当中。

然后CPU在计算运行的时候,就从寄存器/三级缓存中取到数据进行运算

CPU就是家里的厨房,是要进行做饭的,然后做饭要使用材料,材料就是要使用的数据,数据存储在各级存储中,而材料可以存储在妈妈的手上,可以在厨房桌子的盘子上,也可以在附近的冰箱里面,还可以在楼下的小摊中,也可以在更远的超市货架上。相信这样类比着理解更加容易。

3.2.3 顺序表和链表的缓存命中率

 CPU在计算之前,必须从某层存储中取到该数据。而不同层的存储效率是不同的。

我们CPU要取到内存中的数据,首先是把内存中的数据加载到缓存,而我们取数据,比如要取int a,并不是一次很小气的只取int四个字节,而是会把内存中的与变量a的相邻的,比如前10个字节以及后10个字节一同加载到缓存当中

数据在物理内存上是连续的。加载的时候,即数据从内存加载到缓存当中,每加载一次,是一次性取多少字节,所以说,连续存储的数据会很容易被同时加载到进去,比如我们的顺序表,我们要取到a[0]从内存加载到缓存的过程中,在取a[0]的过程中,此时我们会顺便把a[1],a[2]等相邻的元素加载到缓存当中,这也是顺序表数据连续存储的优势。

而链表的各个节点都是不连续存储的,存储在内存的各个地方,所以在访问的时候,就不容易一次性被加到到缓存当中。比如node1加载到缓存中,node2,node3...就不一定会加载到缓存当中。这是链表数据分散存储的劣势。

 如果我们要遍历这个顺序表,就要让CPU取到每个数据,而顺序表的缓存命中率高,加载完a[0]之后,就同时把a[1],a[2]加载到缓存中了,下一次CPU取a[1] a[2]就不用在内存中取,而是直接在缓存中取即可!!!

 如果我们要遍历这个链表,链表由于分散存储,把node1从内存加载到缓存的时候,node2,node3,node4等不一定会同时加载到缓存中,因为他们在物理上不连续一次缓存加载很难都命中,所以每次CPU取数据都要从内存中寻找!!!

 如要了解更多关于CPU缓存的知识,具体可以参考下面这篇文章,看完会有醍醐灌顶的感觉!

与程序员相关的CPU缓存知识 | 酷 壳 - CoolShellhttps://coolshell.cn/articles/20793.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/160263.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【面试题】notify() 和 notifyAll()方法的使用和区别

【面试题】notify() 和 notifyAll()方法的使用和区别 Java中notify和notifyAll的区别 何时在Java中使用notify和notifyAll? 【问】为什么wait()一定要放在循环中? Java中通知和notifyAll方法的示例 Java中通知和notify方法的示例 Java中notify和no…

22年我在CSDN做到了名利兼收

写在前面 hi朋友,我是几何心凉,感谢你能够点开这篇文章,看到这里我觉得我们是有缘分的,因着这份缘分,我希望你能够看完我的分享,因为下面的分享就是要汇报给你听的,这篇文章是在 2022 年 12 月 …

从0到1完成一个Vue后台管理项目(二十三、初代项目完成、已开源)

开源地址 项目地址 项目还在优化,会增加很多新功能,UI也会重新设计,已经在修改啦! 最近打算加一些组件、顺便分享一些好用的开源项目 现在正在做迁移到vue3TS的版本、预计年后会完事,然后迁移到vite、遇到的问题和报…

docker安装prometheus和grafana

docker安装prometheus和grafana docker安装prometheus和grafana 概念简述安装prometheus 第一步:确保安装有docker第二步:拉取镜像第三步:准备相关挂载目录及文件第四步:启动容器第五步:访问测试 安装grafana 第一步&…

分享66个ASP源码,总有一款适合您

ASP源码 分享66个ASP源码,总有一款适合您 66个ASP源码下载链接:https://pan.baidu.com/s/1Jf78pfAPaFo6QhHWWHEq0A?pwdwvtg 提取码:wvtg 下面是文件的名字,我放了一些图片,文章里不是所有的图主要是放不下...&…

Docker容器与镜像命令

文章目录帮助命令镜像命令容器命令其它命令命令总结帮助命令 显示 Docker 版本信息 docker version显示 Docker 系统信息,包括镜像和容器数 docker info 帮助 docker --help 镜像命令 列出本地主机上的镜像 docker images运行结果 REPOSITORY TAG …

Python采集彼岸4K高清壁纸

前言 嗨喽,大家好呀~这里是爱看美女的茜茜呐 又到了学Python时刻~ 环境使用: Python 3.8 解释器 Pycharm 编辑器 模块 import re import requests >>> pip install requests ( 更多资料、教程、文档点击此处跳转跳转文末名片加入君羊,找…

【Leetcode面试常见题目题解】5. 最长公共前缀

题目描述 本文是LC第14题&#xff0c;最长公共前缀&#xff0c;题目描述如下&#xff1a; 编写一个函数来查找字符串数组中的最长公共前缀。 如果不存在公共前缀&#xff0c;返回空字符串 “”。 限制 1 < strs.length < 200 0 < strs[i].length < 200 strs[i] 仅…

数据库 MySQL-window安装和卸载

安装 官网&#xff1a; MySQL :: Download MySQL Community Server 或 MySQL :: Download MySQL Community Server (Archived Versions) 文件目录简述 bin存放了可执行文件&#xff0c;docs是文档&#xff0c;include放的是c语言相关的.h文件&#xff0c;lib是c语言的库文件…

wmv是什么格式?如何录制wmv格式的视频?图文教学

很多小伙伴在使用文件的时候&#xff0c;经常会发现自己的一些文件后缀名是wmv。或者说在工作、学习的过程中&#xff0c;有过被要求使用wmv格式的文件。wmv是什么格式&#xff1f;如何录制wmv格式的视频&#xff1f;今天小编就来详细的跟大家说说。 一、wmv是什么格式&#xf…

SpringBoot复习(一)

底层注解 Configuration 自定义配置类 Bean: 可以通过Bean注解将方法的返回值交给ioc容器来管理 组件id为方法名&#xff0c;组件的类型就是方法的返回类型。 默认组件是单例的 Configuration: 告诉springboot这是一个配置类之前的配置文件 配置类本身也是组件&#xff0c;由s…

【Linux】Makefile/make - 快速理解入门

目录 一、概念理解 1、基本概念 2、举例说明 二、编写 Makefile 1、依赖关系和依赖方法 2、文件清理 3、扩展内容 一、概念理解 1、基本概念 在我们学习 Linux 的过程中&#xff0c;我们可以直接使用 gcc 指令对程序的文本文件逐个进行编译处理&#xff0c;这是因为我…

ASP.NET Core 3.1系列(26)——Autofac中的实例生命周期

1、前言 前面的博客主要介绍了Autofac中的一些注册方法&#xff0c;下面就来介绍一下Autofac中实例的生命周期。之前在介绍ASP.NET Core内置IoC容器的时候说过&#xff0c;实例的生命周期有&#xff1a;瞬时生命周期、域生命周期、全局单例生命周期&#xff0c;而Autofac在这三…

mysql-8.0.31-winx64详细安装教程

一、下载MySQL MySQL官网&#xff1a;https://www.mysql.com/cn/ mysql-8.0.31-winx64下载地址&#xff1a;https://dev.mysql.com/downloads/mysql/ 2、下载结束后&#xff0c;解压到指定目录&#xff0c;笔者存放在D盘 &#xff0c;为求简单&#xff0c;设置目录如下&#…

数据库历史数据年度备份

数据库历史数据年度备份 1、文件说明 matomo_backup.sql 备份库表结构脚本(这个根据自己数据结构准备&#xff0c;对于时间命名的表结构就不要加了&#xff0c;只加非时间命名的表结构) export.sh 数据导出脚本 clean.sh 源数据库历史数据清除脚本 2、需求与思路 需求 对…

怎么把PDF转换成图片?来看看这几个方法吧!

要说我们手机里最多的一种文件格式是什么&#xff1f;那应该就是图片了。相信在智能手机的时代&#xff0c;每个人手机里都会有至少几百上千张照片吧。毕竟有许多的事情我们都希望通过图片、照片的形式来记录下来。所以说如何将其他格式的文件变成图片格式就成了一个不大不小的…

开发那点事(十八)Vue开发PC桌面应用案例

写在前面的话 最近有在研究electron框架&#xff0c;踩了不少坑 &#xff0c;现在把这几天研究的成果分享给大家。 研究成果 vue项目打包成exe可安装程序pc应用版本升级&#xff08;需要配合oss服务器&#xff09; vue应用配置 路由文件base配置为空mode模式为默认的hashv…

智慧门户、信创门户、国产门户、数字化门户,如何构建出七大特色亮点?

作者&#xff1a;郑文平 概述 调研结果显示&#xff0c;世界500强企业100%建设了适合自己的集团门户管理系统&#xff0c;也叫作办公门户或内网门户&#xff0c;并通过统一门户最终提升各自整体的业务管理水平和流转效率&#xff0c;没有建设门户的公司面临如下制约&#xff…

二,Spring IOC以及整合mybatis

0 复习 工厂设计模式 工厂设计模式代替new方式创建对象&#xff0c;目的是解耦合。 Spring做为工厂的使用 applicationContext.xml配置bean标签 如何从工厂中获取对象 //创建工厂 ApplicationContext ctx new ClassPathXmlApplicationContext("classpath:applicationCont…

AWS实战:Aurora到Redshift数据同步

什么是AuroraAmazon Aurora是一种基于云且完全托管关系型数据库服务&#xff0c;与MySQL 和 PostgreSQL 数据库兼容&#xff0c;完全托管意味着自动对数据库进行管理&#xff0c;包括管理数据备份、硬件配置和软件更新等操作Amazon Aurora提供了企业级性能Amazon Aurora提供了多…