【C++】Hash闭散列

news2024/11/28 10:42:33

目录

一、哈希的概念

1.1 哈希冲突

1.2 哈希函数

1.3 装载因子

二、闭散列

2.1 线性探测

2.2 Insert 插入

2.3 Find 查找

2.4 Erase删除

2.5 插入复杂类型

2.6 二次探测

三、源代码与测试用例

3.1 hash源代码

3.2 测试用例


一、哈希的概念

在前面学习了二叉搜索树、AVL树、红黑树之后,我们得知顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须经过关键码的多次比较。顺序查找的时间复杂度为 O(N),平衡树中为树的高度,即(logN),搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。

如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快的找到该元素。

当向该结构中:

  • 插入元素

        根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。

  • 搜索元素

        对元素的关键码进行同样的计算,把求得的函数当作元素的存储位置,在结构中按此位置取元素进行比较,若关键码相等,则搜索成功。

该方法即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(hash Table)(或称散列表)

使用以上方法插入直接进行映射插入,搜索时不必进行关键码的比较,因为搜索速度非常快。

问题:按照上述哈希方式,向集合中插入元素44,会出现什么问题?


1.1 哈希冲突

对于两个数据元素的关键字,其 key1 != key2,但是存在 key1 % p == key2 % p,此时 key1 和key2 就会被映射到 hash Table 中相同的位置。

假设我们将需要存的数n,存的索引值 = n % 10
现在需要将20存入该表中,计算出的键值为0,但是该位置已经有数据了,即发生了哈希冲突.

此时:不同关键字通过相同的哈希函数计算处相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

把具有不同关键码而具有相同哈希地址的数据元素称为 "同义词" 。 
发生哈希冲突该如何处理呢?

1.2 哈希函数

引起哈希冲突的一个原因在于:哈希函数设计的不够合理。

哈希函数的设计原则:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址时,其值域必须在 0 到 m-1 之间。
  • 哈希函数计算出来的地址能均匀的分布在整个空间中。
  • 哈希函数设计应足够简单。 

常见的两种哈希函数

1. 直接定址法

取关键字的某个线性函数为散列地址:hash(key) = A * Key + B

优点:简单、均匀

缺点:需要事先知道关键字的分布情况

使用场景:适合查找比较小且连续的情况。

题目举例:387. 字符串中的第一个唯一字符  

2.除留余数法(常用)

设散列表中允许的地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数,按照哈希函数:hash(key) = key % p(p<= m),将关键码转化为哈希地址。

就是以下这种方法

1.3 装载因子

如果hash的插入,进行映射后如果找不到空位就要一直往后面检测查找空位,所以如果当哈希表中只有一个空位时,插入一个数据的时间复杂度很可能就变成了O(N),所以说再这种情况发生前我们就要对其进行扩容。

那什么情况下进行扩容呢?应该括多大呢?如果是除留余数法,那质数 p又应该是多少呢?

散列表的装载因子定义为:a = 填入表中的元素个数 / 散列表的长度

即。装载因子越小,冲突的概率越小,空间利用率低;装载因子越大,冲突的概率越大,空间利用率高。

而库中的做法是,装载因子如果大于0.7,就进行扩容。

二、闭散列

闭散列:也叫开放地址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把 key 存放到冲突位置中的"下一个"空位置中去

寻找下一个空位置方式有线性探测和二次探测,接下来我们结合理论并实现一下。

2.1 线性探测

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置即可。

结构定义:

首先我们定义结构,我们定义每个位置有三种状态{存在,空位,删除},一个位置要么是以及有数据了,要么是没有数据,没什么有一个删除呢?

如果出现下面这种情况,20在3的后面进行插入,将3删除后,20便无法查找到,所以我们会再添加一个删除状态,防止以下这种情况发生。

使用一个数组进行存储,其中每一个位置位HashDate类型,该类型中会记录当前位置的状态。再添加一个size变量,用于记录当前存储的有效数据个数。结构如下:

enum State {EMPTY,EXIST,DELETE};   //每个位置有三种状态

template<class K,class V>
struct HashDate
{
	pair<K, V> _kv;
	State _state;
};

template<class K,class V>
class HashTable
{
public:
    //成员函数:
private:
	vector<HashDate<K, V>> _table;
	size_t _size=0;				 
};

2.2 Insert 插入

  • 通过哈希函数(保留余数法)获取待插入元素在哈希表中的位置
  • 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。

接下来有几个问题:

1. 确定质数p,我们应该取vector的size()还是capacity()作为质数?

        使用size(),因为capacity()是vector开辟的总空间,超过size()的部分是不能直接使用的,只能使用size()以内的空间,而size要通过我们插入数据或resize进行改变。简而言之,vector中超过size(),小于capacity()的部分我们是不能直接访问的,尽管已经开辟。

2.如果线性探测一直探测到 i 下标超过hash_table.size(),我们应该如何做。

         如果一直探测超过数组的下标,应该绕回数组的开始处,所以每次 i++ 后,我们可以继续进行取模,如果超过了size(),会自动从数组0下标处开始探测;当然,使用if判断 i 超过size(),超过就置0也是可以的。

3.当装载因子超过0.7之后,我们应该怎么做。

        即_size / _table.size() >=7 时,我们要进行扩容,创建一个新哈希表,然后将旧表中的数据拷贝到新表中,此时我们可以复用 Insert 函数,因为新表是不存在扩容问题的,所以会使用 Insert 中插入逻辑的代码,然后将数据全部插入到新表中,最后我们将新表与旧表的进行swap一下,就将新表扩容指向的内容交换给了临时变量,临时变量调用析构函数,自动释放,这样,扩容问题就得到了解决。

代码如下:


bool Insert(const pair<K, V>& kv)
{
	//如果 size==0 或装载因子 >=0.7 进行扩容
	if (_table.size() == 0 || 10 * _size / _table.size() >= 7)
	{
		size_t newSize = _table.size() == 0 ? 10 : _table.size() * 2;
		HashTable<K, V> newHash;
		newHash._table.resize(newSize);
		//将旧表中的数据拷贝到新表中  --- 复用Insert继续拷贝数据
		for (auto e : _table)
		{
			if (e._state == EXIST)
			{
				newHash.Insert(e._kv);
			}
		}
		//进行交换  newHash自动调用其析构函数
		_table.swap(newHash._table);
	}
	size_t hashi = kv.first % _table.size();
	while (_table[hashi]._state == EXIST)   //如果存在数据就一直往后找
	{
		hashi++;
		//如果hashi++超过size(),需要绕回数组的开始
		hashi %= _table.size();
	}
	//找到位置,插入数据
	_table[hashi]._kv = kv;
	_table[hashi]._state = EXIST;
	++_size;
	return  true;
}

注意,如果插入的是负数,会发生整形提升,int类型会转变为我们的size_t 类型,此时负数再进行取模,就可以得到一个合法的映射位置,也可以被查找的。

接下来还有一个问题,如果数据发生冗余怎么办。就是如果插入的是已经存在的值,应该如何处理呢?

2.3 Find 查找

所以我们可以在插入之前编写一个find函数,如果该数据存在,则不进行插入。

HashDate<K, V>* Find(const K& key)
{
	//判断表为空的情况
	if (_table.size() == 0)
		return nullptr;
	size_t hashi = key % _table.size();
	while (_table[hashi]._state != EMPTY)
	{
		//如果找到key了,并且状态不是DELETE
		if (_table[hashi]._kv.first == key && _table[hashi]._state != DELETE)
		{
			return &_table[hashi];
		}
		hashi++;
		//如果超过表的长度,则除以表的大小,让其回到表头。
		hashi %= _table.size();
	}
	return nullptr;
}

此时有一个问题, 我们的循环是_state !=EMPT,如果遍历重回到起点,这些遍历到的数据_state都为EMPTY,就可能导致死循环,所以我们还要保存起始位置的状态,如果重回起点则也返回false(当然,这是一种非常极端的情况,但是会出现)。

2.4 Erase删除

删除的思路非常简单,如果find查找到该值,直接将其对应的state改为DELETE即可。


bool Erase(const K& key)
{
	HashDate<K, V>* ret = Find(key);
	if (ret)
	{
		ret->_state = DELETE;
		--_size;
		return true;
	}
	return false;
}

 然后我们测试一下这插入、查找、删除这三个接口。

2.5 插入复杂类型

那如果我们想实现一个统计次数的哈希表,则 key 值是string类型的怎么办呢?string类型或字符串类型是无法被取模的。那再如果我们想插入一个自己定义的复杂类型呢?

我们先来看看STL库中是如何解决这个问题的。

所以我们要编写默认的hash取key的仿函数作为缺省参数。

在任何进行取模的地方我们要让key值调用默认的仿函数取出key值。

所以我们可以自定义编写将string类型(或其它复杂类型)转化为 size_t 类型的仿函数

struct HashFuncString
{
	size_t operator()(const string& key)
	{
		size_t val = 0;
		for (auto ch : key)
			val += ch;
		return val;
	}
};

然后我们将测试代码跑起来:

void test_hash04_string()
{
	string arr[] = { "苹果","西瓜","菠萝","草莓","菠萝","草莓" ,"菠萝","草莓" 
		, "西瓜", "菠萝", "草莓", "西瓜", "菠萝", "草莓","苹果" };
	HashTable<string,int, HashFuncString> countHT;
	for (auto& str : arr)
	{
		auto ptr = countHT.Find(str);
		if (ptr)
			ptr->_kv.second++;
		else
			countHT.Insert({ str,1 });
	}
	countHT.Print();
}

但是在库中的unordered_map并不需要我们自己传入仿函数,因为string是一个挺常见的类型,库中使用了模板的特化,对string类型进行了特殊处理,我们接下来也将其进行改动为特化的形式。

template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t val = 0;
		for (auto ch : key)
			val += ch;
		return val;
	}
};

此时不传入自定义的仿函数,一样可以成功运行:

 其实,我们模拟实现的string->int的仿函数写的并不规范,因为key值是唯一的,如果出现以下这种情况,即使key值是不同的,但是通过仿函数计算后的映射却是相同的。比如:

 所以,我们要对字符串的Hash函数进行特殊处理,这里有一篇博客进行了详细的讲解:字符串Hash函数对比,这里我就直接使用(BKDR)的规则进行改写了:

template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t val = 0;
		for (auto ch : key)
			val = val * 131 + ch;
		return val;
	}
};

线性探测就实现完了,接下来就是改造为二次探测的实现:

2.6 二次探测

二次探测不是指探测两次,而是 i 的指数方进行探测。

如下是使用线性探测和二次探测插入同一组数据的插入结果,如下:

 然后我们在线性探测的方式上进行改动:

三、源代码与测试用例

3.1 hash源代码


enum State { EMPTY, EXIST, DELETE };   //每个位置有三种状态

template<class K, class V>
struct HashDate
{
	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 val = 0;
		for (auto ch : key)
			val = val * 131 + ch;
		return val;
	}
};



template<class K, class V,class Hash=HashFunc<K>>
class HashTable
{
public:
	bool Insert(const pair<K, V>& kv)
	{
		//如果表中已经存在该数据
		if (Find(kv.first))	return false;
		//如果 size==0 或装载因子 >=0.7 进行扩容
		if (_table.size() == 0 || 10 * _size / _table.size() >= 7)
		{
			size_t newSize = _table.size() == 0 ? 10 : _table.size() * 2;
			HashTable<K, V, Hash> newHash;
			newHash._table.resize(newSize);
			//将旧表中的数据拷贝到新表中  --- 复用Insert继续拷贝数据
			for (auto e : _table)
			{
				if (e._state == EXIST)
				{
					newHash.Insert(e._kv);
				}
			}
			//进行交换  newHash自动调用其析构函数
			_table.swap(newHash._table);
		}

		Hash hash;
		size_t hashi = hash(kv.first) % _table.size();

		while (_table[hashi]._state == EXIST)   //如果存在数据就一直往后找
		{
			hashi++;
			//如果hashi++超过size(),需要绕回数组的开始
			hashi %= _table.size();
		}
		//找到位置,插入数据
		_table[hashi]._kv = kv;
		_table[hashi]._state = EXIST;
		++_size;
		return  true;
	}

	HashDate<K,V>*  Find(const K& key)
	{
		//判断表为空的情况
		if (_table.size() == 0)
			return nullptr;

		Hash hash;
		size_t hashi = hash(key) % _table.size();
		while (_table[hashi]._state != EMPTY)
		{
			//如果找到key了,并且状态不是DELETE
			if (_table[hashi]._kv.first == key && _table[hashi]._state!=DELETE)
			{
				return &_table[hashi];
			}
			hashi++;
			//如果超过表的长度,则除以表的大小,让其回到表头。
			hashi %= _table.size();
		}
		return nullptr;
	}

	bool Erase(const K& key)
	{
		HashDate<K, V>* ret = Find(key);
		if (ret)
		{
			ret->_state = DELETE;
			--_size;
			return true;
		}
		return false;
	}

	void Print()
	{
		for(int i=0;i< _table.size();i++)
		{
			if (_table[i]._state == EXIST)
			cout <<"i:" <<i<<" [" << _table[i]._kv.first << " " << _table[i]._kv.second <<"]" << endl;
		}
	}

private:
	vector<HashDate<K, V>> _table;
	size_t _size=0;
};

3.2 测试用例

void test_hash01()
{
	HashTable<int, int> Hash;
	int a[] = { 1,11,4,15,26,7};
	for (auto e : a)
	{
		Hash.Insert(make_pair(e, e));
	}
	Hash.Print();
	cout << endl;
}
void test_hash02()
{
	HashTable<int, int> Hash;
	int a[] = { 1,11,4,15,26,7,13,5,34,9 };
	for (auto e : a)
	{
		Hash.Insert(make_pair(e, e));
	}
	Hash.Print();
	cout << endl;
}

void test_hash03()
{
	HashTable<int, int> Hash;
	int a[] = { 1,11,4,15,26,7,13,5,34,9 };
	for (auto e : a)
	{
		Hash.Insert(make_pair(e, e));
	}
	Hash.Print();
	cout << endl<<"find:"<<endl;
	
	cout << (Hash.Find(11)->_kv).first << endl;
	cout << (Hash.Find(4)->_kv).first << endl;
	cout << (Hash.Find(5)->_kv).first << endl;
	cout << (Hash.Find(34)->_kv).first << endl;
	cout << "Erase:" << endl;
	Hash.Erase(11);
	cout << Hash.Find(11) << endl;
}

void test_hash04_string()
{
	string arr[] = { "苹果","西瓜","菠萝","草莓","菠萝","草莓" ,"菠萝","草莓" 
		, "西瓜", "菠萝", "草莓", "西瓜", "菠萝", "草莓","苹果" };
	HashTable<string,int> countHT;
	for (auto& str : arr)
	{
		auto ptr = countHT.Find(str);
		if (ptr)
			ptr->_kv.second++;
		else
			countHT.Insert({ str,1 });
	}
	countHT.Print();
}

void test_hash05_string()
{
	HashFunc<string> hash;
	cout << hash({ "abc" }) << endl;
	cout << hash({ "bac" }) << endl;
	cout << hash({ "cba" }) << endl;
	cout << hash({ "bbb" }) << endl;
}

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

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

相关文章

多巴胺聚乙二醇多巴胺,Dopamine-PEG-Dopamine简介,多巴胺是具有正性肌力活动的单胺化合物

产品名称&#xff1a;多巴胺聚乙二醇多巴胺&#xff0c;双多巴胺聚乙二醇&#xff08;Dopamine-PEG-Dopamine&#xff09; 中文别名&#xff1a;多巴胺PEG多巴胺&#xff0c;双多巴胺聚乙二醇 英文名称&#xff1a;Dopamine-PEG-Dopamine 存储条件&#xff1a;-20C&#xff0…

磨金石教育影视技能干货分享|浅析中国CG特效的发展现状

中国CG特效的发展2015年是一个分水岭。在2015年之前&#xff0c;中国CG 特效发展是混乱的&#xff0c;不成熟的。总体表现就是技术水平不足&#xff0c;缺少人才培养的体系。当时从事CG的公司&#xff0c;大概有两个类型&#xff1a;“技术型与业务型”。所谓技术型的公司&…

设计模式_结构型模式 -《装饰器模式》

设计模式_结构型模式 -《装饰器模式》 笔记整理自 黑马程序员Java设计模式详解&#xff0c; 23种Java设计模式&#xff08;图解框架源码分析实战&#xff09; 概述 我们先来看一个快餐店的例子。 快餐店有炒面、炒饭这些快餐&#xff0c;可以额外附加鸡蛋、火腿、培根这些配菜…

PowerDesigner设计表时显示注释列Comment

首先&#xff0c;使用 PowerDesigner 新建模型&#xff0c;File —> New Model 然后&#xff0c;切换到模型类型(Model types)选项卡&#xff0c;选中 Physical Diagram 然后点击右侧表格图标&#xff0c;在左侧面板中创建表格如下。双击表格&#xff0c;找到Columns选项卡…

招标采购中,如何编写有效的RFI(信息邀请书)?

在企业招标采购过程中&#xff0c;RFI&#xff08;信息邀请书&#xff09;是一个从商品或服务的潜在供应商处收集信息的正式流程。RFI旨在由客户编写并发送给潜在供应商。RFI通常是第一个也是最广泛的一系列请求&#xff0c;旨在缩小潜在供应商候选人名单。 当企业对潜在供应…

【实际开发07】- XxxxController 批量处理 × 5 -【model】

目录 1. Mode 1. IotTypeController 基础 7 tips 2. 辅助添加 Validated 无法覆盖的 参数校验 1. 预处理空指针异常 ( 校验 : 核心必填参数 not null ) 3. RequestBody 对应API 存在 feign 调用时 , 注意 : 不可缺省 1. feign API 需要加 RequestBody , Controller 层可…

手工测试 | 黑盒测试方法论—边界值

本文节选自霍格沃兹测试学院内部教材 边界值分析法是一种很实用的黑盒测试用例方法&#xff0c;它具有很强的发现故障的能力。边界值分析法也是作为对等价类划分法的补充&#xff0c;测试用例来自等价类的边界。 这个方法其实是在测试实践当中发现&#xff0c;Bug 往往出现在定…

OpenCV4.6 VS 4.7 QRCode解码功能效果对比

导 读 本文主要对OpenCV4.7.0和4.6.0中QRCode检测/解码功能做简单的测试对比&#xff0c;供大家参考。 背景介绍 最近OpenCV更新到了4.7.0版本&#xff0c;在ChangeLog算法部分除了新增Stackblur滤波算法(详细介绍见下面链接)&#xff0c;还有对QRCode检测和解码的改进。 吊打…

实现通讯录(C语言)

功能实现 实现步骤&#xff1a; 创建通讯录 初始化通讯录 打印菜单 实现选择功能 实现添加功能 实现删除功能 实现查找功能 实现修改功能 实现清空功能 实现排序功能 实现查询所有联系人信息功能 通讯录总代码 创建通讯录 1、创建成员信息结构体 我们用结构体来封装一个联系人…

【系列04】数组 多维数组 冒泡排序 稀疏数组[有目录]

数组声明和创建 变量类型 变量名称 变量的值 声明数组 int []nums;//定义//上面是首选 /*也可以 int nums2[];*/下面这种写法是为了迎合c与c用户的喜爱,更推荐用上面的定义 方式 创建数组 numsnew int[10];定义数组计算数组的和 package com.SunAo.array; //变量类型 变量名称…

TypeScript 数据模型层编程的最佳实践

虽然 TypeScript 主要用于客户端&#xff0c;而数据模型的设计主要是服务端来做的。 但是要写出优雅的代码&#xff0c;也还是有不少讲究的。 让我们从一个简单的我的文章列表 api 返回的数据开始&#xff0c;返回的文章列表的信息如下&#xff1a; {"id": 2018,&qu…

原生PHP及thinkphp6接入阿里云短信

申请accesskey获取到Accesskey ID和Accesskey Secret保存下来&#xff0c;一会要用到添加测试手机号&#xff0c;在接口测试能否正常发送下载阿里云短信sdk&#xff0c;使用composer下载&#xff0c;没有安装请先安装安装可以安装到任意文件夹下&#xff0c;后面代码写好后&…

TDSQL的安装教程(低配体验)

一、了解TDSQL tdsql腾讯云文档 TDSQL-C MySQL 版&#xff08;TDSQL-C for MySQL&#xff09;是腾讯云自研的新一代云原生关系型数据库。融合了传统数据库、云计算与新硬件技术的优势&#xff0c;为用户提供具备极致弹性、高性能、海量存储、安全可靠的数据库服务。TDSQL-C My…

AtCoder Beginner Contest 285解题报告

A - Edge Checker 2 Problem Statement Determine if there is a segment that directly connects the points numbered a and b in the figure below. Constraints 1≤a<b≤15a and b are integers.Input The input is given from Standard Input in the following for…

用SpectorJS调试WebGL应用

随着使用 WebGL 构建的体验不断涌现&#xff0c;以及 WebVR/AR 领域的所有改进&#xff0c;拥有高效的调试工具变得至关重要。 无论你是刚刚起步还是已经是使用 WebGL 开发 3D 应用程序的经验丰富的开发人员&#xff0c;都可能知道工具对于生产力的重要性。 在寻找此类工具时&…

【开发环境】JRE 裁剪 ① ( 裁剪 bin 目录下的 dll 动态库文件 )

文章目录一、JRE 裁剪二、裁剪 bin 目录下的 dll 动态库文件参考博客 : 精简jre1.8精简jre步骤裁剪JRE(嵌入式设备的java环境移植) 资源下载地址 : https://download.csdn.net/download/han1202012/87388400 一、JRE 裁剪 在 【IntelliJ IDEA】使用 exe4j 生成 jre jar 可执…

华为MPLS跨域A、B方案实验配置

目录 MPLS域内配置 MPLS-AS100域内配置 MPLS-AS200域内配置 域间方式A配置 ASBR4和ASBR5配置实例 ASBR之间建立基于实例的EBGP邻居关系 域间方式B配置 ASBR相连接口开启MPLS ASBR之间建立MP-BGP的EBGP邻居 配置取消RT值检测 配置传递路由时更改下一跳为自身 MPLS域内…

程序员必知必会 QPS TPS、URI URL、PV UV GMV

一、QPS和 TPS QPS&#xff1a;Queries Per Second&#xff0c;意思是“每秒查询数”&#xff0c;是一台服务器每秒能够响应的查询次数&#xff0c;是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。 即最大吞吐能力。 TPS&#xff1a;TransactionsPerSecond&…

springboot整合log4j2

导入依赖 <dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>1.2.17</version> </dependency><!--log4j2--> <dependency><groupId>org.apache.logging.log4j</groupId>&…

Spring Boot(五十三):SpringBoot Actuator之实现

1 场景介绍 对于一个大型的几十个、几百个微服务构成的微服务架构系统&#xff0c;在线上时通常会遇到下面一些问题&#xff0c;比如&#xff1a; 1. 如何知道哪些服务除了问题&#xff0c;如何快速定位&#xff1f; (健康状况&#xff09; 2. 如何统一监控各个微服务的性能指标…