数据结构之线性表中的单链表【详解】

news2024/10/5 16:32:59

文章目录

  • 前言:
  • 一、单链表
    • 1.单链表和顺序表的优缺点
    • 2.单链表的概念和学习
    • 3.单链表的各个接口的实现(详解每一步)
      • 3.1.先铺垫一下大致的思路
      • 3.2.然后这边我们看一下我们大致要实现的函数有哪些
      • 3.3.接下来我们就开始实现这些代码,并且附上详细的解释(注释5000字真的很详细)
      • 3.3.1.首先就是打印接口
      • 3.3.2.尾插接口
      • 3.3.3.新增结点接口
      • 3.3.4.头插接口
      • 3.3.5.头删接口
      • 3.3.6.尾删接口
      • 3.3.7.查找接口
      • 3.3.8.任意pos位置插入接口
      • 3.3.9.任意pos后位置插入接口
      • 3.3.10.任意pos位置删除接口
      • 3.3.11.任意pos位置后删除接口
      • 3.3.12.销毁接口
    • 4.整体图片如下
      • (1.)接口图片如下
      • (2.)调试图片如下:

前言:

北京时间 2022/11/26 星期六的下午,神奇学校核酸4天3检,非常打乱时间安排,今天这篇博客的产出算是我从核酸排队时间中挤出来的(在这首先得感谢帮我解决了小问题的网上大佬,还有就是我们班班长的宽容),并且在我们学完顺序表之后一定要自己去想,自己去实现一遍(单链表也是),只有这样你才可以学得更加的扎实一些,所以有觉得自己顺序表还不熟的同学,可以回去再把顺序表复习一下。

一、单链表

1.单链表和顺序表的优缺点

1.1.首先我们讲一下单链表和顺序表各自的优点和缺点,从而引出我们今天的主题单链表

1.1.1.顺序表的缺点:1.空间不够了,需要扩容,扩容是有消耗的 2.头部或者中间位置的插入删除,需要挪动,挪动数据也是有消耗的(效率很低)3.避免频繁的扩容,一次一般都是按倍数进行扩容,可能存在浪费
1.1.2.顺序表的优点:1.支持随机访问(随机访问的好处:有些算法,需要结构支持随机访问,比如:二分查找、优化的快排等等)

1.1.3.单链表的缺点:1.每一个数据,都要在存一个指针去链接后面的数据结点 2.不支持随机访问,就是:要访问最后一个数据,我不能直接访问,我需要一个一个的向后链接的同时到了我需要的那个数据才可以访问(但是如果是顺序表就可以直接用下标直接访问第i个)
不好理解就以图为例,如图:
在这里插入图片描述

1.1.4.单链表的优点:1.按需申请空间,不用了就释放空间(更加合理的使用空间) 2.头部中间插入删除数据,不需要挪动数据 3.不存在空间浪费

2.单链表的概念和学习

2.1.所以此时我们针对着顺序表的缺点我们就设计出了单链表,当然单链表也是有很多缺点的,所以又设计出来双链表和循环链表等等,所以没有东西是完美的,有利有弊(但是只要我们思想不滑坡,办法总比困难多)

2.2.但是我们不应该想着不需要学习顺序表和链表而直接去学那种优点大于缺点的数据结构,(就像是加法都还不会,自然不能想着学乘法是吧),所以我们应该从简单的问题出发,打好基础,才可以完成以后的更高级的数据结构(二叉树,之类的)

2.3.所以我们这边就开始学我们的单链表:但是其实我们以前在学结构体的时候,那时候我们涉及过一些的有关链表的知识,但是只是知道有这个东西,实现起来还是有一些的绕的,所以我们在解题或者是实现接口的时候(最好是要话一个图出来,这样可以更好的利于我们解题),然后等我们学到了更高级的数据结构的时候就更应该画图(画图就是在帮助我们自己走出苦海,解救我们的生命,切记!切记!)

2.4 单链表的概念:就是通过创建结构体,使结构体中的成员变量中有一个指针变量,通过这个指针变量对每一个结构体(结点)进行链接在一起-》这就是所谓的链表,这个是逻辑方法以指针为参考的理解方法,然而在实际的内存之中却是另一种方法存在,意为物理方法:内存中的真实存储

2.4.1.逻辑方法图:
在这里插入图片描述
2.4.2.物理方法图:
在这里插入图片描述
2.5.并且栈上的内存是我定义一个有一个的,所以还是属于静态的但是此时我想有动态的(所以我的链表其实本质上还是向堆上申请空间的,插入一个数据我就开一个结点,插入一个数据我就开一个结点)所以数据结构在申请内存的时候都是在堆上申请的(链表自然也是),所以我们的链表的内存是从推上获取的

3.单链表的各个接口的实现(详解每一步)

介绍完了基本的概念我们这边就开始实现接口

3.1.先铺垫一下大致的思路

首先不管你学什么鬼,我们第一步还是要定义一个结构体出来
在这里插入图片描述
这个就是一个较为完整的结构体的定义,详解见顺序表中的结构体的定义,这边我们就不讲了

3.2.然后这边我们看一下我们大致要实现的函数有哪些

如图:
在这里插入图片描述

3.3.接下来我们就开始实现这些代码,并且附上详细的解释(注释5000字真的很详细)

3.3.1.首先就是打印接口

void SListPrint(SLTNode* phead) //此时的这个phead的意思就是我这个链表的指向第一个结点的指针(所以叫头指针的意思)
{
	//下面这步的详细理解就是:此时我的phead是一个结构体指针,此时这个指针指向了我的第一个结点(就是第一个结构体(也就是第一块空间)),然后我用cur这个指针去指向phead这个指针
	//的意思其实就是此时cur也跟phead一样也指向了第一个结点(原理就是:指针指指针得到的是两个指针指向了同一块空间),所以此时cur也指向了第一个结点
	SLTNode* cur = phead;  //在写链表的时候第一步就一定要拿创建一个同类型的指针来指向我们的头指针(然后利用这个指针来进行一系列指针的相互关联)

	while (cur != NULL)
	{
		if (cur->next != NULL)//这个条件的设定就是为了把最后一个数据后面的->去掉,因为不好看
		{
			printf("%d->", cur->data);
		}
		else if(cur->next == NULL)
		{
			printf("%d", cur->data);
		}
		//下面这步的理解有两种方法(逻辑方法,物理方法),逻辑方法就是指针指向,物理方法就是空间的存储
		cur = cur->next;//反正我的数据能够连续的存储,靠的就是这步(有了这步此时的cur就像是有了翅膀的鸟,就可以随便飞来飞去了,就可以不断的向后指向我的data数据)
		//这步的具体理解不仅要理解它自己(还要把next这个指向结构体的指针拿过来一起理解,根据上面我对next指针的理解:next指针就是一个指向下一个同类型结构体的一个指针(然后这个next指针指向的那个同类型的结构体中又有一个next指针,此时这个next指针就像是data一样,data有几个此时我的next指针就有这个))
		//所以此时可以把我的next指针等价于我的data数据,我想让这些data数据连续存放,我首先就要让我的next指针变的连续起来,所以此时就引入了一个cur指针(所以这个指针的作用就是让我的next指针变的连续(起了一个媒婆的作用),所以有了这个cur指针),我就可以把我的next指针赋给它,此时的这个cur(就跟data数据一样有了无数个,想指向那个结构体就指向那个结构体,想指向那个数据,就指向那个数据)
		
	}
}

不理解就看注释,注释很详细

3.3.2.尾插接口

void SListPushBack(SLTNode** pphead, SLTDataType x)//此时这个函数的意思就是我想在这个SList链表后面再插入一个结点(也就是再插入一个结构体的意思),但是为了学数据结构以后的结构体就理解成一个结点就行
{
	assert(pphead);
	//这边会涉及到一个二级指针的问题(主要就是关于参数传递时的那些事)(例如:我想传参的同时可以让外部影响到我的内部参数,此时我就要进行一个地址的传递)
	//所以这边就涉及到如何传地址,例如:我想改变整形(就把这个存放整形的变量以地址的形式传给函数调用),我想改变一个指针(此时就要把这个指针变量的地址以地址的形式传给函数调用),只有进行这样的传参才可以让函数内部的变量改变我函数外部的变量
	//所以此时就是利用这个开辟结点这个函数帮我么弄一个新的结点出来
	SLTNode* newnode = GreateListNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;//这步的意思就是表示,此时我的phead已经是一个空指针了,此时里面什么东西都没有时,就刚好可以直接指向我新开辟出来的这个结点,这样它就有东西了
	}
	else
	{
	   //这边讲到为插数据的关键(就是尾插时一定要让此时的指向尾的指针指向我想要插入的那个结构体数据,然后把这个我新插入的数据赋值成NULL ,这样才顺利的完成了我的尾插工作)
       //所以在我们进行尾插时(第一步就是应该要先找到我们此时的尾是那个数据(以NULL空指针作为判断条件))
       //然后为了找到我的尾,此时话不多说,我们应该要先定义一个指针出来指向我的第一个结点(然后因为这个指针几乎每一次都要使用(所以为了防止重定义,这边就换了一个名字而已),不然就和上面那个cur指针一样,就会导致重定义,仅此而已)
		SLTNode* tail = *pphead;//此时的这个*号就是表示因为此时传参传过来的是一个地址,所以此时我一定要加上这个*号(表示解引用的意思),只有这样我才可以让我的tail指针指向这块空间(而不是地址(如果是地址就出问题了,就会变成地址存地址(那就完蛋),所以必须是地址和空间,因为指针的本质上就是一个地址嘛))

		while (tail->next != NULL)//找尾
		{
			tail = tail->next;//这个还是那个意思(给鸟加一个翅膀而已)(其实就是把next赋给tail的意思,不要给这句tail->next吓到)

		}
	  //tail = newnode->next;这样写就不对了,因为这样写我拿到了就不是newnode这个结构体的地址,拿到的就是后面的地址了,所以不能这样写
		tail->next = newnode;

	}
}

3.3.3.新增结点接口

SLTNode* GreateListNode(SLTDataType x)
{
	//所以此时就开辟一个新的结点(也就是开辟一个新的结构体出来的意思)
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//这步的这个计算大小sizeof要注意一下(我此时结点是一个结构体,所以此时是开辟一个结构体的大小,所以sizeof里面是一个结构体变量,不敢天天都是int变量)
	//新的结点开辟出来之后,就是对这个结构体中的变量先进行我需要的处理,但是在进行对这个空间处理的情况下,一定要判断一下这个空间到底有没有开辟成功
	if (newnode == NULL)
	{
		printf("malloc fail\n");
		exit(-1);//表直接退出
	}

	newnode->data = x;  //这个就是表示此时结构体中的数据
	newnode->next = NULL;//这个就是表示此时结构体中的那个指针

	return newnode;
}

3.3.4.头插接口

void SListPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	//此时这个不管是头插还是尾插此时都是要新增加一个结点出来的,所以此时如果每一个函数都写(就很重复的感觉,所以此时把新增结点给弄成一个函数,这样就很完美)
	SLTNode* newnode = GreateListNode(x);

	newnode->next = *pphead;//这个就是把此时的头指针的地址给newnode这个结点中的next指针(这样此时的newnode中的next指针就成为了头指针)
	*pphead = newnode;//这个的意思就是把newnode的地址给pphead这个指针(因为此时的pphead已经不是头指针了,是第二个结点的指针,所以此时第二个结点中的指针就应该要存放头结点的地址)
}

3.3.5.头删接口

void SListPoptFront(SLTNode** pphead)//此时根据图(或者说是根据原理)可以得到此时的头删函数不管是一个结点还是多个结点,此时都可以很好的完成任务(唯独只有当没有结点的时候不能完成任务),所以此时还要给一个不为空的条件
{
	assert(pphead);
	if (*pphead == NULL)
	{
		printf("node is empty");
		return;
	}
	else
	{
		SLTNode* next = (*pphead)->next;//这边要注意优先级,所以要给一个括号
		free(*pphead);
		*pphead = next;//这个就是表示把此时的头指针赋给*pphead这个指针
	}

}

3.3.6.尾删接口

void SLTistPoptBack(SLTNode** pphead)
{
	assert(pphead);
	//此时分类讨论(三种情况讨论)
	if (*pphead == NULL)//我选assert(*pphead != NULL)
	{
		return;
	}
    if ((*pphead)->next == NULL)//这个就是表示此时只有一个结点,此时就是一个头删(所以可以直接进行free释放掉赋值成NULL就行了)
	{
		free(*pphead);
		*pphead = NULL;

	}
	else
	{
		SLTNode* prev = NULL;
		SLTNode* tail = *pphead;
		
		//找尾
        //1.
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		while (tail->next != NULL)//while(tail->next);一个道理(更简便)
		{
			//判断
           //if (tail->next != NULL)//这个表示的是我已经找到最后一个结点了(因为只有最后一个结点中的指针是NULL空指针)(但是只要自己明白就行了,不敢写出来)

			prev = tail;//每次把我的位置赋给这个指针,然后再进行移动
			tail = tail->next;
		}
		//	//这边不敢这样写,这样写就会导致把最后一个结点的data数据给变成一个随机值(然后我此时的上一个结点中的那个指针:就会变成一个野指针)
		//	/*free(tail);
		//	  tail = NULL;*/   //一定要把移动前的位置赋给另一个指针,让那个指针最后成为尾,不然就不能这样写
		//	  //更不敢这样写,此时的这个next本来就是一个NULL,你free它不还是一眼
		//	  //free(tail->next);
		//	  //tail->next = NULL;
		//	  //所以应该按下面这样写(再使用一个指针来记录我此时的位置)
		//	  //并且此时如果上面那个循环结束就表示已经找到了
		{
			free(tail);
			tail = NULL;
			prev->next = NULL; //这个就是表示尾结点的上一个位置,然后此时把这个结点的指针赋值成NULL,就表示此时它变成了我的尾结点
		}

3.3.7.查找接口

SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)//表示找到了
		{
			return cur;//就返回此时这个指针(只要有这个地址的返回就一定还要有 SLTNode* 或者 int* 反正就是一点要有一个* 表示此时是一个地址,然后类型最好是用结构体类型)
		}
		else//此时就是表示第一次找的时候并没有找到这个结点中的数据
		{
			//那么就应该继续往后找
			//所以这变就是写
			cur = cur->next;
		}
	}
	//这个表示到最后都找不到,就是无这个结点数据
}

3.3.8.任意pos位置插入接口

void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(pos);
	//首先不啰嗦就是开辟一个新的结点出来
	SLTNode* newnode = GreateListNode(x);
	if (*pphead == pos)//此时的这个意思就是说明,如果pos是第一个元素,就说明是头插
	{
		//下面这个就是一个头插的意思
		newnode->next = *pphead;
		*pphead = newnode;
	}
	//找到pos的前一个位置(为什么要找pos的前一个位置呢?原因就是:我要在pos这个位置插入一个数,那么我就要有pos和pos前一个两个结点,才可以进行插入点操作)
	//这样才可以很好的实现指针的交换,进而实现位置的插入(所以我应该要怎样去找呢?就要使用找尾的那种方式去找(条件为pos))
	else
	{
		SLTNode* posPrev = *pphead;
		while (posPrev->next != pos)//这步就是在判断如何找我的pos前面的那个结点(一定要理解我这个判断条件的意思,意思就是pos前面那个结点的next指针其实是指向第三个结点的,所以只要当这个next指针=pos时,就是说明了此时我找到了pos前一个结点,此时循环停止)
		{
			//posPrev = pos->next;这步很关键,这步不敢写成这个样子(要去真正的理解这步的意思,这个指针的指向的地址一定要搞明白来)
			posPrev = posPrev->next;//这步的理解一定还要去结合上SLTNode* posPrev = *pphead;这步的理解才行,因为这个东西是在一直的往后走,此时就有点像是尾插里面的这步tail = tail->next;就是给鸟来一个翅膀而已,让它可以动起来

		}
		posPrev->next = newnode;//这两步就是在进行我的结点的交换
		newnode->next = pos;

	}
}

3.3.9.任意pos后位置插入接口

void SListInsertAfter(SLTNode** pphead, SLTNode* pos, SLTDataType x)//此时是因为在pos的后面插入,所以此时这个函数不需要把第一个结点的地址传过来给我
{
	assert(pphead);
	SLTNode* newnode = GreateListNode(x);
	newnode->next = pos->next;//这步是为什么呢?原因就是:此时的pos的next指针指向的就是下一个结点的位置,所以此时就是先把下一个结点的位置赋给我的新结点,这样就可以在pos的后面新插入一个结点了,(然后此时再让这个新结点去指向我的刚刚的pos后面的那个结点,此时刚刚pos后面的那个结点就变成立第三个结点,所以我就成功的在pos后面插入了一个结点)

	pos->next = newnode;//所以此时可以看出我的这个pos->next指针是非常有用的(因为它不仅要可以帮我找到pos后面的那个位置,还可以存放此时的新结点的空间的位置),有两个重要作用
	//此时有了这个新结点,我们就可以开始操作了
	//因为两种不同的情况,所以我们先判断
}

3.3.10.任意pos位置删除接口

void SListDele(SLTNode** pphead, SLTNode* pos)   //时间复杂度:O(N)    //不好效率太低,所以删除后一个就会更加的合适
{
	assert(pphead);
	assert(pos);//这两个位置就都是为了防止别人用错了(传过来一个空地址或者传过来一个空位置,所以都可以进行断言)

	if (pos == *pphead)
	{
		SLTNode* next = pos->next;
		free(*pphead);
		*pphead = next;
	}
	else//也就是我删的结点是中间或者最后一个的(非头)
	{
		//按照原理:我应该就一定要找到我pos的前面的那个结点,才可以删(显然如果我不找到pos前面的那个结点,此时我肯定是无法继续将我的链表链接起来的,所以一定要找到)
		SLTNode* tail = *pphead;
		//while (tail->next->next != NULL)//这步不敢这样写,这样写就是在找尾的过程(因为此时我要找到的是pos的前一个,不是尾的前一个,所以这边这样写肯定是有错的)
		while (tail->next != pos)//这步的意思就是:在找pos的前一个结点
		{
			tail = tail->next;//表示没找到就一直找
		}
		//找到了就来到这个位置,进行操作:
		tail->next = pos->next;//这步的意思就是:把pos后面的那个结点赋给pos前面那个结点(这样pos就可以滚了(没有此时pos这个位置我的链表也还可以链接在一起))
		free(pos);
		pos = NULL;//好习惯

	}
}

3.3.11.任意pos位置后删除接口

void SListDeleAfter(SLTNode* pos)
{
	assert(pos->next);
	assert(pos);
	//1.自己的写法(都是可以的)
	SLTNode* next = pos->next->next;
	free(pos->next);
	pos->next = NULL;
	pos->next = next;

	//SLTNode* next = pos->next;
	//pos->next = next->next;
	//free(next);
	//next = NULL;
	
}

3.3.12.销毁接口

void SListDestroy(SLTNode** pphead)
{
	//此时的这个销毁并没有想象的那么的简单,因为这个涉及到内存泄露的问题
	//原因:
/*	free(*pphead);
	*pphead = NULL;*///如果直接这样写,就会有问题(问题:我只释放了头结点的空间,头结点后面的空间我并没有释放掉,所以造成了内存泄露)
	//所以在释放内存的时候,我用我的屁屁想,这里也要给一个循环
	SLTNode* cur = *pphead;
	while (cur != NULL)
	{
		SLTNode* next = cur->next;//反正总的来说就是我们要一个一个的释放,然后每次释放前都一定要把下一个结点的位置先给保留下来,不然就找不到下一个结点,就内存泄露
		free(cur);
		cur = next;

	}
	*pphead = NULL;

}

这边我们并没有具体的去实现菜单函数和整体的一个功能(感兴趣可以自己尝试)

4.整体图片如下

(1.)接口图片如下

在这里插入图片描述

(2.)调试图片如下:

在这里插入图片描述

把一个一个功能分开调式,方便测试自己的代码到底是哪里出了问题,方便我们解决问题

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

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

相关文章

用信号量实现进程同步与互斥(含代码分析)

信号量简单的来说就是一个变量,代表着系统中互斥资源的数量,通常用原语来实现对信号量机制的操作。 一对原语:wait(S)也称为P操作,singnal(S)也称为V操作。S表示信号量 对于wait原语本身的内部逻辑代码如下(这里以一…

黑苹果外接显示器最优解决方案

黑苹果无法外接显示器 黑苹果外接显示器解决方案 先给解决方案 电脑端 > 安装 PC端 Duet Display买个二手电视盒子40块钱左右,还带电源电视盒子 > 安装安卓端 Duet Display(U盘安装就行)电视盒子 > 用鼠标开启开发者模式双头USB&…

SpringBoot SpringBoot 原理篇 1 自动配置 1.12 bean 的加载控制【注解式】

SpringBoot 【黑马程序员SpringBoot2全套视频教程,springboot零基础到项目实战(spring boot2完整版)】 SpringBoot 原理篇 文章目录SpringBootSpringBoot 原理篇1 自动配置1.12 bean 的加载控制【注解式】1.12.1 问题引入1.12.2 bean的加载…

【JVM技术专题】「原理专题」全流程分析Java对象的创建过程及内存布局

前言概要 对应过程则是:对象创建、对象内存布局、对象访问定位的三个过程。 对象的创建过程 对象的创建方式 java中对象的创建方式有很多种,常见的是通过new关键字和反射这两种方式来创建。除此之外,还有clone、反序列化等方式创建。 通过n…

CSS @property,让不可能变可能

本文主要讲讲 CSS 非常新的一个特性,CSS property,它的出现,极大的增强的 CSS 的能力! 根据 MDN -- CSS Property,property CSS at-rule 是 CSS Houdini API 的一部分, 它允许开发者显式地定义他们的 CSS 自定义属性&…

vue项目分环境配置打包处理

vue项目分环境配置打包处理 目录 vue项目分环境配置打包处理 本地环境配置 生产环境配置 打包处理 打包配置处理(扩展) 本地环境配置 定义需要的变量,这里定义了各种变量标识,可参考使用,自定义项目需要的变量&…

设计问卷调查有哪些技巧?

调查问卷可以很好地帮助我们进行市场调研,所以想要做出一份有效的调查问卷,首先要确定问卷的主题。明确的主题就是定基调,可以让我们的后续过程更加顺利。然后我们再开始进行题目的设置和问卷的设计等动作。不过,在问卷设计的过程…

【js】【爬虫】fetch + sessionStorage 快速搭建爬虫环境及各种踩坑

文章目录导读需求开发环境fetch介绍为什么选择fetchfetch的封装使用数据存储数据访问封装多页面处理方案数据过大,拆分处理参考资料导读 需求 一说爬虫,很多人都会向导python,不过,真正省心的方案,应当是通过js控制获…

Reactive源码分析

Reactive用来绑定引用数据类型, 例如对象和数组等,实现响应式。 Reactive API 接口 export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>相关API包括readonly、createReactiveObject、shallowReadonly、isReactive、toReactive。源码运…

Eureka注册中心

文章目录一、认识服务提供者和服务调用者二、Eureka 的工作流程三、服务调用出现的问题及解决方法四、搭建 eureka-server五、注册 user-service、order-service六、在 order-service 完成服务拉取&#xff08;order 模块能访问 user 模块&#xff09;七、配置远程服务调用八、…

从入门到进阶!当下火爆的大数据技术及算法怎么还能不知道 一起来学习互联网巨头的大数据架构实践!

大数据被称为新时代的黄金和石油&#xff0c;相关技术发展迅猛,所应用的行业也非常广泛&#xff0c;从传统行业如医疗、教育、金融、旅游&#xff0c;到新兴产业如电商、计算广告、可穿戴设备、机器人等。大数据技术更是国家科技发展和智慧城市建设的基础。 当前“互联网”新业…

骨传导耳机是怎么传声的?骨传导耳机会伤害耳朵吗?

作为一个耳机发烧友&#xff0c;平时最喜欢的就是捣鼓耳机。这几年入手了几十款耳机&#xff0c;头戴式、入耳式、半入耳、有线无线都会接触一些来玩&#xff0c;其中还有骨传导耳机这个相对小众的款类。 说到骨传导耳机&#xff0c;我应该算是最早一批的用户了&#xff0c;很…

web网页设计—— 指环王:护戒使者(13页) 电影网页设计 在线电影制作 个人设计web前端大作业

HTML实例网页代码, 本实例适合于初学HTML的同学。该实例里面有设置了css的样式设置&#xff0c;有div的样式格局&#xff0c;这个实例比较全面&#xff0c;有助于同学的学习,本文将介绍如何通过从头开始设计个人网站并将其转换为代码的过程来实践设计。 文章目录一、网页介绍一…

数据结构-ArrayList解析和实现代码

arrayList结构的实现是数组结构&#xff0c;数组没有扩容机制&#xff0c;arrayList的扩容机制采用的是复制数组&#xff0c;了解你会发现ArrayList虽然实现比较简单&#xff0c;但是设计还是很巧妙的。咱们先来简单实现下.. 咱们看下定义的全局变量 1.默认初始化空间为10&am…

docker 安装 Jenkins

一、Jenkins 安装 增加挂载目录和权限 # 增加挂载目录和权限mkdir /workspace/jenkins_homechown -R 1000:1000 /workspace/jenkins_home/创建容器 docker run --name jenkins -d \ -p 9999:8080 \ -p 8888:8888 \ -p 50000:50000 \ -v /workspace/jenkins_home:/var/jenkins…

[附源码]java毕业设计智慧教室预约

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

java检验mp4文件完整性的一个方法:使用ffmpeg

问题引入 最近笔者在写一个多线程下载视频文件的程序&#xff0c;打算让这个程序在我的空闲服务器上运行&#xff0c;但是几轮测试之后发现&#xff0c;有时候会存在下载的视频文件不完整的情况&#xff0c;这就导致了有些文件无法正常播放 问题排查 经过一周的排查后&#…

小白学流程引擎-FLowable(一) —FLowable是什么

小白学流程引擎-FLowable(一) | FLowable是什么 一、什么是流程引擎&#xff1f; 通俗的说&#xff0c;流程引擎就是多种业务对象在一起合作完成某件事情的步骤&#xff0c;把步骤变成计算机能理解的形式就是流程引擎。 流程引擎&#xff0c;用来驱动业务按照设定的固定流程…

《Kafka 源码实战》看完面试不慌!

Kafka 一开始是 LinkedIn 公司开发的消息队列&#xff0c;随着 Kafka 代码被贡献给 Apache 软件基金会后&#xff0c;就成功孵化成 Apache 顶级项目&#xff0c;世界上有越来越多的公司和个人开始使用 Kafka&#xff0c;所以 Kafka 使用的范围是很普遍的。 同时&#xff0c;值得…

vue实现文件上传压缩优化处理

vue js实现文件上传压缩优化处理 两种方法 &#xff1a; 第1种是借助canvas的封装的文件压缩上传第2种&#xff08;扩展方法&#xff09;使用compressorjs第三方插件实现 目录 vue js实现文件上传压缩优化处理 借助canvas的封装的文件压缩上传 1.新建imgUpload.js 2.全局引…