【C++ STL】unordered_mapunordered_set (哈希表)

news2024/9/30 21:27:45

文章目录

  • unordered_map&unordered_set
    • 1. unordered容器
      • 1.1 效率对比
    • 2. 哈希
      • 2.1 哈希的定义
        • 哈希函数
          • 除留余数法
          • 自定义哈希函数
        • 哈希冲突
      • 2.2 哈希冲突的解决
        • 闭散列/开放定址法
          • 两种探测方式
          • 闭散列扩容
        • 开散列/拉链法/哈希桶
          • 开散列实现
    • 3. 模拟实现
      • 3.1 改造哈希表
      • 3.2 封装容器
        • unordered_set
        • unordered_map


unordered_map&unordered_set

哈希表(Hash Table)是一种数据结构,用于实现关联数组,它能够通过哈希函数将关键字映射到数组中的一个位置来实现快速的数据查找、插入和删除操作。在C++中,哈希表通常通过标准库中的unordered_map来实现。

1. unordered容器

当节点非常多时,map和set查询效率也不够理想。C++11新增unordered_map, unordered_set等底层为哈希的关联式容器。

unordered意为无序的,即存储遍历不按key排序。所以unordered系列容器只有单向迭代器。

容器底层区别
set / map红黑树排序加去重
multiset / multimap红黑树排序不去重
unordered_set / unordered_map哈希去重不排序
unordered_multiset / unordered_multimap哈希不排序不去重

1.1 效率对比

int n = 10000000;
vector<int> v;
srand((unsigned int)time(nullptr));
for (int i = 0; i < n; i++) {
    v.push_back(rand());
    v.push_back(i);
}

set<int> s;
unordered_set<int> us;

int begin1 = clock();
for (auto e : v) {
    s.insert(e);
}
int end1 = clock();

//
int begin2 = clock();
for (auto e : v) {
    us.insert(e);
}
int end2 = clock();

cout << "set insert: " << end1 - begin1 << endl;
cout << "unordered_set insert: " << end2 - begin2 << endl;
  • 当插入随机值且数据量很大时,unordered_set比set快3倍左右。
  • 当插入有序值且数据量很大时,unordered_set比set慢2倍左右。
  • unordered_set的查找效率极高,这得益于哈希的底层结构。

set更适合有序重复度低的数据,unordered_set更适合随机重复度高的数据。综合来看,unordered系列容器比map/set效率要更好些。

 

2. 哈希

2.1 哈希的定义

哈希也叫做散列,记数排序就体现了哈希思想。

计数排序为统计数字出现的个数,为每个数字都开辟了对应的一块空间,用来记录其出现的个数,每遇到就加一。

将一个元素和一个位置建立映射关系,这就是哈希的本质

哈希函数

搜索树中key与元素的存储位置没有直接映射。因此查找必须经历多次比较key。搜索效率还是不够理想。

最理想的搜索方法是:不经任何比较,一次性直接得出元素的存储位置。

通过某种函数(hashFunc)使元素 key 与它的存储位置之间能够建立映射关系,那么就可以一次性找到该元素。

除留余数法

例如,存在数据集合 [ 1 , 7 , 6 , 4 , 5 , 9 ] [1,7,6,4,5,9] [1,7,6,4,5,9],将哈希函数设置为 h a s h ( k e y ) = k e y    %    c a p a c i t y hash(key)=key\;\%\;capacity hash(key)=key%capacity c a p a c i t y capacity capacity 为存储空间总的大小。

自定义哈希函数

常见的数据类型如整数,库中自带哈希函数,但有些类型比如自定义类型,需要我们自定义哈希函数。

可以把哈希函数设计成仿函数,对不同类型添加特化处理。

template<class K>
struct Hash {
    size_t operator()(const K& key) {
        return key;
    }
};
// 特化
template<>
struct Hash<string> {
    //BKDR
    size_t operator()(const string& s) {
        size_t val = 0;
        for (auto ch : s) {
            val = val * 131 + ch;
        }
        return val;
    }
};

如果我们自定义类型作key,可以单独为其设计一个哈希函数,并在创建哈希表时作参数传进去。

struct Date {
    int _year;
    int _month;
    int _day;
};
struct HashDate {
    size_t operator()(const Date& date) {
        return date._year + date._month + date._day;
    }
};
哈希冲突

很可能存在不同数值映射到同一位置的情况,比如 10 % 10 = 0 ,    20 % 10 = 0 10\%10=0,\;20\%10=0 10%10=0,20%10=0。这就是哈希冲突或称哈希碰撞。

一般哈希冲突是不可避免的,冲突越多效率越低,故提高哈希效率在于如何解决哈希冲突。解决哈希冲突的两种常见的方法是:闭散列和开散列。

2.2 哈希冲突的解决

闭散列/开放定址法

闭散列,又称开放定址法。当发生哈希冲突时,把元素放到冲突位置后的下一个空位中

如何寻找空位呢?一般有线性探测和二次探测。如果没有空位,可以闭散列扩容。

两种探测方式
探测方式解释问题
线性探测从冲突位置向后遇到的第一个空位,就是存放位置连续位置冲突越来越多,引发洪水效应
二次探测每次向后跳过 i 2 i^2 i2 个长度,如遇到空位就是存放位置能缓解拥堵,快满时“绕圈”现象明显。

如何表示元素位置是否存在、为空、被删除?要为每个位置数据增加状态标记。

在这里插入图片描述

二次探测在得到位置后如果发生冲突,第一次向后跳过 1 2 1^2 12个长度、第二次 2 2 2^2 22个长度,如此类推,直到遇到空位置。

bool insert(const pair<K, V>& kv)
{
    if (_table.size() == 0 || _size * 10 / _table.size() >= 7)
    {
        size_t new_size = _size == 0 ? 10 : _table.size() * 2;
        hash_table<K, V> new_hash;
        new_hash._table.resize(new_size);

        for (auto& data : _table)
            if (data._st == EXIST)
                new_hash.insert(data._kv);

        _table.swap(new_hash._table);
    }

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

    size_t i = 1;
    size_t idx = hashi;
    while (_table[idx]._st == EXIST)
    {
        if (_table[idx]._kv.first == kv.first)
            return false;

        idx += i;     // 线性探测
        idx += i * i; // 二次探测
        idx %= _table.size();
    }

    _table[idx]._kv = kv;
    _table[idx]._st = EXIST;
    ++_size;
    return true;
}
闭散列扩容

当二次探测总是在正确位置“绕圈”时,只能用扩容解决问题。但什么时候该扩容呢?扩容多大呢?

载荷因子可以衡量哈希表的装载程度,它的定义是:
α    =    表中元素个数    /    表的总长度 \alpha \; = \;表中元素个数\;/\;表的总长度 α=表中元素个数/表的总长度
负载因子越小,冲突概率越低,效率越高,但空间浪费就多。

使用闭散列时,荷载因子是决定哈希效率的重要因素,应严格限制在0.7-0.8以下。超过0.8,查表的缓存不命中率指数级上升。

哈希表的扩容要维护原有数据的存放位置。数据搬迁很麻烦,不如将原数据插入到新表。

在这里插入图片描述

bool insert(const pair<K, V>& kv)
{
    if (_table.size() == 0 || _size * 10 / _table.size() >= 7)
    {
        size_t new_size = _size == 0 ? 10 : _table.size() * 2;
        hash_table<K, V> new_hash;
        new_hash._table.resize(new_size);

        for (auto& data : _table)
        {
            if (data._st == EXIST)
                new_hash.insert(data._kv);
        }

        _table.swap(new_hash._table);
    }
    // ...
}
HashNode<K, V>* Find(const K& key) {
    if (_table.size() == 0)
        return nullptr;

    HashFunc hf;

    size_t pos = hf(key) % _table.size();
    size_t index = pos;
    size_t i = 0;

    while (_table[index]._status == EXIST) {
        if (_table[index]._kv.first == key) {            
            return &_table[index];
        }

        ++i;
            index = pos + i;
            index %= _table.size();
    }

    return nullptr;
}
bool Erase(const K& key) {
    HashNode<K, V>* ret = Find(key);
    if (ret == nullptr)
        return false;
    else {
        ret->_status = EMPTY;
        --_n;
        return true;
    }
}
开散列/拉链法/哈希桶

闭散列的线性探测和二次探测都会加剧哈希冲突,现实中几乎不会使用闭散列。

开散列又称拉链法、链地址法、开链法、哈希桶。

具有相同地址的key归于一个集合(桶),桶中元素通过链表链接起来,各链表的头结点存储在哈希表中。从根本上解决了冲突的问题。
在这里插入图片描述

template<class K, class V>
struct hash_node
{
    pair<K, V> _kv;
    hash_node<K, V>* _next;

    hash_node<K, V>(const pair<K, V>& kv) : _kv(kv), _next(nullptr)
    {}
};

template<class K, class V>
class hash_table
{
public:
    typedef hash_node<K, V> node;
public:
    ~hash_table<K, V>() {
        for (auto cur : _table) {
            while (cur) {
                node* next = cur->_next;
                delete cur;
                cur = next;
            }
        }
    }

private:
    vector<node*> _table;
    size_t _size;
};
开散列实现

桶由链表实现,本质效率和线性探测一样。如果桶中节点过多会影响哈希表的性能。必须在适当条件下对哈希表增容。让哈希表处于最佳状态。

开散列的最佳情况是每个哈希桶中刚好挂一个节点。如果再插入必然会发生冲突。因此当元素个数等于桶的个数时,可以给哈希表增容

在这里插入图片描述

开散列扩容复用插入逻辑的话,会浪费原节点,最好是遍历原表节点放到新表中。

bool insert(const pair<K, V>& kv)
{
    // 如果哈希表为空或者已经满了(达到了负载因子),需要进行扩容
    if (_table.empty() || _size == _table.size())
    {
        // 计算新的哈希表大小
        size_t new_size = _size == 0 ? 10 : _table.size() * 2;
        // 创建一个新的哈希表
        vector<node*> new_table(new_size);

        // 将旧哈希表中的元素重新哈希到新的哈希表中
        for (auto cur : _table)
        {
            while (cur)
            {
                // 计算当前节点应该存放在新哈希表的位置
                size_t hashi = cur->_kv.first % _table.size();
                node* next = cur->_next;

                // 将当前节点插入到新哈希表中
                cur->_next = new_table[hashi];
                new_table[hashi] = cur;

                cur = next;
            }
            cur = nullptr;
        }

        // 使用新的哈希表替换旧的哈希表
        _table.swap(new_table);
    }

    // 计算待插入节点应该存放的位置
    size_t hashi = kv.first % _table.size();

    // 创建新节点,并插入到哈希表中
    node* new_node = new node(kv);  // 假设 node 是节点类型
    new_node->_next = _table[hashi];
    _table[hashi] = new_node;

    return true;
}

node* find(const K& key)
{
    // 如果哈希表为空,直接返回 nullptr
    if (_table.empty())
        return nullptr;

    // 计算键 `key` 对应的哈希表索引
    size_t hashi = key % _table.size();
    // 获取哈希表中对应索引位置的链表头节点
    node* cur = _table[hashi];

    // 在链表中查找匹配的键值对
    while (cur)
    {
        // 如果找到匹配的键,返回当前节点指针
        if (cur->_kv.first == key)
            return cur;

        // 否则继续遍历链表
        cur = cur->_next;
    }

    // 如果未找到匹配的键,返回 nullptr
    return nullptr;
}

bool erase(const K& key)
{
    // 如果哈希表为空,直接返回 false
    if (_table.empty())
        return false;

    // 计算键 `key` 对应的哈希表索引
    size_t hashi = key % _table.size();
    // 获取哈希表中对应索引位置的链表头节点
    node* cur = _table[hashi];
    node* prev = nullptr;

    // 在链表中查找并删除匹配的键值对
    while (cur)
    {
        // 如果找到匹配的键
        if (cur->_kv.first == key)
        {
            // 如果是链表头节点
            if (!prev)
            {
                _table[hashi] = cur->_next; // 直接修改哈希表中的头指针
            }
            else
            {
                prev->_next = cur->_next; // 修改前一个节点的指针,跳过当前节点
            }
            delete cur; // 释放当前节点的内存
            return true; // 返回删除成功
        }

        // 更新前一个节点和当前节点指针,继续遍历链表
        prev = cur;
        cur = cur->_next;
    }

    // 如果未找到匹配的键,返回 false 表示删除失败
    return false;
}

 

3. 模拟实现

哈希表的实现比红黑树要简单不少,但是它的封装比较繁琐。

根据哈希表原理,实现unordered系列容器,需要对key类型作出如下要求:

参数解释
Hash=hash<Key>支持取模或支持映射到整数进行取模
Pred=equal_to<Key>支持判断相等的函数
template < class Key,  									// unordered_map::key_type
		   class Value,									// unordered_map::mapped_type
		   class Hash = hash<Key>, 						// unordered_map::hasher
		   class Pred = equal_to<Key>, 					// unordered_map::key_equal
		   class Alloc = allocator<pair<const Key,T>>   // unordered_map::allocator_type
		 > class unordered_map;

3.1 改造哈希表

  • unordered_map/unordered_set要复用同一个哈希表,同样要提供仿函数KeyOfVal
  • 哈希只支持单向迭代器,所以只有++。
  • 哈希表遍历是从头到尾遍历每个桶,必须能访问表中的数组,所以必须提供哈希表对象的指针。
template<class K, class V, class HashFunc, class KeyOfVal, class EqualKey>
class hash_table;

template<class K, class V, class HF, class KOV, class Ref, class Ptr>
struct __hash_iterator
{
 	typedef __hash_node<V> node; // 定义哈希表节点类型
    typedef hash_table<K, V, HF, KOV> hash_table; // 定义哈希表类型
    typedef __hash_iterator<K, V, HF, KOV, V&, V*> iterator; // 迭代器类型
    typedef __hash_iterator<K, V, HF, KOV, Ref, Ptr> self; // 自身类型,用于比较

    node* _node;
    const hash_table* _ht;

    __hash_iterator(node* n, const hash_table* ht) : _node(n), _ht(ht)
    {}

    __hash_iterator(const iterator& it) : _node(it._node), _ht(it._ht)
    {}

    Ref operator*() { return _node->_val; }

    Ptr operator->() { return &_node->_val; }

    bool operator==(const self& s) { return _node == s._node; }

    bool operator!=(const self& s) { return !this->operator==(s); }

    self& operator++()
    {
        if (_node->_next)
        {
            _node = _node->_next;
        }
        else
        {
            size_t hashi = _ht->_hash(_ht->_kov(_node->_val)) % _ht->_table.size();
            ++hashi;

            while (hashi < _ht->_table.size())
            {
                if (_ht->_table[hashi])
                {
                    _node = _ht->_table[hashi];
                    break;
                }
                ++hashi;
            }

            if (hashi == _ht->_table.size())
                _node = nullptr;
        }
        return *this;
    }
};
template<class V>
struct __hash_node
{
    V _val;                // 存储的值
    __hash_node<V>* _next; // 指向下一个节点的指针

    // 构造函数,初始化节点值和指针
    __hash_node<V>(const V& val) : _val(val), _next(nullptr)
    {}
};

template<class K, class V, class HashFunc, class KeyOfVal>
class hash_table
{
public:
    typedef __hash_node<V> node; // 定义哈希表节点类型
    typedef __hash_iterator<K, V, HashFunc, KeyOfVal, V&, V*> iterator; // 定义迭代器类型
    typedef __hash_iterator<K, V, HashFunc, KeyOfVal, const V&, const V*> const_iterator; // 定义常量迭代器类型

    // 声明友元,使迭代器能够访问私有成员
    template<class K, class V, class HF, class KOV, class Ref, class Ptr>
    friend struct __hash_iterator;

public:
    // 返回常量迭代器指向第一个非空节点的位置
    const_iterator begin() const
    {
        for (auto cur : _table)
        {
            if (cur) return const_iterator(cur, this);
        }
        return end();
    }

    // 返回常量迭代器指向结束位置(nullptr)
    const_iterator end() const
    {
        return const_iterator(nullptr, this);
    }

    // 返回迭代器指向第一个非空节点的位置
    iterator begin()
    {
        for (auto cur : _table)
        {
            if (cur) return iterator(cur, this);
        }
        return end();
    }

    // 返回迭代器指向结束位置(nullptr)
    iterator end()
    {
        return iterator(nullptr, this);
    }

public:
    // 插入操作,将值插入到哈希表中
    std::pair<iterator, bool> insert(const V& val)
    {
        auto pos = find(_kov(val)); // 查找当前值是否已存在于哈希表中
        if (pos != end())
            return {pos, false}; // 如果存在,返回插入失败

        if (_table.empty() || _table.size() == _size)
        {
            size_t new_size = _table.empty() ? 10 : _table.size() * 2;
            std::vector<node*> new_table(new_size);

            for (auto& cur : _table)
            {
                while (cur)
                {
                    size_t hashi = _hash(_kov(cur->_val)) % new_table.size();
                    node* next = cur->_next;

                    cur->_next = new_table[hashi];
                    new_table[hashi] = cur;

                    cur = next;
                }
            }

            _table.swap(new_table); // 更新哈希表
        }

        size_t hashi = _hash(_kov(val)) % _table.size();
        node* new_node = new node(val); // 创建新节点

        new_node->_next = _table[hashi];
        _table[hashi] = new_node;
        ++_size;

        return {iterator(new_node, this), true}; // 返回插入成功和迭代器指向新节点
    }

    // 查找操作,根据键查找节点
    iterator find(const K& key)
    {
        if (_table.empty())
            return end();

        size_t hashi = _hash(key) % _table.size();
        node* cur = _table[hashi];

        while (cur)
        {
            if (_hash(_kov(cur->_val)) == _hash(key))
                return iterator(cur, this); // 找到节点,返回迭代器

            cur = cur->_next;
        }

        return end(); // 未找到,返回结束迭代器
    }

    // 删除操作,根据键删除节点
    bool erase(const K& key)
    {
        if (_table.empty())
            return false;

        size_t hashi = _hash(key) % _table.size();
        node* cur = _table[hashi];
        node* prev = nullptr;

        while (cur)
        {
            if (_hash(_kov(cur->_val)) == _hash(key))
            {
                if (!prev)
                {
                    _table[hashi] = cur->_next;
                }
                else
                {
                    prev->_next = cur->_next;
                }
                delete cur; // 删除节点
                return true;
            }

            prev = cur;
            cur = cur->_next;
        }
        return false; // 未找到,返回删除失败
    }

private:
    std::vector<node*> _table; // 存储哈希表的数组
    size_t _size = 0; // 哈希表当前元素个数

    KeyOfVal _kov; // 获取值的键
    HashFunc _hash; // 哈希函数
};

3.2 封装容器

unordered_set
template<class K, class Hash = hash<K>>
class unordered_set
{
private:
    struct KeyOfVal {
        const K& operator()(const K& key) const { return key; }
    };

public:
    typedef hash_table<K, K, Hash, KeyOfVal> rep_type;
    typedef typename rep_type::const_iterator iterator;
    typedef typename rep_type::const_iterator const_iterator;

public:
    std::pair<iterator, bool> insert(const K& key) { return _ht.insert(key); }

    size_t erase(const K& key) { return _ht.erase(key); }

    iterator find(const K& key) { return _ht.find(key); }

    iterator begin() { return _ht.begin(); }
    iterator end() { return _ht.end(); }

    const_iterator begin() const { return _ht.begin(); }
    const_iterator end() const { return _ht.end(); }

private:
    rep_type _ht;
};
unordered_map
template<class K, class V, class Hash = hash<K>>
class unordered_map
{
private:
    struct KeyOfVal {
        const K& operator()(const std::pair<const K, V>& kv) const {
            return kv.first;
        }
    };

public:
    typedef hash_table<K, std::pair<const K, V>, Hash, KeyOfVal> rep_type;
    typedef typename rep_type::iterator iterator;
    typedef typename rep_type::const_iterator const_iterator;

public:
    std::pair<iterator, bool> insert(const std::pair<K, V>& kv) { return _ht.insert(kv); }

    size_t erase(const K& key) { return _ht.erase(key); }

    iterator find(const K& key) { return _ht.find(key); }

    iterator begin() { return _ht.begin(); }
    iterator end() { return _ht.end(); }

    const_iterator begin() const { return _ht.begin(); }
    const_iterator end() const { return _ht.end(); }

    V& operator[](const K& key) { return _ht.insert({key, V()}).first->second; }

private:
    rep_type _ht;
};

两者主要区别:

unordered_setunordered_map 都是基于哈希表实现的C++容器,适用于快速插入、查找和删除数据。unordered_set 用于存储唯一的键集合,而 unordered_map 则用于存储键值对映射关系,支持对值的直接访问和修改。选择适合的容器取决于是否需要存储和操作值,以及是否需要保证键的唯一性。

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

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

相关文章

GPIO输入模式之按键控制及光敏传感器控制应用案例

系列文章目录 STM32之GPIO&#xff08;General Purpose Input/Output&#xff0c;通用型输入输出&#xff09; GPIO输出控制之LED闪烁、LED流水灯以及蜂鸣器应用案例 文章目录 系列文章目录前言一、按键简介二、传感器模块简介2.1 AO模拟量输出模块2.2 DO数字量输出模块2.3 指…

【C语言加油站】数据在内存中的存储

数据在内存中的存储 导读一、计算机中的数据类型二、整数在计算机中的存储2.1 整数的存储形式——原码、反码与补码2.2 三种形式之间的相互转换2.3 采用补码存储整数的原因 三、大小端字节序与字节序判断3.1 大端存储与小端存储3.2 为什么会出现大小端存储&#xff1f;3.3 大端…

家用设备轻松搭建 AI 集群,畅跑 Llama 3.1 405B

作者:老余捞鱼 原创不易,转载请标明出处及原作者。 写在前面的话: 本文主要介绍如何在家用设备上运行大型开源语言模型Llama 3.1 405B,首先我会解释构建人工智能集群来提高运行效率的原理,随后会演示如何通过Distributed Llama项目支持在多个设备上运行LLM模型,并…

【STL】05.vector的模拟实现

一、vector的实现 1.1 基本框架 template<class T> class vector {typedef T* iterator;typedef const T* const_iterator; public:private:iterator _startnullptr;iterator _finishnullptr;iterator _end_of_storagenullptr; };1.2 vector的默认成员函数 1.2.1 构造…

如何通过大模型生成业务需要的数据集

现在大模型训练数据的主力都是LLM自己贡献的了。但是也不是说你让它输出什么&#xff0c;然后它就一劳永逸地不停地输出你想要的东西。受限于LLM本身的能力、上下文规定的长度、训练方式导致的有限变化&#xff0c;你需要不断变更你的prompt&#xff0c;以让输出更多样。 接下…

录屏为什么没有声音?一款软件为您解决无声难题

录屏已经成为我们日常工作和生活中不可或缺的一部分。然而&#xff0c;有时在录屏过程中&#xff0c;我们可能会遇到一个令人困惑的问题&#xff1a;录屏为什么没有声音&#xff1f;本文将详细解析电脑录屏没有声音的可能原因&#xff0c;并提供相应的解决方案。同时&#xff0…

YOLOv10问世,登顶GiTHub!性能飞升,【多尺度目标检测】值得大看特看!

【多尺度目标检测】是近年来在深度学习领域中备受关注的一项技术&#xff0c;它通过处理图像中不同尺度的目标&#xff0c;显著提升了模型在复杂场景中的检测精度和鲁棒性。多尺度目标检测技术已经在自动驾驶、安防监控和遥感图像分析等多个领域取得了显著成果&#xff0c;其独…

SQL Server Management Studio的使用

之前在 https://blog.csdn.net/fengbingchun/article/details/140961550 介绍了在Windows10上安装SQL Server 2022 Express和SSMS&#xff0c;这里整理下SSMS的简单使用&#xff1a; SQL Server Management Studio(SSMS)是一种集成环境&#xff0c;提供用于配置、监视和管理SQL…

前端工程师学习springboot2.x之配置idea热更新实现高效率开发节奏

目前已经学习springboot实现了增删改查分页查询&#xff0c;每次修改业财或者是代码重启项目都让我觉得很闹心&#xff0c;现在给出idea2021版本自带热更新操作设置&#xff0c;设置过程分享给大家 总结&#xff1a;以上就是配置的全部过程&#xff0c;祝大家写代码快乐…

鸿蒙(Harmony) NEXT - AlphabetIndexer实现联系人字母索引

鸿蒙(Harmony) NEXT 9月份就要正式上架了&#xff0c;并且不会再兼容安卓平台&#xff0c;于是我也赶紧给App开发鸿蒙版本&#xff0c;接下来会写一系列的Harmony开发教程。 今天使用AlphabetIndexer实现联系人字母索引&#xff0c;AlphabetIndexer是官方封装好的组件 咱们实…

【驱动程序】3.5寸SPI液晶屏_ILI9488_stm32f103c8t6_CubeMX_HAL库

【驱动程序】3.5寸SPI液晶屏_ILI9488_stm32f103c8t6_CubeMX_HAL库 主控芯片&#xff1a; stm32f103c8t6 接线&#xff1a; LED-3.3v其他管脚按main.h文件接: #define LCD_CS_Pin GPIO_PIN_1 #define LCD_CS_GPIO_Port GPIOA #define LCD_RS_Pin GPIO_PIN_2…

武汉流星汇聚:全球化与多元化并进,亚马逊展望电商领域无限可能

在全球电商的浩瀚星空中&#xff0c;亚马逊无疑是最为耀眼的一颗星辰。凭借其多年在跨境市场的深耕细作&#xff0c;亚马逊不仅积累了庞大的高活跃用户群&#xff0c;还构建了显著的平台流量优势。根据Similar Web的权威数据&#xff0c;亚马逊的独立访问用户数量已超过26.59亿…

EGO-Swarm 仿真环境搭建

EGO-Swarm仿真环境搭建 参考教程&#xff1a; https://github.com/ZJU-FAST-Lab/ego-planner-swarm EGO-Swarm是一种分散的异步系统解决方案&#xff0c;用于仅使用机载资源在未知的障碍物丰富的场景中进行多机器人自主导航。 1. 查看系统环境 要运行本仿真程序&#xff0c…

评估测量仪器/传感器时的各种精度解析一览

在工业测量中&#xff0c;精度是一个复合概念&#xff0c;涉及到多个方面&#xff0c;通常用来描述测量结果的准确性和可靠性。 在选择测量仪器/传感器时&#xff0c;面对众多的精度名称&#xff0c;你是否苦恼他们具体描述的是什么精度&#xff0c;是否和评估要求有直接关联&…

开放式耳机有什么好处?开放式耳机该怎么选?

​开放式耳机的好处多多呀&#xff01;如今&#xff0c;开放式耳机已经迅速成为耳机市场上的新宠&#xff0c;它们以其独特的佩戴方式和卓越的音质表现&#xff0c;赢得了广大音乐爱好者和运动达人的喜爱。尤其是对于那些热爱听歌和追求运动自由的人们来说&#xff0c;开放式耳…

电脑录屏软件推荐,6款高效录屏神器(2024最全最新)

电脑录屏软件成为了我们工作、学习和娱乐中不可或缺的工具。无论是录制PPT演示、QQ聊天过程&#xff0c;还是进行专业的直播或教学&#xff0c;都需要一款功能强大、操作简便的录屏软件。 那么&#xff0c;本文将为大家进行电脑录屏软件推荐&#xff0c;让您无论在哪种录屏场景…

共享之道——享元模式(Python实现)

共享之道——享元模式&#xff08;Python实现&#xff09; 大家好&#xff0c;今天我们继续来讲结构型设计模式&#xff0c;上一期我们介绍了外观模式&#xff0c;这一期我们来讲享元模式&#xff08;Flyweight Pattern&#xff09;。 享元模式&#xff08;Flyweight Pattern…

超实用 不再担心猫咪掉毛 一文教你养宠家庭空气净化器怎么选

一到夏天&#xff0c;家中的猫咪给你带来的不仅仅是温暖的陪伴&#xff0c;还有那挥之不去的宠物异味。普通空气净化器虽然能够应对一般的空气净化需求&#xff0c;但对于养猫家庭特有的挑战&#xff0c;如宠物毛发、皮屑和异味等&#xff0c;它们往往难以胜任。专业的宠物空气…

【LLM大模型】大模型Prompt Engineering提示词工程

目录&#xff1a; 1、提示工程简介 2、如何写好提示词 2.1 描述清晰2.2 角色扮演2.3 提供示例2.4 复杂任务分解2.5 使用格式符区分语义2.6 情感和物质激励2.7 使用英语2.8 结构化提示词 1、提示工程简介 1.1 什么是Prompt 提示词&#xff1f; 不论是文生图应用&#xff0c;…