C++ 哈希表

哈希表实现

1. 哈希概念

哈希(hash)⼜称散列,是⼀种组织数据的⽅式。本质就是通过哈希 函数把关键字Key跟存储位置建⽴⼀个映射关系,查找时通过这个哈希函数计算出Key存储的位置,进 ⾏快速查找。

哈希表底层是vector,是一个数组。为什么叫散列,因为数据放在哈希表中是相对无序的。

1.1 哈希冲突:不可避免的问题

在哈希表中两个不同的key可能会映射到同⼀个位置去,这种问题我们叫做哈希冲突, 或者哈希碰撞。理想情况是找出⼀个好的哈希函数避免冲突,但是实际场景中,冲突是不可避免的, 所以我们尽可能设计出优秀的哈希函数,减少冲突的次数,同时也要去设计出解决冲突的⽅案。

1.2 负载因子:哈希表的 "拥挤程度"

负载因子(Load Factor)是衡量哈希表拥挤程度的指标,决定了哈希表是否需要扩容,公式:

  • 负载因子越大:哈希表越拥挤,冲突概率越高,空间利用率越高;
  • 负载因子越小:哈希表越宽松,冲突概率越低,空间利用率越低。

1.3 将key转为整数

我们将关键字映射到数组中位置,⼀般是整数好做映射计算,如果不是整数,我们要想办法转换成整 数。我们传的key不可能都是无符号整数,因此我们得自己设计一个仿函数将key转为无符号整数。

2.哈希函数

⼀个好的哈希函数应该让N个关键字被等概率的均匀的散列分布到哈希表的M个空间中,但是实际中却 很难做到,但是我们要尽量往这个⽅向去考量设计。

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

• 除法散列法也叫做除留余数法,顾名思义,假设哈希表的⼤⼩为M,那么通过key除以M的余数作为 映射位置的下标,也就是哈希函数为:h(key)=key%M。

当使⽤除法散列法时,要尽量避免M为某些值,如2的幂,10的幂等。如果是 ,那么key%2^x 本质相当于保留key的后X位,那么后x位相同的值,计算出的哈希值都是⼀样的,就冲突了。

所以当我们使⽤除法散列法时,建议M取远离2的整数次幂且为质数(素数)的值。

2.2 乘法散列法(了解)

• 乘法散列法对哈希表⼤⼩M没有要求,他的⼤思路第⼀步:⽤关键字K乘上常数A[0<A<1),

取出k*A的⼩数部分。第⼆步:后再⽤M乘以k*A的⼩数部分,再向下取整。

• h(key) = floor(M ×((A ×key)%1.0)) ,其中floor表⽰对表达式进⾏下取整,A∈[0,1),这⾥ 最重要的是A的值应该如何设定,Knuth认为 A =( −5 1)/2 = 0.6180339887.... (⻩⾦分割点) ⽐较好。

3.处理哈希冲突

实践中哈希表⼀般还是选择除法散列法作为哈希函数,当然哈希表⽆论选择什么哈希函数也避免不了 冲突,那么插⼊数据时,如何解决冲突呢?主要有两种两种⽅法,开放定址法和链地址法。

3.1 开放定址法

在开放定址法中所有的元素都放到哈希表⾥,当⼀个关键字key⽤哈希函数计算出的位置冲突了,则按 照某种规则找到⼀个没有存储数据的位置进⾏存储,开放定址法中负载因⼦⼀定是⼩于的。这⾥的规 则有三种:线性探测、⼆次探测、双重探测。

线性探测

• 从发⽣冲突的位置开始,依次线性向后探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果⾛ 到哈希表尾,则回绕到哈希表头的位置。

• h(key) = hash0 = key % M hc(key,i) = hashi = (hash0+i) % M , i = {1,2,3,...,M −1} , hash0位置冲突了,则线性探测公式为: ,因为负载因⼦⼩于1, 则最多探测M-1次,⼀定能找到⼀个存储key的位置。

• 线性探测的⽐较简单且容易实现,线性探测的问题假设,hash0位置连续冲突,hash0,hash1, hash2位置已经存储数据了,后续映射到hash0,hash1,hash2,hash3的值都会争夺hash3位 置,这种现象叫做群集/堆积。下⾯的⼆次探测可以⼀定程度改善这个问题。

• 下⾯演⽰{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

⼆次探测

• 从发⽣冲突的位置开始,依次左右按**⼆次⽅跳跃**式探测,直到寻找到下⼀个没有存储数据的位置为 ⽌,如果往右⾛到哈希表尾,则回绕到哈希表头的位置;如果往左⾛到哈希表头,则回绕到哈希表 尾的位置;

• h(key) = hash0 = key % M , hash0位置冲突了,则⼆次探测公式为: 2 hc(key,i) = hashi = (hash0±i ) % M , i = 2 hashi = (hash0−i )%M M {1,2,3,..., } 2

• ⼆次探测当时,当hashi<0时,需要hashi+=M

• 下⾯演⽰ {19,30,52,63,11,22} 等这⼀组值映射到M=11的表中。

h(19) = 8, h(30) = 8, h(52) = 8, h(63) = 8, h(11) = 0, h(22) = 0

优点:减少堆积;缺点:有空间浪费。

开放定址法代码实现

开放定址法在实践中,不如链地址法,因为开放定址法解决冲突不管使⽤哪种⽅法,占⽤的 都是哈希表中的空间,始终存在互相影响的问题。所以开放定址法我们简单选择线性探测实现即 可。

cpp 复制代码
namespace open_address
{
    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 Hash=HashFunc<K>>
    class HashTable
    {
    public:
        HashTable()
            :_tables(__stl_next_prime(0))
            ,_n(0)
        { }

        bool Insert(const pair<K, V>& kv)
        {
            //不插入重复值
            if (Find(kv.first))
                return false;
            //负载因子>=0.7就扩容
            if (_n*10/_tables.size()>=7)
            {
              //扩容逻辑...
            }
            Hash hash;
            size_t hash0 = hash(kv.first) % _tables.size();
            size_t hashi = hash0;
            size_t i = 1;
            while (_tables[hashi]._state == EXIST)
            {
                //线性探测
                hashi = (hash0 + i) % _tables.size();
                ++i;
            }
            _tables[hashi]._kv = kv;
            _tables[hashi]._state = EXIST;
            ++_n;
            return true;
        }

        HashData<K, V>* Find(const K& key)
        {
            Hash hash;
            size_t hash0 = hash(key) % _tables.size();
            size_t hashi = hash0;
            size_t i = 1;
            while (_tables[hashi]._state != EMPTY)
            {
                if (_tables[hashi]._state == EXIST
                    && _tables[hashi]._kv.first == key)
                {
                    return &_tables[hashi];
                }
                //线性探测
                hashi = (hash0 + i) % _tables.size();
                ++i;
            }
            return nullptr;
        }

    private:
        vector<HashData<K, V>> _tables;
        size_t _n;//记录数据个数
    };
}

3.2链地址法

解决冲突的思路

放定址法中所有的元素都放到哈希表⾥,链地址法中所有的数据不再直接存储在哈希表中,哈希表 中存储⼀个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,我们把 这些冲突的数据链接成⼀个链表,挂在哈希表这个位置下⾯,链地址法也叫做拉链法或者哈希桶

• 下⾯演⽰{19,30,5,36,13,20,21,12,24,96} 等这⼀组值映射到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,h(24) = 2,h(96) = 88

链地址法代码实现
cpp 复制代码
namespace hash_bucket 
{
    template<class T>
    struct HashNode 
    {
        T _data;
        HashNode<T>* _next;

        HashNode(const T& data)
            : _data(data)
            , _next(nullptr)
        {}
    };

    template<class K, class T, class KeyOfT, class Hash>
    class HashTable 
    {
    private:
        vector<HashNode<T>*> _tables;
        size_t _n = 0;

    public:
        bool Insert(const T& data) 
        {
            KeyOfT kot;
            if (Find(kot(data))) return false;

            // 负载因子 = 1 时扩容
            if (_n == _tables.size()) 
            {
                vector<HashNode<T>*> new_tables(GetNextPrime(_tables.size()), nullptr);
   
                _tables.swap(new_tables);
            }

            Hash hs;
            size_t hashi = hs(kot(data)) % _tables.size();

            // 头插
            HashNode<T>* new_node = new HashNode<T>(data);
            new_node->_next = _tables[hashi];
            _tables[hashi] = new_node;
            ++_n;

            return true;
        }
    };
}

4.实现哈希表的难点

4.1扩容

这⾥我们哈希表负载因⼦控制在0.7,当负载因⼦到0.7以后我们就需要扩容了,我们还是按照2倍扩 容,但是同时我们要保持哈希表⼤⼩是⼀个质数,第⼀个是质数,2倍后就不是质数了。

那么如何解决呢,有⼀种⽅案是sgi版本的哈希表使⽤的⽅法,给了⼀个近似2倍的质数表,每次去 质数表获取扩容后的⼤⼩。

cpp 复制代码
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
    };

    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;
}

4.2Key不是整型的问题

当key是string/Date等类型时,key不能取模,那么我们需要给HashTable增加⼀个仿函数,这个仿函 数⽀持把key转换成⼀个可以取模的整形,如果key可以转换为整形并且不容易冲突,那么这个仿函数 就⽤默认参数即可,如果这个Key不能转换为整形,我们就需要⾃⼰实现⼀个仿函数传给这个参数,实 现这个仿函数的要求就是尽量key的每值都参与到计算中,让不同的key转换出的整形值不同。

cpp 复制代码
template<class K>
struct HashFunc
{
    size_t operator()(const K& key)
    {
        return (size_t)key;
    }
};
//特化
template<>
struct HashFunc<string>
{
     //BKDR算法
    size_t operator()(const string & key)
    {
        size_t hash = 0;
        for (auto e : key)
        {
            hash *= 131;
            hash += e;
        }
        return hash;
    }
};

封装实现unordered_map和unordered_set

C++ STL 中的unordered_mapunordered_set,其底层就是链地址法实现的哈希桶,与本文实现的哈希表核心逻辑一致,仅做了更多工程优化:

  1. 支持迭代器遍历(单向迭代器,因链表是单向的);
  2. 实现了更多接口(如size()empty()等);
  3. 对哈希函数做了更全面的特化,支持更多类型;
  4. 对扩容和哈希冲突做了更细致的优化。

可以说,吃透了哈希桶实现,就等于吃透了unordered_map/unordered_set的底层原理。

unordered_map和unordered_set复⽤之前我们实现的哈希表。

cs 复制代码
    // MyUnorderedSet.h
    template<class K, class Hash = HashFunc<K>>
    class unordered_set {
        struct SetKeyOfT {
            const K& operator()(const K& key) {
                return key;
            }
        };
        
    public:
        bool insert(const K& key) {
            return _ht.Insert(key);
        }
        
    private:
        hash_bucket::HashTable<K, K, SetKeyOfT, Hash> _ht;
    };
 
    // MyUnorderedMap.h  
    template<class K, class V, class Hash = HashFunc<K>>
    class unordered_map {
        struct MapKeyOfT {
            const K& operator()(const pair<K, V>& kv) {
                return kv.first;
            }
        };
        
    public:
        bool insert(const pair<K, V>& kv) {
            return _ht.Insert(kv);
        }
        
        V& operator[](const K& key) {
            auto ret = _ht.Insert(make_pair(key, V()));
            return ret.first->second;
        }
        
    private:
        hash_bucket::HashTable<K, pair<K, V>, MapKeyOfT, Hash> _ht;
    };

unordered_map和unordered_set的功能和map和set的大致一样,用封装实现时也是调用哈希表的功能,主要难点是迭代器的operator++。

iterator中有⼀个指向结点的指针,如果当前桶下⾯还有结点, 则结点的指针指向下⼀个结点即可。如果当前桶⾛完了,则需要想办法计算找到下⼀个桶。

++有以下两种情况:

1.在每个哈希节点的链表里面++;

2.已到当前链表的尾部,需要跳到下一个不为空的链表中。

operator++的实现:

cpp 复制代码
 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())
         {
             _node = _ht->_tables[hashi];
             if (_node)
                 break;
             else
                 ++hashi;
         }
         //走完所有桶
         if (hashi == _ht->_tables.size())
         {
             _node = nullptr;
         }
     }
     return *this;
 }

哈希表的完整实现:

cpp 复制代码
#include<iostream>
#include<vector>
using namespace std;

enum State
{
    EXIST,//存在
    EMPTY,//空
    DELETE//删除
};

template<class K, class V>
struct HashData
{
    pair<K, V> _kv;
    State _state = EMPTY;
};

//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& s)
    {
        //BKDR算法
        size_t hash = 0;
        for (auto ch : s)
        {
            hash += ch;
            hash *= 131;
        }
        return hash;
    }
};
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
    };

    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;
}

namespace hash_bucket
{
    template<class T>
    struct HashNode
    {
        T _data;
        HashNode<T>* _next;

        HashNode(const T& data)
            :_data(data)
            ,_next(nullptr)
        { }
    };
    //前置声明,以防编译器找不到
    template<class K,class T,class KeyOfT,class Hash>
    class HashTable;

    template<class K,class T,class Ref,class Ptr,class KeyOfT,class Hash>
    //迭代器部分
    struct HTIterator
    {
        typedef HashNode<T> Node;
        typedef HashTable<K, T, KeyOfT, Hash> HT;
        typedef HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;

        Node* _node;//
        const HT* _ht;//
        
        HTIterator(Node* node,const HT* ht)
            :_node(node)
            ,_ht(ht)
        { }
        Ref operator*()
        {
            return _node->_data;
        }
        Ptr operator->()
        {
            return &_node->_data;
        }
        bool operator!=(const Self&s)
        {
            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())
                {
                    _node = _ht->_tables[hashi];
                    if (_node)
                        break;
                    else
                        ++hashi;
                }
                //走完所有桶
                if (hashi == _ht->_tables.size())
                {
                    _node = nullptr;
                }
            }
            return *this;
        }
    };

    template<class K,class T,class KeyOfT,class Hash>
    class HashTable
    {
        template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
        friend struct HTIterator;

        typedef HashNode<T> Node;
    public:
        typedef HTIterator<K, T, T&, T*, KeyOfT, Hash>Iterator;
        typedef HTIterator<K, T, const T&, const T*, KeyOfT, Hash>ConstIterator;

        Iterator Begin()
        {
            if (_n == 0)
                return End();
            for (size_t i = 0; i < _tables.size(); i++)
            {
                Node* cur = _tables[i];
                if (cur)
                {
                    return Iterator(cur, this);
                }
            }
            return End();
        }

        Iterator End()
        {
            return Iterator(nullptr, this);
        }

        ConstIterator Begin() const
        {
            if (_n == 0)
                return End();

            for (size_t i = 0; i < _tables.size(); i++)
            {
                Node* cur = _tables[i];
                if (cur)
                {
                    return ConstIterator(cur, this);
                }
            }

            return End();
        }

        ConstIterator End() const
        {
            return ConstIterator(nullptr, this);
        }

        HashTable()
            :_tables(__stl_next_prime(0))
            , _n(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 { it,false };

            Hash hash;
            //负载因子==1时扩容
            if (_n == _tables.size())
            {
                vector<Node*> newTable(__stl_next_prime(_tables.size() + 1));
                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)) % newTable.size();
                        cur->_next = newTable[hashi];
                        newTable[hashi] = cur;
                        cur = next;
                    }
                    _tables[i] = nullptr;
                }
                _tables.swap(newTable);
            }
            size_t hashi = hash(kot(data)) % _tables.size();
            //头插
            Node* newnode = new Node(data);
            newnode->_next = _tables[hashi];
            _tables[hashi] = newnode;
            ++_n;

            return { Iterator(newnode,this),false };
        }

        Iterator Find(const K& key)
        {
            KeyOfT kot;
            Hash hash;
            size_t hashi = hash(key) % _tables.size();
            Node* cur = _tables[hashi];
            while (cur)
            {
                if (kot(cur->_data) == key)
                {
                    return Iterator(cur, this);
                }
                cur = cur->_next;
            }
            return End();
        }

        bool Erase(const K& key)
        {
            KeyOfT kot;
            size_t hashi = key % _tables.size();
            Node* prev = nullptr;
            Node* cur = _tables[hashi];
            while (cur)
            {
                if (kot(cur->_data) == key)
                {
                    if (prev == nullptr)
                    {
                        _tables[hashi] = cur->_next;
                    }
                    else
                    {
                        prev->_next = cur->_next;
                    }
                    delete cur;
                    --_n;
                    return true;
                }
                else
                {
                    prev = cur;
                    cur = cur->_next;
                }
            }
            return false;
        }
    private:
        vector<Node*> _tables;//指针数组
        size_t _n = 0;
    };
}

unordered_map的完整实现:

cpp 复制代码
#include"HashTable.h"
namespace my
{
	template<class K,class V,class Hash=HashFunc<K>>
	class unordered_map
	{
		struct MapKeyOfT
		{
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;
			}
		};
	public:
		typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::Iterator iterator;
		typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::ConstIterator const_iterator;

		iterator begin()
		{
			return _ht.Begin();
		}

		iterator end()
		{
			return _ht.End();
		}

		const_iterator begin() const
		{
			return _ht.Begin();
		}

		const_iterator end() const
		{
			return _ht.End();
		}

		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = insert({ key, V() });
			return ret.first->second;
		}

		pair<iterator, bool> insert(const pair<K, V>& kv)
		{
			return _ht.Insert(kv);
		}

		iterator Find(const K& key)
		{
			return _ht.Find(key);
		}

		bool Erase(const K& key)
		{
			return _ht.Erase(key);
		}
	private:
		hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
	};
	void test_map1()
	{
		unordered_map<string, string> dict;
		dict.insert({ "sort", "排序" });
		dict.insert({ "字符串", "string" });

		dict.insert({ "sort", "排序" });
		dict.insert({ "left", "左边" });
		dict.insert({ "right", "右边" });

		dict["left"] = "左边,剩余";
		dict["insert"] = "插入";
		dict["string"];

		for (auto& kv : dict)
		{
			cout << kv.first << ":" << kv.second << endl;
		}
		cout << endl;

		unordered_map<string, string>::iterator it = dict.begin();
		while (it != dict.end())
		{
			// 不能修改first,可以修改second
			//it->first += 'x';
			it->second += 'x';
			cout << it->first << ":" << it->second << endl;
			++it;
		}
		cout << endl;
	}
}

unordered_set的完整实现:

cpp 复制代码
#include"HashTable.h"
namespace my
{
	template<class K,class Hash=HashFunc<K>>
	class unordered_set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::Iterator iterator;
		typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::ConstIterator const_iterator;

		iterator begin()
		{
			return _ht.Begin();
		}

		iterator end()
		{
			return _ht.End();
		}

		const_iterator begin() const
		{
			return _ht.Begin();
		}

		const_iterator end() const
		{
			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:
		hash_bucket::HashTable<K, const K, SetKeyOfT, Hash> _ht;
	};
	void print(const unordered_set<int>& s)
	{
		unordered_set<int>::const_iterator it = s.begin();
		while (it != s.end())
		{
			//*it = 1;
			cout << *it << " ";
			++it;
		}
		cout << endl;

		for (auto e : s)
		{
			cout << e << " ";
		}
		cout << endl;
	}

	void test_set1()
	{
		int a[] = { 3,11,86,7,88,82,1,881,5,6,7,6 };
		unordered_set<int> s;
		for (auto e : a)
		{
			s.insert(e);
		}

		unordered_set<int>::iterator it = s.begin();
		while (it != s.end())
		{
			//*it = 1;
			cout << *it << " ";
			++it;
		}
		cout << endl;

		for (auto e : s)
		{
			cout << e << " ";
		}
		cout << endl;

		print(s);
	}
}
相关推荐
手握风云-3 小时前
优选算法的层序之径:队列专题
数据结构·算法·leetcode
历程里程碑3 小时前
Protobuf总结
大数据·数据结构·elasticsearch·链表·搜索引擎
lg_cool_3 小时前
Python 框架之py_trees
开发语言·数据结构·python
曹牧3 小时前
svn: svn relocate ‌之externals‌
数据结构·svn
Yiyi_Coding3 小时前
一致性哈希算法
算法·哈希算法
北顾笙9804 小时前
day20-数据结构力扣
数据结构·算法·leetcode
深邃-4 小时前
【C语言】-数据在内存中的存储(2):浮点数在内存中的存储
c语言·开发语言·数据结构·c++·算法·html5
洛水水4 小时前
跳表(Skip List):思想、优劣与应用场景完全解读
数据结构·跳表
会编程的土豆4 小时前
【数据结构与算法】堆排序底层原理
数据结构·c++·算法