单链表(一篇带你掌握单链表)

news2025/7/25 8:56:54

     在之前我们已经学习了顺序表,顺序表有一定的缺陷,比如需要扩容,在插入和删除时需要挪动数据等问题,在此基础上我们可以学习一一种新的数据结构-单链表,相对来说它可以按需申请空间,并且不需要挪动数据。

     我们在下面的代码封装了三个文件,分别是:

  • SList.c  用于实现单链表的各个操作。
  • SList.h  用于存放有关单链表的各个函数的声明。
  • test.c    用于测试单链表的性能。

目录

一、单链表的定义

1.1 基本概念与结构

1.2 链表的结点

二、单链表的实现

 2.1 申请内存空间函数

 2.2 创建链表函数

 2.3 打印链表函数

 2.4 第一个测试函数

 2.5 尾插函数

2.5.1 错误示例1

2.5.2 错误示例2

2.5.3 正确示例

 2.6 尾删函数

2.6.1 方法一

2.6.2 方法二

 2.7 第二个测试函数

 2.8 头插函数

 2.9 头删函数

 2.10 第三个测试函数

 2.11 查找函数

 2.12 在pos位置之后插入x

 2.13 在pos之前插入x

 2.14 删除pos位置的后一个元素

 2.15 删除pos位置

 2.16 第四个测试函数

 2.17 释放函数

三、单链表的完整代码

 3.1 test.c文件

 3.2 SList.h文件

 3.3 SList.c文件


一、单链表的定义

1.1 基本概念与结构

     单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素 +指针(下一个元素的地址),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据。

     链表的结构如下:
 

1.2 链表的结点

     由于链表支持存储数据的空间不连续,所以我们要存储一部分数据,我们在存储了一个数据后想要找到下一个数据,这个时候就需要在存储数据的同时存储下一个数据的地址,即一个单链表的元素(结点)需要包含两个内容:数据和下一个数据的地址,类似于下图:


     由于一个结点包含多个数据并且数据的类型不同,所以在这里为了描述单链表的一个结点,我们使用结构体 。

typedef int SLTDataType;   //为使程序有扩展性,在想要改变数据类型时,在这里可以直接改变

typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;
//使用类型重定义

     

二、单链表的实现

     以下是我们欲实现的和单链表有关的函数:

SLTNode* BuySLTNode(SLTDataType x);
//创建一个有n个结点的链表
SLTNode* CreateSList(int n);

//打印链表
void SLTPrint(SLTNode* phead);

//单链表尾插
void SLTPushBack(SLTNode** phead, SLTDataType x);

//单链表尾删
void SLTPopBack(SLTNode** phead);

//单链表的头插
void SLTPushFront(SLTNode** phead, SLTDataType x);

//单链表的头删
void SLTPopFront(SLTNode** phead);

//单链表的查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);

//单链表在pos位置之后插入x
void SListInsertAfter(SLTNode* pos, SLTDataType x);

//单链表在pos位置之前插入x
void SListInsert(SLTNode** pphead,SLTNode* pos, SLTDataType x);

//删除pos位置的后一个元素
void SLTEraseAfter(SLTNode* pos);
//删除pos位置
void SLTErase(SLTNode** pphead, SLTNode* pos);

//销毁
void SLTDestroy(SLTNode** pphead);

2.1 申请内存空间函数

     由于单链表需要满足按需申请和释放空间,在这里我们使用malloc申请空间,对malloc函数不了解的朋友可以去上一篇博客学习。

SLTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

 2.2 创建链表函数

     单链表是按需申请空间,我们想要创建一个有n个结点的单链表,对于单链表而言,单链表之间的每个结点需要链接起来,所以在这里我们使用CreateLisst函数来实现创建一个链表的功能。

     CreateLisst函数主要实现两个功能:1.调用BuySLTNode函数申请一个结点的空间。2.将所有的结点链接起来。

SLTNode* CreateSList(int n)
{
	SLTNode* phead = NULL, * ptail = NULL;
	for (int i = 0; i < n; i++)
	{
		SLTNode* newnode = BuySLTNode(i);
		if (phead == NULL)
		{
			phead = ptail = newnode;
		}
		else
		{
			ptail->next = newnode;
			ptail = newnode;
		}
	}
	return phead;
}

2.3 打印链表函数

     我们在完成链表的增删查改之后想要观察链表是否正确最直观的方式是将他直接打印下来,打印一个链表,我们需要遍历这个链表,对链表的遍历,我们需要一个指针,一般不使用头指针直接遍历,所以我们需要将phead的值赋给另一个指针cur,由它来遍历,链表的结束条件是链表的下一个指针指向空,所以当cur为空的时侯,遍历结束。

     其次我们需要注意的是当链表为空的时候我们是否需要单独处理?答案是不需要,空链表也可以打印。

void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

2.4 第一个测试函数

     我们写完了创建结点函数和创建一个链表的函数之后,我们可以对他进行测试,看看函数功能实现是否正确,避免在最后代码一起运行时执行出错但不知道那个函数出错的情况。

void TestSList1()
{
	SLTNode* plist = NULL;
	plist = CreateSList(10);   //创建一个有10个结点的链表
	SLTPrint(plist);
}

int main()
{
	TestSList1();
	return 0;
}

     代码正常运行,说明我们之前写的三个函数没有问题。

2.5 尾插函数

     我们要进行单链表尾插操作,在写这个函数时有些人都会出错,在这里有两种经典的错误写法,我们先对这个错误写法进行分析再写出正确的写法。

2.5.1 错误示例1

//错误示例1
void SLTPushBack(SLTNode* phead, SLTDataType x)
{
	SLTNode* newnode = BuySLTNode(x);
	SLTNode* tail = phead;
	while (tail)
	{
		tail = tail->next;
	}
	tail = newnode;
}

     上面代码的问题在哪里?我们通过链表的逻辑结构可以看出来。

执行上述代码之前链表的的逻辑结构(假设由三个结点):

执行上述代码之后的逻辑结构如下:

      我们发现执行上述代码并没有改变原来链表的结构,下面我们来解释原因,首先tail是一个局部变量,局部变量在函数调用完成时自动销毁,在上述代码中我们并没有改变链表的链接关系,只是让tail等于newnode,链表本身没有改变,其次没有考虑链表是空链表的情况,所以错误。

2.5.2 错误示例2

     有些同学在写尾插函数时还会像下面一样写:

//错误示例2
void SLTPushBack(SLTNode* phead, SLTDataType x)
{
	SLTNode* newnode = BuySLTNode(x);
	if (phead == NULL)
	{
		phead = newnode;
	}
	else
	{
		SLTNode* tail = phead;
		while (tail->next)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
	
}

     上述的代码有什么问题呢?

     当链表是空的时候,我们进行尾插时,链表的头结点也应该发生改变,而上述代码中phead的改变并不会影响plist,phead是形参,形参的改变并不会对实参造成影响,也就是说,即使在函数内部我们将phead的值改变,对于plist来说不会有任何变化,所以上述代码错误。

2.5.3 正确示例

     在上述代码中的错误主要是当链表为空时,phead改变不会对链表的头节点改变,所以我们这里需要想办法使phead改变时,plist也改变,由此我们想到了传指针的方式。

     对于函数传参传的是指针时,传的是指针改变的就是指针指向的内容,所以这里我们想要改变plist,我们就要传plist的指针,plist本来就是一个指针,在这里我们需要传的是二级指针。
 


void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuySLTNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

2.6 尾删函数

      对于单链表的尾删,我们有两种方式。

      我们要对单链表进行尾删操作,需要注意的是,当链表为空的时候,不能进行尾删。

      我们需要注意的是:尾删也有可能会改变链表的头结点(即链表只有一个结点的时候),如果要改变链表的头节点,我们就需要传一个二级指针。

      我们可以画出大致的逻辑结构来写代码。

     除此之外,我们需要考虑当只有一个结点的情况:
 

2.6.1 方法一


     我们要对单链表进行尾删操作,需要记录要删除元素的前一个位置的指针,所以在这里我们采用双指针的方式。

void SLTPopBack1(SLTNode** pphead)
{
	assert(*pphead);
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* prev = NULL;
		SLTNode* tail = *pphead;
		while (tail->next)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		prev->next = NULL;
	}
}

2.6.2 方法二

void SLTPopBack(SLTNode** pphead)
{
	assert(*pphead);
	SLTNode* ptail = *pphead;
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		while (ptail->next->next)
		{
			ptail = ptail->next;
		}

		free(ptail->next);
		ptail->next = NULL;
	}
}

2.7 第二个测试函数

     我们又完成了尾插和尾删函数的编写,在这里我们可以测试一下两个函数是否正确。

void TestSList2()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 6);
	SLTPushBack(&plist, 7);
	SLTPushBack(&plist, 7);
	SLTPushBack(&plist, 6);
	SLTPrint(plist);

	SLTPopBack1(&plist);
	SLTPopBack(&plist);
	SLTPopBack1(&plist);

	SLTPrint(plist);

}

int main()
{
	TestSList2();
	return 0;
}

2.8 头插函数

     我们要对单链表进行头插(即在第一个结点前插入元素),可以先画出图来分析:

     在头插函数中不需要判空,即使链表为空,我们也能进行头插,由于在头插的过程中需要不断地改变链表的头结点,所以我们在传参的时候需要将链表指针的地址传进来。

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

 2.9 头删函数

       我们要对单链表进行头删(即删除第一个结点的元素),可以先画出图来分析:

      对链表进行头删操作,需要对链表的头结点进行改变,所以在这里我们传参的时候传的是二级指针,其次如果链表为空那么头删操作不能进行,所以在这里我们需要判空。

void SLTPopFront(SLTNode** pphead)
{
	assert(*pphead);
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

2.10 第三个测试函数

     我们把头插函数和头删函数代码写完了,可以通过测试函数来看上面两个函数是否有问题。

void TestSList3()
{
	SLTNode* plist = NULL;
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 6);
	SLTPushFront(&plist, 7);
	SLTPushFront(&plist, 7);
	SLTPushFront(&plist, 6);
	SLTPrint(plist);

	SLTPopFront(&plist);
	SLTPopFront(&plist);
	SLTPopFront(&plist);

	SLTPrint(plist);

}

int main()
{
	TestSList3();
	return 0;
}

2.11 查找函数

     我们在某些时候想要查找某个值在链表中的位置,这个时候就需要有查找函数了。

     注意:空链表是可以查找的,所在在这里我们不需要判空,其次我们只是对链表进行查找操作,不会改变链表,所以也不需要传二级指针。

     在链表中进行查找是否存在x,如果存在返回他的指针,如果不存在返回空指针。

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

2.12 在pos位置之后插入x

     

      在这个函数中,我们主要需要注意以下两个问题:

  1. 要对pos位置进行判空,如果pos为空,那么在pos位置之后插入就没有意义。
  2. 注意在对指针赋值时的顺序,如果顺序不对,很有可能造成在链表遍历时的死循环。
//单链表在pos位置之后插入x
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

2.13 在pos之前插入x

     在pos之前插入x,我们需要考虑特殊情况,即pos是头结点的情况,在这种情况下,如果在pos之前插入x,此时头节点会改变,我们在传参的时候需要传二级指针,其次在这里我们需要对pos判空,在查找函数中,pos如果返回的是空,那么即代表没有查找到,既然没有查找到,我们也不需要在这里进行插入操作。

void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pos);
	if (*pphead == NULL)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		SLTNode* newnode = BuySLTNode(x);
		newnode->next = pos;
		prev->next = newnode;
	}
}

2.14 删除pos位置的后一个元素

      

void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	if (pos->next == NULL)
	{
		return;
	}
	else
	{
		SLTNode* nextNode = pos->next;
		pos->next = nextNode->next;
		free(nextNode);
	}
}

2.15 删除pos位置

     画出删除pos位置可能的情况,大概有以下两种:

 

      我们要着重考虑pos位置是头结点的情况,如果pos位置是头结点,那么整个链表的头结点要发生改变,所以这里我们要传二级指针。

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pos);
	if (pos == *pphead)
	{
		SLTPopFront(pphead);         //头删函数的复用
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		prev->next = pos->next;
		free(pos);
	}
}

2.16 第四个测试函数

void TestSList4()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPushBack(&plist, 5);
	SLTPrint(plist);

	SLTNode* p = SLTFind(plist, 3);
	SLTEraseAfter(p);
	SLTPrint(plist);

	p = SLTFind(plist, 3);
	SLTErase(&plist, p);
	SLTPrint(plist);

}


int main()
{
	TestSList4();
	return 0;
}

 2.17 释放函数

     我们在之前的单链表的结点都是在堆区申请的,在堆区申请的空间需要手动释放,所以我们要写一个释放函数。

void SLTDestroy(SLTNode** pphead)
{
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

三、单链表的完整代码

3.1 test.c文件

#include "SList.h"

void TestSList1()
{
	SLTNode* plist = NULL;
	plist = CreateSList(10);   //创建一个有10个结点的链表
	SLTPrint(plist);
}

void TestSList2()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 6);
	SLTPushBack(&plist, 7);
	SLTPushBack(&plist, 7);
	SLTPushBack(&plist, 6);
	SLTPrint(plist);

	SLTPopBack1(&plist);
	SLTPopBack(&plist);
	SLTPopBack1(&plist);

	SLTPrint(plist);

}

void TestSList3()
{
	SLTNode* plist = NULL;
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 6);
	SLTPushFront(&plist, 7);
	SLTPushFront(&plist, 7);
	SLTPushFront(&plist, 6);
	SLTPrint(plist);

	SLTPopFront(&plist);
	SLTPopFront(&plist);
	SLTPopFront(&plist);

	SLTPrint(plist);

}

void TestSList4()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPushBack(&plist, 5);
	SLTPrint(plist);

	SLTNode* p = SLTFind(plist, 3);
	SLTEraseAfter(p);
	SLTPrint(plist);

	p = SLTFind(plist, 3);
	SLTErase(&plist, p);
	SLTPrint(plist);

}


int main()
{
	TestSList4();
	return 0;
}

3.2 SList.h文件

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>


typedef int SLTDataType;   //为使程序有扩展性,在想要改变数据类型时,在这里可以直接改变

typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

SLTNode* BuySLTNode(SLTDataType x);
//创建一个有n个结点的链表
SLTNode* CreateSList(int n);

//打印链表
void SLTPrint(SLTNode* phead);

//单链表尾插
void SLTPushBack(SLTNode** phead, SLTDataType x);

//单链表尾删
void SLTPopBack(SLTNode** phead);

//单链表的头插
void SLTPushFront(SLTNode** phead, SLTDataType x);

//单链表的头删
void SLTPopFront(SLTNode** phead);

//单链表的查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);

//单链表在pos位置之后插入x
void SListInsertAfter(SLTNode* pos, SLTDataType x);

//单链表在pos位置之前插入x
void SListInsert(SLTNode** pphead,SLTNode* pos, SLTDataType x);

//删除pos位置的后一个元素
void SLTEraseAfter(SLTNode* pos);
//删除pos位置
void SLTErase(SLTNode** pphead, SLTNode* pos);

//销毁
void SLTDestroy(SLTNode** pphead);

3.3 SList.c文件

#include "SList.h"


SLTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

SLTNode* CreateSList(int n)
{
	SLTNode* phead = NULL, * ptail = NULL;
	for (int i = 0; i < n; i++)
	{
		SLTNode* newnode = BuySLTNode(i);
		if (phead == NULL)
		{
			phead = ptail = newnode;
		}
		else
		{
			ptail->next = newnode;
			ptail = newnode;
		}
	}
	return phead;
}

void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuySLTNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}



void SLTPopBack1(SLTNode** pphead)
{
	assert(*pphead);
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* prev = NULL;
		SLTNode* tail = *pphead;
		while (tail->next)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		prev->next = NULL;
	}
}


void SLTPopBack(SLTNode** pphead)
{
	assert(*pphead);
	SLTNode* ptail = *pphead;
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		while (ptail->next->next)
		{
			ptail = ptail->next;
		}

		free(ptail->next);
		ptail->next = NULL;
	}
}

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

void SLTPopFront(SLTNode** pphead)
{
	assert(*pphead);
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

//单链表在pos位置之后插入x
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pos);
	if (*pphead == NULL)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		SLTNode* newnode = BuySLTNode(x);
		newnode->next = pos;
		prev->next = newnode;
	}
}



void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	if (pos->next == NULL)
	{
		return;
	}
	else
	{
		SLTNode* nextNode = pos->next;
		pos->next = nextNode->next;
		free(nextNode);
	}
}

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pos);
	if (pos == *pphead)
	{
		SLTPopFront(pphead);         //头删函数的复用
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		prev->next = pos->next;
		free(pos);
	}
}

void SLTDestroy(SLTNode** pphead)
{
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

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

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

相关文章

Spring Security登录流程分析

本文内容来自王松老师的《深入浅出Spring Security》&#xff0c;自己在学习的时候为了加深理解顺手抄录的&#xff0c;有时候还会写一些自己的想法 登录流程分析 要搞请求Spring Security认证流程&#xff0c;我们先得认识与之相关的三个基本组件&#xff1a;AuthenticationMa…

Redis——》事务

推荐链接&#xff1a; 总结——》【Java】 总结——》【Mysql】 总结——》【Spring】 总结——》【SpringBoot】 总结——》【MyBatis、MyBatis-Plus】 Redis——》事务一、概念二、示例multi、execdiscardwatch三、事务发生错误1、入队阶段发生错误2、执…

数据库课程设计——学籍管理系统

目录 1 问题的提出 1 2 需求分析 2 2.1需求描述 2 2.2数据字典 3 2.2.1数据项 3 2.2.2主要的数据流定义 6 2.3数据流图和业务流图 7 2.3.1顶层数据流图 7 2.3.2第一层数据流图 8 2.3.3第一层数据流图 8 2.3.4第一层数据流图 9 2.3.5第一层数据流图 9 3 概念结构设计 10 3.1实…

docker基础命令

docker基础 docker中的三个基本概念: 镜像: Docker 镜像&#xff08;Image&#xff09;&#xff0c;就相当于是一个 root 文件系统 容器: 镜像&#xff08;Image&#xff09;和容器&#xff08;Container&#xff09;的关系&#xff0c;就像是面向对象程序设计中的类和实例一…

基于STM32的DS18B20多点测温系统(Proteus仿真+程序)

编号&#xff1a;22 基于STM32的DS18B20多点测温系统 功能描述&#xff1a; 本设计由STM32F103单片机三路DS18B20温度传感器1602液晶显示模块组成。 1、主控制器是STM32F103单片机 2、三路共用“单总线”DS1820温度传感器测量温度 3、1602液晶显示温度&#xff0c;保留一位小…

[附源码]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…

Meta开源新工具啊,Git地位危险了?

程序员宝藏库&#xff1a;https://gitee.com/sharetech_lee/CS-Books-Store 从事编程开发的同学&#xff0c;绝大多数都会和版本控制系统打交道。 提到版本控制系统&#xff0c;目前比较主流的就是Git和SVN&#xff0c;尤其是Git&#xff0c;使用最为广泛。 关于Git和SVN之间…

python绘制Bubble气泡图pltscatter

python绘制Bubble气泡图pltscatter 先上结果&#xff1a; 基础语法&#xff1a; Axes.scatter(x, y**,** sNone**,** cNone**,** markerNone**,** cmapNone**,** normNone**,** vminNone**,** vmaxNone**,** alphaNone**,** linewidthsNone**,** , edgecolorsNone,* plotnonf…

nVisual 场景搭建所需接口

使用nVisua在创建新的项目步骤是搭建场景、创建对象、创建对象连接&#xff0c;本章小编带大家先了解搭建场景需要的接口。 场景搭建可根据自身项目需要搭建园区、建筑、楼层、机房这几类场景。分别用到了地图场景创建接口、CAD场景创建接口、静态图片背景创建接口。 1.地图场…

企业内训系统源码,为企业量身定制学习平台

如何进行企业内训系统开发&#xff1f;不同的直播平台的功能是不同的&#xff0c;企业的发展与员工的素质、能力、工作人效等不可分割&#xff0c;如何提高员工的工作能力&#xff0c;企业内部培训是离不开的&#xff0c;那么企业内训系统如何开发&#xff1f;怎么做一个企业学…

Spring Cloud面试题

什么是Spring Cloud Spring Cloud是目前最常用的微服务开发框架&#xff08;微服务的特点就是"模块化、功能化"&#xff0c;微服务架构的本质是将原来的整体项目划分成多个功能模块&#xff0c;每个功能模块都可以独立运行提供服务&#xff09;&#xff0c;它利用Sp…

wps和office可以同时装吗?

wps和office是很多用户都在使用的办公软件&#xff0c;那就有小白用户问了一台电脑可以存在wps和office吗&#xff1f;两个软件兼容吗&#xff1f;wps和office性质上都是办公软件&#xff0c;但是并不算重复&#xff0c;因此是可以同时安装的。 wps和office能同时安装吗 答&…

合并多个PDF怎么合并?建议学会这几个合并方法

你们平时工作的时候&#xff0c;看到自己的电脑桌面有很多文档文件&#xff0c;会不会觉得很杂乱&#xff1f;如果不将这些资料好好整理一番&#xff0c;都不能好好完成接下来的工作。其实如果是同种类型的PDF文件&#xff0c;我们可以将它们合并&#xff0c;这样既可以归类&am…

STM32单片机DS18B20测温液晶1602显示例程(Proteus仿真+程序)

编号&#xff1a;21 STM32单片机DS18B20测温液晶1602显示例程 功能描述&#xff1a; 本设计由STM32F103C8T6单片机最小系统DS18B20温度传感器1602液晶显示模块组成。 1、主控制器是STM32F103C8T6单片机 2、DS1820温度传感器测量温度 3、1602液晶显示温度&#xff0c;保留一位…

理解Linux32位机器下虚拟地址到物理地址的转化

文章目录前言一、基本概念介绍二、虚拟地址到物理地址的转化过程总结前言 简要介绍LINUX32位系统下虚拟地址到物理地址的转化过程。 一、基本概念介绍 在32位机器下&#xff0c;IO的基本单位是块&#xff08;块&#xff1a;4kb),在程序编译成可执行程序时也划分好了以4kb为单…

Linux的前世今生

14天学习训练营导师课程&#xff1a; 互联网老辛《 符合学习规律的超详细linux实战快速入门》 努力是为了不平庸~ 学习有些时候是枯燥的&#xff0c;但收获的快乐是加倍的&#xff0c;欢迎记录下你的那些努力时刻&#xff08;学习知识点/题解/项目实操/遇到的bug/等等&#xf…

使用STM32CubeMX实现按下按键,电平反转

需提前学习&#xff1a;使用STM32CubeMX实现LED闪烁 目录 原理图分析 按键部分原理图分析 LED部分原理图分析 STM32CubeMX配置 关于STM32CubeMXSYS的Debug忘记配置Serial Wire处理办法 GPIO配置 LED的GPIO配置 KEY1配置 关于PA0后面这个WKUP是什么&#xff1f; 那么啥…

Linux开发工具(4)——Makefile

文章目录Makefilemakefile语法makefile原理Linux小程序倒计时小程序进度条程序Makefile Makefile是Linux下的项目自动化构建工具。 Makefile包含两部分&#xff0c;make是一个指令&#xff0c;makefile是一个文件。 在makefile这个文件里面需要写两部分内容&#xff1a; 依赖…

【LeetCode】891.子序列宽度之和

**> ## 题目描述 一个序列的 宽度 定义为该序列中最大元素和最小元素的差值。 给你一个整数数组 nums &#xff0c;返回 nums 的所有非空 子序列 的 宽度之和 。由于答案可能非常大&#xff0c;请返回对 109 7 取余 后的结果。 子序列 定义为从一个数组里删除一些&#xff…

Scala009--Scala中的数据结构【映射】

目录 一&#xff0c;概述 二&#xff0c;map的声明 1&#xff0c;不可变map 三&#xff0c;HashMap的声明 1&#xff0c;可变hashmap 四&#xff0c;map常用函数 1&#xff0c;查看map中的元素个数 size 2&#xff0c;获取map集合中key对应的value值 1&#xff09;使…