哈希表哈希桶(C++实现)

news2024/11/26 11:38:09

哈希的概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素 时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即 O( l o g 2 N log_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立 一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:

  • 插入元素 根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
  • 搜索元素 对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置 取元素比较,若关键码相等,则搜索成功

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

比如将数据{1,4,6,8,15}存储到哈希表中:
哈希函数设置为hashi = key % capacity(其中hashi就是元素存储的位置,key就是数据项,capacity为存储元素底层空间总的大小,比如vector),存储结构如下:

image.png
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快。

哈希冲突

如果继续向上面哈希表中插入14,hashi(14) = 4。而4这个位置已经被占用,此时就会发生不同的值映射到同一个位置上。这种情况就叫做哈希冲突

哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
一个好的哈希函数设计应该遵循以下条件:

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

最常见的哈希函数有:
除留余数法(最常用)
对于哈希表长度为m的哈希函数公式为:
hashi(key) = key % p (p <= m)

解决哈希冲突的方法

处理哈希冲突有两种常用的方法:

  • 闭散列
  • 开散列

闭散列

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

  • 线性探测
  • 二次探测

线性探测

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

比如对下面数据项集合{12,67,56,16,25,37,22,29,15,47,48,34}。哈希表长为12。
哈希函数为hashi(key) = key % p (p <= m)。

计算前五个{12,67,56,16,25}时,都不会发生冲突。直接存入哈希表。如下:

image.png
当计算37时,hashi(37) = 1,此时就会与25发生冲突,此时就需要将37存放到25的下一个位置,即下标为2的位置处。

image.png
继续存放{22,29,15,47}都没有发生冲突,正常存放。

image.png
存放48时,hashi(48) = 0,与12所在的位置发生了冲突,此时下一个位置还是会发生冲突,一直线性探测到29的下一个,才有空位。将48存放到29的下一个。

image.png
继续存放34,hash(i) = 10。和22,47,12,37,15,16,29,48,67,56都会发生冲突。只能存放到56的下一个。

image.png

二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位 置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:
hashi = ( H 0 H_0 H0 + i 2 i^2 i2 )% m, 或者:hashi = ( H 0 H_0 H0 - i 2 i^2 i2 )% m。
其中:i = 1,2,3…,
H 0 H_0 H0是通过散列函数Hash(key)对元素的关键码 key 进行计算得到的位置,
m是表的大小。

image.png
比如上面哈希表存放最后一个34时, H 0 H_0 H0 = 10,i = 1,hashi = (10-1)%12 = 9。而下标9正好是空,只需一次二次探测即可存放34。

负载因子

以上就是闭散列寻址的两种方式。当然哈希表也是需要扩容的,如果继续对上面哈希表存放数据,哈希表已满,无法存放了。
关于哈希表扩容,引入了一个新的概念,负载因子。
哈希表的负载因子α = 表中的数据个数 / 哈希表长度。

比如下面哈希表的负载因子 = 6 / 12 = 0.5

image.png
负载因子是哈希表反馈装满程度的标志,由于表长是定值,负载因子和哈希表中的元素个数成正比,所以负载因子越大,表明哈希表中元素个数越多。产生哈希冲突的概率越大,反之,负载因子越小,哈希表中元素个数越少,发生哈希冲突的概率越低。
对于闭散列,哈希表的负载因子是非常重要的,一般严格控制在0.7~0.8以下(也就是说哈希表的负载因子超过0.8就要扩容,避免产生更多的哈希冲突)。

哈希表闭散列的实现

哈希表的存储结构

哈希表中每个位置除了存储数据元素之外,还要存储当前位置的状态(空,已存放,已删除)。因为采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素 会影响其他元素的搜索。因此线性探测采用标记的伪删除法(就是给每个存储位置添加一个存储状态)来删除一个元素。
哈希表存储位置的状态可以使用枚举常量来定义,如下:

enum Status
{
    EXIST,//已存储
    EMPTY,//空
    DELETE//已删除
};

整个哈希表闭散列的结构:

//存储位置状态
enum Status
{
    EXIST,
    EMPTY,
    DELETE
};
//存储位置结构
template<class K, class V>
struct HashData
{
    pair<K, V> _kv;//哈希表存储数据元素。
    Status _status = EMPTY;//存储位置状态默认为空
};
//哈希表
template<class K, class V>
class HashTable
{
    typedef HashData<K, V> HashTableNode;
public:
    //哈希表提供的三个方法(插入、删除、查找)
    bool Insert();
    bool Erase();
    HashTableNode* find();
private:
    vector<HashTableNode> _tables;//使用vector做为哈希表底层存储结构
    size_t n = 0;//哈希表元素个数(方便计算哈希表的负载因子)
};

插入

向哈希表中插入数据的步骤如下:

  1. 查看哈希表中是否存在该键值的键值对,若已存在则插入失败。
  2. 判断是否需要调整哈希表的大小,若哈希表的大小为0,或负载因子过大都需要对哈希表的大小进行调整。
  3. 将键值对插入哈希表。
  4. 哈希表中的有效元素个数加一。
bool Insert(const pair<K, V> kv)
{
    //不允许键值冗余
    if (find(kv.first) != nullptr)
    {
        return false;
    }
    //空哈希表初始化10个存储位置
    if (_tables.size() == 0 )//扩容
    {
        _tables.resize(10);
    }
    else if((double)_tables_size / (double)_tables.size() > 0.8)//负载因子大于0.8扩容
    {
        //创建新哈希表对象
        HashTable<K,V> newtables;
        newtables._tables.resize(_tables.size() * 2);
        //遍历旧表,重新映射到新表中
        for (auto& data : _tables)
        {
            if (data._status == EXIST)
            {
                newtables.Insert(data._kv);
            }
        }
        //交换这两个哈希表
        _tables.swap(newtables._tables);
    }

    //线性探测确认哈希地址
    size_t hashi = kv.first % _tables.size();
    size_t i = 1;
    size_t index = hashi;
    while (_tables[index]._status == EXIST)
    {
        index = hashi + i;
        ++i;
        index %= _tables.size();//从0继续寻找位置。
    }

    //确认位置进行存储
    _tables[index]._kv = kv;
    _tables[index]._status = EXIST;
    _tables_size++;
    return true;
}

注意细节:

  • 扩容时创建新哈希表对象,让新哈希表对象调用insert不构成函数递归,来完成扩容后重新建立映射关系。
  • index %= _tables.size();//从0继续寻找位置。

查找

HashTableNode* find(const K& key)
{
    //空表
    if (_tables.size() == 0)
    {
        return nullptr;
    }
    //线性探测
    size_t hashi = key % _tables.size();
    size_t start = hashi;
    size_t i = 1;
    while (_tables[start]._status != EMPTY)
    {
        if (_tables[start]._kv.first == key 
         && _tables[start]._status == EXIST)
        {
            return &_tables[start];
        }
        start = hashi + i;
        i++;
        start %= _tables.size();
        //已经找了一圈了
        if (start == hashi)
        {
            break;
        }
    }
    return nullptr;
}

删除

删除哈希表中的元素采用伪删除法,只需将被删除元素找到,将其状态修改为DELETE即可。

bool Erase(const K& key)
{
    HashTableNode* ret = find(key);
    if (ret == nullptr)
    {
        return false;
    }
    else
    {
        ret->_status = DELETE;
        --_tables_size;
    }
    return true;
}

开散列

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地 址的关键码归于同一子集合,每一个子集合称为一个哈希桶,各个哈希桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

哈希桶和vector< list >本质是一样的(本质就是指针数组)。逻辑图如下:

image.png

比如对下面数据项集合{1,33,3,5,55,7,9},存放到表长为10的哈希表中。同样哈希函数采用除留余数法,存放到哈希表中如下:

image.png
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
实际中哈希表的开散列比闭散列更实用。
开散列同样也要考虑扩容问题,开散列一般负载因子为1时在发生扩容。
这样的话,如果哈希表中所有的元素都发生冲突,都存放在一个哈希桶中,有负载因子的控制扩容,几乎不会出现这种极端情况。
如果真的出现极端场景,可以考虑将这个桶由单链表改为红黑树。
由此可见,哈希表的开散列效率是极高的。增删查改的平均时间复杂度都是O(1)级别的存在,非常的恐怖。

哈希表开散列的模拟实现

在哈希表的开散列中,每个位置存储的是单链表的结点,这里需要实现一个哈希表的结点类。结点类和单链表一样,除了存储数据元素之外,还需要存储一个结点的指针用来指向下一个结点。

template<class K, class V>
struct HashNode
{
	HashNode<K, V>* _next;
	pair<K, V> _kv;
	HashNode(const pair<K,V> kv)
		:_next(nullptr)
		,_kv(kv)
	{}
};

哈希桶框架

template<class K, class V>
class HashBucket
{
	typedef HashNode<K, V> Node;
public:
	//哈希桶提供的方法...
	~HashBucket();
	bool Insert(const pair<K, V> kv);
	Node* Find(const K& key);
	bool Erase(const K& key);
	size_t size();
private:
	vector<Node*> _tables;
	size_t _size = 0;
};

下面就一一实现哈希桶提供的方法

insert函数

  1. 检查所插入元素是否存在
  2. 检查容量以及负载因为为1时进行扩容
  3. 确认插入元素所在桶的位置申请结点进行头插
bool Insert(const pair<K,V> kv)
{
        //1.检查所插入元素是否存在
        if (Find(kv.first) != nullptr)
        {
                return false;
        }
        //2.检查容量以及负载因为为1时进行扩容
        if (_tables.size() == 0)
        {
                _tables.resize(10);
        }
        else if (_size == _tables.size())//扩容
        {
                vector<Node*> new_bucket(_tables.size() * 2, nullptr);
                for (size_t i = 0; i < _tables.size(); ++i)
                {
                        Node* cur = _tables[i];
                        while (cur != nullptr)
                        {
                                Node* next = cur->_next;
                                size_t hashi = cur->_kv.first % new_bucket.size();
                                cur->_next = new_bucket[hashi];
                                new_bucket[hashi] = cur;
                                cur = next;
                        }
                }
                _tables.swap(new_bucket);
        }
        //3.确认插入元素所在桶的位置申请结点进行头插
        size_t hashi = kv.first % _tables.size();
        Node* newnode = new Node(kv);
        newnode->_next = _tables[hashi];
        _tables[hashi] = newnode;
        ++_size;
        return true;
}

注意: 这里的扩容不像闭散列一样,创建新的哈希表对象调用insert完成插入,会造成同样的数据再次申请结点,还要释放结点。虽然可以解决问题,但是效率不好。采用上面的方式更优,直接与新表重新建立映射关系即可。

// 创建新的哈希表对象调用insert完成插入。会造成同样的数据再次申请和释放。
HashBucket<K, V> new_bucket;
new_bucket._tables.resize(_tables.size() * 2);
for (size_t i = 0; i < _tables.size(); ++i)
{
        Node* cur = _tables[i];
        while (cur != nullptr)
        {
                new_bucket.Insert(cur->_kv);
                cur = cur->_next;
        }
}
_tables.swap(new_bucket._tables);

这里哈希表中的结点需要申请,那么也需要写析构函数来完成资源的释放,否则就会造成内存泄漏。

~HashBucket()
{
        for (size_t i = 0; i < _tables.size(); ++i)
        {
                Node* cur = _tables[i];
                while (cur != nullptr)
                {
                        Node* next = cur->_next;
                        delete cur;
                        cur = next;
                }
                cur = nullptr;
        }
        cout << "~HashBucket" << endl;
}

Find

  1. 找到查找元素在桶的位置
  2. 遍历单链表进行查找
Node* Find(const K& key)
{
    if (_tables.size() == 0)
    {
            return nullptr;
    }
    //1. 找到查找元素所在桶的位置
    size_t hashi = key % _tables.size();//注意/0错误
    Node* cur = _tables[hashi];
    //2. 遍历单链表查找
    while (cur != nullptr)
    {
            if (cur->_kv.first == key)
            {
                    return cur;
            }
            cur = cur->_next;
    }
    return nullptr;
}

Erase

  1. 找到删除元素所在桶的位置
  2. 单链表的删除
bool Erase(const K& key)
{
    if (_tables.size() == 0)
    {
            return false;
    }
    //1.找到删除元素所在桶的位置
    size_t hashi = key % _tables.size();//防止除0错误
    Node* cur = _tables[hashi];
    Node* prev = nullptr;
    //2.单链表的删除
    while (cur != nullptr)
    {
            if (cur->_kv.first == key)
            {
                    if (prev == nullptr)
                    {
                            _tables[hashi] = cur->_next;
                    }
                    else
                    {
                            prev->_next = cur->_next;
                    }
                    delete cur;
                    --_size;
                    return  true;
            }
            else
            {
                    prev = cur;
                    cur = cur->_next;
            }
    }
    return false;
}

开散列的其他问题

当存储的元素不是整形,而是其他类型,比如string等,如何让解决?

哈希函数采用除留余数法,所以被摸的key必须要为整形才可以。解决方法是提供一个仿函数,将key转化为整形,更准确的来说是无符号整形,如果是负数,转化为正整数进行存储。

struct HashFunc
{
	size_t operator()(const K& key)
	{
		return key;
	}
};

此时,哈希桶需要第三个模板参数将key转化为整形做除余运算(这里第三个模板参数给了缺省值,可以根据自己的需要来实现)。
这里以Find为例,凡是需要%运算的都需要转化。

template<class K, class V,class Hash = HashFunc<K>>
class HashBucket
{
    Node* Find(const K& key)
    {
            if (_tables.size() == 0)
            {
                    return nullptr;
            }
            //1. 找到查找元素所在桶的位置
            size_t hashi = _hash(key) % _tables.size();//注意/0错误
            //....
    }                 
private:
	Hash _hash;
}

如果数据类型是string,可以使用模板的特殊来完成,也可以自己实现做为模板参数来实例化。
这里以模板的特化为例

template<>
struct HashFunc<string>
{
	size_t operator()(const string& str)
	{
		return str[0];//取字符串第一个字符作为key进行 求%
	}
};

这样写的话,如果首字母相同的话,全部在一个桶里面,这样就会造成哈希桶的极端场景。使哈希桶的效率降低,有一种字符串哈希算法BKDRHash(此算法由于在Brian Kernighan与Dennis Ritchie的《The C Programming Language》一书被展示而得 名,是一种简单快捷的hash算法,也是Java目前采用的字符串的Hash算法(累乘因子为31))。这个算法就是每次累加然后乘31即可(原理目前不知道)。

//BKDRHash算法
template<>
struct HashFunc<string>
{
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto ch : s)
		{
			hash += ch;
			hash *= 31;// 也可以乘以31、131、1313、13131、131313..
		}
		return hash;
	}
};

哈希桶的效率

产生一百万个随机数,存放到哈希桶中,取出最大桶下面的个数。求最大桶个数成员函数如下:

size_t MaxBucketSize()
{
    size_t max = 0;
    for (size_t i = 0; i < _tables.size(); ++i)
    {
            Node* cur = _tables[i];
            size_t count = 0;
            while (cur != nullptr)
            {
                    count++;
                    cur = cur->_next;
            }

            printf("[%d]->%d\n", i, count);//打印每个桶下面的元素个数

            if (count > max)
            {
                    max = count;
            }
    }
    return max;//最大桶的元素个数
}

测试代码如下:
这里rand()函数产生的不重复随机数最大只用32768个,让rand()+i后 能产生636105个不重复的随机数。

int main()
{
	HashBucket<int, int> hb1;
	srand((unsigned int)time(0));
	for (int i = 0; i < 1000000; ++i)
	{

		hb1.Insert(make_pair(rand() + i, rand() + i));
	}
	cout << hb1.MaxBucketSize() << endl;
	cout << hb1.size() << endl;
	return 0;
}

运行结果:

image.png
对于这636105个不重复的随机数,最大的哈希桶数据个数才是2。哈希表的性能非常恐怖。

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

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

相关文章

JavaScript Let 块级作用域

JavaScript Let 学习手记 最近在学习 JavaScript ES6 (2015) 标准时&#xff0c;我发现了let这个关键字&#xff0c;它为声明变量提供了一种新的方式&#xff0c;而且这种方式具有块级作用域的特点&#xff0c;真的很有趣呢&#xff01; 理解块作用域 在 ES6 之前的版本中&a…

【html学习笔记】2.基本元素

1.标题 标题会自动粗体其中大写的内容&#xff0c;并带有换行的效果会使用<h1>到<h6>表示不同大小的标题 <h1>标题1</h1> <h2>标题2</h2> <h3>标题3</h3> <h4>标题4</h4> <h5>标题5</h5> <h6>…

【Web】从零开始的js逆向学习笔记(上)

目录 一、逆向基础 1.1 语法基础 1.2 作用域 1.3 窗口对象属性 1.4 事件 二、浏览器控制台 2.1 Network Network-Headers Network-Header-General Network-Header-Response Headers Network-Header-Request Headers 2.2 Sources 2.3 Application 2.4 Console 三、…

基于3种机器学习法的黄土高原农业干旱监测比较研究_王晓燕_2022

基于3种机器学习法的黄土高原农业干旱监测比较研究_王晓燕_2022 摘要关键词1 引言2 研究区与数据6 结论#pic_center =x260) 摘要 本文集成 MODIS、TRMM、GLDAS 和再分析等多源数据,选取了 13 个与干旱有关的变量,并与基于气象数据的 3 个月时间尺度的标准化降水蒸发指数(SP…

算法沉淀——优先级队列(堆)(leetcode真题剖析)

算法沉淀——优先级队列 01.最后一块石头的重量02.数据流中的第 K 大元素03.前K个高频单词04.数据流的中位数 优先队列&#xff08;Priority Queue&#xff09;是一种抽象数据类型&#xff0c;它类似于队列&#xff08;Queue&#xff09;&#xff0c;但是每个元素都有一个关联的…

【精品】关于枚举的高级用法

枚举父接口 public interface BaseEnum {Integer getCode();String getLabel();/*** 根据值获取枚举** param code* param clazz* return*/static <E extends Enum<E> & BaseEnum> E getEnumByCode(Integer code, Class<E> clazz) {Objects.requireNonN…

C#,二分法(Bisection Method)求解方程的算法与源代码

1 二分法 二分法是一种分治算法&#xff0c;是一种数学思维。 对于区间[a&#xff0c;b]上连续不断且f&#xff08;a&#xff09;f&#xff08;b&#xff09;<0的函数yf&#xff08;x&#xff09;&#xff0c;通过不断地把函数f&#xff08;x&#xff09;的零点所在的区间…

OpenCV Mat 实例详解 二

构造函数 OpenCV Mat实例详解一中已介绍了部分OpenCV Mat构造函数&#xff0c;下面继续介绍剩余部分构造函数。 Mat (const std::vector< _Tp > &vec, bool copyDatafalse)&#xff1b; vec 包含数据的vec对象 copyData 是否拷贝数据&#xff0c;true— 拷贝数据&…

蓝桥杯真题:纸张尺寸

import java.util.Scanner; // 1:无需package // 2: 类名必须Main, 不可修改public class Main {public static void main(String[] args) {Scanner scan new Scanner(System.in);//在此输入您的代码...String s scan.nextLine();char[] c s.toCharArray();char c1 c[1];in…

鸿蒙开发系列教程(二十二)--List 列表操作(1)

列表是容器&#xff0c;当列表项达到一定数量&#xff0c;内容超过屏幕大小时&#xff0c;可以自动提供滚动功能。 用于呈现同类数据类型或数据类型集&#xff0c;例如图片和文本 List、ListItemGroup、ListItem关系 列表方向 1、概念 列表的主轴方向是指子组件列的排列方…

【数据结构】无向图创建邻接表以及深度遍历、广度遍历(C语言版)

数据结构——无向图创建邻接表以及深度遍历、广度遍历 一、邻接表概念二、邻接表实现 &#xff08;1&#xff09;准备前提——结构体定义&#xff08;2&#xff09;创建边链表&#xff08;3&#xff09;打印边链表&#xff08;4&#xff09;深度优先遍历&#xff08;5&#xff…

前端可能需要的一些安装

Node.js Node.js 官网 Node.js 中文网 Node.js is an open-source, cross-platform JavaScript runtime environment. Node.js是一个开源、跨平台的JavaScript运行时环境。Recommended for most users 推荐大多数用户使用哔哩哔哩安装视频 安装 node.js 的时候&#xff0c;会…

安卓TextView 拖动命名

需求&#xff1a;该布局文件使用线性布局来排列三个文本视图和一个按钮&#xff0c;分别用于显示两个动物名称以及占位文本视图。在占位文本视图中&#xff0c;我们为其设置了背景和居中显示样式&#xff0c;并用其作为接收拖放操作的目标 效果图&#xff1b; 实现代码 第一布…

如何解决缓存和数据库的数据不一致问题

数据不一致问题是操作数据库和操作缓存值的过程中&#xff0c;其中一个操作失败的情况。实际上&#xff0c;即使这两个操作第一次执行时都没有失败&#xff0c;当有大量并发请求时&#xff0c;应用还是有可能读到不一致的数据。 如何更新缓存 更新缓存的步骤就两步&#xff0…

【C++】---类和对象(中)默认成员函数 和 操作符重载

前言&#xff1a; 假如一个类中既没有成员变量也没有成员函数&#xff0c;那么这个类就是空类&#xff0c;空类并不是什么都没有&#xff0c;因为所有类都会生成如下6个默认成员函数&#xff1a; 一、构造函数 1、构造函数的定义及其特性 对于日期类对象&#xff0c;我们可…

C语言---指针进阶

1.字符指针 int main() {char str1[] "hello world";char str2[] "hello world";const char* str3 "hello world.";const char* str4 "hello world.";if (str3 str4){//常量字符串在内存里面是无法修改的&#xff0c;所以没必要…

数据检索:倒排索引加速、top-k和k最邻近

之前在https://www.yuque.com/treblez/qksu6c/wbaggl2t24wxwqb8?singleDoc# 《Elasticsearch: 非结构化的数据搜索》我们看了ES的设计&#xff0c;主要侧重于它分布式的设计以及LSM-Tree&#xff0c;今天我们来关注算法部分&#xff1a;如何进行检索算法的设计以及如何加速倒排…

RapidMiner数据挖掘2 —— 初识RapidMiner

本节由一系列练习与问题组成&#xff0c;这些练习与问题有助于理解多个基本概念。它侧重于各种特定步骤&#xff0c;以进行直接的探索性数据分析。因此&#xff0c;其主要目标是测试一些检查初步数据特征的方法。大多数练习都是关于图表技术&#xff0c;通常用于数据挖掘。 为此…

嵌入式系统中常见传感器介绍

&#xff08;本文为简单介绍&#xff0c;内容取材网络&#xff09; 传感器是嵌入式系统接入外部环境信息的重要接口,根据测量物理量的不同,传感器可以分为温度传感器、湿度传感器、压力传感器、加速度传感器等多种类型。选择合适的传感器,对于实现嵌入式系统的控制和互动功能至…

Java微服务架构的选择:Spring Cloud、Kubernetes还是Kubernetes + Istio?

微服务架构已经成为现代软件开发的趋势&#xff0c;其可以带来高度可伸缩性、松耦合性和团队自治性等优势。 在Java开发领域中&#xff0c;选择适合的微服务架构是非常关键的决策&#xff0c;本文将探讨Spring Cloud、Kubernetes和KubernetesIstio这三个架构选择的优势和劣势。…