C++进阶:哈希(1)

目录

  • 1. 简介unordered_set与unordered_map
  • 2. 哈希表(散列)
    • 2.1 哈希表的引入
    • 2.2 闭散列的除留余数法
      • 2.2.1 前置知识补充与描述
      • 2.2.2 闭散列哈希表实现
    • 2.3 开散列的哈希桶
      • 2.3.1 结构描述
      • 2.3.2 开散列哈希桶实现
      • 2.3.3 哈希桶的迭代器与key值处理仿函数
  • 3. unordered_set与unordered_map的封装
    • 3.1 哈希桶的细节调整
    • 3.2 具体封装

1. 简介unordered_set与unordered_map

  1. 在C++库中,除开map与set这两个关联式容器外,还存在着另外两个此类容器,unordered_set,unordered_map。
  2. unordered中文释义为无序的,这也正是这一对容器使用时的表征特点,这一对容器分别对应set与map,即K模型与KV模型的存储数据结点。
  3. 那么,除开使用迭代器遍历时,其内存储数据无序外,这一对容器与map与set容器有何不同,为什么要在已有map与set的情况下,再向库中加入这一对乍看功能冗余且劣于原本map与set的容器呢?我们来看下面的一组对照试验。
  4. unordered_set与unordered_map其的关键性常用接口与使用和map,set相同,不同的是其只支持正向迭代器且多了一些桶,负载因子相关的接口。
#include <iostream>
using namespace std;
#include <unordered_set>
#include <set>
#include <stdlib.h>
#include <time.h>
#include <vector>int main()
{const int N = 1000000;vector<int> data(N);set<int> s;unordered_set<int> us;srand((unsigned int)time(NULL));for (int i = 0; i < N; i++){//data.push_back(rand());//重复数据多//data.push_back(rand() + i);//重复数据少data.push_back(i);//有序数据}//插入int begin1 = clock();for (auto e : data){s.insert(e);}int end1 = clock();int begin2 = clock();for (auto e : data){us.insert(e);}int end2 = clock();cout << "insert number:" << s.size() << endl << endl;//查找int begin3 = clock();for (auto e : data){s.find(e);}int end3 = clock();int begin4 = clock();for (auto e : data){us.find(e);}int end4 = clock();int begin5 = clock();for (auto e : data){s.erase(e);}int end5 = clock();int begin6 = clock();for (auto e : data){us.erase(e);}int end6 = clock();cout << "set Insert:" << end1 - begin1 << endl;cout << "unordered Insert:" << end2 - begin2 << endl << endl;cout << "set Find:" << end3 - begin3 << endl;cout << "unordered Find:" << end4 - begin4 << endl << endl;cout << "set Erase:" << end5 - begin5 << endl;cout << "unordered Erase:" << end6 - begin6 << endl << endl;return 0;
}

运行结果:
在这里插入图片描述

  1. 由运行结果可得
    <1> 当数据无序时在重复数据较多的情况下,unordered系列的容器,插入删除效率更高
    <2> 当数据无序但重复数据较少的情况下,两种类型的容器,两者插入数据效率仿佛
    <3> 当数据有序时,红黑树系列的容器插入效率更高
  2. 在日常的应用场景中,很少出现有序数据情况,虽然map,set有着遍历自得有序这一优势,但关联式容器的主要功能为映射关系与快速查询,其他优点尽是附带优势。所以,unordered系列容器的加入与学习是必要的。

2. 哈希表(散列)

2.1 哈希表的引入

  1. 在初阶数据结构的学习中,我们学习了一种排序方式,叫做基数排序,其使用数组下标表示需排序数据,下标对应的元素代表相应数据的出现次数。以此映射数据并排序,时间复杂度只有O(n)。
  2. 基数排序创建的数据存储数组,除可以用于数据排序外,我们不难发现其用来查询一个数据在或不再,可以通过访问下标对应数据直接得到,查询效率及其高。
  3. 这一为排序所创建的存储数组,就是一个简单的哈希表,我们也称之为散列,即数据并非连续而是散列在一段连续的空间中。
  4. 哈希表中的哈希,是指一种数据结构的设计思想,为通过某种映射关系为存储数据创建一个key值,其的映射关系不一,但都可以通过key值找到唯一对应的一个数据,且使得数据散列在存储空间中。
  5. 上述的存储结构为常见哈希表结构的一种,我们称之为直接定址法哈希表,但此种哈希表其使用上存在诸多限制,当存储数据的范围跨度较大时,就会使得空间浪费十分严重。接下来,我们来学习几种在其基础上进行优化具有实用价值的常用哈希表结构。

2.2 闭散列的除留余数法

2.2.1 前置知识补充与描述

  1. 除留余数法:为了解决存储数据大小范围跨度过大的问题,我们不再直接使用存储数据左key值映射,而是通过存储数据除以哈希表大小得到的余数做key值。这样就能将所有数据集中映射至一块指定大小的空间中。
  2. 哈希冲突/哈希碰撞:取余数做key值的方式虽然能够使得数据映射到一块指定空间中,并大幅度减少空间的浪费,可是这也会产生一个无法避免的问题,那就是不同数据的经过取余得到的余数可能相同,如此就会导致同一key值映射多个数据,使得无法进行需要的存储与查询。这一问题就被称为哈希碰撞。
  3. 线性探测与二次探测:哈希碰撞的解决存在多种方法策略,这里我们简单了解两种常用方式
    <1> 线性探测:当前key值对应数据位被占用时,向后遍历(hashi + i),直至找到为空的数据位再将数据存入,而探测查询时,也以线性逻辑搜索直至遍历至空,则代表查询数据不存在,越界回绕
    <2> 二次探测:当前key指对数据位被占用时,当前key值依次加正整数的平方(hashi + i 2 i^2 i2)直至遍历至空存入,探测查询时,依次逻辑或至空结束,越界回绕。
  4. 补充:
    <1> 负载因子:哈希表中存储数据个数与哈希表长度的比值,一般控制在0.7左右,若负载因子超过,进行扩容
    <2> 线性探测容易导致数据的拥堵与踩踏,二次探测的方式为对此的优化
    <3> 处理非数字类型数据,将其转换为size_t类型后,再进行key值映射,采用仿函数的方式

在这里插入图片描述

2.2.2 闭散列哈希表实现

  1. 哈希表结构
//结点状态
enum State
{EMPTY,//可插入,查询结束EXIST,//不可插入,向后查询DELETE//可插入,向后查询
};//数据结点
template<class K, class V>
struct HashNode
{pair<K, V> _kv;State _state;HashNode(const pair<K, V>& kv = make_pair(K(), V())):_kv(kv),_state(EMPTY){}
};//哈希表
template<class K, class V, class hash = HashFunc<K>>
class HashTable
{typedef HashNode<K, V> HashNode;typedef Hash_iterator<K, V, hash> iterator;
public:HashTable(size_t n = 10){_table.resize(n);}//迭代器相关iterator begin();iterator end();	//查找HashNode* Find(const K key);//插入bool Insert(const pair<K, V>& kv);//删除bool Erase(const K key);private:vector<HashNode> _table;//结点size_t n = 0;//存储数据个数hash hs;//仿函数,处理key值
};
  1. 操作实现
//查找
HashNode* Find(const K key)
{int hashi = hs(key) % _table.size();while (_table[hashi]._state != EMPTY){if (_table[hashi]._state == EXIST && hs(_table[hashi]._kv.first) == hs(key)){return &_table[hashi];}hashi++;hashi %= _table.size();}return nullptr;
}//插入
bool Insert(const pair<K, V>& kv)
{//扩容,类型转换//重新建立映射关系,插入(现代写法)if ((double)n / (double)_table.size() >= 0.7){HashTable<K, V, hash> newtable(_table.size() * 2);//迭代器for (auto& e : _table){newtable.Insert(e._kv);}_table.swap(newtable._table);}//找到要插入的位置int hashi = hs(kv.first) % _table.size();//线性探测while (_table[hashi]._state == EXIST){if (_table[hashi]._kv.first == kv.first){return false;}//可能越界,需要回绕hashi++;hashi %= _table.size();}//插入,单参数的构造函数支持隐式类型转换_table[hashi] = kv;_table[hashi]._state = EXIST;n++;return true;
}//删除
bool Erase(const K key)
{//将要删除结点的状态改为deleteint hashi = key % _table.size();//复用findHashNode* ret = Find(key);if (ret){ret->_state = DELETE;n--;return true;}else{return false;}
}
  1. 迭代器相关接口
iterator begin()
{for (size_t i = 0; i < _table.size(); i++){HashNode* cur = _table[i];if (cur){return iterator(cur, this);}}return end();//补
}iterator end()
{return iterator(nullptr, this);
}
  1. key指出,类型转换仿函数
//仿函数
template<class K>//数字类型
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};//string类型全特化,常用
template<>
struct HashFunc<string>
{size_t operator()(string str){size_t key = 0;for (auto e : str){key += e;key *= 131;}return key;}
};//其他类型,自定义类型
struct Date
{int _year;int _month;int _day;
};struct HashDate
{size_t operator()(const Date& d){size_t key = 0;key += d._day;key *= 131;//科学建议值key += d._month;key *= 131;key += d._year;key *= 131;return key;}
};struct Person
{int name;int id;//有唯一项用唯一项,无唯一项,乘权值拼接int add;int tel;int sex;
};

2.3 开散列的哈希桶

2.3.1 结构描述

在这里插入图片描述

  1. 除留余数法后线性探测的存储方式会导致数据的拥堵踩踏,随着数据存储数量的增加,踩踏与拥挤的现象会越来越频繁,二次探测的优化也仅仅只是饮鸩止渴。
  2. 由此,我们来学习一种新的哈希结构也是使用最广泛的一种,其存储方式为:
    顺序表中存储链表指针,相同key值的数据构造成链表结构的数据结点,挂入同一链表中,此种数据结构就被称为哈希桶

2.3.2 开散列哈希桶实现

  1. 哈希桶结构
//数据结点
template <class K, class V>
struct HashNode
{pair<K, V> _kv;HashNode<K, V>* _next;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, hash> HashNode;
public://构造HashTable(size_t n = 10){_table.resize(n, nullptr);_n = 0;}//析构~HashTable(){for (size_t i = 0; i < _table.size(); i++){HashNode* cur = _table[i];while (cur){HashNode* next = cur->_next;delete cur;cur = next;}_table[i] = nullptr;}}private:vector<HashNode*> _table;size_t _n;//插入数据个数,计算负载因子与扩容hash hs;//非数字类型,key值处理friend struct iterator;//迭代器会需要访问存储数据的vector
};
  1. 操作实现
//插入
bool Insert(const pair<K, V>& kv)
{//不支持键值冗余if (Find(kv.first))return false;//何时扩容,负载因子到1就扩容,插入数据个数if (_n == _table.size()){vector<HashNode*> newtable(2 * _table.size());//不再重新申请创建结点,而是搬运for (size_t i = 0; i < _table.size(); i++){HashNode* cur = _table[i];while (cur){HashNode* next = cur->_next;size_t hashi = hs(cur->_kv.first) % newtable.size();cur->_next = newtable[hashi];newtable[hashi] = cur;cur = next;}_table[i] = nullptr;}_table.swap(newtable);}//计算插入位置size_t hashi = hs(kv.first) % _table.size();//头插HashNode* newnode = new HashNode(kv);newnode->_next = _table[hashi];_table[hashi] = newnode;_n++;return true;
}//查找
HashNode* Find(const K& key)
{for (size_t i = 0; i < _table.size(); i++){HashNode* cur = _table[i];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}}return nullptr;
}//删除
bool Erase(const K& key)
{size_t hashi = hs(key) % _table.size();HashNode* cur = _table[hashi];HashNode* prev = nullptr;while (cur){if (cur->_kv.first == key){if (prev){prev->_next = cur->_next;}else{_table[hashi] = cur->_next;}delete cur;_n--;return true;}prev = cur;cur = cur->_next;}return false;
}

2.3.3 哈希桶的迭代器与key值处理仿函数

  1. 迭代器结构
//将迭代器代码写于哈希桶之前
//迭代器与哈希桶存在互相引用,添加前置声明
template<class K, class V, class hash>
class HashTable;template<class K, class V, class hash = HashFunc<K>>
struct Hash_iterator
{typedef HashNode<K, V> HN;typedef HashTable<K, V, hash> HT;typedef Hash_iterator Self;HN* _node;HT* _ht;hash hs;Hash_iterator(HN* node, HT* ht):_node(node),_ht(ht){}//前置++Self& operator++();V& operator*();pair<K, V>* operator->();bool operator!=(const Self& it);};
  1. 操作实现
//前置++
Self& operator++()
{if (_node->_next){_node = _node->_next;}else{size_t hashi = hs(_node->_kv.first) % _ht->_table.size() + 1;_node = nullptr;//注while (hashi < _ht->_table.size()){if (_ht->_table[hashi]){_node = _ht->_table[hashi];break;}hashi++;}//后续没有元素,越界//_node = _ht->_table[hashi];}return *this;
}V& operator*()
{return _node->_kv.second;
}pair<K, V>* operator->()
{return &_node->_kv;
}bool operator!=(const Self& it)
{return _node != it._node;
}
  1. 缺省的key值处理仿函数
template<class K>
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};//特化,常用string类型
template<>
struct HashFunc<string>
{size_t operator()(const string& str){size_t key = 0;for (auto e : str){key += e;key *= 131;}return key;}
};

3. unordered_set与unordered_map的封装

3.1 哈希桶的细节调整

  1. 使用哈希桶unordered_set与unordered_map的封装方式与红黑树封装map,set相似,调整模板参数,使得仅用一套模板即可封装出u_map与u_set,具体操作如下:
  1. 数据结点结构的调整
template <class T>//改
struct HashNode
{T _kv;HashNode<T>* _next;HashNode(const T& kv):_kv(kv),_next(nullptr){}
};
  1. 迭代器结构的调整
template<class K, class T, class KeyOfT, class hash = HashFunc<K>>
struct Hash_iterator
{typedef HashNode<T> HN;typedef HashTable<K, T, KeyOfT, hash> HT;typedef Hash_iterator Self;HN* _node;HT* _ht;hash hs;KeyOfT kot;//其后细节随之调整	
};
  1. 哈希桶结构的调整
//将hash模板参数的缺省值提维
template <class K, class T, class KeyOfT, class hash>
class HashTable
{typedef HashNode<T> HashNode;typedef Hash_iterator<K, T, KeyOfT, hash> iterator;
private:vector<HashNode*> _table;size_t _n;hash hs;KeyOfT kot;friend struct iterator;	public://其他内部方法细节随之调整//为适配上层unordered_map的operator[],返回值与实现细节做调整pair<iterator, bool> Insert(const T& kv);iterator Find(const K& key);
};

3.2 具体封装

  1. unordered_set
//缺省提维的原因
//并非直接使用哈希桶,而是通过us,um来间接使用**
template<class K, class hash = HashFunc<K>>
class myordered_set
{struct KeyOfT{K operator()(const K& key){return key;}};typedef Hash_iterator<K, K, KeyOfT, hash> iterator;typedef HashNode<K> HashNode;
public:iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}pair<iterator, bool> Insert(const K& key){return _ht.Insert(key);}iterator Find(const K& key){return _ht.Find(key);}bool Erase(const K& key){return _ht.Erase(key);}
private:HashTable<K, K, KeyOfT, hash> _ht;
};
  1. unordered_map
template<class K, class V, class hash = HashFunc<K>>
class myordered_map
{struct KeyOfT{K operator()(const pair<K, V>& kv){return kv.first;}};typedef Hash_iterator<K, pair<K, V>, KeyOfT, hash> iterator;typedef HashNode<pair<K, V>> HashNode;
public:iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}pair<iterator, bool> Insert(const pair<K, V>& kv){return _ht.Insert(kv);}iterator Find(const K& key){return _ht.Find(key);}V& operator[](const K& key){pair<iterator, bool> ret = Insert(make_pair(key, V()));return (ret.first)->second;}bool Erase(const K& key){return _ht.Erase(key);}private:HashTable<K, pair<K, V>, KeyOfT, hash> _ht;
};

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

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

相关文章

7B2 PRO主题5.4.2 免授权开心版源码 | WordPress主题

简介&#xff1a; B2 PRO 5.4.2 最新免授权版不再需要改hosts&#xff0c;和正版一样上传安装就可以激活。 直接在WordPress上传安装即可 点击下载

信息化总体架构方法_1.信息化的一般概念

通常&#xff0c;信息化包含了七个主要平台&#xff1a;知识管理平台、日常办公平台、信息集成平台、信息发布平台、协同工作平台、公文流转平台和企业通信平台。 1.信息化的一般概念 1&#xff09;信息化 “信息化是指培育、发展以智能化工具为代表的新的生产力并使之造福于…

RSA非对称加密解密,前端公钥加密后端私钥解密

RSA非对称加密解密&#xff0c;前端公钥加密后端私钥解密&#xff0c;可以防止陌生人直接通过后端接口篡改数据。有数据泄露的风险。 前端&#xff1a;Vue框架 后端&#xff1a;sprintboot&#xff08;Java&#xff09; 工具类&#xff1a;hutool 前端Vue获取公钥&#xff1a…

巴奴火锅翻车,杜中兵后悔暗讽海底捞

曾经喊出“服务不过度&#xff0c;样样都讲究”、内涵海底捞的巴奴火锅&#xff0c;又改回了2012年的广告语&#xff0c;试图重回“产品主义”。 巴奴火锅于2001年创立于河南安阳&#xff0c;彼时被视作火锅界的黑马。巴奴火锅创始人的杜中兵&#xff0c;坚信“产品主义”一定…

基于SpringBoot + Vue的学生宿舍课管理系统设计与实现+毕业论文(15000字)+开题报告

系统介绍 本系统包含管理员、宿管员、学生三个角色。 管理员&#xff1a;管理宿管员、管理学生、修改密码、维护个人信息。 宿管员&#xff1a;管理公寓资产、管理缴费信息、管理公共场所清理信息、管理日常事务信息、审核学生床位安排信息。 学生&#xff1a;查看公共场所清理…

【C++】 string类:应用与实践

&#x1f49e;&#x1f49e; 前言 hello hello~ &#xff0c;这里是大耳朵土土垚~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f4a5;个人主页&#x…

ASP.NET Web Api 如何使用 Swagger 管理 API

前言 Swagger 是一个开源的框架&#xff0c;支持 OpenAPI 规范&#xff0c;可以根据 API 规范自动生成美观的、易于浏览的 API 文档页面&#xff0c;包括请求参数、响应示例等信息&#xff0c;并且&#xff0c;Swagger UI 提供了一个交互式的界面&#xff0c;可以帮助我们快速…

Java——多线程

一.多线程 1.什么是多线程 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中&#xff0c;是进程的实际运作单位 简单理解多线程就是应用软件中相互独立&#xff0c;可以同时运行的功能(也可以理解为人体内相互独立&#xff0c;但可以同时运行的器官⌓‿⌓) 我们…

如何用时尚新姿讲好中国品牌故事?

品牌建设在推动高质量发展中扮演了双重角色&#xff0c;既是高质量发展的重要“承载者”&#xff0c;也是强有力的“助推器”。5月10日-11日&#xff0c;中国时尚品牌URBAN REVIVO&#xff08;以下简称UR&#xff09;以中国品牌日为起点&#xff0c;联合天猫超级品牌日&#xf…

Linux提权--SUDO(CVE-2021-3156)Polkit(CVE-2021-4034)

免责声明:本文仅做技术学习与交流... 目录 SUDO(CVE-2021-3156) 影响版本 -判断&#xff1a; -利用&#xff1a; Polkit(CVE-2021-4034&#xff09; ​ -判断&#xff1a; -利用: 添加用户 SUDO(CVE-2021-3156) another: SUDO权限配置不当. 影响版本 由系统的内核和发…

Redis 5.0 Stream数据结构深入分析

Redis 5.0 Stream数据结构深入分析 目录 Redis 5.0 Stream数据结构深入分析 一、Stream数据结构概述 二、核心概念解析 三、Stream的特性与用途 四、案例研究&#xff1a;实时消息系统 五、性能优化与最佳实践 六、总结与展望 一、Stream数据结构概述 Redis Stream是Red…

FastAPI - Tortoise ORM 数据库基础操作

文章目录 1. 安装 Tortoise ORM2. 定义模型3. 初始化数据库连接4. 数据库操作4.1 创建数据4.2 查询数据4.3 更新数据4.4 删除数据 5. 使用 Pydantic 模型6. 关闭数据库连接7. fields类相关操作1. fields.IntField2. fields.BigIntField3. fields.SmallIntField4. fields.CharFi…

【LAMMPS学习】八、基础知识(6.3)使用 LAMMPS GUI

8. 基础知识 此部分描述了如何使用 LAMMPS 为用户和开发人员执行各种任务。术语表页面还列出了 MD 术语,以及相应 LAMMPS 手册页的链接。 LAMMPS 源代码分发的 examples 目录中包含的示例输入脚本以及示例脚本页面上突出显示的示例输入脚本还展示了如何设置和运行各种模拟。 …

西门子博途WINCC动画之旋转运动

概述 本例将介绍在西门子 TIA Portal HMI 中旋转运动动画的一种实现方法。本例以风机、搅拌器和传送带为例&#xff0c;按下启动按钮开始转动&#xff0c;按下停止按钮停止转动。 第1步&#xff1a; 添加 PLC 设备。​博途TIA/WINCC社区VX群 ​博途TIA/WINCC社区VX群 选择西…

PyQt5中的QGraphicsView()

文章目录 1. 简介2. 一个简单的示例2. 加载一幅图片3. 常用方法示例 1. 简介 QGraphicsView是PyQt5中用于显示图形场景的小部件&#xff0c;它提供了许多常用的方法来控制视图的行为和属性。下面是一些常用的QGraphicsView方法&#xff1a; setScene(scene): 设置要显示的场景…

从零开始写 Docker(十四)---重构:实现容器间 rootfs 隔离

本文为从零开始写 Docker 系列第十四篇&#xff0c;实现容器间的 rootfs 隔离&#xff0c;使得多个容器间互不影响。 完整代码见&#xff1a;https://github.com/lixd/mydocker 欢迎 Star 推荐阅读以下文章对 docker 基本实现有一个大致认识&#xff1a; 核心原理&#xff1a;…

SpringCloud 集成 RocketMQ 及配置解析

文章目录 前言一、SpringCloud 集成 RocketMQ1. pom 依赖2. yml 配置3. 操作实体4. 生产消息4.1. 自动发送消息4.2. 手动发送消息 5. 消费消息 二、配置解析1. spring.cloud.stream.function.definition 前言 定义 Spring Cloud Stream 是一个用来为微服务应用构建消息驱动能力…

spacy微调BERT-NER模型

数据准备 加载数据集 from tqdm.notebook import tqdm import osdataset [] with open(train_file, r) as file:for line in tqdm(file.readlines()):data json.loads(line.strip())dataset.append(data)你可以按照 CLUENER 的格式准备训练数据&#xff0c; 例如&#xff1…

(done) Beam search

参考视频1&#xff1a;https://www.bilibili.com/video/BV1Gs421N7S1/?spm_id_from333.337.search-card.all.click&vd_source7a1a0bc74158c6993c7355c5490fc600 &#xff08;beam search 视频&#xff09; 参考博客1&#xff1a;https://jasonhhao.github.io/2020/06/19/…

在word中创建宏来多级列表的编号不显示的bug

出现问题的示意图如下&#xff0c;可以看出标题前面1.1消失了 第一步&#xff1a;选择开发工具 第二步&#xff1a; 第三步&#xff1a;选择当前文件&#xff08;创建宏后&#xff0c;方便查找&#xff09; 第四步&#xff1a; 第五步&#xff1a;打卡VB 第七步&#xf…