详解c++---哈希闭散列

news2024/11/13 18:43:11

目录标题

  • 一道题了解哈希
  • 哈希的实现原理
    • 方法一
    • 方法二
  • 准备工作
  • insert
  • find函数
  • erase函数
  • 检测代码

一道题了解哈希

在这里插入图片描述
点击此处来尝试做这道题

首先题目告诉我们这个字符串中只含有小写的英文字母而小写的英文字母只有26个,所以我们可以创建一个大小为26的字符数组用来记录字符串中每个字符出现的次数,然后就创建一个for循环遍历整个字符串得到字符串中每个字符出现的次数,比如说下面的代码:

 char firstUniqChar(string s) {
    int arr[26]={0};
    for(auto ch:s)
    {
        arr[ch-'a']++;
     }
}

得到每个字符出现的次数之后我们就可以按照从左往右的顺序再遍历字符串根据数组中相应字符出现的次数来得到第一个只出现一次的字符,最后将其进行返回即可,如果不存在只出现一次的字符的话就返回空格字符,那么这里的代码就如下:

char firstUniqChar(string s) {
    int arr[26]={0};
    for(auto ch:s)
    {
        arr[ch-'a']++;
    }
    for(auto ch:s)
    {
        if(arr[ch-'a']==1)
        {
            return ch;
        }
    }
    return ' ';
}

提交代码就可以看到这里的代码运行的是正确的:
在这里插入图片描述
那么上面的思路就类似于哈希,我们创建一个含有26个元素的整型数组,数组中的每个元素都代表着字符串中的某个元素比如说下标为0的元素就代表的是字符a,下标为1的元素就代表的是字符b等等依次类推,数组中每个元素的大小反应的就是字符串中某个元素出现的个数,如果数组下标为1的元素大小为3的话就表示字符串中元素b出现的次数为3,如果数组下标为2的元素大小为4的话就说明字符串中元素c出现的次数为4等等依此类推,那么我们就把这种通过数组来实现的一一对应的关系称为哈希结构,一堆数据中每个元素都在数组中对应着一个位置,而该位置元素的大小则反应了数据某个元素的属性,比如说上面数组中的元素大小就反应的是字符串中某个元素出现的个数,那么这就是哈希结构希望大家能够理解。

哈希的实现原理

方法一

哈希的本质就是哈希映射也就是让key值跟存储位置建立关联关系,比如说我们想要得到一堆数据中每个数据出现的个数,这些数据的特征是都是整型且数据大小的范围是0到1000,那么我们就可以创建一个大小为1001的数组,数组中的元素表示的就是数据中的元素出现的个数,这种方法我们就称为直接定值法:就是直接拿值来确定位置,或者用值来相对的确定位置比如说数据的范围是1000到2000,那么我们还是创建一个大小为1001的数组但是数组中下标为0的数组表示的是数据1000那么这就是相对对应,这种方法仅仅适用于数据十分集中的情况如果数据十分分散的话这种方法的效率就非常的低,比如说数据的范围是1到10000,那么我们就得创建一个大小为10000的数组,但是大多数数据都分布在9900到10000之间比如说只有一个数据为1其他都大于9900,那么这就会导致有9899个空间产生了浪费没有起到任何作用,所以这种做法就只适合数据十分集中的情况。

方法二

除留余数法:这个方法就不会管你的空间有多大,而是根据你提供的数据个数来确定要开辟多大的空间,比如说当前有7个数据,那么这里就会开辟10个空间,然后用空间的值摸上数据的值,得到的结果就是要存储位置的下标,比如说当前空间的大小为10如果处理的数据是18的话就将18摸上10得到的结果就是8,所以就将18放到下标为8的位置上,这样不管多大的数据都可以在一个很小的数组上找到对应的位置,但是这样的处理方法又会带来一个新的问题就是哈希碰撞,比如说一个值为3另外一个值为13,数组的空间大小为10,那么这两个数摸出来的结果是一样的,那下标为3的空间到底存储什么?这就存在一个问题,我们把这种现象称为哈希冲突,那么我们这里就有两个方法来解决,我们接下来就首先讲解一下第一个方法:闭散列—开放顶址法,这个方法就是映射的位置已经有值了,那就按某种规律找其他的位置比如说向后占领这个就是线性探测,你占领了我的位置,我就占领当前位置的后一个位置,如果后一个位置也被占领了我就占领后面的后面的位置,比如说下面的图片:
在这里插入图片描述
3对应的位置上存储了13如果再插入23的话,这个23就应该对应到下标为3的位置上,可是3上已经有数据了啊,所以我们就往后插入放到3的后面也就是4的位置上:
在这里插入图片描述
如果我们再插入数据33的话他对应的位置依然是3,可是3上存储了数据,3后面的4也存储了数据但是4后面的5没有存储数据所以我们就将数据放到5上面比如说下面的图片:
在这里插入图片描述
那么这就是插入的规则,对于查找也是相同的道理从对应位置开始一直往后进行查找如果找到了或者查找的位置为空的话就停止查找,那删除呢?这里的删除怎么删除呢?通过上面的思路我们确实可以找到想要删除的数据,那删除的话将这个数值置为什么呢?置为空吗?空是多少呢?是0吗?好像不是吧如果我们要查找的数据刚好为0的话是不是就出错了啊,那可以将空视为负数吗?好像也不行对吧,如果空为负数的话那这个哈希表就肯定不能存储负数,那这个空为多少呢?对于这个问题我们先不解决,假设我们找到了一个合理的值为空,删除就是将指定的数据置为空的话,那这个方法就真的没有问题吗?比如说下面的图片:
在这里插入图片描述
我们想要删除23的话就是将下标为4的数据置为空,那这里的代码就如下:
在这里插入图片描述
这时我们想要查找数据33的话是不是就会出错啊,查找的规则是找到元素或者当前的元素为空的话就停止查找,如果这里要查找元素33的话是从下标3开始查找,3对应的值是13显然不相等所以就往后跳动了一格来到了4,可是下标4对应的值为空,根据规则我们就应该结束了查找所以虽然数据33确实存在但是因为删除元素的方法不正确使得查找失败,那这就说明将一个元素置为空并不能解决问题,首先空不知道是哪个值其次这样的方法还会导致查找出现问题,所以有小伙伴就想到另外一格方法,能不能将被删除元素的后面的值依次往前一格来实现删除呢?比如说下面的图片
在这里插入图片描述
如果删除23的话就得将后面的数据全部向前挪动一格一直遇到空为止,比如说下面的图片:
在这里插入图片描述
挪动数据使得之前原本对应上来的数据现在无法对应起来了所以这里肯定不能采用直接挪动的方法来删除数据,那么这里解决的方法就是添加一个数组用来表示状态,这里的状态分为三种一个是empty表示当前位置没有数据,一个是erase表示当前位置之前有数据但是数据被删除了,一个是exist表示当前位置的元素存在,那么删除就是将当前位置的状态修改成为exist存在即可,查找的时候就是从指定位置查找一直找到状态为空的位置即可,如果找到了数据但是该数据对应的位置状态为不存在的依然返回不存在,那么这就是查找插入和删除的逻辑,接下来我们将一一模拟实现上述功能代码。

准备工作

首先每个节点都有状态,状态有三种所以我们这里可以创建一个枚举来描述三个状态,哈希的底层是一个vector其次每个元素的都有一个pair来存储数据还有一个名为state变量来记录当前节点的状态,所以这里得创建一个结构体来描述节点,又因为节点要存储各种类型的数据,所以这里要添加模板给节点类并且模板中存在两个参数,那么这里的代码如下:

enum state
{
	EMPTY,
	EXIST,
	ERASE,
};

template<class K, class V>
struct HashNode
{
	pair<K, V> _kv;
	state _state = EMPTY;
};

因为哈希是通过数组来模拟实现的,所以这里的哈希类的底层就是通过vector来进行实现,其中vector中的数据类型就是HashNode,因为哈希会面临各种各样的数据,所以这里也得添加一个模板并且模板中有两个参数,那么这里的代码就如下:

 template<class K,class V>
 struct HashTable
 {
 public:

 private:
	 vector<HashNode<K,V>> _tables;
 };

因为我们插入的逻辑是将数据转换成为一个正整数然后根据这个正整数将数据·插入到vector当中,但是哈希表处理的数据可以会存在自定义类型,所以这里得给模板添加一个参数用来接收仿函数,仿函数的作用就是得到存储数据的类型所转换出来的值,我们用这个值来计算数据应该存储的位置,比如说一个string类型的数据内容为abcd,那么经过仿函数的处理之后abcd会转换成为一个值假设这个值就是每个字符的总和,所以abcd就会转换成为394,然后我们就会通过这个394来计算存储的位置比如说此时的数组的容量为10,那么它存储的位置就是4但是大家平时在使用哈希表处理整型数据和string类型数据时没有传递仿函数依然能够正常运行那这是为什么呢?答案是给这个模板参数传递了处理整型数据的仿函数缺省值,并且处理string类型的仿函数是处理整型数据的特化。这里有一个问题,哈希表有效字符的个数能等于vector的大小吗?好像可以吧,但是真的可以吗?当vector中含有很多数据的时候我们查找删除插入的时候,哈希冲突会十分的明显,这会导致哈希冲突变得越来越明显,接着就会导致哈希表的效率变得越来越低,所以这里得添加一个东西叫做负载因子,它的值等于表中有效数据的个数/表的大小,这个负载因子的值最好在0.7左右最好,如果负载因子等于0.7的话我们就扩容,负载因子越小的话冲突额度概率会越小,但是消耗的空间也就越大,所以我们得在类中添加一个变量用来记录当前表中的有效值,那么这里的代码就如下:

template<class K, class V>
struct HashNode
{
	pair<K, V> _kv;
	state _state = EMPTY;
};
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
	//对于内置类型直接将其转换成为无符号整型进行处理
		return (size_t)key;
	}
};
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t res = 0;
		for (auto ch : key)
		{
			res *= 131;
			res += 131;
		}
		//这样的处理方式可以更好的降低重复率
		return res;
	}
};
 template<class K,class V,class Hash=HashFunc<K>>
 struct HashTable
 {
 public:

 private:
	 vector<HashNode<K,V>> _tables;
	 size_t _n;//记录有效值的个数
 };

那么最后一步就是添加构造函数,这里的构造函数就是将vector数据进行扩容,将变量_n初始化为0即可,那么这里的代码就如下:

HashTable()
	:_n(0)
{
	 _tables.resize(10);
}

那么这就是准备工作接下来我们就要实现insert函数

insert

首先insert函数需要一个pair类型的参数,然后函数体的第一步就是创建一个仿函数对象,其次就是计算该数据应该插入的位置,那么这里的代码就如下:

 bool Insert(const pair<K, V>& kv)
 {
	 Hash hf;
	 size_t hashi = hf(kv.first) %  _tables.size();
 }

那么找到要位置之后我们就可以从该位置一直往后遍历,如果当前位置的状态为存在的话就继续往后,如果为删除或者为空的话就可以插入元素,当然这里为了防止越界每次遍历都将i的值模等上size,找到了就插入元素并修改当前的状态,并且对size变量加一,那么这里的代码就如下:


bool Insert(const pair<K, V>& kv)
 {
	 Hash hf;
	 size_t hashi = hf(kv.first) % _tables.size();
	 while (_tables[hashi]._state == EXIST)
	 {
		 if (_tables[hashi]._kv.first == kv.first)
		 {
			 cout << "数据重复" << endl;
			 return false;
		 }
		 ++hashi;
		 hashi%= _tables.size();
	 }
	 _tables[hashi]._kv = kv;
	 _tables[hashi]._state = EXIST;
	 ++_n;
	 return true;
 }

接下来就要实现扩容,当有效数据的个数占表中所有元素的70%时就得对其进行扩容,所以我们这里就再创建一个新哈希表该表的大小时老表的两倍,然后通过for循环遍历原来哈希表的每个元素,然后将每个元素都插入到新哈希表里面,最后再交换两个哈希表中的内部vector,那么这里的代码如下:

bool Insert(const pair<K, V>& kv)
 {
	 if (_n * 10 / _tables.size()>=7)
	 {
		 HashTable<K, V, HashFunc<K>> newHash;
		 newHash._tables.resize(_tables.size() * 2);
		 for (auto &ch : _tables)
		 {
			 if (ch._state == EXIST)
			 {
				 newHash.Insert(ch._kv);
			 }
		 }
		 _tables.swap(newHash._tables);
	 }
	 Hash hf;
	 size_t hashi = hf(kv.first) % _tables.size();
	 while (_tables[hashi]._state == EXIST)
	 {
		 if (_tables[hashi]._kv.first == kv.first)
		 {
			 cout << "数据重复" << endl;
			 return false;
		 }
		 ++hashi;
		 hashi%= _tables.size();
	 }
	 _tables[hashi]._kv = kv;
	 _tables[hashi]._state = EXIST;
	 ++_n;
	 return true;
 }

这里在扩容的时候不能简单的将内部的vector扩容为两倍,因为扩容之后内部数据的对应关系就全部都改变了,比如说容量为10的时候18对应的数据是8,扩容之后大小变成了20这时18就对应到了18位置上,所以这里不能简单的扩容,其次这里还有一个方法来实现扩容就是将数组的再创建vector变量将其大小变为现在的两倍,然后遍历原来的老数组找到每个值所对应的新下标再一个一个的插入,但是这么实现的话就会出现很多的重复工作,我们上面的写法就是借用了已有的东西减少了很多重复的工作。

find函数

先找到这个元素应该存在的位置,然后再往后查找为空的位置,如果为空了则返回false,如果出现值相等了并且当前位置的元素是存在的我们就返回true,但是找到之后会出现可以修改值得情况我们这里没找到就返回nullptr,如果找到了我们就返回这个元素所在的地址,那么这就是find函数的实现思路,该函数的代码实现如下:

 HashNode<K,V>* Find(const K& key)
 {
	 HashFunc<K> hf;
	 size_t hashi = hf(key) % _tables.size();
	 while (_tables[hashi]._state != EMPTY)
	 {
		 if (_tables[hashi]._kv.first == key&&
			 _tables[hashi]._state==EXIST)
		 {
			 return &_tables[hashi];
		 }
		 ++hashi;
		 hashi%= _tables.size();
	 }
	 return nullptr;
 }

erase函数

首先通过find函数找到数据存在的位置,因为find函数返回的是数据的地址,所以当数据存在的话我们就直接将数据的状态修改成为erase,如果数据不存在的话就直接返回false,那么这里的代码就如下:

bool erase(const K& key)
 {
	 HashNode<K, V>* Date = Find(key);
	 if (Date)
	 {
		 Date->_state = ERASE;
		 --_n;
		 return true;
	 }
	 else
	 {
		 return false;
	 }
 }

检测代码

检测的代码如下:

void TestHT1()
{
	HashTable<int, int> ht;
	int a[] = { 18, 8, 7, 27, 57, 3, 38, 18 };
	for (auto e : a)
	{
		ht.Insert(make_pair(e, e));
	}
	ht.Insert(make_pair(17, 17));
	ht.Insert(make_pair(5, 5));
	if (ht.Find(7)){cout << "存在" << endl;}
	else{cout << "不存在" << endl;}
	ht.erase(7);
	if (ht.Find(7)) { cout << "存在" << endl; }
	else { cout << "不存在" << endl; }
}

代码的运行结果如下:
在这里插入图片描述
符合我们的预期,那么再来看看下面的检测代码:

void TestHT2()
{
	string arr[] = { "苹果", "西瓜", "香蕉", "草莓", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
	//HashTable<string, int, HashFuncString> countHT; 
	HashTable<string, int> countHT;
	for (auto& e : arr)
	{
		HashNode<string, int>* ret = countHT.Find(e);
		if (ret)
		{
			ret->_kv.second++;
		}
		else
		{
			countHT.Insert(make_pair(e, 1));
		}
	}
}

这段代码的运行结果如下:
在这里插入图片描述
符合我们的预期,那么这就说明我们的代码完成的是正确的。本篇文章到此结束。

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

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

相关文章

【python爬虫应用03】csdn个人所有文章质量分查询

&#x1f6e0;️ 环境准备 在开始编写代码之前&#xff0c;我们需要进行一些环境准备。以下是所需的环境和库&#xff1a; 操作系统&#xff1a;Windows编程语言&#xff1a;Python 3编辑器&#xff1a;VSCode&#xff08;可选&#xff09; 安装所需的库&#xff1a; reque…

2023年最新IDEA中 Java程序 | Java+Kotlin混合开发的程序如何打包成jar包和exe文件(gradle版本)

文章内容&#xff1a; 一. JAVA | JAVA和Kotlin混开开发的程序打包成jar方法 1.1 方法一 &#xff1a;IDEA中手动打包 1.2 方法二 &#xff1a;build.gradle中配置后编译时打包 二. JAVA | JAVA和Kotlin混合开发的程序打包成exe的方法 一. JAVA | JAVA和Kotlin混开开发的程序…

Javalin:一个轻量的 Web Framework

说起 Java 语言下的 Web 框架那就非 Spring Framework 不可了&#xff0c;但是今天在和别人在聊天的过程中发现了一个新奇的项目 Javalin。Javalin 是一个轻量的 Web 框架。支持 WebSocket, HTTP2 和异步请求。简单的看了一下官方的说明文档&#xff0c;确实非常轻量&#xff0…

【ISO26262】汽车功能安全第5部分:硬件层面

对于每种方法,应用相关方法的推荐等级取决于 ASIL等级,分类如下: ———“”表示对于指定的 ASIL等级,高度推荐该方法; ———“”表示对于指定的 ASIL等级,推荐该方法; ———“o”表示对于指定的 ASIL等级,不推荐也不反对该方法。 表2 硬件设计的安全分析 单点故障度量 图 C.…

共享平台用户行为研究

背景 “促进平台经济、共享经济健康发展”是我国第十四个五年规划和二〇三五年远景目标中的重要内容。 研究工作概述 围绕共享平台和双边用户价值创造一般理论和实践&#xff0c;本研究综合多种方法&#xff08;LDA、社会网络分析、计量经济模型等&#xff09;和多种数据源交…

chatgpt赋能python:用Python编写SEO文章的技巧

用Python编写SEO文章的技巧 SEO&#xff08;Search Engine Optimization&#xff09;是优化网站以在搜索引擎中获得更高排名的过程。编写有趣、有价值且SEO友好的内容是重要的一环。在这篇文章中&#xff0c;我们将探讨使用Python编写SEO文章的技巧。 着重标记加粗的标题 搜…

[Eigen中文文档] 在 BLAS/LAPACK 、英特尔® MKL 和 CUDA 中使用 Eigen

文档总目录 本文目录 在BLAS/LAPACK使用 Eigen在英特尔 MKL使用 Eigen链接 在 CUDA 内核中使用 Eigen 在BLAS/LAPACK使用 Eigen 英文原文(Using BLAS/LAPACK from Eigen) 自Eigen 3.3版本以及以后&#xff0c;任何F77兼容的BLAS或LAPACK库都可以用作稠密矩阵乘积和稠密矩阵分…

第九十四天学习记录:C++核心:类和对象Ⅲ(五星重要)

深拷贝与浅拷贝 深浅拷贝是面试经典问题&#xff0c;也是常见的一个坑 浅拷贝&#xff1a;简单的赋值拷贝操作 深拷贝&#xff1a;在堆区重新申请空间&#xff0c;进行拷贝操作 #define _CRT_SECURE_NO_WARNINGS 1#include<iostream> using namespace std;class Perso…

简述环保用电监管云平台

1、概述 推进打赢蓝天保卫战&#xff0c;打好碧水保卫战&#xff0c;打胜净土保卫战&#xff0c;加快生态环境保护、建设美丽中国&#xff0c;各省市结合物联网和大数据政策&#xff0c;也相继颁布有关污染治理设施用电监管平台等相关政策。针对企业内的环保设施、设备运行状况…

Spring Boot中Elasticsearch的连接配置、原理与使用

Spring Boot中Elasticsearch的连接配置、原理与使用 引言 Elasticsearch是一种开源的分布式搜索和数据分析引擎&#xff0c;它可用于全文搜索、结构化搜索、分析等应用场景。在Spring Boot中&#xff0c;我们可以通过Elasticsearch实现对数据的搜索和分析。本文将介绍Spring …

力扣 -- 62.不同路径、63.不同路径2

题目链接&#xff1a;64. 最小路径和 - 力扣&#xff08;LeetCode&#xff09; 63. 不同路径 II - 力扣&#xff08;LeetCode&#xff09; 以下是用动态规划的思想来解决这两道类似的动规的题目&#xff0c;相信各位老铁都是能够学会并且掌握这两道经典的题目的。 参考代码&am…

mac上使用brew安装mysql5.7

使用Homebrew进行MySQL数据库的安装需要MacOS系统中已经安装了相关环境 1.查询软件信息 首先使用search命令搜索MySQL数据库完整名称&#xff1a; brew search mysql可以看到5.7版本的MySQL数据库完整名称是mysql5.7 2. 执行安装命令 使用install命令进行软件安装&#xf…

Proxy vs Object.defineProperty:哪种对象拦截机制更适合你?

文章目录 简述版详解版相同点不同点1. 功能不同2. 语法和使用方式不同3. 支持程度不同4. 性能差异 简述版 Proxy 和 Object.defineProperty 是 JavaScript 中用于实现对象拦截和代理的两种不同机制。 Object.defineProperty 是一种在对象上定义新属性或修改现有属性的方式。 …

eNSP-DHCP全局配置和接口配置

eNSP-DHCP全局配置和接口配置 文章目录 eNSP-DHCP全局配置和接口配置一、题目要求二、题目分析三、拓扑结构搭建四、基本配置五、测试验证 一、题目要求 要求 &#xff1a; 1 、GE0/0/0 口所在广播域使用全局配置模式 2 、 GE0/0/1 口所在广播域使用接口配置模式 3 、 租期时间…

远古 Windows 98 SE 和 putty 0.63 连接 SSH

远古 Windows 98 SE 和 putty 0.63 连接 SSH 不忘初心一、故障表现二、产生原因三、解决办法四、重启 SSHD 服务生交配置参考 作者&#xff1a;高玉涵 时间&#xff1a;2023.7.1 操作系统&#xff1a; Windows 98 第二版 4.10.2222 A Linux version 5.19.0-32-generic (build…

慧灵六轴机械臂和电动夹爪,适配全自动粉面烘焙机器人

近日&#xff0c;慧灵科技HITBOT推出的六轴机械臂和平行电动夹爪等产品&#xff0c;出现在一台全自动粉面&烘焙机器&#xff0c;为煮饺子、云吞、面条的传统方式注入了现代科技的力量。这台机器结合了先进的六轴机械臂产品和精密的智能控制系统&#xff0c;能够在短时间内完…

CleanMyMac X中文版Mac电脑系统清理工具使用测评

有些人可能会说&#xff0c;mac系统根本就不需要清理&#xff0c;但是真正到你磁盘快满的时候就不是这种想法了&#xff0c;不管什么系统&#xff0c;它都会缓存一些系统、用户等文件&#xff0c;可能当时能够帮上大忙&#xff0c;能够在操作的时候更快、更便捷&#xff0c;但是…

78、基于STM32单片机步进电机速度调速控制系统设计(程序+原理图+PCB源文件+参考论文+开题报告+流程图+元器件清单等)

摘 要 伴随着时代的快速发展&#xff0c;单片机的应用也越来越广泛&#xff0c;促进了微电子和计算机的快速发展。我们日常生活中步进电机扮演着很重要的角色在我们身边随处可以见。因为步进电机本身的结构组成相对于比较简单、价格也比较便宜廉价。比如压榨机&#xff0c;打印…

【FATE联邦学习】get out put data报错output dir = result[“directory“]KeyError:directory“

报错信息不清不楚的。 经过几个周的排查&#xff0c;有以下原因&#xff1a; 自定义的trainer里面的predict函数没有返回有效的返回值。也有可能是自定义的网络没有使用softmax结尾。&#xff08;若没有&#xff0c;加上即可&#xff09; 应该是二者满足其一就可以。。因为有…

利用spleeter库实现人声和音乐分离(踩过的坑及解决方法)

0&#xff1a;起因 事情的起因是&#xff1a;想用一首歌的伴奏剪视频&#xff0c;但找遍各大平台&#xff0c;都只有原曲&#xff0c;没有伴奏。能找到的进行人声和背景音乐分离的软件都要收费&#xff0c;最后决定用spleeter库&#xff0c;尝试进行音频分离。 1&#xff1a;…