oi~,让我告诉你如何实现哈希表

oi~,让我告诉你如何实现哈希表

1.哈希概念

        哈希(hash)又称散列,是一种组织数据的方式。从译名来看有散乱排列的意思,其本质是通过哈希函数将关键字key值与存储位置建立映射关系,查找时通过哈希函数计算出key值的位置,实现快速查找。

1.1直接定址法

        当关键字key值比较集中时,直接定址法是最简单也是最有效的方法。比如一组数据集中在[ 0 - 99 ],我们开一组大小为100的数组即可,数据直接作为下标来定位存储位置。再比如一组数据集中在[ a - z],我们开一个大小为26的数组即可,让数据减去字符‘a'的结果来定位存储位置。
        例题:387. 字符串中的第一个唯一字符 - 力扣(LeetCode)

class Solution { public: int firstUniqChar(string s) { int arr[26]={0}; //范围for 遍历 for(auto e: s) { arr[e-'a']++; } for (int i = 0; i < s.size(); i++) { if (arr[s[i] - 'a'] == 1) { return i; } } return -1; } };

 1.2哈希冲突

        直接定址法适用于数据比较集中的情况,当数据过于分散时这意味这我们将会开一个过大的数组来存储,而这会极大的浪费内存空间,不可取。

        所以当面对这种情况的时候,我们会使用哈希函数:h(key),通过哈希函数对关键字key值在存储空间的映射,来定位key值的存储位置。而这里存在一个问题:不同的值通过哈希函数的映射后的位置可能相同,这就会导致哈希冲突(哈希碰撞)。我们可以通过设计一个好的哈希函数来减少哈希冲突,但是在实际情况下,哈希冲突是不可避免的。

1.3负载因子

        假设存储空间的大小为M,已经放入存储空间的数据个数是N,那么负载因子就是:N/M。负载因子也称载荷因子。负载因子越大,哈希冲突概率越大,空间利用率越高。负载因子越小,哈希冲突概率越小,空间利用率越低。

1.4哈希函数

1.4.1除法散列法/除留余数法

        顾名思义就是通过取余,其余值就是映射的存储位置。假设存储空间的大小为M,关键字为key,哈希函数h(key)=key%M。

        使用除法散列法时,对M有一个要求:要求M不能为2的幂、10的幂。假设M为 2^3,这相当于直接保留了2进制中的后3位,那么只要key值的后3位相同那么就一定冲突,例如:3,11,19,27......,它们二进制的后三位都是011,所以它们同时取余M都等于3。所以当M为2的幂时会导致很高的冲突概率,不可取。同理当M为10的幂时,假设为10^3,这相当于保留了10进制中的后3位,那么只要key值的后3位相同就一定冲突,例如:1001,2001,3001.....,所以也不可取。

        综上所述,建议M的取值为不接近2的幂的值(素数)

        但在实际操作中也有存在直接去2的幂,10的幂的情况。比如java中实现的哈希表就是直接取的2的幂。当然他不仅仅的取余这么简单,还有其他操作保证了较低的冲突概率。比如M为2^16,先取余得到2进制的后16位,再将key>>16,将取余的结果和右移的结果做异或运算。这样的操作将每个数都参与的运算,使其分布的更加的均匀。(了解,在后续的代码实现中我们还是以取素数为例)

1.4.2乘法散列法(了解)

        使用乘法散列法对M没用要求,其大体思路为:关键值key乘上一个常量A(0<A<1),然后在对其结果取余,得到小数部分。再将M乘上小数部分,再向下取整,得到最终结果。哈希函数为:h(key)= floor( M*( ( key*A ) %1.0) )  ,floor表示向下取整

        A的取值范围为0~1,结论表明A的取值为:(根号5 - 1)/2 = 0.6180339887....(黄金分割点)比较好

1.5将关键字转化为整型

        在哈希函数里面我们说到了,要通过哈希函数将key值映射到存储位置。不管是那种方法,我们可以看到key值都要参与具体的运算的。但是如果key不为整型时,这么办?

        这里我们就需要将key值转化为整型,如果为负数、浮点数等等就强制类型转化为整型。如果为string类型,可以通过将每个字符的ascll码相加,来得到整型。其他的都类似,通过映射的值来转化为整型。

1.6解决哈希冲突

        哈希表中使用的哈希函数主要还是除法散列法,但对于哈希冲突,不论选择什么哈希函数都不可避免。所以我们需要知道可以解决哈希冲突的方法,主要分为两种:开放地址法、链地址法。

1.6.1开放地址法

        在开放地址法中,所有元素都存于哈希表中,当通过哈希函数映射key的存储位置发生冲突时,按照一定规则去找下一个空白位置进行存储。开放地址法的负载因子一定是小于1的。这里的规则一共有3种:线性探测、二次探测、双重探测(对于开放地址法,不论什么规则都是治标不治本,这里就主要讲讲线性探测,后面我会详细解释为什么说是治标不治本)

线性探测

        发生冲突时,从映射位置开始向后遍历,找到第一个空白位置时就存储,当找到表尾就返回到表头继续向后找。

        下面演示 {19,30,5,36,13,20,21,12} 等这一组值映射到M=11的表中

        h(19) = 8,h(30) = 8,h(5) = 5,h(36) = 3,h(13) = 2,h(20) = 9,h(21) = 10,h(12) = 1

        我们可以看到30与19发生冲突,30存储到了9这个位置。随后20与30也发生了冲突,20存储到了10这个位置。21又与20发生冲突,21返回到表头存储到了0这个位置。

        这就是开发地址法的弊端,发生了冲突就去占别人的位置。当别人来找自己的位置时发现被占用了,又只能去占用其他人的位置。由此不断的恶性循环,这种现象叫做群集/堆积,会极大的影响插入和查找效率。

        而二次探测法和双重探测法,都只是去减小群集效应,并不能真正的解决这个弊端。链地址法没有这个弊端,是相对而言更优的方法,也是我们实际上更常用的方法。

开放地址法的代码实现
哈希表的基本结构
//通过枚举哈希表的节点状态,以便于我们后面更好的管理 enum State { EXIST, EMPTY, DELETE }; template<class K, class V> struct HashData { pair<K, V> _kv; State _state = EMPTY; }; template<class K, class V> class HashTable { private: vector<HashData<K, V>> _tables; size_t _n = 0; // 表中存储数据个数 };

        当我们删除节点时,为了不影响后续数据的查找,这里我们枚举哈希表节点的状态。如图,如果这里我们删除30这个值,后续查找时发现20这个值冲突,根据插入规则,我们会去往后面找,当找到空时查找停止,查找20就会失败。我们不用真的去删除30,只需将状态 EXIST修改为 DELETE,并不影响为 EMPTY时查找停止的条件

 扩容

        这里我们将负载因子控制到0.7左右,当负载因子超过0.7时我们就需要扩容。前面我们提到M的大小建议为素数,但扩容我们一般是扩2倍,扩2倍之后M就不再为素数了,这怎么办呢?这里c++库的解决方法是直接列出素数表

inline unsigned long __stl_next_prime(unsigned long n) { // Note: assumes long is at least 32 bits. 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 }; const unsigned long* first = __stl_prime_list; const unsigned long* last = __stl_prime_list + __stl_num_primes; const unsigned long* pos = lower_bound(first, last, n); return pos == last ? *(last - 1) : *pos; }

        扩容后,我们需要重新遍历原表,重新计算映射位置,重新插入元数据。因为扩容后,M的大小改变了,通过哈希函数计算出的映射位置也改变了 

如果key不能取余的问题

        这里我们可以传一个仿函数来实现将key值转化为整型的功能,对于负数、浮点数这种可以直接转化为整型的类型,我们可以直接将其写成一个仿函数并作为缺省值传给模板,这样我们初始化的时候,就可以不用反复写仿函数了。

        对于string类型,将每个字符的ascll码相加后返回,鉴于string类作为key就是很常用的情况,所以这里我们可以考虑特化。这样也避免就我们反复的传仿函数。

        对于自定义类型,我们就只能自己单独写仿函数,并显示的传仿函数

template<class K> class HashFunc { public: size_t operator()(const K& key) { return (size_t)key; } }; //偏特化(可以单独写成一个仿函数,但是string作为key是常有的情况,为避免反复的传仿函数,写成偏特化) template<> class HashFunc<string> { public: size_t operator()(const string& key) { int ch = 0; for (auto& e : key) { ch += e; ch *= 131; //使用BKDR哈希的思路:每次结果乘131,避免“abcd” “dcba”这种不相同字符,转化为整型却相同的情况 } return ch; } }; template<class K,class V,class Hash = HashFunc<K>> //直接给出缺省值,避免常用的key值,反复传仿函数 class HashTable { public: private: vector<HashData<K, V>> _tables; size_t _n = 0; // 表中存储数据个数 };
完整代码实现
//HashTable.h #include<iostream> #include<vector> using namespace std; //使用开放地址法,实现HashTable //仅实现探测法 inline unsigned long __stl_next_prime(unsigned long n) { // Note: assumes long is at least 32 bits. 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 }; const unsigned long* first = __stl_prime_list; const unsigned long* last = __stl_prime_list + __stl_num_primes; const unsigned long* pos = lower_bound(first, last, n); return pos == last ? *(last - 1) : *pos; } //枚举状态 enum STATE { EMPTY, EXIST, DELETE }; template<class K,class V> struct HashDate { pair<K, V> _kv; STATE _state=EMPTY; }; template<class K> class HashFunc { public: size_t operator()(const K& key) { return (size_t)key; } }; //偏特化(可以单独写成一个仿函数,但是string作为key是常有的情况,为避免反复的传仿函数,写成偏特化) template<> class HashFunc<string> { public: size_t operator()(const string& key) { int ch = 0; for (auto& e : key) { ch += e; ch *= 131; //使用BKDR哈希的思路:每次结果乘131,避免“abcd” “dcba”这种不相同字符,转化为整型却相同的情况 } return ch; } }; template<class K,class V,class Hash = HashFunc<K>> //直接给出缺省值,避免常用的key值,反复传仿函数 class HashTable { public: HashTable() :_n(0) , _table(11) {} Hash hash; bool Insert(const pair<K, V>& kv) { //负载因子 控制到0.7左右 if (_n * 10 / _table.size() >= 7) { size_t size = __stl_next_prime(_table.size()); HashTable newHash; newHash._table.resize(size); for (auto& e : _table) { if(e._state==EXIST) newHash.Insert(e._kv); } _table.swap(newHash._table); } //找位置 size_t pos = hash(kv.first) % _table.size(); size_t i = 1; while (_table[pos]._state!= EMPTY)//探测 { pos = (pos + i) % _table.size(); //处理越界情况 i++; } //插入 _table[pos]._kv = kv; _table[pos]._state = EXIST; _n++; return true; } HashDate<K, V>* Find(const K& key) { size_t pos = hash(key) % _table.size(); size_t i = 1; while (_table[pos]._state != EMPTY) { if (_table[pos]._kv.first == key && _table[pos]._state != DELETE) { return &_table[pos]; } pos = (pos + i)% _table.size(); //处理越界情况 i++; } return nullptr; } //删除不是真正的删除,只需要将其状态修改为DELETE即可 bool Erase(const K& key) { HashDate<K, V>* ret = Find(key); if (ret) { ret->_state = DELETE; return true; } else return false; } private: //HashDate包含:pair、节点的状态 vector<HashDate<K, V>> _table; size_t _n;//记录元素个数 }; //test.cpp #include"HashTable.h" //处理key为string的仿函数,但作为常用的key类型,建议直接写成偏特化,避免反复传仿函数 struct StringHashFunc { size_t operator()(const string& key) { int ret = 0; for (auto& e : key) { //采用BKDR哈希思路 ret += e; ret *= 131; } return ret; } }; //日期类(自定义类型要支持 = 符号,用于Find函数) struct Date { bool operator==(Date& key) { return _year == key._year && _month == key._month && _day == key._day; } //重点!!!:这里给缺省值,是为了形成默认构造函数 //template<class K, class V> //struct HashDate //{ // pair<K, V> _kv; // STATE _state = EMPTY; //}; //在头文件的HashDate函数在初始化时,会调用自定义类型的默认构造!如果默认构造不存在就会报错 Date(const int year = 1, const int month = 1, const int day = 1) { _day = day; _month = month; _year = year; } int _day; int _month; int _year; }; //处理key为Date的仿函数 struct DateHashFunc { size_t operator()(const Date& key) { //采用BKDR哈希思路 int ret = 0; ret += key._day; ret *= 131; ret += key._month; ret *= 131; ret += key._year; ret *= 131; return ret; } }; int main() { //测试key为int的情况 HashTable<int,int> hash; for (int i = 0; i < 15; i++) { hash.Insert({ i,i }); } cout << hash.Find(10)<<endl; hash.Erase(10); cout << hash.Find(10)<<endl; //测试key为string的情况 HashTable<string, string,StringHashFunc> hash1; vector<string> arr = { "abc","asd","qwe","fgh" }; for (auto& e : arr) { hash1.Insert({ e,e }); } cout << HashFunc<string>()("abc") <<endl; cout << HashFunc<string>()("asd") << endl; cout << HashFunc<string>()("qwe") << endl; cout << HashFunc<string>()("fgh") << endl; //cout << StringHashFunc()("abc") << endl; //cout << StringHashFunc()("asd") << endl; //cout << StringHashFunc()("qwe") << endl; //cout << StringHashFunc()("fgh") << endl; //测试key为自定义的情况 HashTable<Date, Date, DateHashFunc> hash2; Date r1(2025, 4, 1); Date r2(2025, 1, 4); hash2.Insert({ r1, r1 }); hash2.Insert({ r2,r2 }); }

链地址法 

        链地址法中,哈希表中的每个节点不再存储某个具体的值,而是存储“链表”。当发生冲突时,不在去寻找,而是直接将其数据“挂”在当前冲突的位置

 

扩容 

        链地址法的负载因子我们控制到1左右,超过1就要扩容。而扩容的方法我们也使用上述的素数表。

        扩容后,我们也需要将原数据重新映射到新表中。因为扩容后M的大小改变,通过哈希函数映射出的位置也改变了

链地址法代码实现
//HashTable.h #include<iostream> #include<vector> using namespace std; //实现链地址法 inline unsigned long __stl_next_prime(unsigned long n) { // Note: assumes long is at least 32 bits. 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 }; const unsigned long* first = __stl_prime_list; const unsigned long* last = __stl_prime_list + __stl_num_primes; const unsigned long* pos = lower_bound(first, last, n); return pos == last ? *(last - 1) : *pos; } template<class K,class V> struct Node { Node(const pair<K,V>& kv) :_kv(kv) ,next(nullptr) {} Node() {} pair<K, V> _kv; Node* next = nullptr; }; //仿函数(将key值转化为正整数) template<class K> struct HashFunc { size_t operator()(const K& key) { return size(key); } }; //偏特化(处理常用key值类型:string) template<> struct HashFunc<string> { size_t operator()(const string& key) { int ret = 0; for (auto& e : key) { //BKDR哈希的思路 ret += e; ret *= 131; } return ret; } }; template<class K,class V,class Hash = HashFunc<K>> class HashTable { public: //typedef Node<K> Node; using Node = Node<K,V>; HashTable() :_table(__stl_next_prime(0)) ,_n(0) {} ~HashTable() { for (int i = 0; i < _table.size(); i++) { Node* cur = _table[i].next; while (cur) { Node* next = cur->next; delete cur; cur = next; } } } Hash hash; bool Insert(const pair<K, V>& kv) { //不允许键值冗余 if (Find(kv.first)) { return false; } //扩容(链地址法的负载因子控制在1左右) //更好的扩容方法(将原节点移下来,避免不断的开空间) if (_n / _table.size() >= 1) { vector<Node> newtable(__stl_next_prime(_table.size() + 1)); for (int i = 0; i < _table.size(); i++) { Node* cur = _table[i].next; while (cur) { Node* next = cur->next; //映射新表位置 size_t pos = hash(cur->_kv.first) % newtable.size(); //头插到哈希 cur->next = newtable[pos].next; newtable[pos].next = cur; cur = next; } } _table.swap(newtable); } //有点拉 //复用Insert函数,再交换,我们不断的创建Node节点,需要显示的写出析构函数,避免内存泄露 if (_n / _table.size() >= 1) { HashTable<K, V,Hash> newHash; newHash._table.resize(__stl_next_prime(_table.size() + 1)); for (int i = 0; i < _table.size(); i++) { Node* cur = _table[i].next; while (cur) { //复用 newHash.Insert(cur->_kv); cur = cur->next; } } _table.swap(newHash._table); } //映射位置 size_t hash0 = hash(kv.first) % _table.size(); Node* cur = _table[hash0].next; Node* newNode = new Node(kv); //需要显示写出构造函数 //头插 _table[hash0].next = newNode; newNode->next = cur; //统计元素个数 _n++; return true; } Node* Find(const K& key) { size_t pos = hash(key) % _table.size(); Node* cur = _table[pos].next; while (cur) { if (cur->_kv.first == key) return cur; cur = cur->next; } return nullptr; } bool Erase(const K& key) { //映射位置 size_t pos = hash(key) % _table.size(); Node* cur = _table[pos].next; Node* per = &_table[pos]; //记录cur的前节点 while (cur) { if (cur->_kv.first == key) { per->next = cur->next; delete cur; return true; } per = cur; cur = cur->next; } return false; } private: vector<Node> _table; int _n = 0; //记录插入元素个数 }; //test.cpp #include"HashTable.h" struct Date { bool operator==(const Date& key) { return _day == key._day && _month == key._month && _year == key._year; } Date(const int year = 2025,const int month = 4,const int day = 2) :_day(day) ,_month(month) ,_year(year) {} int _day; int _month;; int _year; }; struct DateFunc { size_t operator()(const Date& key) { int ret = 0; ret += key._day; ret *= 131; ret += key._month; ret *= 131; ret += key._year; ret *= 131; return ret; } }; int main() { //测试key值为int //HashTable<int, int> Hash0; //Hash0.Insert({ 1,1 }); //Hash0.Insert({ 2,2 }); //Hash0.Insert({ 3,3 }); //Hash0.Insert({ 4,4 }); //cout << Hash0.Find(3) << endl; //cout << Hash0.Erase(3) << endl; //cout << Hash0.Find(3) << endl; //cout << Hash0.Erase(10) << endl; //测试key值为string //HashTable<string, string> Hash0; //Hash0.Insert({ "abc","abc"}); //Hash0.Insert({ "huang","huang"}); //Hash0.Insert({ "yu","yu"}); //Hash0.Insert({ "chi","chi"}); //cout << Hash0.Find("huang") << endl; //cout << Hash0.Erase("huang") << endl; //cout << Hash0.Find("huang") << endl; //cout << Hash0.Erase("asdf") << endl; //测试key值为自定义类型 HashTable<Date, Date, DateFunc> Hash0; Date r1({ 2025,4,2 }); Date r2({ 2025,2,4 }); Hash0.Insert({ r1,r1 }); Hash0.Insert({ r2,r2}); cout << Hash0.Find(r2) << endl; cout << Hash0.Erase(r2) << endl; cout << Hash0.Find(r2) << endl; /*cout << Hash0.Erase({ 2000,1,1 }) << endl;*/ }

以上就是本文的全部内容,如有错误还请大佬斧正。
点个赞再走吧~

Read more

【AI绘画】Midjourney进阶:色调详解(上)

【AI绘画】Midjourney进阶:色调详解(上)

博客主页: [小ᶻ☡꙳ᵃⁱᵍᶜ꙳]本文专栏: AI绘画 | Midjourney 文章目录 * 💯前言 * 💯Midjourney中的色彩控制 * 为什么要控制色彩? * 为什么要在Midjourney中控制色彩? * 💯色调 * 白色调 * 淡色调 * 明色调 * 💯小结 💯前言 【AI绘画】Midjourney进阶:色相详解     https://blog.ZEEKLOG.net/2201_75539691?type=blog 在上一篇文章中,我们详细探讨了色相的基本概念和运用。而色相作为色彩的基础,虽然能帮助我们区分颜色的种类,但它并不能完全满足实际创作中的需求。尤其在 AI绘画中,颜色的呈现往往需要更加精细的调控,颜色的表达也需要超越单纯的“色相”维度。例如,当我们谈到蓝色时,仅仅知道它是蓝色并不足够。在不同的创作场景中,蓝色可以呈现为淡蓝、深蓝、灰蓝或纯蓝等多种形式,而每一种形式都能传递不同的氛围与视觉感受。 对这些变化的理解与运用,其实是对色调的掌握。色调可以看作是颜色的性格特征,

By Ne0inhk
Flutter for OpenHarmony: Flutter 三方库 husky 守卫鸿蒙项目的 Git 提交规范(前端工程化必备)

Flutter for OpenHarmony: Flutter 三方库 husky 守卫鸿蒙项目的 Git 提交规范(前端工程化必备)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在 OpenHarmony 项目的团队协作中,我们最怕遇到“带病提交”的代码。比如:某位开发者提交的代码没经过 dart format 美化、或是包含明显的 lint 警告,甚至导致整个鸿蒙工程编译失败。如果在 CI(持续集成)阶段才发现,修复成本就太高了。 husky 是从前端生态圈引进的 Git Hooks 管理神器。它能让你极简地配置 Git 的各个钩子(如 pre-commit),在代码真正提交到远端(AtomGit)之前,强制执行格式化或单元测试,确保入库的代码永远是高质量的。 一、Git Hook 工作流模型 husky 在本地提交阶段建立了一道自动化的“安检门”。 通过 失败

By Ne0inhk
【汉化中文版】OpenClaw(Clawdbot/Moltbot)第三方开源汉化中文发行版部署全指南:一键脚本/Docker/npm 三模式安装+Ubuntu 环境配置+中文汉化界面适配开源版

【汉化中文版】OpenClaw(Clawdbot/Moltbot)第三方开源汉化中文发行版部署全指南:一键脚本/Docker/npm 三模式安装+Ubuntu 环境配置+中文汉化界面适配开源版

OpenClaw这是什么? OpenClaw(曾用名 Clawdbot / Moltbot)是一个开源的个人 AI 助手平台(GitHub 120k+ Stars),可以通过 WhatsApp、Telegram、Discord 等聊天软件与 AI 交互。简单说就是:在你自己的机器上运行一个 AI 助手,通过常用聊天软件跟它对话。 forks项目仓库 :https://github.com/MaoTouHU/OpenClawChinese 文章目录 * OpenClaw这是什么? * 汉化效果预览 * 环境要求 * 安装方式 * 方式 A:一键脚本(推荐新手) * 方式 B:npm 手动安装 * 方式 C:Docker 部署(服务器推荐) * 首次配置 * 运行初始化向导 * 安装守护进程(

By Ne0inhk

全面的System Verilog教程:从基础到高级验证

本文还有配套的精品资源,点击获取 简介:System Verilog是用于系统级验证、芯片设计与验证以及FPGA实现的强大硬件描述语言。它扩展了Verilog的基础特性,支持高级语言结构,如类、接口、任务和函数,优化了验证流程。教程内容涵盖System Verilog的基础概念、结构化编程元素、并发与同步机制、现代验证方法学、UVM验证方法论以及标准库的应用。旨在教授学生掌握System Verilog语法和高级特性,实现高效、可维护的验证代码。 1. System Verilog概述及应用领域 1.1 System Verilog的起源与发展 System Verilog是作为硬件设计和验证领域的重要语言,由Verilog发展而来,随后被进一步扩展以满足现代电子设计自动化的需要。其发展始于20世纪90年代,目的是在原有Verilog HDL的基础上,提供更为强大的设计验证功能。 1.1.1 Verilog与VHDL的区别 虽然Verilog和VHDL都是硬件描述语言(HDL),但它们在语法和使用方法上存在差异。Verilog更接近于C语言,而VHDL的语法结构则更接近

By Ne0inhk