• [C国演义] 哈希的使用和开闭散列的模拟实现


    1. 使用

    1.1 unordered_map的接口

    1. 构造
    void test1()
    {
        // 空的unordered_map对象
        unordered_map<int, int> m1(10);
        cout << "桶的实际个数->" << m1.bucket_count() << endl;
    
        // 用列表初始化来进行初始化
        unordered_map<int, int> m2{ {1,1}, {2,2}, {3,3} };
        cout << "列表初始化-> " << endl;
        for (const auto& e : m2)
        {
            cout << e.first << " " << e.second << endl;
        }
        cout << endl;
    
        // 迭代器区间初始化
        unordered_map<int, int> m3(m2.begin(), m2.end());
        cout << "迭代器区间初始化-> " << endl;
        for (const auto& e : m2)
        {
            cout << e.first << " " << e.second << endl;
        }
        cout << endl;
    }
    
    int main()
    {
        test1();
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    运行结果 :

    桶的实际个数->16
    列表初始化->
    1 1
    2 2
    3 3
    
    迭代器区间初始化->
    1 1
    2 2
    3 3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. 容量
    2. 元素访问
    void test2()
    {
        unordered_map<string, int> mat{ {"小呆呆", 1}, {"波比", 2} };
        cout << "初始化-> " << endl;
        for (const auto& e : mat)
        {
            cout << e.first << " " << e.second << endl;
        }
        cout << endl;
    
        mat["猪猪侠"];
        cout << "插入功能-> " << endl;
        for (const auto& e : mat)
        {
            cout << e.first << " " << e.second << endl;
        }
        cout << endl;
    
        mat["波比"] = 5;
        cout << "修改功能-> " << endl;
        for (const auto& e : mat)
        {
            cout << e.first << " " << e.second << endl;
        }
        cout << endl;
    
        mat["超人强"] = 6;
        cout << "插入 + 修改功能-> " << endl;
        for (const auto& e : mat)
        {
            cout << e.first << " " << e.second << endl;
        }
        cout << endl;
    }
    
    int main()
    {
        test2();
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    运行结果 :

    初始化->
    小呆呆 1
    波比 2
    
    插入功能->
    小呆呆 1
    猪猪侠 0
    波比 2
    
    修改功能->
    小呆呆 1
    猪猪侠 0
    波比 5
    
    插入 + 修改功能->
    小呆呆 1
    猪猪侠 0
    波比 5
    超人强 6
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    1. 查询
    void test3()
    {
        unordered_map<string, int> mat{ {"小呆呆", 1}, {"波比", 2} };
        mat["猪猪侠"] = 5;
        mat["猪猪侠"] = 7;
        size_t cnt = mat.count("猪猪侠");
        cout << cnt << endl;
    
        auto it = mat.find("小呆呆");
        if (it != mat.end())
        {
            cout << it->first << " is " << it->second << endl;
        }
        else
        {
            cout << "查无元素! " << endl;
        }
        cout << endl;
    
        auto git = mat.find("超人强");
        if (git != mat.end())
        {
            cout << git->first << " is " << git->second << endl;
        }
        else
        {
            cout << "查无元素! " << endl;
        }
    
    }
    
    int main()
    {
        test3();
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    运行结果 :

    1
    小呆呆 is 1
    
    查无元素!
    
    • 1
    • 2
    • 3
    • 4
    1. 修改
    void test4()
    {
        unordered_map<string, int> mat1{ {"小呆呆", 1}, {"波比", 2} };
    
        // 插入
        mat1.insert({ "小呆呆", 5 });
        mat1.insert({ "超人强", 6 });
        for (const auto& e : mat1)
        {
            cout << e.first << " " << e.second << endl;
        }
        cout << endl;
    
        // 删除
        mat1.erase("波比");
        mat1.erase("猪猪侠");
        for (const auto& e : mat1)
        {
            cout << e.first << " " << e.second << endl;
        }
        cout << endl;
    
        // 交换
        unordered_map<string, int> mat2{ {"迪迦",1}, {"戴拿",2}};
        mat1.swap(mat2);
        cout << "交换后的mat1-> " << endl;
        for (const auto& e : mat1)
        {
            cout << e.first << " is " << e.second << endl;
        }
        cout << endl;
    
        cout << "交换后的mat2-> " << endl;
        for (const auto& e : mat2)
        {
            cout << e.first << " is " << e.second << endl;
        }
    }
    
    int main()
    {
        test4();
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    运行结果 :

    小呆呆 1
    波比 2
    超人强 6
    
    小呆呆 1
    超人强 6
    
    交换后的mat1->
    迪迦 is 1
    戴拿 is 2
    
    交换后的mat2->
    小呆呆 is 1
    超人强 is 6
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1. 桶操作
    void test5()
    {
        unordered_map<string, int> mat1{ {"小呆呆", 1}, {"波比", 2}, {"迪迦", 3} };
    
        // 桶的个数
        cout << mat1.bucket_count() << endl << endl;
    
        // 每个桶的有效个数
        for (int i = 0; i < mat1.bucket_count(); i++)
        {
            printf("[%d] -> %d\n", i, mat1.bucket_size(i));
        }
        cout << endl;
    
        // 各个key所在的桶
        for (const auto& e : mat1)
        {
            cout << mat1.bucket(e.first) << endl;
        }
    }
    
    int main()
    {
        test5();
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    运行结果 :

    8
    
    [0] -> 1
    [1] -> 0
    [2] -> 1
    [3] -> 0
    [4] -> 0
    [5] -> 0
    [6] -> 1
    [7] -> 0
    
    2
    6
    0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    1.2 unordered_set的接口

    unordered_set 和 unordered_map的接口大致一样, 但是没有 operator [ ]

    2. 哈希底层

    2.1 概念

    unordered_set 和 unordered_map的效率高的原因 ⇒ 底层是哈希结构

    • 理想中的搜索方法 :
      顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( l o g 2 N log_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数。
      理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。
      如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素.

    使元素的存储位置和关键码建立一 一映射的关系, 这个方法称为 哈希(散列)方法,
    通过一个函数使得存储位置和关键码建立一 一映射的关系, 这个函数称为 哈希函数,
    最终形成的结构, 称为 哈希(散列)表, HashTable
    不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为 哈希冲突或哈希碰撞

    • 先浅浅地看一下哈希结构, 来理解一下概念

    • 常见的哈希函数
      哈希冲突的一个重要原因就是 哈希函数设置的不好
      那么, 我们来了解一下最常见的两个哈希函数

      1. 直接寻址法 : 一个key对应一个位置
        前提 : 知道数据集合的大小 和 分布情况
        适合场景: 数据量小且均匀

      2. 除留余数法 : 准备一个基准值去估计数据量的多少, 设为m, 采用 hash(key) = key % m的方法去建立元素和下标的一 一 映射关系

    • 采用直接寻址法 — — 数据量小 且 集中
      字符串中第一个唯一字符

    class Solution {
    public:
        int firstUniqChar(string s) 
        {
            // 
            int hash[26] = {0};
    
            // 映射
            for(auto e : s)
            {
                hash[e-'a']++;
            }
    
            // 查找
            for(int i = 0; i < s.size(); i++)
            {
                if(hash[s[i] - 'a'] == 1)
                {
                    return i;
                }
            }
            return -1;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 采用除留余数法 — — 任何场景下都可
      下面的 哈希冲突解决 和 实现 都是采用的除留余数法

    • 哈希函数设置的越巧妙, 哈希冲突就越低, 但是 哈希冲突无法避免

    2.2 解决哈希冲突

    解决哈希冲突主要有两种方法 : 闭散列 和 开散列

    1. 闭散列
      闭散列, 也叫 开放寻址法,
      思路是 : 当冲突发生时, 必然有空位置, 那么把冲突的元素放到 "下一个空位置" 即可!
    • 插入逻辑

    • 查找逻辑

      • 这从另一方面也体现了 哈希表的有效数据不应该占比太大 ⇒ 否则就是遍历这个哈希结构, O(N)
        但是 也不能占比太少 ⇒ 浪费空间
        一般, 控制 有效个数 / 哈希结构的大小 在 [0.7, 0.8]的范围内是比较合理的
    • 删除逻辑
      首先, 能确定的是不能直接把这个位置去掉

      那么该位置要进行保留, 那么 值该怎么处理呢 ?
      改为 0, -1 … … 等无意义的数值?
      其实这些都是不行的, 你怎么知道你修改后的数据是无意义的呢 ⇒ 能确定的是 该位置的值也要进行保留
      该位置要进行保留, 值也要进行保留 && 不能影响后面的查找逻辑 那么该怎么把它删掉呢? ⇒ 引入每个下标的状态 : 删除状态, 空白状态, 存在状态

    • 由于删除逻辑而导致新的插入逻辑

    • 由于删除逻辑而导致新的查找逻辑

    1. 开散列
      开散列, 又叫作 拉链法
      上面的 开放地址法 解决哈希冲突的办法是 将经过哈希函数处理过的 相同的key, "延后落座"
      拉链法的解决思路是 将经过哈希函数处理的 相同的key 放到一个单链表中, 然后将每一个单链表的头结点放到一个数组里面. 本质是一个 指针数组
    • 这里的插入删除, 查找逻辑就是在 key那个桶进行单链表操作

    🗨️ 有同学就会说, 这不是单链表操作吗, 不过如此!

    • 我们可以控制 有效数据个数 / 桶的大小 = 1 ⇒ 平均下来就是一个桶一个数据

    3. 实现

    这里都先实现 数据位pair类型的

    3.1 开放寻址法

    1. STATE类型
    enum STATE
    {
    	EXIT,
    	DELETE,
    	EMPTY
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1. HashData类
    template<class K, class V>
    struct HashData
    {
    
    public:
    	HashData()
    	{}
    
    	HashData(const pair<K, V>& kv)
    		:_data(kv)
    	{}
    
    public:
    	pair<K, V> _data;
    	STATE _st = EMPTY;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    1. Hash类
    template<class K, class V, class Com = DEFAULT<K>>
    class hash
    {
    
    public:
    	hash()
    	{
    		// 1. 先给4个空间
    		// 2. size 和 capacity一样大
    		_table.resize(4);
    	}
    
    	bool insert(const pair<K, V>& kv)
    	{
    		// 扩容逻辑
    		if ((double)_sz / _table.size() >= 0.7)
    		{
    			size_t newsize = _table.size() * 2;
    			hash<K, V> new_ht;
    			new_ht._table.resize(newsize);
    
    			// 挪动数据
    			for (size_t i = 0; i < _table.size(); i++)
    			{
    				// 不用挪动删除状态的值
    				if (_table[i]._st == EXIT)
    				{
    					new_ht.insert(_table[i]._data);
    				}
    			}
    
    			std::swap(*this, new_ht);
    		}
    
    		// 线性探测
    		for (const auto& e : _table)
    		{
    			if (kv.first == e._data.first)
    			{
    				return false;
    			}
    		}
    		size_t hashi = com(kv.first) % _table.size();
    		while (_table[hashi]._st == EXIT)
    		{
    			++hashi;
    			hashi %= _table.size();
    		}
    
    		_sz++;
    		_table[hashi] = kv;
    		_table[hashi]._st = EXIT;
    
    		return true;
    	}
    	
    	// 返回有key,
    	// 不允许用户在外面更改key,
    	// 所以返回*
    	HashData<const K, V>* find(const K& key)
    	{
    		size_t hashi = com(key) % _table.size();
    		while (_table[hashi]._st != EMPTY)
    		{
    			if (_table[hashi]._st == EXIT &&  _table[hashi]._data.first == key)
    			{
    				return (HashData<const K, V>*)&_table[hashi];
    			}
    
    			hashi++;
    			hashi %= _table.size();
    		}
    
    		return nullptr;
    	}
    	
    
    	bool erase(const K& key)
    	{
    		// 复用find
    		HashData<const K, V>* res = find(key);
    
    		if (res)
    		{
    			res->_st = DELETE;
    			_sz--;
    			return true;
    		}
    		else
    		{
    			return false;
    		}
    
    		//for (auto e : _table)
    		//{
    		//	if (e._data.first == key)
    		//	{
    		//		e._st = DELETE;
    		//		_sz--;
    		//		return true;
    		//	}
    		//}
    
    		//return false;
    	}
    
    private:
    	vector<HashData<K, V>> _table;
    	size_t _sz = 0;
    	Com com;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    1. DEFAULT — 通过仿函数来解决 字符串 不能进行 % 的问题
    // 通过仿函数来解决 字符串 不能进行 % 
    template<class K>
    struct DEFAULT
    {
    	size_t operator()(const K& key)
    	{
    		return (size_t)key;
    	}
    };
    
    // 模版的特化 -- 全特化
    // 解决 字符串问题
    template<>
    struct DEFAULT<string>
    {
    	size_t operator()(const string& key)
    	{
    		int res = 0;
    		for (auto e : key)
    		{
    			res += e * 131;
    		}
    
    		return res;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    3.2 拉链法

    1. HashData类
    template<class K, class V>
    struct HashData
    {
    public:
    HashData(const pair<K, V>& kv)
    	:_data(kv)
    {}
    
    public:
    pair<K, V> _data;
    HashData<K, V>* _next;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    1. Hash类
    template<class K, class V, class Com = DEFAULT<K>>
    class hash
    {
    	typedef HashData<const K, V> Node;
    public:
    
    	hash()
    	{
    		_table.resize(4, nullptr);
    	}
    
    	Node* find(const K& key)
    	{
    		size_t hashi = com(key) % _table.size();
    		Node* cur = _table[hashi];
    		while (cur)
    		{
    			if (com(cur->_data.first) == com(key))
    			{
    				return cur;
    			}
    
    			cur = cur->_next;
    		}
    
    		return nullptr;
    	}
    
    	bool insert(const pair<K, V>& kv)
    	{
    		Node* res = find(kv.first);
    		if (res)
    		{
    			return false;
    		}
    
    		// 扩容逻辑
    		if (_sz == _table.size())
    		{
    			vector<Node*> new_table;
    			new_table.resize(_table.size() * 2, nullptr);
    			for (size_t i = 0; i < _table.size(); i++)
    			{
    				Node* cur = _table[i];
    				
    				// 顺手牵走这个桶的内容
    				while (cur)
    				{
    					// 提前保存 next, 后面会改变的
    					Node* next = cur->_next;
    
    					size_t hashi = com(cur->_data.first) % new_table.size();
    					// 先让cur链接上新表中该桶的内容
    					cur->_next = new_table[hashi];
    					// 再让cur成为新表中该桶的头节点
    					new_table[hashi] = cur;
    
    					cur = next;
    				}
    			}
    			
    			_table.swap(new_table);
    		}
    
    		// 插入逻辑
    		size_t hashi = com(kv.first) % _table.size();
    		Node* newnode = new Node(kv);
    
    		newnode->_next = _table[hashi];
    		_table[hashi] = newnode;
    
    		++_sz;
    		return true;
    	}
    
    	bool erase(const K& key)
    	{
    		Node* res = find(key);
    		if (res == nullptr)
    		{
    			return false;
    		}
    		else
    		{
    			size_t hashi = com(key) % _table.size();
    			Node* cur = _table[hashi];
    			Node* prev = nullptr;
    			while (cur)
    			{
    				if (cur->_data.first == key)
    				{
    					if (prev == nullptr)
    					{
    						_table[hashi] = cur->_next;
    					}
    					else
    					{
    						prev->_next = cur->_next;
    					}
    
    				}
    
    				prev = cur;
    				cur = cur->_next;
    			}
    
    			--_sz;
    			delete cur;
    		}
    
    		return true;
    	}
    
    	void print()
    	{
    		for (int i = 0; i < _table.size(); i++)
    		{
    			Node* cur = _table[i];
    			printf("[%d]->", i);
    			while (cur)
    			{
    				printf("%d", cur->_data.first);
    				cur = cur->_next;
    			}
    			cout << "NULL" << endl;
    		}
    		cout << endl;
    	}
    
    private:
    	vector<Node*> _table;
    	size_t _sz = 0;
    	Com com;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    1. DEFAUL — 通过仿函数来解决 字符串 不能 % 的问题
    // 通过仿函数来解决 字符串 不能进行 % 
    template<class K>
    struct DEFAULT
    {
    	size_t operator()(const K& key)
    	{
    		return (size_t)key;
    	}
    };
    
    // 模版的特化 -- 全特化
    // 解决 字符串问题
    template<>
    struct DEFAULT<string>
    {
    	size_t operator()(const string& key)
    	{
    		int res = 0;
    		for (auto e : key)
    		{
    			res += e * 131;
    		}
    
    		return res;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    无心买酒谒青春,对镜空嗟白发新。
    花下少年应笑我,垂垂羸马访高人。
    — — 岳飞 <过张溪赠张完>

  • 相关阅读:
    Elasticsearch如何聚合查询多个统计值,如何嵌套聚合?并相互引用,统计索引中某一个字段的空值率?语法是怎么样的
    源码分析之上下文构建
    java计算机毕业设计vue架构云餐厅美食订餐系统源码+mysql数据库+系统+lw文档+部署
    Flink -- 状态与容错
    15 OpenCV Sobel算子
    ElementUI-tree拖拽功能与节点自定义
    两个难搞的Java Error/Exception
    网络扫描与网络监听
    Git基本使用介绍
    【算法-动态规划】最长公共子序列
  • 原文地址:https://blog.csdn.net/qq_67549203/article/details/134087051