单链表复习 (C语言版)

目录

一.顺序表与链表的区别

二.链表概念

三.单链表

1.单链表的开始与初始化

2.单链表的打印

3.单链表的尾插

重难点:单链表实现时的指针详解

4.单链表的头插

5.单链表的尾删

6.单链表的头删

小结:

7.单链表的查找

8.在指定位置前插入数据

9.在指定位置后插入数据

10.删除pos结点

11.删除pos结点之后的结点

12.删除链表

13.可能存在的疑惑解答

 14.全部用于测试的代码

四.文章链接


 

一.顺序表与链表的区别

顺序表的问题及思考:

  1. 中间/头部插入删除,涉及到移动数据,时间复杂度为o(n)
  2. 增容需要申请新空间,拷贝数据,释放旧空间,会有损耗。(realloc)
  3. 增容一般呈2倍的增长,当前容量为100,增加到200,只存放5字节,浪费了95字节
  4. 顺序表中间头部插入效率低下,增容造成运行效率降低
  5. 链表解决了上述问题

顺序表相关文章链接:顺序表复习(C语言版)

f6f0317216824cfe8941ab0aff0724b3.jpg

 上图就形象地表示了线性表与链表的区别,链表存储位置不连续,是用指针相连;线性表(SeqList)在内存中是连续存放的

 

二.链表概念

线性表是一类相同元素的集合,例如苹果和香蕉(都是水果)

逻辑结构:一定线性

物理结构:不一定线性(因为在内存中的存放位置是不连续的)

int a =10;float f =0.1 变量a和f的物理空间不一定连续,即存放位置不一定连续

0c1f39d1dc964dc4b3c867789eca40af.jpg

链表和火车很相似

通过一个钩子连在一起,地址空间是链接在一起的,车头也是车厢,因为它也可以装人

旺季:增加车厢

淡季:减少车厢

链表是由一个一个节点组成,结点可以看成车厢

c048c7bf10a9408a860546cd6ab64c9a.jpg

 plist位置称之为头结点,头结点指向的结点叫做首元结点,最后一个指针域指向NULL的结点叫做尾元结点

结点和节点:同一个东西,随便用哪个

结点由什么组成的呢?

有两个组成部分

1.数据域  --->在该域内存储了数据

2.指针域  --->存储了指向下一个结点的指针

三.单链表

1.单链表的开始与初始化

链表就是在定义链表的结点结构

struct SListNode single list node

{

    int data;                          //int a =10;//int* pa =&a

    struct SListNode* next;            //指向下一个节点的指针

}SLTNode;

typedef int SLTTDataType;

上述代码就是在对链表进行初始化,定义了一个结构体,并在里面定义了一个data还有一个指针next(next指向下一个结点)

void SListTest01()

{

    //创建节点

    SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));

    node1->data = 1;

    SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));

    node2->data = 2;

    SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));

    node3->data = 3;

    SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));

    node4->data = 4;

    //将四个节点连接起来

    node1->next = node2;

    node2->next = node3;

    node3->next = node4;

    node4->next = NULL;

    //调用链表的打印

    SLTNode* plist = node1;

    void SLTPrint(plist); //为了让代码逻辑更加清晰

}

上述代码依旧是对链表的初始化,将四个结点进行创建,然后相连接;在初始化代码写完以后,直接跟了一个链表打印函数

2.单链表的打印

920ec114d1554024aa1a8dbc538c0809.jpg

紧接上文代码,上文中 SLTNode* plist = node1就是在说明,plist是个指针,且与node1一样都是一级指针;因此它指向node1所在的这块空间,这就可以看成让node1和plist两个指针指向同一块空间,且这块空间在早些时候已经通过node1指针初始化完毕了;因此如果创建一个pcur,让他等于plist,那么pcur依旧指向node1所指向的空间,这就可以看出三个指针指向同一块空间

7fd278659e714d43aed2a9cc3fdf06e8.jpg

先是打印pcur所指向空间的data,然后让pcur里指向下一个结点的指针覆盖pcur这个结点,pcur就自然而然向后移动了

e99983d4b84f497582bb0098d17178bf.jpg

重复上述操作,先打印所指向空间的data值,然后再让其指向的下一个结点地址将现在这个结点地址覆盖

084955dd4e1d4191829f3f40f46e67b7.jpg

由上图可知,当pcur指向的空间为NULL时,打印操作完成;由上四图,不难得出下述代码:

void SLTPrint(SLTNode* phead) //传的是首元结点

{

    SLTNode* pcur = phead;

    while (pcur)  //pcur != NULL

    {

        printf("%d->", pcur->data);

        pcur = pcur->next;

    }

    printf("NULL\n");

}

 

3.单链表的尾插

因为在此以后我们需要多次进行单链表的增删查改操作,因此我们可以专门写一个函数,这个函数是专门用来创建新结点的,在顺序表那篇文章中已经讲解过,故在此省略

	SLTNode* SLTBuyNode(SLTDataType x)
	{
		SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode*));
		if (newnode == NULL)
		{
			perror("malloc fail!");
			exit(-1);
		}
		newnode->data = x;
		newnode->next = NULL; //此处指向空指针是为了方便后续操作
	}

接下来就要开始进行尾插操作

89f24ff48fd94fe1a9a10b62150ffa27.jpg

77fc5ec222b34612b9b24215ac2a42b7.jpg

	void SLTPushBack(SLTNode** pphead, SLTDataType x)
	{
		SLTNode* newnode = SLTBuyNode(x);
		//空链表的情况
		if (*pphead == NULL)
		{
			*pphead = newnode; //newnode的data有了,next指向空指针,将pphead地址覆盖,指向新的newnode空间,完成尾插
		}
		//非空链表的情况
		else
		{
			//找尾
			SLTNode* ptail = *pphead; //不想让*pphead变动位置(变动了就需要使用二级指针了),因此又定义了一个变量ptail指针
			while (ptail->next)
			{
				ptail = ptail->next;
			}
			//退出循环就说明已经找到尾了,此时ptail指向空指针
			ptail->next = newnode; //让ptail指向新结点
		}
}

上述代码指针部分较难理解,因此笔者将在下文进行详细解释

重难点:单链表实现时的指针详解

上述代码中,为什么使用二级指针?
在函数中,想要通过形参改变实参的值(即对其进行某些操作,且这些操作是针对该参数而言的,而不是该参数针对别的变量而言的),就必须在传参时传地址。

即使实际参数是一个指针,我们也需要通过指针的地址来改变该指针本身。(如下图所示)

所以,我们在函数传参时,传的是指针的地址,因此需要函数用二级指针来接收。

但请注意,只有二级指针类型的形式参数,在进行了一次解引用操作状态下的改变,才需要用到二级指针类型的形式参数。

此处我们可以类比一级指针和整型变量。要通过形式参数改变实际参数,那么需要形式参数是一个指向需改变变量的指针,然后才可以通过形式参数改变该变量;而想要改变一级指针变量,那么就需要通过二级变量来改变。(前者是结点里的指针域、数据域的内容的改变,后者是结点本身的改变)

所以,如果需要改变结点,就用二级指针;如果只需要改变某个结点指针域、数据域内容,那就传一级指针,此时就没有必要传二级指针了。

上述代码中,为什么有些地方需要解引用,而有些地方不需要呢?

解引用(即*操作符)可以看作是把指针降级(文章链接部分有指针其他内容复习),把二级指针变成一级指针,把一级指针变成指针指向的内容;所以上述代码中,部分地方使用了一次解引用操作,即从指向结点的指针地址变为了指向结点的指针(也可以看成是结点本身)。

为什么单链表的打印不需要二级指针,而单链表的尾插就需要呢?

因为单链表的打印并没有改变链表的首元结点本身,只是完成了打印操作,所以不需要通过形式参数来改变实际参数;而尾插操作需要改变链表的首元结点本身(从NULL变成了newnode),因此需要通过形参来改变实参。

SLTNode* 和 * 的区别是?

SLTNode* 是在告诉编译器,这个变量是个指针,指向了SLTNode这个类型的数据,而并不是在对变量进行解引用操作;*是解引用操作符,在上文已经讲解完毕;而在函数当中,对二级指针进行解引用,其本质上还是个二级指针,因为形式参数依然还是二级指针。

045a54bf69824b03a12d409368acadfe.jpg

 实参形参(前面的*是指函数中使用的解引用操作符个数)
第一个结点的内容*plist(即xxx)

ptail -> xxx

(即一般情况下的**pphead,此处只是因为是结构体指针,所以需要用结构体指针的解引用方式)

指向第一个结点的指针plist*pphead
指向第一个结点的指针的地址&plistpphead
	SLTNode* plist = NULL; //代码1
	SLTNode* plist1 = node1; //代码2
	SLTPushBack(&plist1, 1); //代码3
	SLTPrint(plist1); //代码4
	plist1->next = NULL; //代码5

代码1:结点为空

代码2:一个有效结点

代码3:传输有效结点的地址

代码4:传输有效结点

代码5:对有效结点解引用,结构体里套了一个指向结构体类型(即为其本身)的指针变量,让该变量指向空,这一操作即是让该结点的下一个结点为空

4.单链表的头插

3abd1fb12b9347c194e420fdf3028572.jpg 4f25ddd568754c5a8f5324e4e3ae0bf0.jpg

	void SLTPushFront(SLTNode** pphead, SLTDataType x)
	{
		assert(pphead);
		SLTNode* newnode = SLTBuyNode(x); 
			newnode->next = *pphead; //newnode指向首元结点
			*pphead = newnode; //将头指针指向新创建的结点
	}

 

5.单链表的尾删

88de588083eb4ca795eb5a760e86e716.jpg

	void SLTPopBack(SLTNode** pphead) 
	{
		assert(pphead && *pphead) //指向链表结点的指针不能为空,链表也不能为空
		SLTNode* prev = *pphead; //为防止野指针,要让尾元结点的next指针指向空,因此需要再创建一个指针,来完成这一操作
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			prev = ptail; //prev需要指向尾元结点的前一个结点,因此需要跟着ptail变动
			ptail = ptail->next; //ptail指向尾元结点
		}
		free(ptail); //释放ptail所指向的空间,就能达到尾删的目的
		ptail = NULL; //动态内存开辟内容,下文有链接
		prev->next = NULL; //要让prev里的next指针从指向野指针到指向空指针
	}

 

4b7639d7a4f14bb59bc72213fb92393f.jpg

当只有一个结点的时候,循环直接跳过,ptail和prev指向同一个结点,在将ptail空间释放并变成空指针以后,又一次对prev(即ptail的同一结点)进行了解引用操作,这样代码会报错(对空指针解引用)

因此当链表只有一个节点时,代码如下所示:

	//链表只有一个结点
	if ((*pphead)->next = NULL) //加括号是因为 -> 优先级高于 *
	{
		free(*pphead);
		*pphead = NULL;
	}

尾删的全部代码:

	void SLTPopBack(SLTNode** pphead) 
	{
    assert(pphead && *pphead) //链表不能为空,指向链表结点的指针也不能为空
	//链表只有一个结点
	if ((*pphead)->next = NULL) //加括号是因为 -> 优先级高于 *
	{
		free(*pphead);
		*pphead = NULL;
	}
        else
    {
		SLTNode* prev = *pphead; //为防止野指针,要让尾元结点的next指针指向空,因此需要再创建一个指针,来完成这一操作
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			prev = ptail; //prev需要指向尾元结点的前一个结点,因此需要跟着ptail变动
			ptail = ptail->next; //ptail指向尾元结点
		}
		free(ptail); //释放ptail所指向的空间,就能达到尾删的目的
		ptail = NULL; //动态内存开辟内容,下文有链接
		prev->next = NULL; //要让prev里的next指针从指向野指针到指向空指针
    }
	}

 

6.单链表的头删

dde786103c6c45bcad2d18e0b3569991.jpg cc8648526b6647a383ed9598cf4afc93.jpg

先要保存好头删前的链表第二个节点,头删以后把*pphead指针指向新的节点

void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}
//只有一个结点的情况上述代码也能够解决

小结:

上述的插入、删除代码中,因为四种代码所以必须使用二级指针:
1.free(*pphead);

2.*pphead = NULL;

3.*pphead = newnode;

4.*pphead = next;

即对一级指针类型的参数本身进行了某些操作

7.单链表的查找

在使用查找函数以后,要分为找到了和没找到两种情况,那么我们可以通过如下代码来区分(此处的3是指数据域为3的结点):

SLTNode* find = SLTFind(plist, 3);
if (find == NULL)
{
	printf("没有找到");
}
else
{
	printf("找到了");
}

上述代码中,plist即为首元结点的指针

不需要传输首元结点指针的地址,这是因为并不需要通过查找函数来改变链表的首元结点。 

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* pcur = phead; //不想形式参数是个二级指针,因此可以在函数中定义一个指针变量
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

 

8.在指定位置前插入数据

该函数必须要有三个参数,分别是链表的首元结点(SLTNode** pphead)、在链表的哪个结点前插入(SLTNode* pos)、所需要插入的数据(x)。如下所示:

void SLTInsert(SLTNode** pphead,SLTNode* pos,SLTDataType x)

一个参数使用二级指针,一个参数使用一级指针的原因:

前一个二级指针类型是链表的首元结点地址,是需要通过形式参数来改变首元结点的;下一个一级指针类型是链表指定位置的结点,可以直接通过解引用,对结点的指针域、数据域进行操作。

6fac2faf81f9429c8c9982e47ca39e66.jpg 579a96948e964c49be9432242decbd8f.jpg

 

如果要在第3个结点前插入数据,就需要先创建一个结点,然后把这个结点和第3个结点相连接,然后断开第2个结点和第3个结点的连接,让第2个结点和第3个结点相连。而第2个结点就需要通过遍历获得,循环语句的退出条件是:prev->next != pos (prev为函数中定义的指针变量,遍历以前,prev == *pphead)

aa755b4a649c4be7ad1be126a1474017.jpg

如果是在链表的首元结点之前插入数据,那么prev的next就不可能会等于pos,因此我们在代码实现里要考虑到一般情况和在首元结点之前插入数据两张情况。并且在首元结点前插入数据可以通过头插的函数来实现(这种情况也是函数需要使用二级指针的原因,因为函数需要传一个二级指针)。

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(*pphead && pphead); //链表不能为空,因为为空了,就无法确定“某一位置”了
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	if (pos == *pphead) //在首元结点之前插入
	{
		SLTPushFront(pphead, x);
	}

	else //一般情况
	{
		//找到pos结点的前一个结点
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next; //没有找到就找下一个
		}
		newnode->next = pos; //完成上文的操作
		prev->next = newnode;
	}
}

//pos指针即为上文查找部分出现的find

 

9.在指定位置后插入数据

在指定位置后插入数据,不需要再创建一个指针,直接通过"指定位置"结点的next以及该结点next的next来插入即可

91182df3ec8647e4b229c4a88a65f201.jpg

第一种:先红色,再绿色

第二种:先绿色,再红色

以上两种方式是否相同?

//第一种方式的代码
newnode->next = pos->next;
pos->next = newnode;

//第二种方式的代码
pos->next = newnode;
newnode->next = pos->next;

9d37c24988ea4d60811acab0693d4e2d.jpg

第一种方式可以完成我们需要的操作,即先让新节点指向链表的指定节点的下一个结点(next),然后再让指定节点指向新结点

第二种方式让指定节点指向新节点,此时指定结点的下一个结点即为新节点,因此让新节点指向指定的下一个结点时,即为新节点本身,无法完成需要的操作(如上图所示)

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}


//pos指针即为上文查找部分出现的find

 

10.删除pos结点

删除pos结点需要我们将pos结点的前一个结点和pos的后一个结点相接,因此还需要有个prev指针去保存pos结点的前一个结点,如下图所示 

首元结点、尾元结点的删除请读者自行判断

 ea52e351378b48f48101055f5421b69a.jpg

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);
	//pos是首元结点
	if (pos == *pphead)
	{
		//头删
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
	}
}

//pos指针即为上文查找部分出现的find
//在使用完find指针以后,需要在函数外进行free、置空操作

 

11.删除pos结点之后的结点

删除pos->next,让pos结点和pos->next->next相连即能完成操作,如下图所示: 

ad6dcc92da534b8699185df2fbbbfc13.jpg

void SLTEraseAfter(SLTNode* pos)
{
	assert(pos && pos->next);
	SLTNode* del = pos->next; //保存pos的下一个结点
	pos->next = del->next;  //让pos的下一个结点(即del)和pos的下一个结点(即del)的下一个节点相链接
	free(del);
	del = NULL;
}

 

12.删除链表

d1a63226464048a1821ef0bdcdb021d5.jpg

 ed7295ad506d4d47a02a840a64af2344.jpg

传入首元结点,然后保存好首元结点的下一个结点(next指针),然后free掉pcur以后,让next指针和pcur指针都往后走一个结点,循环退出条件为pcur为空指针。(如上图所示)

void SListDesTroy(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	while (pcur)
	{
		SLTNode* next = pcur->next; 
		free(pcur);
		pcur = next;
	}
	*pphead = NULL; //已经释放完所有的内容了,将pphead置为空
}

 

13.可能存在的疑惑解答

**pphead作为形参出现那么多次,那么他到底对应哪个结点呢?

他既可以对应空指针(链表完全没有结点,对应了插入等的情况),同时也可以对应首元结点(头删和尾删等的情况)

为什么在部分函数里有时候free不需要二级指针?

例如free(ptail),在尾删板块出现。这是因为ptail是在函数里创建、使用的,无需通过函数来改变任何一个实参(它也没有对应的实参、因为它并不是一个形式参数)。在出了函数以后,ptail指针就没有作用了,如果在函数内没有进行free、置空操作,就会变成一个野指针。 

单链表的开始和初始化必须要像1那样写吗?
并不是,其实完全可以通过尾插、头插等等方式进行链表的初始化的。

所有函数都必须要传二级指针吗?

有些一定需要传,例如*pphead=newnode(在头插板块),因为这条代码如果不写在函数里,就完成不了头插操作;在某些情况下不一定需要传,例如free(*pphead),放到函数实现外的部分,可以通过free(plist)完成操作,不写在函数里也不会影响函数的功能实现。

如果打印函数里使用形参进行打印,不创建变量了,是否可以?
其实是可以的,因为形参是一级指针,不会影响实参。

在函数中,定义一个变量pcur = *pphead了以后,为什么影响了pcur就能影响整个链表了?

在函数当中,实际上pcur能直接看成pphead解引用了一次,即可以把所有的pcur看成是(*pphead),这就像是定义宏一样;这就像是传入了一个一级指针  int *a,然后在函数了假设变量int b = *a ,然后通过b的改变来改变传入进来的*a一样。而我们会在函数里使用到pcur = *pphead有两个原因,其一就是为了代码的可读性,其二就是为了让pphead指针一直指向首元结点。

 14.全部用于测试的代码

请注意,以下代码有些是不兼容的,就比如plist指针一会是NULL,一会是首元结点;同时有些功能,例如单链表的查找,不能链表是空的,需要有前提条件。因此请读者对函数进行一一尝试,需尝试的功能直接取消注释即可。

int main()
{
	//SListTest01(); //对于开始和初始化

	//SLTNode* plist = NULL; //从没有链表开始
	对于尾插
	//SLTPushBack(&plist, 1);
	//SLTPrintf(plist); //此处plist又变成了首元结点,在使用完该操作以后,plist由于
	//SLTPushBack(&plist, 2);
	//SLTPrintf(plist);
	//SLTPushBack(&plist, 3);
	//SLTPrintf(plist);
	//SLTPushBack(&plist, 4);
	//SLTPrintf(plist);
	对于头插(尾插、头插写一块了)
	//SLTPushFront(&plist, 6);
	//SLTPrintf(plist);
	//SLTPushFront(&plist, 2);
	//SLTPrintf(plist);
	//SLTPushFront(&plist, 3);
	//SLTPrintf(plist);
	//SLTPushFront(&plist, 4);
	//SLTPrintf(plist);

	尾删、头删(先决条件:不为空)
	//SLTPopBack(&plist);
	//SLTPrint(plist);
	//SLTFrontBack(&plist);
	//SLTPrint(plist);


	查找(先决条件:不为空)
	//SLTNode* find = SLTFind(plist, 3);
	//if (find == NULL)
	//{
	//	printf("没有找到");
	//}
	//else
	//{
	//	printf("找到了");
	//}

	指定位置的插入删除(先决条件:不为空+有具体指定位置)
	//SLTNode* find = SLTFind(plist, 3); //find即为具体指定位置
	//SLTInsert(&plist, find, 10);
	//SLTPrint(plist);
	//SLTInsertAfter(find, 10);
	//SLTPrint(plist);
	//SLTErase(&plist, find);
	//SLTPrintf(plist);
	//SLTEraseAfter(find);
	//SLTPrintf(plist);

	//销毁
	//SListDesTroy(&plist);
	//SLTPrintf(plist);
}
//以上代码是在vscode2022中进行的

 

四.文章链接

指针讲解:指针复习

动态内存开辟讲解:动态内存开辟复习

 

 

 

 

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

制作AI问答机器人:从0到1的完整指南

在数字化转型的浪潮中,企业正追求更高效、智能的客户服务解决方案。AI问答机器人以其快速响应、全天候服务和持续学习的能力,成为了提升客户满意度和加速业务发展的关键工具。本文将深入探讨如何制作一个企业级的AI问答机器人,并强调其功能体…

【Linux】(五)—— SSH远程登录和XShell使用

SSH Linux中的SSH(Secure Shell)是一个强大的网络协议,用于在不安全的网络环境中提供安全的远程登录和资料拷贝等其他网络服务。以下是有关Linux中SSH的关键点和操作指南: SSH的基础概念 安全性:SSH通过对所有传输的…

编程规范-代码检测-格式化-规范化提交

适用于vue项目的编程规范 – 在多人开发时统一编程规范至关重要 1、代码检测 --Eslint Eslint:一个插件化的 javascript 代码检测工具 在 .eslintrc.js 文件中进行配置 // ESLint 配置文件遵循 commonJS 的导出规则,所导出的对象就是 ESLint 的配置对…

【Pytorch】一文向您详细介绍 torch.Tensor() 的常见用法

【Pytorch】一文向您详细介绍 torch.Tensor() 的常见用法 下滑即可查看博客内容 🌈 欢迎莅临我的个人主页 👈这里是我静心耕耘深度学习领域、真诚分享知识与智慧的小天地!🎇 🎓 博主简介:985高校的普通…

WebAPI AOP方式 异常方式 FilterAttribute、ActionFilterAttribute

》》 自定义异常处理特性 using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web; using System.Web.Http.Filters;namespace WebApplication11 {/// <summary>/// 异常处理特性/// </sum…

Transformer动画讲解:注意力计算Q、K、V

暑期实习基本结束了&#xff0c;校招即将开启。 不同以往的是&#xff0c;当前职场环境已不再是那个双向奔赴时代了。求职者在变多&#xff0c;HC 在变少&#xff0c;岗位要求还更高了。提前准备才是完全之策。 最近&#xff0c;我们又陆续整理了很多大厂的面试题&#xff0c…

九种mfc140u.dll丢失的解决方法,全面解决mfc140u.dll文件丢失

mfc140u.dll是 Microsoft Visual C 2015 Redistributable 的一部分&#xff0c;它与 Microsoft 基础类库&#xff08;MFC&#xff09;的 Unicode 版本有关。当您在运行使用 Visual C 2015 开发的应用程序时&#xff0c;可能会碰到关于mfc140u.dll丢失的错误。下面列出了一些解决…

WPF前端:一个纯Xaml的水平导航栏

效果图&#xff1a; 代码&#xff1a; 1、样式代码&#xff0c;可以写在窗体资源处或者样式资源文件中 <Style x:Key"MenuRadioButtonStyle" TargetType"{x:Type RadioButton}"><Setter Property"FontSize" Value"16" />…

Zabbix实现邮件和钉钉实时告警(使用python脚本)

告警和通知 告警是监控的主要职能,是指将到达某一阈值事件的消息发送给用户,让用户在事件发生的时候即刻知道监控项处于不正常状态,从而让用户来决定是否采取相关措施。 zabbix中,告警是由一系列的流程组成的,⾸首先是触发器到达阈值,接下是Active对事件信息进行处理,其…

谁能赢?阿里的通义 VS 百度的文心

关注卢松松&#xff0c;会经常给你分享一些我的经验和观点。 国产AI大模型领域&#xff0c;当前有两大阵营&#xff1a; (1)以百度文心一言为代表的闭源大模型。李彦宏曾说过&#xff1a;AI大模型开源意义不大&#xff0c;百度绝不抢开发者饭碗。 (2)以阿里通义AI为代表的开…

个股场外期权个人如何参与买卖?

个股场外期权作为一种金融衍生品&#xff0c;为个人投资者提供了多样化的投资选择和风险管理工具。想要参与个股场外期权的买卖&#xff0c;以下是一些关键步骤和考虑因素。 文章来源/&#xff1a;财智财经 第一步&#xff1a;选择合适的金融机构 首先&#xff0c;个人投资者需…

Android 常用开源库 MMKV 源码分析与理解

文章目录 前言一、MMKV简介1.mmap2.protobuf 二、MMKV 源码详解1.MMKV初始化2.MMKV对象获取3.文件摘要的映射4.loadFromFile 从文件加载数据5.数据写入6.内存重整7.数据读取8.数据删除9.文件回写10.Protobuf 实现1.序列化2.反序列化 12.文件锁1.加锁2.解锁 13.状态同步 总结参考…

17- Redis 中的 quicklist 数据结构

在 Redis 3.0 之前&#xff0c;List 对象的底层数据结构是双向链表或者压缩列表&#xff0c;然后在 Redis 3.2 的时候&#xff0c;List 对象的底层改由 quicklist 数据结构实现。 其实 quicklist 就是【双向链表 压缩列表】组合&#xff0c;因为一个 quicklist 就是一个链表&…

视频修复工具,模糊视频变清晰!

老旧视频画面效果差&#xff0c;视频效果模糊。我们经常找不到一个好的工具来让视频更清晰&#xff0c;并把它变成高清画质。相信很多网友都会有这个需求&#xff0c;尤其是视频剪辑行业的网友&#xff0c;经常会遇到这个问题。今天给大家分享一个可以把模糊视频修复清晰的工具…

【ffmpeg】本地格式转换 mp4转wav||裁剪mp4

个人感受&#xff1a;太爽了&#xff01;&#xff01;&#xff01;&#xff08;可能用惯了转换网站和无良的转换软件&#xff09; ———— 使用FFmpeg把mp4文件转换为WAV文件 - 简书 (jianshu.com) FFMPEG 视频分割和合并 - 简书 (jianshu.com) ———— 示例 ffmpeg -i …

C# MemoryCache 缓存应用

摘要 缓存是一种非常常见的性能优化技术&#xff0c;在开发过程中经常会用到。.NET提供了内置的内存缓存类 MemoryCache&#xff0c;它可以很方便地存储数据并在后续的请求中快速读取&#xff0c;从而提高应用程序的响应速度。 正文 通过使用 Microsoft.Extensions.Caching.Me…

树的重心-java

主要通过深度优先搜索来完成树的重心&#xff0c;其中关于树的重心的定义可以结合文字多加理解。 文章目录 前言☀ 一、树的重心☀ 二、算法思路☀ 1.图用邻接表存储 2.图的遍历 3.算法思路 二、代码如下☀ 1.代码如下&#xff1a; 2.读入数据 3,代码运行结果 总结 前言☀ 主…

【中间件系列】浅析redis是否适合做消息队列

文章目录 一、简单的list消息队列1.命令示例2.伪代码示例3.方案优劣 二、Pub/Sub发布订阅1.消息丢失2.消息堆积 三、相对成熟的Stream1.redis命令介绍2.多消费者组测试3.Stream会持久化吗&#xff1f;4.消息堆积如何解决&#xff1f; 总结 用redis也是比较久了&#xff0c;并且…

AI数据分析:用deepseek根据Excel数据绘制分裂饼形图

工作任务&#xff1a;要绘制下面表格中月活用户占比的分裂饼形图 在deepseek中输入提示词&#xff1a; 你是一个Python编程专家&#xff0c;要完成一个Python脚本编写的任务&#xff0c;具体步骤如下&#xff1a; 读取Excel文件"F:\AI自媒体内容\AI行业数据分析\poetop5…

保姆级教程:以SAR图像目标检测为例

一、项目出发点 AI Studio为我们提供了免费的GPU资源&#xff0c;当我们在NoteBook环境中把代码调试成功后&#xff0c;通常一个训练任务耗时较长&#xff0c;而Notebook离线运行有时长限制&#xff0c;一不小心就容易被kill掉。 如何解决这一问题&#xff1f; 后台任务帮到…