【数据结构】动图详解单向链表

news2024/9/21 14:52:20

目录

1.什么是链表

        1.问题引入

        2. 链表的概念及结构

        3. 问题解决

2.单向链表接口的实现

        1.接口1,2---头插,尾插

        2. 接口3,4---头删,尾删

        3. 接口5---查找

         4. 接口6,7---插入,删除

        5. 接口8---打印

         6. 注意事项总结

3.完整代码及效果展示 


1.什么是链表

        1.问题引入

        上期我们讲解了顺序表的基本概念和实现方法(传送门:详解顺序表)。但是顺序表存在着如下三个问题:

  1. 顺序表中间及头部的插入与删除,需要对原有数据进行移动,时间复杂度为O(N),成本较高
  2. 使用realloc进行增容时需要申请新空间,释放旧空间,拷贝数据,消耗较高。
  3. 由于我们无法知道用户实际需要多少空间,在增容时往往可能会有大量的空间剩余。例如在容量为100时满了进行2倍增容到200,如果后续只需插入5个数据,则会浪费95个数据空间;而如果我们每次只扩大1个数据空间,当需要插入95个数据时,就需要进行realloc操作95次,成本较高。

        那么这些问题要如何解决呢?通过链表, 我们就可以很轻松的解决以上问题。下面,就让我们感受链表的魅力吧!

        2. 链表的概念及结构

        链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。它的结构如下所示(以单向链表为例):

        我们把date和next形成的结构体称为链表的一个结点,我们可以看到,链表就是由一个个结点链接起来的非连续线性结构,不同结点通过next指针连接的,最后一个结点的next指针指向空。链表也是线性表中的一种。

当然,在实际应用中,链表的结构多种多样,通过以下几种情况组合而成的就有8种链表结构

1. 单 向 、 双 向

2. 带 头结点 、 不 带 头结点

3. 循 环 、 非 循 环


本期我们讲解的是实际应用中最常用的两种结构之一:单向不带头非循环链表。(另外一种是带头双向循环链表

        3. 问题解决

        通过以上链表结构,我们就能很好地解决顺序表的局限:

  1. 每当我们需要新增数据时,我们只需申请一个新结点用来保存数据,然后用指针将链表与新结点链接起来即可,并不需要进行数据拷贝。
  2. 由于使用链表存储数据总是处于满载状态,每个结点都是有效数据,需要插入时则申请新结点并与链表连接,因此就不存在空间浪费的问题。
  3. 链表的结构是线性非连续的,各结点通过指针连接。如果需要在头部或中间插入数据,只需改变结点指针的指向即可,无需再移动数据。

2.单向链表接口的实现

        首先,在实现各种接口函数前,我们需要定义一个结构体来代表每一个结点,用date保存结点中的数据,用next保存下一结点的地址。同样的,我们采取typedef的方式将类型重命名方便代码的编写与维护。如下:

由于我们实现的单向链表是不带头结点的,所以我们还需要定义一个指针指向链表的第一个结点(下面称为头指针)。

        1.接口1,2---头插,尾插

        对于头插,我们只需要创建新结点保存数据,然后将头指针指向新结点,新结点指向原来头指针指向的结点即可,动态效果如下:

        具体代码如下:

//用于创建新结点
SLNode* CreateNode(SLDateType x)
{
	SLNode* cur = (SLNode*)malloc(sizeof(SLNode));
	cur->date = x;
	cur->next = NULL;
	return cur;
}

//头插
void SListPushFront(SLNode** pphead, SLDateType x)
{
	SLNode* NewNode = CreateNode(x); //获取新结点
	NewNode->next = *pphead; 
	*pphead = NewNode; //修改头指针
}

值得注意的是,函数中我们传入的是头指针的地址。这是由于我们需要修改头指针使其指向新的结点,因此需要采用址传递的方式,用二级指针接收,否则只会修改临时变量造成出错。


         对于尾插,我们需要先找到链表的尾结点,然后将尾结点的next指向新结点,动态效果如下:

        代码如下: 

//尾插,初稿
void SListPushBack(SLNode** pphead, SLDateType x)
{
	SLNode* NewNode = CreateNode(x);
	SLNode* tail = *pphead;
	while (tail->next != NULL) //不为尾结点
	{
		tail = tail->next; //tail指向下一结点
	}

    //找到尾结点
	tail->next = NewNode;
}

        但是,上述代码是存在问题的。我们知道->相当于是一种解引用,当链表为空时,即tail==NULL时,此时我们进行tail->next操作显然是非法的,所以我们还需对链表为空的情况做出讨论,最终代码如下:

//尾插,终稿
void SListPushBack(SLNode** pphead, SLDateType x)
{
	SLNode* NewNode = CreateNode(x);
	if (*pphead == NULL)
	{
		*pphead = NewNode; //为空就直接修改头指针指向新结点,因此需要传二级指针
	}
	else
	{
		SLNode* tail = *pphead;
		while (tail->next != NULL) //不为尾结点
		{
			tail = tail->next; //指向下一结点
		}

        //找到尾结点
		tail->next = NewNode; 
	}
}

        2. 接口3,4---头删,尾删

        对于头删,我们只需要将头指针指向下一个位置,然后将原来指向的空间free()掉即可。如果链表为空,我们就让函数直接返回,具体动态效果如下:

        在代码实现中,我们需要先创建一个临时变量next来保存下一个结点的地址,这是因为free()后就无法通过->得到下一个结点的地址了。具体代码如下:

//头删
void SListPopFront(SLNode** pphead)
{
	if (*pphead == NULL)
	{
		return;  //链表为空直接返回
	}
	else
	{
		SLNode* next = (*pphead)->next; //存储下一个结点地址
		free(*pphead);
		*pphead = next; //改变头指针指向,因此需要传二级指针
	}
}

        对于尾删,我们同样需要找到尾结点。这里和尾插不同的是,我们除了要找到尾结点,还需找到尾结点的前一个结点并使其next置为空,因此我们可以使用两个指针prev和tail进行移动,prev指向tail的上一个结点,具体动图如下:

        具体代码如下:

//尾删,初稿
void SListPopBack(SLNode** pphead)
{
	if (*pphead == NULL) //没有结点
	{
		return;
	}
	else  //一个以上结点
	{
		SLNode* tail = *pphead;
		SLNode* prev = NULL;
		while (tail->next != NULL) //tail不为尾结点
		{
			prev = tail;
			tail = tail->next; //tail指向下一结点
		}

        //tail为尾结点,此时prev为尾结点的前一个结点
        free(tail); //释放掉尾结点
        tail=NULL;
		prev->next = NULL;
	}
}

        这里和头删一样,当链表为空时直接让函数返回。但是以上代码依旧存在着一个问题,就是当链表只有一个结点时,tail直接指向尾结点,此时prev为NULL,禁止对其进行->操作。所以我们还需要对只有一个结点的情况进行讨论,最终代码如下:

//尾删,终稿
void SListPopBack(SLNode** pphead)
{
	if (*pphead == NULL) //没有结点
	{
		return;
	}
	else if ((*pphead)->next == NULL)  //只有一个结点
	{
		free(*pphead);
		*pphead = NULL; //直接将结点释放掉,头指针改为NULL,因此需要传二级指针
	}
	else  //一个以上结点
	{
		SLNode* tail = *pphead;
		SLNode* prev = NULL;
		while (tail->next != NULL) //tail不为尾结点
		{
			prev = tail;
			tail = tail->next; //tail指向下一结点
		}

        //tail为尾结点,此时prev为尾结点的前一个结点
        free(tail); //释放掉尾结点
        tail=NULL;
		prev->next = NULL;
	}
}

        3. 接口5---查找

        对于查找,我们只需遍历链表的所有结点即可,故不需要头指针,只需传一级指针即可。当找到需要查找的date时,返回对应结点的指针,若没找到或者链表为空时,返回NULL,代码如下:

//查找
SLNode* SListFind(SLNode* phead, SLDateType x)
{
	while (phead)
	{
		if (phead->date == x)
		{
			return phead; //找到了,返回结点指针
		}
		phead = phead->next; //向后查找
	}

    //没有找到,返回空指针
	return NULL;
}

         4. 接口6,7---插入,删除

        对于插入,我们可以实现一个在指定结点前插入一个新结点的接口,而这个指定结点我们可以通过查找接口来获取。相同的,在进行插入操作时我们还需要得到前一个结点的地址,我们用prev来存储,然后将prev->next改为新结点地址,新结点的next为我们传入的结点的地址。动态效果如下:

//插入,初稿
void SListInsert(SLNode** pphead, SLNode* pos, SLDateType x)
{
	if (pos == NULL) //指定结点为空直接返回
	{
		return;
	}
	SLNode* NewNode = CreateNode(x); //获取新结点
	SLNode* prev = *pphead;
	while (prev->next != pos) //prev不指向pos上一结点
	{
		prev = prev->next; //prev指向下一结点
	}

    //当prev指向pos上一结点,插入新结点
	prev->next = NewNode;
	NewNode->next = pos;
}

         (嘿嘿,我又来了qwq),上述的代码其实还是存在bug的,就是当我们传入的指定结点为头结点时,prev->next永远不可能等于pos,prev不断向后更新,最终为NULL导致程序崩溃。动态分析如下:

         因此我们需要进行分类讨论,而我们发现如果pos指向头结点,那在其前面插入不就相当于头插吗?所以我们可以直接调用头插接口,改进后的代码如下:

//插入,终稿
void SListInsert(SLNode** pphead, SLNode* pos, SLDateType x)
{
	if (pos == NULL) //指定结点为空直接返回
	{
		return;
	}
	if (*pphead==pos) //pos为头结点指针,直接头插
	{
		SListPushFront(pphead,x);
	}
    else
    {
	    SLNode* NewNode = CreateNode(x); //获取新结点
	    SLNode* prev = *pphead;
	    while (prev->next != pos) //prev不指向pos上一结点
	    {
		    prev = prev->next; //prev指向下一结点
	    }

        //当prev指向pos上一结点,插入新结点
	    prev->next = NewNode;
	    NewNode->next = pos;
    }
}

        对于删除,我们同样可以实现一个删除指定结点的接口,而这个指定结点我们依旧可以通过查找接口来获取。同样,除了将指定的结点free()掉,还需将其上一个结点的next指向pos的下一结点。与插入一样都需要找到上一个结点的位置,在这过程中可能引发的问题也是一样的,这里就不再赘述了,直接上代码:

//删除
void SListErase(SLNode** pphead, SLNode* pos)
{
	if (pos == NULL) //指定结点为空直接返回
	{
		return;
	}
	if (*pphead == pos) //pos为头结点指针,直接头删
	{
		SListPopFront(pphead);
	}
	else
	{
		SLNode* prev = *pphead;
		while (prev->next != pos) //prev不指向pos上一结点
		{
			prev = prev->next; //使prev指向下一个结点
		}

        //当prev指向pos上一结点,删除pos指向结点,更新prev->next
		prev->next = pos->next;
		free(pos);
	}
}

        5. 接口8---打印

        对于打印,只需从头结点开始,向后遍历链表,打印每个结点的date直到走到链表尾即可,此时指针指向NULL。由于不修改头指针指向,因此采用值传递即可,用一级指针接收。代码如下:

//打印
void SListPrint(SLNode* phead)
{
	while (phead != NULL) //遍历链表直到链表尾
	{
		printf("%d->", phead->date);
		phead = phead->next;
	}

    //到达链表尾,打印NULL
	printf("NULL");
}

         6. 注意事项总结

通过上面一个个接口的实现,我们得出以下两个值得注意的地方:

  1. 当我们需要修改头指针时,如插入删除操作时,需要使用址传递,用二级指针来接收头指针的地址。而如果我们不需要修改头指针时,如打印,查找等,建议使用值传递,用一级指针来接收,避免头指针被意外修改。
  2. 在实现链表接口时,需要时刻考虑到当链表为空,链表只有一个结点,对头结点进行操作,对尾结点进行操作等特殊情况是否会出现bug。

3.完整代码及效果展示 

        我们可以采用多文件编写的形式,将上述接口的定义实现放在SList.c文件中,然后将接口的声明和结构体的定义放于SList.h头文件中,以达到封装的效果。这样我们如果需要使用单向链表,就只需要在文件中包含对应的头文件SList.h就可以使用我们上面定义的各种接口。以下为本文实现的单向链表完整代码以及效果展示:

//SList.h文件,用于声明接口函数,定义结构体
#pragma once
#include<stdio.h>
#include<stdlib.h>
typedef int SLDateType;
struct SListNode
{
	SLDateType date;
	struct SListNode* next;
};
typedef struct SListNode SLNode;


// 不会改变链表的头指针,传一级指针
void SListPrint(SLNode* phead);
SLNode* SListFind(SLNode* phead, SLDateType x);

// 可能会改变链表的头指针,传二级指针
void SListPushBack(SLNode** pphead, SLDateType x);
void SListPushFront(SLNode** pphead, SLDateType x);
void SListPopFront(SLNode** pphead);
void SListPopBack(SLNode** pphead);
// 在pos的前面插入x
void SListInsert(SLNode** phead, SLNode* pos, SLDateType x);
// 删除pos位置的值
void SListErase(SLNode** phead, SLNode* pos);
//SList.c文件,用于定义接口函数
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
void SListPrint(SLNode* phead)
{
	while (phead != NULL)
	{
		printf("%d->", phead->date);
		phead = phead->next;
	}
	printf("NULL");
}
SLNode* CreateNode(SLDateType x)
{
	SLNode* cur = (SLNode*)malloc(sizeof(SLNode));
	cur->date = x;
	cur->next = NULL;
	return cur;
}

void SListPushBack(SLNode** pphead, SLDateType x)
{
	SLNode* NewNode = CreateNode(x);
	if (*pphead == NULL)
	{
		*pphead = NewNode; //为空就直接修改头指针指向新结点,因此需要传二级指针
	}
	else
	{
		SLNode* tail = *pphead;
		while (tail->next != NULL) //不为尾结点
		{
			tail = tail->next; //指向下一结点
		}
		//找到尾结点
		tail->next = NewNode;
	}
}

void SListPushFront(SLNode** pphead, SLDateType x)
{
	SLNode* NewNode = CreateNode(x); //获取新结点
	NewNode->next = *pphead;
	*pphead = NewNode; //修改头指针
}

void SListPopFront(SLNode** pphead)
{
	if (*pphead == NULL)
	{
		return;  //链表为空直接返回
	}
	else
	{
		SLNode* next = (*pphead)->next; //存储下一个结点地址
		free(*pphead);
		*pphead = next; //改变头指针指向,因此需要传二级指针
	}
}

void SListPopBack(SLNode** pphead)
{
	if (*pphead == NULL) //没有结点
	{
		return;
	}
	else if ((*pphead)->next == NULL)  //只有一个结点
	{
		free(*pphead);
		*pphead = NULL; //直接将结点释放掉,头指针改为NULL,因此需要传二级指针
	}
	else  //一个以上结点
	{
		SLNode* tail = *pphead;
		SLNode* prev = NULL;
		while (tail->next != NULL) //tail不为尾结点
		{
			prev = tail;
			tail = tail->next; //tail指向下一结点
		}
		//tail为尾结点,此时prev为尾结点的前一个结点
		free(tail); //释放掉尾结点
		tail = NULL;
		prev->next = NULL;
	}
}

SLNode* SListFind(SLNode* phead, SLDateType x)
{
	while (phead)
	{
		if (phead->date == x)
		{
			return phead; //找到了,返回结点指针
		}
		phead = phead->next; //向后查找
	}
	//没有找到,返回空指针
	return NULL;
}

void SListInsert(SLNode** pphead, SLNode* pos, SLDateType x)
{
	if (pos == NULL) //指定结点为空直接返回
	{
		return;
	}
	if (*pphead == pos) //pos为头结点指针,直接头插
	{
		SListPushFront(pphead, x);
	}
	else
	{
		SLNode* NewNode = CreateNode(x); //获取新结点
		SLNode* prev = *pphead;
		while (prev->next != pos) //prev不指向pos上一结点
		{
			prev = prev->next; //prev指向下一结点
		}
		//prev指向pos上一结点,插入新结点
		prev->next = NewNode;
		NewNode->next = pos;
	}
}



void SListErase(SLNode** pphead, SLNode* pos)
{
	if (pos == NULL) //指定结点为空直接返回
	{
		return;
	}
	if (*pphead == pos)
	{
		SListPopFront(pphead);
	}
	else
	{
		SLNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}
}

       最后, 我们在text.c文件调用单向链表各个接口进行测试,如下:

//text.c文件,用于测试
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
void SListText()
{
	SLNode* phead = NULL;
	printf("起始数据: \n");
	SListPrint(phead);
	//头插
	SListPushFront(&phead, 1);
	SListPushFront(&phead, 2);
	SListPushFront(&phead, 3);
	printf("\n头插入数据后: \n");
	SListPrint(phead);
	//尾插
	SListPushBack(&phead, 4);
	SListPushBack(&phead, 5);
	SListPushBack(&phead, 6);
	printf("\n尾插入数据后: \n");
	SListPrint(phead);
	//头删
	SListPopFront(&phead);
	printf("\n头删数据后: \n");
	SListPrint(phead);
	//尾删
	SListPopBack(&phead);
	printf("\n尾删数据后: \n");
	SListPrint(phead);
	//找出数据为5的结点并在其前面插入8
	SLNode* cur1 = SListFind(phead, 5);
	if (cur1)
	{
		SListInsert(&phead, cur1, 8);
	}
	printf("\n5前面插入数据后: \n");
	SListPrint(phead);
	//删除数据为1的结点
	SLNode* cur2 = SListFind(phead, 1);
	if (cur2)
	{
		SListErase(&phead, cur2);
	}
	printf("\n删除数据1的结点后: \n");
	SListPrint(phead);
}
int main()
{
	SListText();
	return 0;
}

        以下就是测试的最终效果:


 以上,就是本期的全部内容。

制作不易,能否点个赞再走呢qwq

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

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

相关文章

CVE-2021-36934提权复现

CVE-2021-369342021年7 月 20 日&#xff0c;微软确认了一个新的本地提权漏洞(CVE-2021-36934)&#xff0c;被称为HiveNightmare。该漏洞由于Windows中多个系统文件的访问控制表(ACL)过于宽松&#xff0c;使得任何标准用户都可以从系统卷影副本中读取包括SAM、SYSETM、SECURITY…

Windows系统运行iOS设备管理软件iMazing2023

iMazing2023免费版是一款运行在Windows系统上的iOS设备管理软件&#xff0c;软件功能非常强大&#xff0c;界面简洁清晰&#xff0c;操作方便快捷&#xff0c;设计的非常有人性化&#xff0c;iMazing官方版为用户提供了多种设备管理功能&#xff0c;每一位用户都能以自己的形式…

MyISAM和InnoDB存储引擎的区别

目录前言存储引擎区别事务外键表单的存储数据查询效率数据更新效率如何选择前言 MyISAM和InnoDB是使用MySQL最常用的两种存储引擎&#xff0c;在5.5版本之前默认采用MyISAM存储引擎&#xff0c;从5.5开始采用InnoDB存储引擎。 存储引擎 存储引擎是&#xff1a;数据库管理系统…

小白如何快速入门Verilog HDL ?一文为你讲解清楚

在学习的过程中&#xff0c;无论学什么都不可能一蹴而就。都是从一个初步认识到慢慢了解再到精通掌握的过程&#xff0c;学习Verilog HDL语法也是一样的道理&#xff0c;首先你要清楚什么是Verilog HDL&#xff0c;然后结合实践再遵从理论&#xff0c;这样后面的学习才能理解的…

P1359 租用游艇

# 租用游艇 ## 题目描述 长江游艇俱乐部在长江上设置了 $n$ 个游艇出租站 $1,2,\cdots,n$。游客可在这些游艇出租站租用游艇&#xff0c;并在下游的任何一个游艇出租站归还游艇。游艇出租站 $i$ 到游艇出租站 $j$ 之间的租金为 $r(i,j)$&#xff08;$1\le i\lt j\le n$&#xf…

MPLS实验(1.31)

目标&#xff1a; 一、首先为公网的每个路由器配置对应的ip和环回并且用OSPF进行动态选路 r2&#xff1a; [r2]int gi 0/0/2 [r2-GigabitEthernet0/0/2]ip add 23.1.1.1 24 [r2-GigabitEthernet0/0/2]int lo0 [r2-LoopBack0]ip add 2.2.2.2 24 r3&#xff1a; [r3]int gi 0…

Oracle的学习心得和知识总结(十)|Oracle数据库PL/SQL语言循环控制语句之LOOP语句技术详解

目录结构 注&#xff1a;提前言明 本文借鉴了以下博主、书籍或网站的内容&#xff0c;其列表如下&#xff1a; 1、参考书籍&#xff1a;《Oracle Database SQL Language Reference》 2、参考书籍&#xff1a;《PostgreSQL中文手册》 3、EDB Postgres Advanced Server User Guid…

yolov5篇---yolov5训练pt模型并转换为rknn模型,部署在RK3588开发板上——从训练到部署全过程

yolov5训练pt模型并转换为rknn模型&#xff0c;部署在RK3588开发板上——从训练到部署全过程一、任务介绍二、实验过程2.1 使用正确版本的yolov5进行训练(平台&#xff1a;x86机器ubuntu22.04系统)2.2 best.pt转换为best.onnx(平台&#xff1a;x86机器ubuntu22.04系统)2.3 best…

2023年西安Java培训机构十强榜单出炉,快来看看哪家榜上有名~

关于“西安Java培训机构哪家好&#xff1f;”“如何选择适合自己的Java培训机构&#xff1f;”这类问题常常出现网络上&#xff0c;其实关于这类问题没有统一的答案&#xff0c;一千个人有一千个哈姆雷特&#xff0c;每个人关注的重点不一样&#xff0c;需求更是不同&#xff0…

投入3.6亿美元!加拿大启动国家量子战略

&#xff08;图片来源&#xff1a;网络&#xff09;量子科学和技术处于研究和创新的前沿&#xff0c;具有巨大的商业化潜力和突破性进展&#xff0c;包括更有效的药物设计、更好的气候预测、改进的导航系统和清洁技术的创新。加拿大政府支持这一新兴行业的持续发展&#xff0c;…

用队列实现栈+用栈实现队列+循环队列(oj题)

1.题目: 力扣https://leetcode.cn/problems/implement-stack-using-queues/ 思路: 核心思路: 1、入数据&#xff0c;往不为空的队列入、保持另一个队列为空 2、出数据的时候&#xff0c;依次出队头的数据&#xff0c;转移另一个队列保存。只剩最后一个事&#xff0c;Pop掉 typ…

【数据结构之排序系列】校招热门考点:快速排序

目录前言一、hoare版本1. 单排2. 单排的代码实现3. 综合排序的实现4. 测试二、挖坑法1. 单排2. 单排的代码实现3. 综合排序的实现4. 测试三、前后指针法1. 单排2. 单排的代码实现3. 综合排序的实现4. 测试四、快排的时间复杂度五、快排的优化1. 三数取中2. 小区间优化六、快排的…

[Vulnhub] DC-9

前言&#xff1a; 打了这么长时间&#xff0c;终于&#xff0c;DC系列1-9 靶机已经干完了。通过这九个靶机的历练&#xff0c;都是单靶机 感觉远远不足&#xff0c;但也不是没有收获&#xff0c;学到了一些工具和提权的简单利用&#xff0c;大概知道了什么是反弹shell&#xf…

UE实现人物角色沿样条线移动效果

文章目录 1.实现目标2.实现过程2.1 实现思路2.2 具体过程3.参考资料1.实现目标 实现人物角色Character按样条线Spline自动移动效果,GIF动图如下。 2.实现过程 2.1 实现思路 (1)获取某一时刻样条线上点的位置,直接使用GetLocationAtTime函数即可。 (2)实时更新Characte…

Python Windows Apache部署Django项目运行环境(含Mysql)以及解决过程中遇到的问题

上一篇请移步Python Windows Apache部署Django项目运行环境_水w的博客-CSDN博客 本文是在上一篇的基础上&#xff0c; 进行后续工作。 目录 一、配置数据库&#xff08;MySQL&#xff09; 1、Mysql配置 2、安装MySQL管理工具SQLyog 【解决过程中遇到的问题】 解决报错&am…

java泛型2

真正搞定泛型&#xff01;&#xff01;&#xff01;------- >类型形参 所谓泛型&#xff0c;就是允许在定义类、接口、方法时使用类型形参&#xff0c;这个类型形参&#xff08;或叫泛型&#xff09;将在声明变量、创建对象、调用方法时动态地指定&#xff08;即传入实际的类…

【看表情包学Linux】进程的概念 | 进程控制块 PCB | 父进程与子进程 | 进程 ID | task_struct

&#x1f923; 爆笑教程 &#x1f449; 《看表情包学Linux》&#x1f448; 猛戳订阅 &#x1f525; &#x1f4ad; 写在前面&#xff1a;本章我们将带着大家深入理解 "进程" 的概念&#xff0c;"进程" 这个概念其实使我们一直在接触的东西&#xff0c;只不…

一文深入搞懂 mmap 涉及的所有内容

内存映射&#xff0c;简而言之就是将内核空间的一段内存区域映射到用户空间。映射成功后&#xff0c;用户对这段内存区域的修改可以直接反映到内核空间&#xff0c;相反&#xff0c;内核空间对这段区域的修改也直接反映用户空间。那么对于内核空间与用户空间两者之间需要大量数…

一文讲清同步异步,消息队列,宏任务 微任务...

单线程多线程 什么是线程进程? 进程&#xff1a;是cpu分配资源的最小单位&#xff1b;&#xff08;是能拥有资源和独立运行的最小单位&#xff09; 线程&#xff1a; 是cpu调度的最小单位&#xff1b;&#xff08;线程是建立在进程的基础上的一次程序运行单位&#xff0c;一…

Kafka第一章:环境搭建

系列文章目录 Kafka第一章&#xff1a;环境搭建 文章目录系列文章目录前言一、环境安装1.前置环境2.软件下载3.上传集群并解压4.编写配置文件5.分发配置文件6.修改参数7.环境变量8.启动服务9.编写启动脚本二、主题命令行操作1.查看topic2.创建 first topic3.查看主题的详情4.修…