数据结构 | 单链表

news2024/10/5 13:14:45

🌳🌲🌱本文已收录至:数据结构 | C语言
更多知识尽在此专栏中!

道阻且长,行则将至
欢迎标头


文章目录

  • 🌳前言
  • 🌳正文
    • 🌲链表打印与销毁
      • 🪴打印
      • 🪴销毁
    • 🌲尾部插入与删除
      • 🪴节点申请
      • 🪴尾插
      • 🪴尾删
    • 🌲头部插入与删除
      • 🪴头插
      • 🪴头删
    • 🌲节点查找与修改
      • 🪴查找
      • 🪴修改
    • 🌲任意位置插入与删除
      • 🪴简单版
        • 🌱插入
        • 🌱删除
      • 🪴困难版
        • 🌱插入
        • 🌱删除
    • 🌲源码区
      • 🪴功能声明头文件部分
      • 🪴功能实现源文件部分
      • 🪴主函数调用源文件部分
    • 🌲相关OJ试题推荐
  • 🌳总结


🌳前言

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

这是百度百科对于 单链表 的解释,通俗来说,单链表 就是一种数据结构,它包含了一个 数据域 data 和一个 指针域 next ,最大的特点就是 链式存储 。链表有很多种,其中 单链表 是最基本、最简单的一种结构,很多OJ题都会利用 单链表 出题,后面的部分高阶数据结构也会用到 单链表 ,因此学好 单链表 很重要。除了 单链表 外,还有 双向带头循环链表 (后面会介绍)等链表类型,先来进入 单链表 的学习吧!

引图


🌳正文

🌲链表打印与销毁

🪴打印

单链表 创建时是一个结构体类型的指针,一开始指向空,只有在经过插入数据后才会有自己的指向 ,因此我们可以根据这一特点,遍历 整个 单链表 ,并输出其中的 数据域 data
单链表打印

void SLTPrint(const SLT** pphead)	//单链表的打印
{
	assert(pphead);	//不需要断言 *pphead ,因为存在空链表打印的情况,是合法的

	printf("\n\n当前链表为: ");
	const SLT* cur = *pphead;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;	//cur要向后移动
	}

	printf("NULL\n\n\n");	//链表最后为空
}

🪴销毁

销毁 单链表 也需要将其 遍历 一遍,因为链表中的元素不是连续存放的,只能逐个节点销毁 ,销毁思想为:利用 *pphead 遍历整个 单链表 ,保存头节点 *pphead 的下一个节点信息至 cur,释放原头节点,更新头节点信息(把 cur 的值赋给头节点 *pphead )如此重复,直到释放完所有节点即可。
销毁

//不带哨兵位的单链表不需要初始化
void SLTDestroy(SLT** pphead)	//单链表的销毁
{
	assert(pphead);	//一二级指针都不能为空

	//空表不走销毁程序,但不能报错
	if (*pphead)
	{
		while (*pphead)
		{
			SLT* cur = (*pphead)->next;	//记录头的下一个位置
			free(*pphead);
			*pphead = cur;	//向后移动,不断销毁
		}
	}
}

🌲尾部插入与删除

🪴节点申请

单链表 是由一个一个独立存在的节点组成的结构,当涉及插入操作时,向内存申请节点,涉及删除操作时,就要把对应的节点销毁

static SLT* buyNode(const SLTDataType x)	//买节点
{
	SLT* newnode = (SLT*)malloc(sizeof(SLT));
	assert(newnode);	//防止申请失败的情况

	newnode->data = x;
	newnode->next = NULL;

	return newnode;	//返回买好的节点
}

🪴尾插

单链表 尾插是比较费劲的,因为得先通过头节点指针向后 遍历 找到尾节点,然后将尾节点与新节点之间建立链接关系,其中还得分情况尾插

  • 链表为空,直接把新节点赋给头节点
  • 不为空,就需要找到尾节点,建立链接关系

关于 单链表 中函数用二级指针的问题:
插入或删除时,如果是第一次操作,需要对头节点本身造成改变,且头节点是一个 一级指针 ,因此需要通过 二级指针 的方式来在函数中改变头节点的值。至于后续的操作,都只是改变了结构体中的 next 值,因此使用 一级指针 就够了,但是为了函数设计时的普适性,单链表 中的函数参数都设计成了 二级指针 的形式。

尾插

void SLTPushBack(SLT** pphead, const SLTDataType x)	//尾插
{
	assert(pphead);

	SLT* newnode = buyNode(x);
	SLT* tail = *pphead;
	//尾插分情况,链表为空,直接赋值,不为空,找到尾,再链接
	if (tail == NULL)
	{
		*pphead = tail = newnode;	//直接赋值
	}
	else
	{
		while (tail->next != NULL)
		{
			tail = tail->next;	//找到尾节点
		}
		tail->next = newnode;	//链接
	}

	//SLTInsert(pphead, NULL, x);		//可以复用任意位置前插
}

🪴尾删

尾删操作与尾插基本一致,同样是需要找到尾节点,不过每次 tail 指针在向后移动前,会先使用一个 prev 指针保存 tail 的信息,当 tail 为尾节点时,释放 tail ,并将 prev->next 置空,此时的 prev 就是新的尾节点,因为原理都差不多,这里就不用动图展示了,值得注意的是尾删也分两种情况

  • 只有一个节点,此时直接释放头节点(尾节点),链表置空
  • 存在多个节点,需要先找到尾节点与尾节点的上一个节点,确定新的尾
  • 并不是所有情况都能删除,比如表空的情况,是不能执行操作的,可以用断言处理

尾删第一步
尾删第二步

void SLTPopBack(SLT** pphead)	//尾删
{
	assert(pphead);
	assert(*pphead);	//如果链表为空,是删不了的

	SLT* tail = *pphead;
	SLT* prev = NULL;
	while (tail->next)
	{
		prev = tail;	//保存上一个节点信息
		tail = tail->next;	//找到尾节点
	}

	free(tail);
	
	//分情况,如果prev是空,说明删除的尾节点同时也是头节点
	if (prev)
		prev->next = tail = NULL;	//把尾节点的上一个节点指向空
	else
		*pphead = NULL;	//此时直接把prev置空

	/*SLT* tail = *pphead;
	while (tail->next)
	{
		tail = tail->next;
	}
	SLTErase(pphead, tail);*/	//可以复用任意位置删除
}

🌲头部插入与删除

🪴头插

对于头部操作来说,单链表 是很轻松的,比如 单链表 头插的本质就是将 新节点 newnode头节点 *pphead 链接,然后更新头节点信息就行了,即 *pphead = newnode ,三行代码就解决了。
头插第一步
头插第二步

void SLTPushFront(SLT** pphead, const SLTDataType x)	//头插
{
	assert(pphead);

	SLT* newhead = buyNode(x);
	newhead->next = *pphead;	//直接链接
	*pphead = newhead;	//更新头节点信息
	//代码简洁之道
	
	//SLTInsert(pphead, *pphead, x);	//可以复用任意位置前插
}

🪴头删

头删也是比较简单的,先用 cur 指向头节点,先保存 头节点 cur 的下一个节点信息至 节点 newhead,释放 原头节点 cur,更新 newhead 为链表的新头,即 *pphead = newnode 当然涉及删除的操作,都需要进行表空检查,如果链表为空,是不能执行删除的。
头删第一步
头删第二步

void SLTPopFront(SLT** pphead)	//头删
{
	assert(pphead);
	assert(*pphead);	//头删同样存在空不能删的情况

	//先找到头的下一个节点,然后赋值新头
	SLT* cur = *pphead;	//指向头节点
	SLT* newhead = cur->next;	//保存头节点的下一个节点信息

	free(cur);
	cur = NULL;

	*pphead = newhead;	//赋值新头

	//SLTErase(pphead, *pphead);	//可以复用任意位置删除
}

🌲节点查找与修改

🪴查找

查找函数很简单,遍历+比较 就行了,找到目标元素值后,返回相关节点信息,其实查找这个函数主要是为了配合后面任意插入删除函数的 ,所以比较简单。

SLT* SLTFind(const SLT** pphead, const SLTDataType x)	//查找值为x的节点(第一次出现)
{
	assert(pphead);

	SLT* cur = (SLT*)*pphead;	//指向头节点
	while (cur)
	{
		if (cur->data == x)
			return cur;	//找到了,返回相关节点信息
		cur = cur->next;
	}

	return NULL;	//没有找到的情况
}

🪴修改

修改函数是在查找函数基础上进行的:当我们输入元素值交给查找函数,找到目标节点后返回其节点信息,然后直接通过返回的节点改变 data 值就行了,在调用查找函数的前提下,一行代码就解决了。

void SLTModify(SLT* node, const SLTDataType val)	//修改 node 节点处的值
{
    //注意:在调用函数时,可以通过链式访问,将查找函数的返回值作为形参一传入就行了
	assert(node);	//断言,如果节点node是空指针,是不能做修改的
	node->data = val;
}

🌲任意位置插入与删除

🪴简单版

简单版是在任意位置后插入元素,删除任意位置后的节点,根据 单链表 的特性,对后面节点进行操作是比较简单,无非就是改变链接关系。但是这种对后操作存在缺陷:不适合实现头插、头删

🌱插入

插入(后插)主要分两步

  • 获取信息
  • 改变链接关系

获取信息:有三个关键信息:被插入节点 cur、待插入节点 newnodecur 的下一个节点 tail

改变链接关系:很简单,cur->next = newnode,后插嘛,先把待插入节点链接到 cur 后面,然后再把 newnodetail 链接起来就行了

这里的 nodeAfter 是需要通过查找函数找出来的,它是一个 一级指针

//这两个有缺陷,不能对头节点进行操作,但实现起来比较简单
void SLTInsertAfter(SLT* nodeAfter, const SLTDataType x)	//任意插,后插法
{
	assert(nodeAfter);

	SLT* cur = nodeAfter;
	SLT* newnode = buyNode(x);
	SLT* tail = cur->next;	//先保存被插入节点的下一个节点信息
	
	//更改链接关系,后插完成
	cur->next = newnode;
	newnode->next = tail;
}

🌱删除

删除思路,和头删差不多

  • 找到待插入节点 cur
  • 找到 cur 的下一个节点 tail
  • 找到 tail 的下一个节点 newtail

接下来就很简单了,释放 tail 节点,更改链接关系,当然断言是少不了
任意删,后删,第一步
任意插,后插,第二步

void SLTEraseAfter(SLT* nodeAfter)	//任意删,删除后面节点
{
	assert(nodeAfter);
	assert(nodeAfter->next);	//如果目标节点的下一个为空,是后删不了的

	SLT* cur = nodeAfter;
	SLT* tail = cur->next;	//待删除的节点
	SLT* newtail = tail->next;	//新尾

	free(tail);
	tail = NULL;

	cur->next = newtail;	//更改链接关系
}

🪴困难版

困难版就比较麻烦了,因为它要实现的是任意位置前插元素、删除任意位置的节点,单链表 的最大缺点是不能回退,这就意味着即使我们得到了待删除节点,也是很难求出它的上一个节点的 ,对于这种尴尬情况,只能老老实实从头节点处开始向后 遍历 寻找,直到找到待删除节点的上一个节点。

🌱插入

其实这个也没有多困难,无非就是比上一种多个参数,然后多了一步遍历操作而已,内核思想任然不变

  • 获取信息
  • 更改链接关系

任意位置前插

//这两个实现起来比较麻烦,但是很全能
void SLTInsert(SLT** pphead, SLT* node, const SLTDataType x)	//任意插,前插法
{
	assert(pphead);

	SLT* newnode = buyNode(x);
	SLT* cur = *pphead;
	SLT* prev = NULL;
	while (cur != node)
	{
		prev = cur;	//要找到目标节点的上一个节点
		cur = cur->next;	
	}

	//判断一下,是否为空表插入
	//走的是尾插的那一套思想
	if (prev)
	{
		prev->next = newnode;
		newnode->next = node;
	}
	else
	{
		newnode->next = node;
		*pphead = newnode;	//空表需要更新头节点信息
	}
}

🌱删除

删除是一样的逻辑,不过多了一个 tail 而已,指向的位置是 node 的下一个节点,其余步骤跟插入基本一致,之后也是一样的更改链接关系,一样需要判断是否为空表,如果为空表需要更新头节点信息

其实现在看来,困难版的插入删除,就像是尾部插入删除的升级版,有些麻烦,但很可靠,困难版的任意位置插入删除可以完全替代头尾插入删除,也就是说写这一对函数就够了。

void SLTErase(SLT** pphead, SLT* node)	//任意删,删除当前节点
{
	assert(pphead);
	assert(node);	//不必检查*pphead的合法性,查验node就行了

	SLT* cur = *pphead;	//走的是尾删的那一套思想
	SLT* prev = NULL;
	SLT* tail = node->next;
	while (cur != node)
	{
		prev = cur;
		cur = cur->next;
	}

	free(node);
	//跟尾插一样,需要判断一下
	if (prev)
		prev->next = tail;
	else
		*pphead = tail;
}

🌲源码区

代码放一起看,会更好理解一些~

🪴功能声明头文件部分

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

typedef int SLTDataType;	//单链表的数据类型

typedef struct SListNode
{
	SLTDataType data;	//数据域
	struct SListNode* next;	//指针域
}SLT;	//重命名为 SLT

void SLTDestroy(SLT** pphead);	//单链表的销毁
void SLTPrint(const SLT** pphead);	//单链表的打印

void SLTPushBack(SLT** pphead, const SLTDataType x);	//尾插
void SLTPopBack(SLT** pphead);	//尾删

void SLTPushFront(SLT** pphead, const SLTDataType x);	//头插
void SLTPopFront(SLT** pphead);	//头删

//这两个有缺陷,不能对头节点进行操作,但实现起来比较简单
void SLTInsertAfter(SLT* nodeAfter, const SLTDataType x);	//任意插,后插法
void SLTEraseAfter(SLT* nodeAfter);	//任意删,删除后面节点

//这两个实现起来比较麻烦,但是很全能
void SLTInsert(SLT** pphead, SLT* node, const SLTDataType x);	//任意插,前插法
void SLTErase(SLT** pphead, SLT* node);	//任意删,删除当前节点

SLT* SLTFind(const SLT** pphead, const SLTDataType x);	//查找值为x的节点(第一次出现)

void SLTModify(SLT* node, const SLTDataType val);	//修改 node 节点处的值

🪴功能实现源文件部分

#define _CRT_SECURE_NO_WARNINGS 1	
#include"SList.h"

//不带哨兵位的单链表不需要初始化
void SLTDestroy(SLT** pphead)	//单链表的销毁
{
	assert(pphead);	//一二级指针都不能为空

	//空表不走销毁程序,但不能报错
	if (*pphead)
	{
		while (*pphead)
		{
			SLT* cur = (*pphead)->next;	//记录头的下一个位置
			free(*pphead);
			*pphead = cur;	//向后移动,不断销毁
		}
	}
}

void SLTPrint(const SLT** pphead)	//单链表的打印
{
	assert(pphead);	//不需要断言 *pphead ,因为存在空链表打印的情况,是合法的

	printf("\n\n当前链表为: ");
	const SLT* cur = *pphead;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;	//cur要向后移动
	}

	printf("NULL\n\n\n");	//链表最后为空
}

static SLT* buyNode(const SLTDataType x)	//买节点
{
	SLT* newnode = (SLT*)malloc(sizeof(SLT));
	assert(newnode);	//防止申请失败的情况

	newnode->data = x;
	newnode->next = NULL;

	return newnode;	//返回买好的节点
}

void SLTPushBack(SLT** pphead, const SLTDataType x)	//尾插
{
	assert(pphead);

	SLT* newnode = buyNode(x);
	SLT* tail = *pphead;
	//尾插分情况,链表为空,直接赋值,不为空,找到尾,再链接
	if (tail == NULL)
	{
		*pphead = tail = newnode;	//直接赋值
	}
	else
	{
		while (tail->next != NULL)
		{
			tail = tail->next;	//找到尾节点
		}
		tail->next = newnode;	//链接
	}

	//SLTInsert(pphead, NULL, x);		//可以复用任意位置前插
}

void SLTPopBack(SLT** pphead)	//尾删
{
	assert(pphead);
	assert(*pphead);	//如果链表为空,是删不了的

	SLT* tail = *pphead;
	SLT* prev = NULL;
	while (tail->next)
	{
		prev = tail;	//保存上一个节点信息
		tail = tail->next;	//找到尾节点
	}

	free(tail);
	
	//分情况,如果prev是空,说明删除的尾节点同时也是头节点
	if (prev)
		prev->next = tail = NULL;	//把尾节点的上一个节点指向空
	else
		*pphead = NULL;	//此时直接把prev置空

	/*SLT* tail = *pphead;
	while (tail->next)
	{
		tail = tail->next;
	}
	SLTErase(pphead, tail);*/	//可以复用任意位置删除
}

void SLTPushFront(SLT** pphead, const SLTDataType x)	//头插
{
	assert(pphead);

	SLT* newhead = buyNode(x);
	newhead->next = *pphead;	//直接链接
	*pphead = newhead;	//更新头节点信息
	//代码简洁之道
	
	//SLTInsert(pphead, *pphead, x);	//可以复用任意位置前插
}

void SLTPopFront(SLT** pphead)	//头删
{
	assert(pphead);
	assert(*pphead);	//头删同样存在空不能删的情况

	//先找到头的下一个节点,然后赋值新头
	SLT* cur = *pphead;	//指向头节点
	SLT* newhead = cur->next;	//保存头节点的下一个节点信息

	free(cur);
	cur = NULL;

	*pphead = newhead;	//赋值新头

	//SLTErase(pphead, *pphead);	//可以复用任意位置删除
}

//这两个有缺陷,不能对头节点进行操作,但实现起来比较简单
void SLTInsertAfter(SLT* nodeAfter, const SLTDataType x)	//任意插,后插法
{
	assert(nodeAfter);

	SLT* cur = nodeAfter;
	SLT* newnode = buyNode(x);
	SLT* tail = cur->next;	//先保存被插入节点的下一个节点信息
	
	//更改链接关系,后插完成
	cur->next = newnode;
	newnode->next = tail;
}

void SLTEraseAfter(SLT* nodeAfter)	//任意删,删除后面节点
{
	assert(nodeAfter);
	assert(nodeAfter->next);	//如果目标节点的下一个为空,是后删不了的

	SLT* cur = nodeAfter;
	SLT* tail = cur->next;	//待删除的节点
	SLT* newtail = tail->next;	//新尾

	free(tail);
	tail = NULL;

	cur->next = newtail;	//更改链接关系
}

//这两个实现起来比较麻烦,但是很全能
void SLTInsert(SLT** pphead, SLT* node, const SLTDataType x)	//任意插,前插法
{
	assert(pphead);

	SLT* newnode = buyNode(x);
	SLT* cur = *pphead;
	SLT* prev = NULL;
	while (cur != node)
	{
		prev = cur;	//要找到目标节点的上一个节点
		cur = cur->next;	
	}

	//判断一下,是否为空表插入
	//走的是尾插的那一套思想
	if (prev)
	{
		prev->next = newnode;
		newnode->next = node;
	}
	else
	{
		newnode->next = node;
		*pphead = newnode;	//空表需要更新头节点信息
	}
}

void SLTErase(SLT** pphead, SLT* node)	//任意删,删除当前节点
{
	assert(pphead);
	assert(node);	//不必检查*pphead的合法性,查验node就行了

	SLT* cur = *pphead;	//走的是尾删的那一套思想
	SLT* prev = NULL;
	SLT* tail = node->next;
	while (cur != node)
	{
		prev = cur;
		cur = cur->next;
	}

	free(node);
	//跟尾插一样,需要判断一下
	if (prev)
		prev->next = tail;
	else
		*pphead = tail;
}


SLT* SLTFind(const SLT** pphead, const SLTDataType x)	//查找值为x的节点(第一次出现)
{
	assert(pphead);

	SLT* cur = (SLT*)*pphead;	//指向头节点
	while (cur)
	{
		if (cur->data == x)
			return cur;	//找到了,返回相关节点信息
		cur = cur->next;
	}

	return NULL;	//没有找到的情况
}

void SLTModify(SLT* node, const SLTDataType val)	//修改 node 节点处的值
{
	//注意:在调用函数时,可以通过链式访问,将查找函数的返回值作为形参一传入就行了
	assert(node);	//断言,如果节点node是空指针,是不能做修改的

	node->data = val;
}

🪴主函数调用源文件部分

#define _CRT_SECURE_NO_WARNINGS 1	
#include"SList.h"

void menu()
{
	printf("0.退出	1.打印\n");
	printf("2.尾插	3.尾删\n");
	printf("4.头插	5.头删\n");
	printf("6.任意插(后插)	7.任意删(后删)\n");
	printf("8.任意插(前插)	9.任意删(当前)\n");
	printf("10.查找	11.修改\n");
}

int main()
{
	SLT* s = NULL;
	int input = 1;
	while (input)
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		system("cls");	//清屏函数,让显示效果更好
		int val = 0;	//待插入值
		int pos = 0;	//待查找节点值
		switch (input)
		{
		case 0:
			printf("成功退出\n");
			break;
		case 1:
			SLTPrint(&s);
			break;
		case 2:
			printf("请输入一个数:>");
			scanf("%d", &val);
			SLTPushBack(&s, val);
			break;
		case 3:
			SLTPopBack(&s);
			break;
		case 4:
			printf("请输入一个数:>");
			scanf("%d", &val);
			SLTPushFront(&s, val);
			break;
		case 5:
			SLTPopFront(&s);
			break;
		case 6:
			printf("请输入被插入的节点值:>");
			scanf("%d", &pos);
			printf("请输入一个数:>");
			scanf("%d", &val);
			SLTInsertAfter(SLTFind(&s, pos), val);
			break;
		case 7:
			printf("请输入被删除的节点值:>");
			scanf("%d", &pos);
			SLTEraseAfter(SLTFind(&s, pos));
			break;
		case 8:
			printf("请输入被插入的节点值:>");
			scanf("%d", &pos);
			printf("请输入一个数:>");
			scanf("%d", &val);
			SLTInsert(&s, SLTFind(&s, pos), val);
			break;
		case 9:
			printf("请输入被删除的节点值:>");
			scanf("%d", &pos);
			SLTErase(&s, SLTFind(&s, pos));
			break;
		case 10:
			printf("请输入被查找的节点值:>");
			scanf("%d", &pos);
			SLT* tmp = SLTFind(&s, pos);
			printf("当前节点的地址为:%p\n", tmp);
			break;
		case 11:
			printf("请输入被修改的节点值:>");
			scanf("%d", &pos);
			printf("请输入一个数:>");
			scanf("%d", &val);
			SLTModify(SLTFind(&s, pos), val);
			break;
		default :
			printf("选择错误,重新选择!\n");
			break;
		}
	}
	SLTDestroy(&s);	//每次程序运行结束,都会执行销毁函数
	return 0;
}

🌲相关OJ试题推荐

下面是几道关于 单链表 的OJ试题,可以试着解决一下,加强对链表的认识

  1. 203.移除链表元素
  2. 206.反转单链表
  3. 876.链表的中间结点
  4. 链表中倒数第k个结点
  5. 21.合并两个有序链表
  6. CM11 链表分割
  7. OR36 链表的回文结构
  8. 160.相交链表
  9. 141.环形链表
  10. 141.环形链表 ||
  11. 138.复制带随机指针的链表

🌳总结

以上就是关于 单链表 的全部内容了,单链表 中用到了 二级指针 这个东西,如果使用带哨兵位的 单链表 就可以不用 二级指针 ,但是这种结构用的比较少,单纯的学好 单链表 ,能快速提高我们对链表的认识,毕竟链表这个工具后续还会用到。从文中可以看出,单链表 相对于 顺序表 ,不用考虑空间问题,且头插头删效率很高,可惜 单链表 不支持下标的随机访问。总之,顺序表单链表 各有各的用途,二者相辅相成,都是很不错的数据结构。

如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!

如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正

星辰大海

相关文章推荐
数据结构 | 顺序表
数据结构 | 时间复杂度与空间复杂度
C语言进阶——动态内存管理

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

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

相关文章

javaweb JAVA JSP销售系统购物系统jsp购物系统购物商城系统源码(jsp电子商务系统)网上在线销售

JSP销售系统购物系统jsp购物系统购物商城系统源码&#xff08;jsp电子商务系统&#xff09;网上在线销售

Linux基础 - 虚拟化介绍(KVM)

‍‍&#x1f3e1;博客主页&#xff1a; Passerby_Wang的博客_CSDN博客-系统运维,云计算,Linux基础领域博主 &#x1f310;所属专栏&#xff1a;『Linux基础』 &#x1f30c;上期文章&#xff1a; Linux基础 - 服务管理&#xff08;systemd&#xff09; &#x1f4f0;如觉得博…

爬取医药卫生知识服务系统的药品数据——超详细流程

爬取医药卫生知识服务系统的药品数据——超详细流程 文章目录爬取医药卫生知识服务系统的药品数据——超详细流程前言一、寻找药品数据二、爬取药品ID1.资源获取2.数据提取3.资源保存4.主函数5.总体代码三、爬取药品信息1.加载资源ID2.获取数据3.数据提取4.保存信息5.主函数6.总…

SpringBoot-属性绑定和bean属性校验

目录 属性绑定 自定义类属性绑定 第三方bean属性匹配 规则:松散绑定&#xff08;宽松绑定&#xff09; Bean属性校验 属性绑定 属性绑定&#xff1a;我们可以使用配置文件对类的属性进行赋值绑定。 自定义类属性绑定 我们自定义一个类&#xff0c;在此使用yml文件进行类…

【滤波器】基于matlab实现微波带低通高通带通滤波器设计

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

python科研做图系列之雷达图

文章目录参考资料matplotlib库画的复现一个 pyecharts的雷达图尝试在上面的基础上&#xff0c;把pyecharts 导出存为一般的png图尝试在上面的基础上&#xff0c;把pyecharts 导出存为一般的矢量图用pygal画雷达图参考资料 参考知乎 CSDN给出了一些参数 matplotbib库雷达图官网 …

Python实现九九乘法表的几种方式,入门必备案例~超级简单~

前言 嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! 们在学习Python的过程中需要不断的积累和练习&#xff0c;这样才能够走的更远&#xff0c; 今天一起来学习怎么用Python写九九乘法表~ 第一种方法&#xff1a;for-for 代码&#xff1a; for i in range(1, 10):for j in…

数据挖掘面试经总结【私人版,仅供参考】

1特征归一化 1.1为什么需要对数值类型的特征做归一化&#xff1f; 线性函数归一化零均值归一化 1.2在对数据进行预处理时&#xff0c;应该怎样处理类别型特征&#xff1f; 序号编码独热编码二进制编码 1.3什么是组合特征&#xff1f;如何处理高维组合特征&#xff1f; 例…

【python】云打印实现

这两天为了实现云打印功能找了很多相关的文章 记录一下这一篇&#xff0c;python云打印实现-朝花夕拾&#xff0c;代码通过监听文件夹有无产生新文件来判断是否执行&#xff0c;我尝试运行了下没问题&#xff0c;于是打算转载一下 程序运行结果 由于对方的代码和我实现的有点出…

【Maven】你好,Maven >>> 与Maven的初次见面~

个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ 个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的道路上摸爬滚打&#xff0c;记录学习的过程~ 与Maven的初次见面~一、了解Maven二、Maven的…

Flink双流join导致数据重复

大家都知道flink sql 中 left join 数据不会互相等待&#xff0c;存在 retract 问题&#xff0c;会导致写入 kafka 的数据量变大&#xff0c;就会导致出现数据重复的问题。 举例&#xff1a;即常见的曝光日志流&#xff08;show_log&#xff09;通过 log_id 关联点击日志流&am…

SQL:数据去重的三种方法

1、使用distinct去重 distinct用来查询不重复记录的条数&#xff0c;用count(distinct id)来返回不重复字段的条数。用法注意&#xff1a; distinct【查询字段】&#xff0c;必须放在要查询字段的开头&#xff0c;即放在第一个参数&#xff1b;只能在SELECT 语句中使用&#…

spring整合fastdfs客户端

解决Dependency ‘com.github.tobato:fastdfs-client:1.27.2’ not found 错误问题。 一、 将fastdfs客户端git下来 git https://github.com/happyfish100/fastdfs-client-java.gitcd fastdfs-client-java然后将fastdfs-client-java构建到本地maven仓库 mvn clean install&…

Pandas的数据结构

Pandas的数据结构 处理CSV 文件 CSV&#xff08;Comma-Separated Values&#xff0c;逗号分隔值&#xff0c;有时也称为字符分隔值&#xff0c;因为分隔字符也可以不是逗号&#xff09;&#xff0c;其文件以纯文本形式存储表格数据&#xff08;数字和文本&#xff09;。 Pan…

【强化学习】深入浅出强化学习--机器人找金币

文章目录Grid_mdp.py定义和初始化从环境状态构建观测值ResetStepRenderingClose注册环境参考文章Grid_mdp.py 定义和初始化 首先自定义环境&#xff0c;自定义的环境将继承gym.env环境。在初始化的时候&#xff0c;可以指定环境支持的渲染模式&#xff08;例如human,rgb_arra…

项目实战 | YOLOv5 + Tesseract-OCR 实现车牌号文本识别

项目实战 | YOLOv5 Tesseract-OCR 实现车牌号文本识别 最近看到了各种各样的车牌识别&#xff0c;觉得挺有意思&#xff0c;自己也简单搞一个玩玩&#x1f63c;。 传统的图像处理算法我也不太会&#xff0c;就直接用深度学习的方法实现吧。 文章目录项目实战 | YOLOv5 Tesser…

docker基础篇——万字解读小鲸鱼

目录 前言 为什么会出现docker&#xff1f; 背景 docker理念 容器和虚拟机比较 容器发展简史 容器虚拟化技术 Why Docker docker的基本组成 镜像(image) 容器(container) 仓库(repository&#xff09; 总结 第一个docker镜像——hello-world run干了什么 …

Spring Boot与Shiro实现权限管理04

1.实现用户管理 1.1 用户列表 首先创建dto&#xff0c;用于请求与响应数据的传输。在common包下创建dto包&#xff0c;在该包下创建UserDto.java类。 Data AllArgsConstructor NoArgsConstructor public class UserDto implements Serializable {private Integer id;private…

云原生|kubernetes|本地存储hostpath-provisioner部署以及无token密码方式登陆dashboard的部署

前言&#xff1a; kubernetes的存储类大家应该都知道&#xff0c;常用的有nfs-client-provisioner这样插件形式&#xff0c;其实还有一种本地存储类的插件&#xff0c;只是这个估计很冷门&#xff0c;生产上网络存储持久卷还是主流的&#xff0c;本文将介绍一种本地存储类插件…

Linux基本命令简单介绍

Linux基本命令前言ls命令pwd命令cd命令touch命令mkdirrmdir指令rm命令前言 本文主要简单介绍一下高频使用的Linux基本命令和一些比较快捷的热键&#xff1b; 废话不多说&#xff0c;直接进入主题&#xff01;&#xff01;&#xff01; ls命令 语法&#xff1a; ls 选项目录…