当前位置: 首页 > news >正文

【C++】开散列实现unordered_map与unordered_set的封装

本文主要介绍unordered_map与unordered_set的封装,此次封装主要用上文所说到的开散列,通过开散列的一些改造来实现unordered_map与unordered_set的封装

文章目录

    • 一、模板参数
    • 二、string的特化
    • 三、正向迭代器
    • 四、构造与析构
    • 五、[]的实现
    • 六、unordered_map的实现
    • 七、unordered_set的实现
    • 八、哈希表代码

一、模板参数

由于unordered_set 是 K 模型的容器,而 unordered_map 是 KV 模型的容器,所以需要对结点的参数进行改造,unordered_set可以使用,unordered_map也可以使用,改为T _data即可

	template<class T>struct HashNode{T _data;HashNode<T>* _next;HashNode(const T& data):_data(data), _next(nullptr){}};

T模板参数可能是键值Key,也可能是Key和Value构成的键值对。如果是unordered_map容器,那么它传入底层哈希表的模板参数就是Key和Key和Value构成的键值对,如果是unordered_set容器,那么它传入底层哈希表的模板参数就是Key和Key

	template<class K,class V,class Hash=HashFunc<K>>class unordered_map{private:buckethash::HashTable<K, pair<const K, V>, Hash, MapKeyOfT> _ht;};
	template<class K,class Hash = HashFunc<K>>class unordered_set{private: buckethash::HashTable<K, K, Hash, SetKeyOfT> _ht;};

如此就可以实现泛型了,如果是unordered_set,结点当中存储的是键值Key;如果是unordered_map,结点当中存储的就是<Key, Value>键值对:

image-20230301073400540

哈希表仿函数的支持:KeyOfT👇

我们通过哈希计算出对应的哈希地址:但是插入的时候就不能直接用data去进行比较了

对于unordered_set:data是key是可以比较的,对于unordered_map:data是键值对,我们需要取出键值对的first。而data既可以是unordered_set的,也可以是unordered_map的,所以我们需要仿函数来实现不同容器所对应的需求,然后传入:

unordered_map返回kv.first

template<class K,class V,class Hash=HashFunc<K>>class unordered_map{struct MapKeyOfT{const K& operator()(const pair<const K, V>& kv){return kv.first;}};}private:buckethash::HashTable<K, pair<const K, V>, Hash, MapKeyOfT> _ht;};

unordered_set返回key:

template<class K,class Hash = HashFunc<K>>class unordered_set{struct SetKeyOfT{const K& operator()(const K& key){return key;}};private: buckethash::HashTable<K, K, Hash, SetKeyOfT> _ht;};

这也就是Hashtable需要KeyOfT的原因所在。

二、string的特化

字符串无法取模,在这里重新写一遍,字符串无法取模的问题写库的大神们早就想到了

image-20230301074411165

预留一个模板参数,无论上层容器是unordered_set还是unordered_map,我们都能够通过上层容器提供的仿函数获取到元素的键值

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 hash = 0;for (auto ch : key){hash *= 131;//顺序?abc,cbahash += ch;}return hash;}
};

string的特化:符合string类型的优先走string类型

template<class K,class V,class Hash=HashFunc<K>>
class unordered_map
template<class K,class Hash = HashFunc<K>>
class unordered_set

三、正向迭代器

哈希表的正向迭代器对哈希表指针和结点指针进行了封装,++运算符重载,可以访问下一个非空的桶,所以每个正向迭代器里面存的是哈希表地址。

template<class K, class T, class Hash, class KeyOfT>
class HashTable;template<class K,class T,class Hash,class KeyOfT>struct __HTIterator{typedef HashNode<T> Node;typedef __HTIterator<K, T, Hash, KeyOfT> Self;typedef HashTable<K, T, Hash, KeyOfT> HT;Node* _node;HT* _ht;}

所以我们构造迭代器的时候需要知道节点的地址和哈希表的地址

	__HTIterator(Node*node,HT*ht):_node(node),_ht(ht){}

++运算符重载的实现:如果当前的桶还有节点,那么++就是当前桶下一个节点,如果当前元素是所在的桶的最后一个元素,那么++就是下一个非空的桶了

image-20230301080113773

如何去找下一个非空桶:其实很简单,通过当前节点的值算出当前桶的hashi,然后++hashi就是下一个桶了,找到下一个非空桶即可

		T& operator*(){return _node->_data;}T* operator->(){return &_node->_data;}bool operator !=(const Self& s) const{return _node != s._node;}Self& operator++(){if (_node->_next)//当前桶还有节点{_node = _node->_next;}//找下一个非空的桶else{KeyOfT kot;Hash hash;size_t hashi = hash(kot(_node->_data)) % _ht->_tables.size();//当前桶的哈希地址++hashi;while (hashi < _ht->_tables.size()){if (_ht->_tables[hashi])//非空桶{_node = _ht->_tables[hashi];break;}else{++hashi;}}if (hashi == _ht->_tables.size()){_node = nullptr;}}return *this;}};

–问题:哈希表中的迭代器是单向迭代器,并没有反向迭代器,所以没有实现–-运算符的重载,若是想让哈希表支持双向遍历,可以考虑将哈希桶中存储的单链表结构换为双链表结构。

存在的小细节:👇

	template<class K,class T,class Hash,class KeyOfT>class HashTable{typedef HashNode<T> Node;template<class K,class T,class Hash,class KeyOfT>friend struct __HTIterator;public:typedef __HTIterator<K, T, Hash, KeyOfT> iterator;iterator begin(){for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]){return iterator(_tables[i], this);}}return iterator(nullptr, this);}iterator end(){return iterator(nullptr, this);}private:vector<Node*> _tables;size_t _n = 0;};

++运算符重载去寻找下一个结点时,会访问_tables,而_tables是哈希表中的私有成员,所以我们需要把迭代器__HTIterator声明为哈希表的友元

正向迭代器__HTIterator的typedef放在了public,这是为了外部能够使用我们的typedef之后的正向迭代器

还需要注意的是,哈希表的 const 迭代器不能复用普通迭代器的代码,我们查看源码:

image-20230301210902049

这与我们之前所复用的不同,上面stl源码中可以看到并没有用以前的复用:

这是因为如果使用const版本,那么_tables使用[]返回的就是const版本,那么Node*就相当于是const Node*,就会导致权限放大,无法构造;如果改成const HT* _ht; const Node* _node;,又会导致[]不能修改的问题:

image-20230301212422965

四、构造与析构

默认构造

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

析构函数

哈希表当中存储的结点都是new出来的,所以哈希表被析构时必须delete。在析构哈希表时我们只需要遍历取出非空的哈希桶,遍历哈希桶当中的结点并进行释放即可👇

		~HashTable(){for (size_t i = 0; i < _tables.size(); ++i){// 释放桶Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}}

五、[]的实现

要想实现[],我们需要先把Insert的返回值修改成pair<iterator,bool>,最后的返回值也要一起修改

如果有重复的元素就返回这个找到it迭代器
没有重复的就返回newnode迭代器

		pair<iterator, bool> Insert(const T&data){KeyOfT kot;iterator it = Find(kot(data));if (it != end()){return make_pair(it, false);}if (_tables.size() == _n){vector<Node*> newTables;newTables.resize(__stl_next_prime(_tables.size()), nullptr);for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;size_t hashi = Hash()(kot(cur->_data))% newTables.size();cur->_next = newTables[hashi];newTables[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTables);}size_t hashi = Hash()(kot(data)) % _tables.size();Node* newnode = new Node(data);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return make_pair(iterator(newnode,this),true);}

六、unordered_map的实现

#pragma once#include "HashTable.h"
namespace hwc
{template<class K,class V,class Hash=HashFunc<K>>class unordered_map{struct MapKeyOfT{const K& operator()(const pair<const K, V>& kv){return kv.first;}};public:typedef typename buckethash::HashTable<K, pair<const K, V>, Hash, MapKeyOfT>::iterator iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}pair<iterator, bool> insert(const pair<K, V> data){return _ht.Insert(data);}V& operator[](const K& key){pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));return ret.first->second;}private:buckethash::HashTable<K, pair<const K, V>, Hash, MapKeyOfT> _ht;};void test_unordered_map(){string arr[] = { "苹果","香蕉","苹果","西瓜","哈密瓜"};unordered_map<string, int> countMap;for (auto& e : arr){countMap[e]++;}for (const auto& kv : countMap){cout << kv.first << ":" << kv.second << endl;}}
}

七、unordered_set的实现

#pragma once#include "HashTable.h"namespace hwc
{template<class K,class Hash = HashFunc<K>>class unordered_set{struct SetKeyOfT{const K& operator()(const K& key){return key;}};public:typedef typename buckethash::HashTable<K, K, Hash, SetKeyOfT>::iterator iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}pair<iterator, bool> insert(const K& key){return _ht.Insert(key);}private: buckethash::HashTable<K, K, Hash, SetKeyOfT> _ht;};void test_unordered_set(){unordered_set<int> us;us.insert(13);us.insert(3);us.insert(23);us.insert(5);us.insert(5);us.insert(6);us.insert(15);us.insert(223342);us.insert(22);unordered_set<int>::iterator it = us.begin();while (it != us.end()){cout << *it << " ";++it;}cout << endl;for (auto e : us){cout << e << " ";}cout << endl;}
}

八、哈希表代码

#pragma once
#pragma once#include <iostream>
#include <string>
#include <vector>
using namespace std;
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 hash = 0;for (auto ch : key){hash *= 131;//顺序?abc,cbahash += ch;}return hash;}
};//开散列
namespace buckethash
{template<class T>struct HashNode{T _data;HashNode<T>* _next;HashNode(const T& data):_data(data), _next(nullptr){}};template<class K, class T, class Hash, class KeyOfT>class HashTable;template<class K,class T,class Hash,class KeyOfT>struct __HTIterator{typedef HashNode<T> Node;typedef __HTIterator<K, T, Hash, KeyOfT> Self;typedef HashTable<K, T, Hash, KeyOfT> HT;Node* _node;HT* _ht;__HTIterator(Node*node,HT*ht):_node(node),_ht(ht){}T& operator*(){return _node->_data;}T* operator->(){return &_node->_data;}bool operator !=(const Self& s) const{return _node != s._node;}Self& operator++(){if (_node->_next){_node = _node->_next;}else{KeyOfT kot;Hash hash;size_t hashi = hash(kot(_node->_data)) % _ht->_tables.size();++hashi;while (hashi < _ht->_tables.size()){if (_ht->_tables[hashi]){_node = _ht->_tables[hashi];break;}else{++hashi;}}if (hashi == _ht->_tables.size()){_node = nullptr;}}return *this;}};template<class K,class T,class Hash,class KeyOfT>class HashTable{typedef HashNode<T> Node;template<class K,class T,class Hash,class KeyOfT>friend struct __HTIterator;public:typedef __HTIterator<K, T, Hash, KeyOfT> iterator;iterator begin(){for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]){return iterator(_tables[i], this);}}return iterator(nullptr, this);}iterator end(){return iterator(nullptr, this);}HashTable():_n(0){_tables.resize(__stl_next_prime(0));}~HashTable(){for (size_t i = 0; i < _tables.size(); ++i){// 释放桶Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}}pair<iterator, bool> Insert(const T&data){KeyOfT kot;iterator it = Find(kot(data));if (it != end()){return make_pair(it, false);}if (_tables.size() == _n){vector<Node*> newTables;newTables.resize(__stl_next_prime(_tables.size()), nullptr);for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;size_t hashi = Hash()(kot(cur->_data))% newTables.size();cur->_next = newTables[hashi];newTables[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTables);}size_t hashi = Hash()(kot(data)) % _tables.size();Node* newnode = new Node(data);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return make_pair(iterator(newnode,this),true);}iterator Find(const K& key){KeyOfT kot;size_t hashi = Hash()(key) % _tables.size();Node* cur = _tables[hashi];while (cur){if (kot(cur->_data) == key){return iterator(cur, this);}else{cur = cur->_next;}}return end();}bool Erase(const K& key){size_t hashi = Hash()(key) % _tables.size();Node* prev = nullptr;Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){if (cur == _tables[hashi]){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;--_n;return true;}else{prev = cur;cur = cur->_next;}}return false;}inline unsigned long __stl_next_prime(unsigned long n){static const int __stl_num_primes = 28;static const unsigned long __stl_prime_list[__stl_num_primes] ={53, 97, 193, 389, 769,1543, 3079, 6151, 12289, 24593,49157, 98317, 196613, 393241, 786433,1572869, 3145739, 6291469, 12582917, 25165843,50331653, 100663319, 201326611, 402653189, 805306457,1610612741, 3221225473, 4294967291};for (int i = 0; i < __stl_num_primes; ++i){if (__stl_prime_list[i] > n){return __stl_prime_list[i];}}return __stl_prime_list[__stl_num_primes - 1];}private:vector<Node*> _tables;size_t _n = 0;};
}

本篇到此结束…

相关文章:

【C++】开散列实现unordered_map与unordered_set的封装

本文主要介绍unordered_map与unordered_set的封装&#xff0c;此次封装主要用上文所说到的开散列,通过开散列的一些改造来实现unordered_map与unordered_set的封装 文章目录一、模板参数二、string的特化三、正向迭代器四、构造与析构五、[]的实现六、unordered_map的实现七、u…...

华为OD机试真题Python实现【删除指定目录】真题+解题思路+代码(20222023)

删除指定目录 题目 某文件系统中有 N 个目录, 每个目录都一个独一无二的 ID。 每个目录只有一个付目录, 但每个目录下可以有零个或多个子目录, 目录结构呈树状结构。 假设 根目录的 ID 为0,且根目录没有父目录 ID 用唯一的正整数表示,并统一编号 现给定目录 ID 和其付目…...

CSS选择器大全(上)

基础选择器&#xff1a; id选择器&#xff1a;#id{} 类选择器&#xff1a; .class{} 标签选择器&#xff1a; h1{} 复合选择器&#xff1a; 交集选择器&#xff1a;作用&#xff1a;选中同时符合多个条件的元素 语法&#xff1a;选择器1选择器2选择器3选择器n{} 注意&#xff…...

JavaScript 俄罗斯方块 - setTimeout和rAF

本节内容需要有些基础知识,如进程和线程,队列数据结构 一、setTimeout和setInterval 只要使用过JavaScript的朋友,对setTimeout和setInterval应该不会默生,如果光说怎样去使用这个API,并不难,无非就是隔多少毫秒再执行某个函数,把变化的内容封装在函数中,就可以制作出动…...

LeetCode:构造最大二叉树;使用中序和后序数组构造二叉树;使用前序和中序数组遍历二叉树。

构造二叉树最好都是使用前序遍历&#xff1b;中左右的顺序。 654. 最大二叉树 中等 636 给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建: 创建一个根节点&#xff0c;其值为 nums 中的最大值。递归地在最大值 左边 的 子数组前缀上 构建…...

nodejs实现jwt

jwt是json web token的简称&#xff0c;本文介绍它的原理&#xff0c;最后后端用nodejs自己实现如何为客户端生成令牌token和校验token 1.为什么需要会话管理 我们用nodejs为前端或者其他服务提供resful接口时&#xff0c;http协议他是一个无状态的协议&#xff0c;有时候我们…...

结构体占用内存大小如何确定?-->结构体字节对齐 | C语言

目录 一、什么是结构体 二、为什么需要结构体 三、结构体的字节对齐 3.1、示例1 3.2、示例2 3.3、示例3 3.4、示例4 3.5、示例5 四、结构体字节对齐总结 一、什么是结构体 结构体是将不同类型的数据按照一定的功能需 求进行整体封装&#xff0c;封装的数据类型与大小均…...

Vue和Uniapp:优缺点比较

Vue和Uniapp是两个流行的前端框架&#xff0c;都是用于开发跨平台应用程序的工具。虽然两者都有很多相似之处&#xff0c;但它们也有一些不同之处&#xff0c;这些不同之处可以影响你的选择。下面将对Vue和Uniapp的优缺点进行比较和分析&#xff0c;以帮助你做出更明智的决策。…...

AMBA-AXI(二)AXI的序,保序与乱序

&#x1f4a1;Note&#xff1a;本文是根据AXI协议IHI0022F_b_amba_axi_protocol_spec.pdf&#xff08;issue F&#xff09;整理的。主要是分享AXI3.0和4.0部分。如果内容有问题请大家在评论区中指出&#xff0c;有补充或者疑问也可以发在评论区&#xff0c;互相学习&#x1f64…...

APIs and Open Interface--非工单领、发料(含调拨)

表名 MTL_TRANSACTIONS_INTERFACEMTL_TRANSACTION_LOTS_INTERFACE序列 MTL_MATERIAL_TRANSACTIONS_S.NEXTVALAPIs INV_TXN_MANAGER_PUB.PROCESS_TRANSACTIONS案例 杂发/杂收&#xff08;代码&#xff09;Declare v_user_id number : fnd_global.user_id; v_login_id number …...

互联网医院系统软件开发|互联网医院管理系统开发的好处

互联网医院一直是现在的热门行业&#xff0c;很多的医院已经开发了互联网医院&#xff0c;并且已经在良好的运行中&#xff0c;而有一些医院和企业正在开发中&#xff0c;或者打算开发互联网医院系统&#xff0c;其实这些企业和医院还是很有远见的&#xff0c;因为他们知道并了…...

2.单例模式

基本概念 单例模式&#xff1a;保证一个类只有一个实例&#xff0c;并提供一个访问该实例的全局访问点 常见应用场景 读取配置文件的类一般设计为单例模式网站计数器应用程序的日志应用&#xff0c;因为共享日志文件一直处于打开状态&#xff0c;只能有一个实例去操作Spring…...

【保姆级】Java后端查询数据库结果导出xlsx文件+打印xlsx表格

目录前言一、需求一&#xff1a;数据库查询的数据导出成Excel表格1.1 Vue前端实现导出按钮点击事件1.2 后端根据数据库查询结果生成xlsx文件二、需求二&#xff1a;对生成的xlsx文件调用打印机打印2.1 Vue前端实现按钮事件2.2 后端实现打印前言 最近在弄一个需求&#xff0c;需…...

Java数据库部分(MySQL+JDBC)(二、JDBC超详细学习笔记)

文章目录1 JDBC&#xff08;Java Database Connectivity&#xff09;1.1 什么是 JDBC&#xff1f;1.2 JDBC 核心思想2 JDBC开发步骤【重点】2.0 环境准备2.1 注册数据库驱动2.2 获取数据库的连接2.3 获取数据库操作对象Statement2.4 通过Statement对象执行SQL语句2.5 处理返回结…...

vue3生命周期

一、Vue3中的生命周期 1、setup() : 开始创建组件之前&#xff0c;在 beforeCreate 和 created 之前执行&#xff0c;创建的是 data 和 method 2、onBeforeMount() : 组件挂载到节点上之前执行的函数&#xff1b; 3、onMounted() : 组件挂载完成后执行的函数&#xff1b; 4、…...

Python学习笔记10:开箱即用

开箱即用 模块 python系统路径 import sys, pprint pprint.pprint(sys.path) [,D:\\Program Files\\Python\\Lib\\idlelib,D:\\Program Files\\Python\\python310.zip,D:\\Program Files\\Python\\DLLs,D:\\Program Files\\Python\\lib,D:\\Program Files\\Python,D:\\Progr…...

详解JAVA反射

目录 1.概述 2.获取Class对象 3.API 3.1.实例化对象 3.2.方法 3.3.属性 1.概述 反射&#xff0c;JAVA提供的一种在运行时获取类的信息并动态操作类的能力。JAVA反射允许我们在运行时获取类的属性、方法、构造函数等信息&#xff0c;并能够动态地操作它们。 2.获取Class…...

在nestjs中进行typeorm cli迁移(migration)的配置

在nestjs中进行typeorm cli迁移(migration)的配置 在学习nestjs过程中发现typeorm的迁移配置十分麻烦,似乎许多方法都是旧版本的配置&#xff0c;无法直接使用. 花了挺长时间总算解决了这个配置问题. db.config.ts 先创建db.config.ts, 该文件export了两个对象&#xff0c;其…...

前端工程构建问题汇总

1.less less-loader安装失败问题 npm install less-loader --save --legacy-peer-deps 加上–legacy-peer-deps就可以了 在NPM v7中&#xff0c;现在默认安装peerDependencies&#xff0c;这会导致版本冲突&#xff0c;从而中断安装过程。 –legacy-peer-deps标志是在v7中引…...

某马程序员NodeJS速学笔记

文章目录前言一、什么是Node.js?二、fs文件系统模块三、Http模块四、模块化五、开发属于自己的包模块加载机制六、Express1.初识ExpressGET/POSTnodemon2.路由模块化3.中间件中间件分类自定义中间件4. 跨域问题七、Mysql模块安装与配置基本使用Web开发模式Session认证JWT八、m…...

css实现圆环展示百分比,根据值动态展示所占比例

代码如下 <view class""><view class"circle-chart"><view v-if"!!num" class"pie-item" :style"{background: conic-gradient(var(--one-color) 0%,#E9E6F1 ${num}%),}"></view><view v-else …...

Opencv中的addweighted函数

一.addweighted函数作用 addweighted&#xff08;&#xff09;是OpenCV库中用于图像处理的函数&#xff0c;主要功能是将两个输入图像&#xff08;尺寸和类型相同&#xff09;按照指定的权重进行加权叠加&#xff08;图像融合&#xff09;&#xff0c;并添加一个标量值&#x…...

1688商品列表API与其他数据源的对接思路

将1688商品列表API与其他数据源对接时&#xff0c;需结合业务场景设计数据流转链路&#xff0c;重点关注数据格式兼容性、接口调用频率控制及数据一致性维护。以下是具体对接思路及关键技术点&#xff1a; 一、核心对接场景与目标 商品数据同步 场景&#xff1a;将1688商品信息…...

linux arm系统烧录

1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 &#xff08;忘了有没有这步了 估计有&#xff09; 刷机程序 和 镜像 就不提供了。要刷的时…...

cf2117E

原题链接&#xff1a;https://codeforces.com/contest/2117/problem/E 题目背景&#xff1a; 给定两个数组a,b&#xff0c;可以执行多次以下操作&#xff1a;选择 i (1 < i < n - 1)&#xff0c;并设置 或&#xff0c;也可以在执行上述操作前执行一次删除任意 和 。求…...

【JavaSE】绘图与事件入门学习笔记

-Java绘图坐标体系 坐标体系-介绍 坐标原点位于左上角&#xff0c;以像素为单位。 在Java坐标系中,第一个是x坐标,表示当前位置为水平方向&#xff0c;距离坐标原点x个像素;第二个是y坐标&#xff0c;表示当前位置为垂直方向&#xff0c;距离坐标原点y个像素。 坐标体系-像素 …...

如何在网页里填写 PDF 表格?

有时候&#xff0c;你可能希望用户能在你的网站上填写 PDF 表单。然而&#xff0c;这件事并不简单&#xff0c;因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件&#xff0c;但原生并不支持编辑或填写它们。更糟的是&#xff0c;如果你想收集表单数据&#xff…...

使用Spring AI和MCP协议构建图片搜索服务

目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式&#xff08;本地调用&#xff09; SSE模式&#xff08;远程调用&#xff09; 4. 注册工具提…...

宇树科技,改名了!

提到国内具身智能和机器人领域的代表企业&#xff0c;那宇树科技&#xff08;Unitree&#xff09;必须名列其榜。 最近&#xff0c;宇树科技的一项新变动消息在业界引发了不少关注和讨论&#xff0c;即&#xff1a; 宇树向其合作伙伴发布了一封公司名称变更函称&#xff0c;因…...

C# 表达式和运算符(求值顺序)

求值顺序 表达式可以由许多嵌套的子表达式构成。子表达式的求值顺序可以使表达式的最终值发生 变化。 例如&#xff0c;已知表达式3*52&#xff0c;依照子表达式的求值顺序&#xff0c;有两种可能的结果&#xff0c;如图9-3所示。 如果乘法先执行&#xff0c;结果是17。如果5…...