Hash表(C++)

        本篇将会开始介绍有关于 unordered_map 和 unordered_set 的底层原理,其中底层实现其实就是我们的 Hash 表,本篇将会讲解两种 Hash 表,其中一种为开放定址法,另一种为 hash 桶,在unordered_map 和 unordered_set 的底层实现中主要是使用的是 hash 桶。

        本篇分别介绍了 hash 表的两种实现方法,接着讲解 hash 冲突、扩容与 hash 函数的设计原则。最后使用代码实现 hash 表的数据结构。

目录

hash概念

1. 开放定址法

2. hash 桶

hash冲突与hash函数

1. hash冲突

2. hash 函数

3. hash 表的扩容

代码实现

1. 开放定址法

2. hash 桶

hash概念

        我们在顺序结构或者平衡树中,想要查找一个数必须经过关键码的多次比较,顺序查找的时间复杂度为O(n),平衡树的查找时间复杂度为O(logn)。但是是否存在一种搜索方法,我们可以不经过任何比较,直接从表中找到我们想要的元素

        我们构建的 hash 表就可以达到这样的效果:通过某种函数(hashfunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么我们在查找时通过该函数可以很快的找到该元素

        构造出相应的结构:

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

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

        本篇一共构造两种结构:开放定址法、hash 桶。如下:

1. 开放定址法

        关于开放定址法,其中的主要思想为:构建一个存放数据的 hash 表,然后通过 hashfunc 计算出元素对应的关键码,然后将元素对应的放置在相应的 hash 表中的位置,如下:

        假设我们存放元素集合:{1,7,6,4,5,9}。

        当我们想要进行搜索元素的时候,我们就可以直接使用对应的 hash 函数搜索对应的元素。

2. hash 桶

        关于 hash 桶,其主要思想为:我们首先先建立一个 hash 表,在 hash 表中的每一个位置,我们都直接将使用哈希函数计算出元素对应的关键码存放到对应位置,如下:

        假设我们存放的元素集合为:{1,7,6,4,5,9,44}。

        当其中同一个位置存在多个元素的时候,我们只需要将多的元素链接到最后元素的后面即可。

hash冲突与hash函数

1. hash冲突

        在实现 hash 表,计算元素对应的关键码的时候,难免会遇到关键码相同的情况,也就是同一个位置将会存放两个元素,也就是 hash 冲突。对于开放定址法而言就会存在 hash 冲突,而 hash 桶则不用担心会出现 hash 冲突

        关于 hash 冲突的解决方法:

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

        插入:首先通过哈希函数获取插入元素在哈希表中的位置,若该位置没有元素则直接插入新的元素,若该位置中有元素发生哈希冲突,我们则使用线性探测找到下一个空位置,然后在插入元素,如下:

        删除:关于使用线性探测法删除元素,我们不能直接删除元素,直接删除元素将会影响其他元素的搜索。比如删除元素4,如果直接将其删掉,44的查找将会受到影响,所以线性探测采用标记的伪删除法来删除一个元素

enum State {EMPTY, EXIST, DELETE};
// EMPTY此位置为空
// EXIST此位置存在元素
// DELETE此位置元素已经删除

        二次探测:线性探测的缺陷是产生冲突的数据会堆积在一块,这与查找下一个空位置有关系,因为找空位置的方法就是挨着往后逐个去找,因此二次探索为了避免该问题,找到下一个空位置的方法为:Hi = (H0 + i ^ 2) % m 或者 Hi = (H0 - i ^ 2) % m,其中 H0 为通过散列函数计算出的关键码对应的位置,m 是表的大小。

2. hash 函数

        关于引起 hash 冲突的一个原因为:hash 函数的设计不合理。

        关于 hash 函数的设计原则有

        1. hash 函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址的时候,其值域必须在 0 ~ m - 1之间。

        2. hash 函数计算出来的地址能均匀分布在整个空间中。

        3. hash 函数的设计应该比较简单。

        常见的 hash 函数:直接定址法除留余数法、平方取中法、折叠法、随机数法……

        直接定址法:取关键字的某个线性函数为散列地址:hash(key) = A * key + B。直接定址法的优点为:简单且均匀;缺点为:需要事先知道关键字的分布情况。适合查找比较小且连续的情况。

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

        对于其他的 hash 函数不是很常用,便不在介绍。

3. hash 表的扩容

        虽然在使用 hash 表存储元素的时候,我们可以将元素不断的映射到 hash 表中来,但是我们的 hash 表也会存在存满的情况(使用开放定址法下的 hash 表),那么也就会存在一个 hash 表扩容的问题。

        hash 表的载荷因子定义为:α = 填入表中的元素个数 / hash 表的长度。

        关于 α 的取值,对于开放定址法而言,一般取值为0.7 ~ 0.8 以下。当表中的载荷因子大于 α 的时候,我们就需要对我们的 hash 表进行扩容。

代码实现

1. 开放定址法

        该代码的实现原理为开放定址法,如下:

#pragma once
#include <vector>
#include <string>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 e : key)hash = hash * 131 + e;return hash;}
};// 开放定值法
namespace Open_Address {enum status { EMPTY, DELETE, EXIST };template <class K, class V>struct HashData {pair<K, V> _kv;status _status;HashData() = default;HashData(const pair<K,V>& data): _kv(data){}};template <class K, class V, class Hash = HashFunc<K>>class HashTable {typedef HashData<K, V> Data;public:// 构造函数HashTable() : _n(0){// 先开十个空间_tables.resize(10);}bool insert(const pair<K, V>& data) {if (find(data.first))return false;Hash hf;if (10 * _n / _tables.size() >= 7) {// 扩容HashTable newTable;newTable._tables.resize(2 * _tables.size());for (size_t i = 0; i < _tables.size(); i++) {if (_tables[i]._status == EXIST)newTable.insert(_tables[i]._kv);}_tables.swap(newTable._tables);}size_t cur = hf(data.first);cur %= _tables.size();while (_tables[cur]._status == EXIST) {cur++;cur %= _tables.size();}_n++;_tables[cur] = Data(data);_tables[cur]._status = EXIST;return true;}HashData<K, V>* find(const K& key) {Hash hf;size_t cur = hf(key);cur %= _tables.size();while (_tables[cur]._status != EMPTY) {if (_tables[cur]._status != DELETE && _tables[cur]._kv.first == key)return &_tables[cur];cur++;cur %= _tables.size();}return nullptr;}//bool erase(const K& key) {//	Hash hf;//	size_t cur = hf(key);//	cur %= _tables.size();//	if (find(key) == false)//		return false;//	while (_tables[cur]._kv.first != key) {//		cur++;//		cur %= _tables.size();//	}//	_tables[cur] = Data();//	_tables[cur]._status = DELETE;//	_n--;//	return true;//}bool erase(const K& key) {Data* Fd = find(key);if (Fd) return false;*Fd = Data();Fd->_status = DELETE;_n--;return true;}private:vector<Data> _tables;size_t _n;};
}

        关于此代码的实现,我们首先先实现计算元素关键码的类,其中主要包括两种元素,一种为数字类元素,另一类为字符串元素(计算字符串元素的关键码存在多种方式,本篇实现的只是其中的一种)。我们只需要使用一个类模板就可以实现计算关键码的类,其中字符串元素类的计算为特化的类,如下:

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 e : key)hash = hash * 131 + e;return hash;}
};

        对于 hash 表的建立,我们还需要建立它的数据类型和状态类型(用来表征当前位置的状态),如下:

enum status { EMPTY, DELETE, EXIST 
};template <class K, class V>
struct HashData {pair<K, V> _kv;status _status;HashData() = default;HashData(const pair<K,V>& data): _kv(data){}
};

        接着是插入函数,其中插入函数主要的实现为判断当前是否需要扩容,以及计算出元素的关键码,然后将元素放入到对应的位置。如下:

bool insert(const pair<K, V>& data) {if (find(data.first))return false;Hash hf;// 判断是否需要扩容if (10 * _n / _tables.size() >= 7) {// 扩容HashTable newTable;newTable._tables.resize(2 * _tables.size());for (size_t i = 0; i < _tables.size(); i++) {if (_tables[i]._status == EXIST)newTable.insert(_tables[i]._kv);}_tables.swap(newTable._tables);}// 计算关键码size_t cur = hf(data.first);cur %= _tables.size();// 若当前位置存在元素,则放在下一个位置while (_tables[cur]._status == EXIST) {cur++;cur %= _tables.size();}_n++;_tables[cur] = Data(data);_tables[cur]._status = EXIST;return true;
}

        对于查找函数,我们需要计算出搜索元素的关键码,然后使用在表中不断的查找,若找到的位置为 EMPTY,则查找失败,若找到位置为 EXIST,则说明我们找到相应的元素了,如下:

HashData<K, V>* find(const K& key) {Hash hf;size_t cur = hf(key);cur %= _tables.size();while (_tables[cur]._status != EMPTY) {if (_tables[cur]._status != DELETE && _tables[cur]._kv.first == key)return &_tables[cur];cur++;cur %= _tables.size();}return nullptr;
}

        最后则是我们的删除元素,我们首先需要使用查找函数,查看是否能找到对应的元素,若找不到则直接返回 false(表示删除失败),若找到,直接将对应位置的状态设置为 DELETE,如下:

bool erase(const K& key) {Data* Fd = find(key);if (Fd) return false;*Fd = Data();Fd->_status = DELETE;_n--;return true;
}

2. hash 桶

        接下来的代码为实现 hash 桶的代码,如下:

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 e : key)hash = hash * 131 + e;return hash;}
};// hash桶
namespace Hash_Bucket {template<class K, class V>struct HashNode {pair<K, V> _kv;HashNode* next = nullptr;HashNode() = default;HashNode(const pair<K,V>& kv): _kv(kv), next(nullptr){}};template <class K, class V, class Hash = HashFunc<K>>class HashTable {typedef HashNode<K, V> Node;public:HashTable() {_n = 0;_tables.resize(10, nullptr);}// 拷贝构造函数HashTable(const HashTable<K, V>& ht) {_tables.resize(ht._tables.size());size_t size = ht._tables.size();for (size_t i = 0; i < size; i++) {Node* cur = ht._tables[i];while (cur) {Node* next = cur->next;// 插入元素insert(cur->_kv);cur = next;}}}~HashTable() {for (size_t i = 0; i < _tables.size(); i++) {Node* cur = _tables[i];if (cur != nullptr) {Node* next = cur->next;delete cur;cur = next;}_tables[i] = nullptr;}}bool insert(const pair<K, V>& kv) {if (find(kv.first))return false;// 扩容Hash hf;//if (_n == _tables.size()) {//	// 扩容的时候,只需要转换一整只即可//	HashTable newTable;//	newTable._tables.resize(2 * _tables.size());//	for (size_t i = 0; i < _tables.size(); i++) {//		if (_tables[i] != nullptr) {//			//Node* cur = _tables[i];//			//while (cur) {//			//	newTable.insert(cur->_kv);//			//	cur = cur->next;//			//}//			//			// 将所有节点取下来连接上去//			Node* cur = _tables[i];//			while (cur) {//				Node* next = cur->next;//				// 将节点取下来重新加到新表上//				size_t index = hf(cur->_kv.first) % newTable._tables.size();//				cur->next = newTable._tables[index];//				newTable._tables[index] = cur;//				cur = next;//			}//		}//	}//	_tables.swap(newTable._tables);//}if (_n == _tables.size()) {vector<Node*> newTables;newTables.resize(_tables.size() * 2, nullptr);size_t size = _tables.size();for (int i = 0; i < size; i++) {Node* cur = _tables[i];while (cur) {Node* next = cur->next;// 将节点取下来放在新表上size_t index = hf(cur->_kv.first) % newTables.size();cur->next = newTables[index];newTables[index] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTables);}size_t index = hf(kv.first);index %= _tables.size();Node* newNode = new Node(kv);newNode->next = _tables[index];_tables[index] = newNode;_n++;return true;}Node* find(const K& key) {Hash hf;size_t index = hf(key);index %= _tables.size();Node* cur = _tables[index];if (cur == nullptr)return nullptr;while (cur) {if (cur->_kv.first == key)return cur;cur = cur->next;}return nullptr;}bool erase(const K& key) {if (find(key) == nullptr)return false;Hash hf;size_t index = hf(key);index %= _tables.size();Node* cur = _tables[index];Node* prev = _tables[index];if (cur->_kv.first == key) {_n--;_tables[index] = cur->next;delete prev;return true;}while (cur->_kv.first != key) {if (cur->_kv.first == key) {prev->next = cur->next;delete cur;_n--;return true;}prev = cur;cur = cur->next;}return false;}private:vector<Node*> _tables;size_t _n;};
}

        对于计算关键码的类,我们仍然沿用开放定址法的计算方法。

        但是对于 hash 表中的元素,我们则是采用指针的方法存放对应的元素,并将其初始化为 nullptr,表示当前位置没有元素。所以对于插入元素的设计,我们则将其设计为结点,当插入元素时,只需要连接到对应位置即可。

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

        接下来是插入元素,对于需要插入的元素,我们仍然是需要先判断该 hash 表是否需要扩容(这里的扩容和开放定址法的扩容不一样,元素个数和容量一致的时候我们才扩容)。然后通过计算关键码找到需要插入的位置,如下:

bool insert(const pair<K, V>& kv) {if (find(kv.first))return false;// 扩容Hash hf;// 扩容if (_n == _tables.size()) {vector<Node*> newTables;newTables.resize(_tables.size() * 2, nullptr);size_t size = _tables.size();for (int i = 0; i < size; i++) {Node* cur = _tables[i];while (cur) {Node* next = cur->next;// 将节点取下来放在新表上size_t index = hf(cur->_kv.first) % newTables.size();cur->next = newTables[index];newTables[index] = cur;cur = next;}// 将原 hash 表对应位置设置为null_tables[i] = nullptr;}_tables.swap(newTables);}size_t index = hf(kv.first);index %= _tables.size();Node* newNode = new Node(kv);newNode->next = _tables[index];_tables[index] = newNode;_n++;return true;
}

        接下来是查找函数,此查找函数就较为简单,因为直接就可以通过计算关键码查询是否能找到该元素。如下:

Node* find(const K& key) {Hash hf;size_t index = hf(key);index %= _tables.size();Node* cur = _tables[index];if (cur == nullptr)return nullptr;while (cur) {if (cur->_kv.first == key)return cur;cur = cur->next;}return nullptr;
}

        最后是删除元素函数,仍然是先查找元素是否在 hash 表中,查找到之后在进行删除,其中会分为删除元素的位置在当前 hash 表的最后链接位置和中间位置进行分别讨论,如下:

bool erase(const K& key) {if (find(key) == nullptr)return false;Hash hf;size_t index = hf(key);index %= _tables.size();Node* cur = _tables[index];Node* prev = _tables[index];if (cur->_kv.first == key) {_n--;_tables[index] = cur->next;delete prev;return true;}while (cur->_kv.first != key) {if (cur->_kv.first == key) {prev->next = cur->next;delete cur;_n--;return true;}prev = cur;cur = cur->next;}return false;
}

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

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

相关文章

智驭未来:人工智能与目标检测的深度交融

在科技日新月异的今天&#xff0c;人工智能&#xff08;AI&#xff09;如同一股不可阻挡的浪潮&#xff0c;正以前所未有的速度重塑着我们的世界。在众多AI应用领域中&#xff0c;目标检测以其独特的魅力和广泛的应用前景&#xff0c;成为了连接现实与智能世界的桥梁。本文旨在…

20240715 每日AI必读资讯

&#x1f310; 代号“ 草莓 ”&#xff0c;OpenAI 被曝研发新项目&#xff1a;将 AI 推理能力提至新高度 - OpenAI 公司被曝正在研发代号为“ 草莓 ”的全新项目&#xff0c;进一步延伸去年 11 月宣布的 Q* 项目&#xff0c;不断提高 AI 推理能力&#xff0c;让其更接近人类的…

基于Java的休闲娱乐代理售票系统

你好&#xff0c;我是专注于Java开发的码农小野&#xff01;如果你对系统开发感兴趣&#xff0c;欢迎私信交流。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;Java技术、SpringBoot框架、B/S架构 工具&#xff1a;Eclipse IDE、MySQL数据库管理工具…

牛客小白月赛98 (个人题解)(补全)

前言&#xff1a; 昨天晚上自己一个人打的小白月赛&#xff08;因为准备数学期末已经写烦了&#xff09;&#xff0c;题目难度感觉越来越简单了&#xff08;不在像以前一样根本写不了一点&#xff0c;现在看题解已经能看懂一点了&#xff09;&#xff0c;能感受到自己在不断进步…

2024年是不是闰年?

闰年的由来 闰年的概念最早可以追溯到古罗马时期的朱利叶斯凯撒。当时的罗马历法是根据太阳年来制定的&#xff0c;每年大约有365.25天。为了使日历与季节保持同步&#xff0c;人们需要定期插入一个额外的日子。朱利叶斯凯撒在公元前46年颁布了一项法令&#xff0c;规定每四年增…

SAP PP学习笔记26 - User Status(用户状态)的实例,订单分割中的重要概念 成本收集器,Confirmation(报工)的概述

上面两章讲了生产订单的创建以及生产订单的相关内容。 SAP PP学习笔记24 - 生产订单&#xff08;制造指图&#xff09;的创建_sap 工程外注-CSDN博客 SAP PP学习笔记25 - 生产订单的状态管理(System Status(系统状态)/User Status(用户状态)),物料的可用性检查&#xff0c;生…

最长下降序列

如何理解这个题目呢,我们可以每个人的分数放到排名上&#xff0c;然后求解最长下降序列即可 #include<bits/stdc.h> using namespace std;int n; const int N (int)1e5 5; int a[N]; int b[N]; int d[N]; int dp[N]; int t;int main() {cin >> t;while (t--) {…

Apache Hadoop之历史服务器日志聚集配置

上篇介绍绍了Apache Hadoop的分布式集群环境搭建&#xff0c;并测试了MapReduce分布式计算案例。但集群历史做了哪些任务&#xff0c;任务执行日志等信息还需要配置历史服务器和日志聚集才能更好的查看。 配置历史服务器 在Yarn中运行的任务产生的日志数据不能查看&#xff0…

【计算机毕业设计】013新闻资讯微信小程序

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

Python数据分析案例51——基于K均值的客户聚类分析可视化

案例背景 本次案例带来的是最经典的K均值聚类&#xff0c;对客户进行划分类别的分析&#xff0c;其特点是丰富的可视化过程。这个经典的小案例用来学习或者课程作业在合适不过了。 数据介绍 数据集如下: 客户的编码&#xff0c;性别&#xff0c;年龄&#xff0c;年收入&#…

Vue2-集成路由Vue Router介绍与使用

文章目录 路由&#xff08;Vue2&#xff09;1. SPA 与前端路由2. vue-router基本使用创建路由组件声明路由链接和占位标签创建路由模块挂载路由模块 3. vue-router进阶路由重定向嵌套路由动态路由编程式导航导航守卫 本篇小结 更多相关内容可查看 路由&#xff08;Vue2&#xf…

安全防御----防火墙综合实验2

安全防御----防火墙综合实验2 一、题目 二、实验要求&#xff1a; 1&#xff0c;DMZ区内的服务器&#xff0c;办公区仅能在办公时间内&#xff08;9&#xff1a;00 - 18&#xff1a;00&#xff09;可以访问&#xff0c;生产区的设备全天可以访问. 2&#xff0c;生产区不允许访…

雷赛运动控制卡编程(1)

一、运动控制卡选择 电气常用知识-CSDN博客 如下旋转控制卡 DMC3800八轴高性能点位卡 - 东莞市雅恰达机电有限公司 轴少的时候选择脉冲系列卡 轴多的话就选总线型系列控制卡 样品 架构&#xff1a; 二、 添加文件 dll 添加接口文件 【最全&#xff0c;带注释版】雷赛运动…

OpenCV中使用Canny算法在图像中查找边缘

操作系统&#xff1a;ubuntu22.04OpenCV版本&#xff1a;OpenCV4.9IDE:Visual Studio Code编程语言&#xff1a;C11 算法描述 Canny算法是一种广泛应用于计算机视觉和图像处理领域中的边缘检测算法。它由John F. Canny在1986年提出&#xff0c;旨在寻找给定噪声条件下的最佳边…

Python+wxauto=微信自动化?

Pythonwxauto微信自动化&#xff1f; 一、wxauto库简介 1.什么是wxauto库 wxauto是一个基于UIAutomation的开源Python微信自动化库。它旨在帮助用户通过编写Python脚本&#xff0c;轻松实现对微信客户端的自动化操作&#xff0c;从而提升效率并满足个性化需求。这一工具的出现&…

详细分析Sql Server中的declare基本知识

目录 前言1. 基本知识2. Demo3. 拓展Mysql4. 彩蛋 前言 实战探讨主要来源于触发器的Demo 1. 基本知识 DECLARE 语句用于声明变量 声明的变量可以用于存储临时数据&#xff0c;并在 SQL 查询中多次引用 声明变量&#xff1a;使用 DECLARE 语句声明一个或多个变量变量命名&a…

SpringBoot整合JAX-RS接口

目录 二、创建RESTful资源 三、注册JAX-RS资源 四、修改配置等信息 五、启动SpringBoot程序、访问服务 六、遇到的问题 七、与feign进行配合使用 1、接口定义 2、接口实现 3、注册资源 4、调用方web服务实现&#xff0c;跟注入普通服务一样 5、启动两个服务&#xff…

html5——表单

目录 表单基本结构 表单标签 常用表单元素 文本框 密码框 邮箱 单选按钮 复选框 文件域 隐藏域 列表框 多行文本域 lable标签 表单按钮 常用表单属性 只读与禁用 placeholder required pattern autofocus autocomplete 用于指定表单是否有自动完…

NoSQL之redis的配置与优化

一、redis数据库的基础介绍与对比 Redis(RemoteDictionaryServer&#xff0c;远程字典型)是一个开源的、使用C语言编写的NoSQL数据库。Redis 基于内存运行并支持持久化&#xff0c;采用 key-value(键值对)的存储形式&#xff0c;是目前分布式架构中不可或缺的一环。 1.非关系…

百日筑基第二十天-一头扎进消息队列3-RabbitMQ

百日筑基第二十天-一头扎进消息队列3-RabbitMQ 如上图所示&#xff0c;RabbitMQ 由 Producer、Broker、Consumer 三个大模块组成。生产者将数据发送到 Broker&#xff0c;Broker 接收到数据后&#xff0c;将数据存储到对应的 Queue 里面&#xff0c;消费者从不同的 Queue 消费数…