STL中的哈希表简单实现【封装unordered_map与unordered_set】


前言


一、哈希概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(N*logN),搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素

当向该结构中:

  • 插入元素:根据待插入元素的关键码以此函数计算出该元素的存储位置并按此位置进行存放
  • 搜索元素:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功.

该方式即为哈希(散列)方法,哈希方法中使用的 转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)

注意:哈希表的效率取决于哈希函数的设计和哈希表的装载因子

  • 一个好的哈希函数应该能够将元素均匀地映射到不同的位置,从而最小化冲突的发生。
  • 装载因子则是指哈希表中元素的个数与哈希表大小的比值,过高的装载因子会导致冲突的发生概率增加,从而影响搜索效率。

因此,在设计哈希表时需要综合考虑哈希函数的选择和装载因子的控制,以达到较好的性能。


二、常用的哈希函数

1. 直接定制法(常用)

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B

  • 优点:简单、均匀
  • 缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况
    • 处理范围比较大的一组数据时,会浪费许多空间
    • 不能处理浮点数,字符串等等数据

2. 除留余数法(常用)

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

  • 优点:可以处理数据范围比较大的数据;对于非int类型的数据可以转换为int类型数据
  • 缺点:可能将不同关键码(Key)的数据转换到相同的哈希地址上,即存在哈希冲突。
    例如:
    在这里插入图片描述

如果在该集合中插入44,会将不同的值映射到同一个位置上,会出现冲突。

3. 哈希冲突

对于两个数据元素的关键字 k i k_i ki k j k_j kj,有 k i ≠ k j k_i ≠ k_j ki=kj,但有: H a s h ( k i ) = H a s h ( k j ) Hash(k_i) = Hash(k_j) Hash(ki)=Hash(kj),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

发生哈希冲突该如何处理呢?

答:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突,一个好的哈希函数应该能够将元素均匀地映射到不同的位置,从而最小化冲突的发生。

4.哈希冲突解决

解决哈希冲突两种常见的方法是:闭散列和开散列

(1) 闭散列

也叫 开放定址法当发生哈希冲突时,若哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?

<1> 线性探测

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

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

在这里插入图片描述

  • 删除:采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。这也是我们会定义一个HashData的类型而不是直接使用pair/THashData中存放pair/Tstate
//给哈希表中的每一个位置一个状态
//Empty表示该位置是kon,Erase表示该位置的元素被删除,Exit表示该位置有元素
namespace CloseHash
{
	enum State{EMPTY, EXIST, DELETE};
	
	template<class T>
	struct HashData
	{
		T _kv;
		State _state = EMPTY; //默认为空
	}

	template<class K, class T>
	class HashTable
	{
		typedef HashData<T> Node;
	private:
		vector<Node> _table;
		size_t n = 0; //有效数据个数
	}
}

  • 查找只有找到空(empty)才可以说明没有找到这个元素,因为这个元素可以放在Erase和Exit俩个状态的后面。

线性探测:模出来的映射位置已经有值,那就向后线性找一个空位置,存数据。

  • 优点:解决了哈希冲突
  • 缺点
    • 数据越满冲突的可能性越大,因为一个位置越有可能存放其它值;增容可以缓解冲突
    • 易发生踩踏效应,一个位置冲突可能导致连续大位置冲突;此时查找大效率变低;增容可以缓解踩踏效应,但是有没有更好的解决方法呢?
<2>二次探测

二次探测:线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题

找下一个空位置的方法为: H i = ( H 0 + i 2 ) H_i = (H_0 + i^2) Hi=(H0+i2)% m m m,其中: i = 1 , 2 , 3 … i = 1,2,3… i=1,2,3, 是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小

<3> 闭散列哈希表何时增容?如何扩容?

在这里插入图片描述
负载因子 = 存储的有效数据的个数/空间的大小 = n / _table.size()
(这里除以_table的size是因为vectot的 [] 只允许访问_size内的数据)

  • 负载因子越大冲突的概率越高,增删查改的效率越低
  • 负载因子越小冲突的概率越小,增删查改的效率越高,但是空间利用率低,浪费空间

负载因子大于0.7时扩容,扩容后需要重新计算每个数据在新_table中的位置

因此闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。

三、哈希表实现

1. 哈希表的思考

(1) 只能存储key为整形的元素

即只可以存储<int, int> 类型的,其它类型的存储如何解决?

在这里插入图片描述
所以我们在这里提供将Key转换为整形的方法:仿函数

仿函数(Functor)是可以像函数一样被调用的对象。它实际上是一个,重载了函数调用运算符 ​operator()​,使得该类或结构体的对象可以像函数一样被调用

// 哈希函数采用处理余数法,被模的key必须要为整形才可以处理,此处提供将key转化为整形的方法
// 整形数据不需要转化

template<class T>
class HashFun
{
	public:
		size_t operator()(const T& val)
		{
			//对整形数据不处理
			return val;
		}
};

// 模版特化
template<>
class Hashfun<string>
{
	public:
		size_t operator()(const string& str)
		{
			size_t val = 0;
			for(string::iterator it = str.begin(); it!=str.end(); it++)
			{
				val += *it;
				val *= 131;
			}
			return val;
		}
};

注意不可以使用地址来作为它的int值,因为地址会变,相同的字符串会有不同的地址。

(2) 封装时的问题

我们需要使用这个哈希函数来封装unorder_mapunorder_set, 他们的存储的数据是 pair<K,T>和K在插入数据的时候是通过他们的 K转化为int 来进行,查找数据是通过 Key 来完成的;他们在表中的位置都需要由 K 决定:通过HashFun(K)来得到他们的地址,我们如何得到pair中的 K 呢?

答:使用仿函数,对于unorder_set传入的是K,就直接使用;对于unorder_map传入的是pair<K, V>, 使用仿函数对象 Kot 取出对象中的K,才可以使用HashFun(Kot(_kv))

在这里插入图片描述

其中KeyofT我们放在unorder_mapunorder_set中分别进行实现

template<class K>
struct set_KeyofT
{
	//set中
	const K& operator()(const K& key)
	{
		return key;
	}
}

template<class K,class T>
struct map_KeyofT
{
	//map中
	//在map中T实际上就是pair<K,V>
	const K& operator()(const T& kv)
	{
		return kv.first;
	}
};

(3) 哈希表中的模版参数的作用

template<class K, class T, class KeyofT, class HashFunc = HashFun< K >>

  • 参数K:因为 Find( ) 函数的参数是 Key,而我们在使用find的时候不会输入pair<>,所以模版参数重才需要一个Key。对于map不可以用pair中的第一个参数类型代替,因我们无法取出pair中的第一个参数类型,只可以取出pair对象的第一个参数数据。
  • 参数T:是哈希表中实际存储的数据类型,map中是pair<K, V>;并且Insert( )数据时参数是pair,插入参数的方式有两种:1是使用piar<string,int>()构造匿名对象;2是使用make_pair()构造。
  • 参数KeyofT:取T中的Key; 因为V的类型不同,通过valuekey的方式就不同
  • 参数HashFunc:将Key转换为int类型从而确定存储位置
在这里插入代码片

2. 哈希表闭散列实现

哈希表整体框架

概述: 这里采用线性探测的方式构建哈希表,下面是整体框架,其中模板参数第一个是key关键字,第二个是哈希表存储的元素的数据类型,可以是K,也可以是pair<K,V>类型,主要就是为了同时实现K模型KV模型。第三个参数就是一个仿函数,为了获取T中K的值,这里要实现两个仿函数,一个是对K模型,一个是对KV模型。哈希表底层我们借用vector容器来实现。

整体框架如下:

enum State
{
	EMPTY,
	EXITS,
	DELETE
};
template<class T>
struct HashData
{
	T _data;
	State _state;
};

template<class K>
class HashFun
{
    public:
    size_t operator()(const K& key)
    {
        return key;
    }
};


template<>
class HashFun<string>
{
    public:
    size_t operator()(const string& str)
    {
        size_t val = 0;
        //const类型的str需要const类型的迭代器
        for(string::const_iterator it = str.cbegin(); it != str.cend(); it++)
        {
            val += *it;
            val *= 131;
        }
        return val;
    }
};


//使用尖括号 ​<>​ 可以表示模板的实例化。在这里,​HashFun<K>​ 是被实例化为一个具体的类型
template<class K, class T, class KeyofT, class HashFunc=HashFun<K>>
class HashTable
{
	typedef HashData<T> Node;
private:
	vector<Node> _table;
	size_t _num = 0;// 记录已经存放了多少个数据 
};

代码实现如下:

#include <iostream>
#include <string>
#include <vector>
using namespace std;

namespace CloseHash
{
    enum State
    {
        EMPTY,
        EXIT,
        DELETE,
    };

    template<class T>
    struct HashData
    {
        T _data;
        State _state = EMPTY;

        //默认的拷贝构造足够
    };

	//注意仿函数定义为公有
    template<class K>
    class HashFun
    {
        public:
        size_t operator()(const K& key)
        {
            return key;
        }
    };


    template<>
    class HashFun<string>
    {
        public:
        size_t operator()(const string& str)
        {
            size_t val = 0;
            //const类型的str需要const类型的迭代器
            for(string::const_iterator it = str.cbegin(); it != str.cend(); it++)
            {
                val += *it;
                val *= 131;
            }
            return val;
        }
    };

    /*
    //------KeyofT交给map和set的类里面去实现,不用在Hash中实现
    template<class T>
    class KeyofT
    {
        T operator()(const T& key)
        {
            return key;
        }
    };
    */

    //template<class K, class T, class KeyofT, class HashFunc = HashFun<K>>
    //开散列因为需要传入table才需要这些参数,需要找到桶的位置才需要,闭散列需要它来判断end的地方

	//前置声明,因为迭代器和哈希表相互调用
    template<class K, class T, class KeyofT, class HashFunc>
    class HashTable;

    template<class K, class T, class KeyofT, class HashFunc = HashFun<K>>
    class _HIterator
    {
        typedef HashData<T> Node;
        typedef _HIterator<K,T,KeyofT,HashFunc> Self;
        typedef HashTable<K,T,KeyofT,HashFunc> HF;
        //传入哈希表是为了确定_table的大小,从而确定end的位置
    public:
        _HIterator(Node* node = nullptr,HF* phf = nullptr)
        {
            _node = node;
            _phf = phf;
        }

        Self& operator++()
        {
            //遍历无序
            //只有存在才停止

            // 找到当前迭代器所指向表的下标
            KeyofT kot;
            HashFunc hf;
            size_t index = hf(kot(_node->_data)) % (_phf->_table.size());
            //寻找下一个位置 需要先指向下一个位置
            index++;
            cout<<"index:"<<index<<endl;
            //查看该位置是否越界
            if(index >= _phf->_table.size())
            {   
                // 越界则nullptr
                _node = nullptr;
                return *this;
            }
            // 没有越界则使_node指向下一个数据结点
            _node++;

            while(_node->_state!=EXIT)
            {
                // 在没有越界的情况下循环查找下一个数据的位置
                // 注意:不可以将第一次index++加入循环中,否则会返回自身
                // 注意:第一次需要单独判断

                // 除去第一次需要在外面判断,其余在循环内判断是否越界
                if(index >= _phf->_table.size())
                {
                    // 如果在寻找的过程中没有找到,越界
                    _node = nullptr;
                    return *this;
                }
                // 没有越界则找下一个位置
                index++;
                _node++;
                
                // 测试代码
                cout<<"index:"<<index<<endl;
            }

            // 循环退出,表示找到 ExIT 了
            return *this;
        }


        bool operator != (const Self& it)const
        {
            return _node != it._node;
        }

        bool operator == (const Self& it)const
        {
            return _node == it._node;
        }

        T& operator*()
        {
            return _node->_data;
        }

        T* operator->()
        {
        	// _data是一个pair对象,存在k和v
        	// 重载->可以访问k和v
        	// 如:it->k
            return &_node->_data;
        }

        Node* _node;
        HF* _phf; //指向哈希表的指针
    };


    template<class K, class T, class KeyofT, class HashFunc = HashFun<K>>
    class HashTable
    {
        typedef HashData<T> Node;
        typedef HashTable<K,T,KeyofT,HashFunc> Self;

        //-------------------------------
        //类模板作为类模板的友元时,模板参数不可以取一样的!!!!!
        template<class K1, class T1, class KeyofT1, class HashFunc1>
        friend class _HIterator;  //迭代器访问了HT的私有变量_table,需要友员
        //友元表示迭代器可以方法HT的私有变量

    public:
        typedef _HIterator<K,T,KeyofT,HashFunc> iterator;
        // 这里目前不实现哈希的 const_iterator

        HashTable() = default;

        //拷贝构造函数必须使用引用传参,否则无穷递归
        HashTable(const HashTable& ht)
        {
            _n = ht._n;
            _table.resize(ht.size());
            //在编译到这里的时候vec没有生成具体的类,所以无法在vector中找迭代器类,加typename解决
            typename vector<Node>::iterator it = _table.begin();
            typename vector<Node>::const_iterator cit = ht._table.cbegin();
            while(cit != ht._table.end())
            {
            	// 赋值完成后才进行递增
                *it++ = *cit++;
            }
        }

        //赋值运算符重载返回引用可以减少拷贝构造,因为函数结束时会创建一个临时的 HashTable 对象来存储返回的值。
        //参数不能使用引用,使用引用会导致原HT被修改
        //通过返回 *this,即当前对象的引用,可以实现链式赋值操作
        Self& operator=(const HashTable hf)
        {
            _table.swap(hf._table);
            _n = hf._n;
            return *this;
        }

        iterator begin()
        {
            size_t index = 0;
            while(index < _table.size())
            {
                if(_table[index]._state==EXIT)
                {
                    return iterator(&_table[index],this);
                }
                index++;
            }
            return end();
        }

        //~HashTable()

        iterator end()
        {
            return iterator(nullptr,this);
        }

        size_t size()
        {
            return _table.size();
        }

        //查找
        iterator Find(const K& key)
        {
            if(_table.size() == 0)
                return end();

            HashFunc hf;  //计算转为整型的哈希函数
            KeyofT kot;  //使用kot来取_table中data的key
            int index = hf(key) % _table.size();
            //因为有对index取余,所以index不会超过_table的大小

			// 注意:如果当前index对应的位置为空,并不表示没有找到元素
			// 而是表示在哈希表中没有该键对应的元素
            while(_table[index]._state != EMPTY)
            {
                if(_table[index]._state == EXIT && kot(_table[index]._data)==key)
                {
                    return iterator(&_table[index],this);
                }
                index++;
                //----------------
                index %= _table.size();
            }
            return end();
        }

        //插入
        pair<iterator,bool> Insert(const T& data)
        {
            HashFunc hf;
            KeyofT kot;
            //int index = hf(kot(data));
            iterator ret = Find(kot(data));
            //注意判断方式,ret无法独自作为判断依据
            if(ret!=end())
               return make_pair(ret,false);

            if(_table.size()==0 || _n/_table.size() > 0.7)
            {
                //不可以在原来table的基础上resize,因为要把旧数据重新放入新表中,使用同一个表会导致数据丢失
                vector<Node> newtable;
                size_t newsize = _table.size()==0? 10:_table.size()*2;
                newtable.resize(newsize);
                for(int i = 0; i < _table.size(); i++)
                {
                    //旧表中存在数据重新计算新位置
                    //删除 的数据并不需要重新计算
                    if(_table[i]._state == EXIT)
                    {
                        size_t newindex = hf(kot(_table[i]._data)) % newsize;
                        newtable[newindex] = _table[i];
                    }
                }
                _table.swap(newtable);
            }

            _n += 1;
            size_t i = 1; //二次探测
            size_t newindex = hf(kot(data)) % _table.size();
            size_t start = newindex;

            while(_table[newindex]._state == EXIT)  //如果数据存在就一直向后探测
            {
                newindex = start + i*i;
                newindex %= _table.size();
                i++;
            }
            _table[newindex]._data = data;
            _table[newindex]._state = EXIT;

            return make_pair(iterator(&_table[newindex],this),true);
        }

        //删除
        bool Erase(const K& key)
        {
            iterator ret = Find(key);
            if(ret==end())
            {
                return false;
            }
            else
            {
                //ret->_node._state = DELETE;
                //迭代器里面的_node指针指向hashdata中的_state
                (ret._node)->_state = DELETE;
                _n--;
                return true;
            }

            /*
            HashFunc hf;
            KeyofT kot;
            size_t index = hf(key) % _table.size();
            size_t start = index. i = 1;
            while(kot(_table[index]._data) != key)
            {
                index = start + i*i;
                index %= _table.size();
                i++;
            }
            */
        }

    private:
        vector<Node> _table;
        size_t _n = 0; //有效数据个数
    };
    
} //命名空间后无封号

int test1()
{
    CloseHash::HashTable<int,int,KOT<int>> HT;
    HT.Insert(2);
    auto ret = HT.Find(2);
    if(ret!=HT.end())
        cout<<"存在"<<'\n';

    cout<<HT.size()<<endl;
    HT.Erase(2);
    cout<<HT.size()<<endl;
    //cout<<HT._n<<endl;
    ret = HT.Find(2);
    if(ret==HT.end())
        cout<<"不存在"<<'\n';

    HT.Insert(17);
    HT.Insert(12);
    HT.Insert(3);
    HT.Insert(8);
    HT.Insert(6);
    HT.Insert(21);
    CloseHash::HashTable<int,int,KOT<int>>::iterator it = HT.begin();
    for(;it!=HT.end();++it)
    {
        cout<<*it<<" ";
        cout<<it._node<<endl;
    }
    //结果21 12 3 6 17 8;是无序的
    return 0;
}
存在
10
10
不存在
size:10
21 0x131ac28 
index:2
12 0x131ac30 
index:3
3 0x131ac38 
index:4
index:5
index:6
6 0x131ac50 
index:7
17 0x131ac58 
index:8
8 0x131ac60 
index:9
index:10

(1)typename的作用

在C++中,typename的作用主要有以下几点:

  1. 模板定义中:C++标准模板中typename用来指定后面的名称为类型。在C++模板编写中,如果要在模板内定义一个嵌套从属于模板参数的类型时,需要使用typename关键字说明。如:
template<class T>
class X {
 public:
   typedef typename T::size_type size_type;
};

这里typename是用来声明T::size_type是一个类型,而不是一个静态成员。

  1. 解决嵌套/依赖名问题:C++编译器在处理模板定义时,通常不能知道某个特定名字是否表示类型,这时候可以通过typename关键字告诉编译器,它后面的名字应当被视为类型名。例如:
template<class T>
class X {
public:
    void fun() {
        typename T::SubType * ptr;   // 这里告诉编译器,T::SubType 是一个类型名
    }
};

注意,typename不可以用在基类列表或者成员初始化列表里面,也不能用于指定模板参数。这些地方必须得用class。

(2)返回非引用类型

在C++中,当函数返回时,会进行以下操作:

  1. 返回值的拷贝:如果函数的返回类型是一个非引用类型,那么会将返回值从函数内部拷贝到调用者的栈空间或寄存器中。这个过程涉及到对拷贝构造函数的调用。
  2. 清理局部变量:函数返回后,局部变量和临时对象会被销毁和清理。析构函数将在这些对象的生命周期结束时进行调用,以释放资源或执行其他必要的清理操作。
  3. 恢复调用栈:返回操作会从函数调用栈中弹出当前函数的帧,将控制权返回到调用者。此过程涉及恢复调用者保存的上下文信息,例如程序计数器(PC)和活动的栈帧指针。

返回操作涉及对返回值的拷贝、局部变量的清理和调用栈的恢复。当函数返回一个引用时,实际上返回的是对某个对象的别名或引用。这意味着返回的引用与被引用的对象共享相同的内存地址。返回引用类型 可以避免额外的拷贝构造函数调用和内存拷贝操作。

(3)常量对象\常量成员函数\常引用

<1> 常量对象

常量对象(constant object)是指被声明为 const 的对象。也就是说,常量对象在声明后不可被修改。常量对象可以是基本数据类型(如 intdouble 等),也可以是自定义的类对象。

常量对象值不可修改:常量对象的值在声明后不能被修改。任何试图修改常量对象的操作都会引发编译错误。

常量对象主要用于以下情况:

  1. 表示常量值:常量对象用于表示某个值在整个程序中保持恒定不变的情况。
  2. 作为函数参数:常量对象可以被传递给接受常量引用参数的函数。这样可以确保函数内部不会修改传入的对象。

以下是一个示例:

class Point {
public:
    int getX() const { return x; }
    int getY() const { return y; }
private:
    int x;
    int y;
};

void printPoint(const Point& p) {
    std::cout << "Point: (" << p.getX() << ", " << p.getY() << ")" << std::endl;
}

int main() {
    const int num = 10;            // 声明一个常量整数对象
    const double PI = 3.14159;     // 声明一个常量浮点数对象
    const Point origin {0, 0};     // 声明一个常量 Point 对象

    // 试图修改常量对象的值会导致编译错误
    // num = 20;   // 错误:常量对象值不可修改

    printPoint(origin);  // 通过常量引用参数传递常量 Point 对象

    return 0;
}

在上述示例中,numPI 是常量对象,它们的值在声明之后不可修改。origin 是一个常量 Point 对象,也就是常量对象的一个实例。这些常量对象在程序执行过程中是只读的,并且可以被传递给常量引用参数的函数使用。

<2> 常量成员函数

在函数后面使用 const 关键字修饰的作用是将函数声明为常量成员函数。常量成员函数不会修改对象的状态,并且可以在常量对象上调用。具体作用如下:

  1. 保证不修改对象状态:通过将函数声明为常量成员函数,可以确保在函数内部不会修改对象的成员变量。这样做有助于提高代码的安全性和可维护性。
  2. 支持在常量对象上调用:常量成员函数可以在常量对象上调用,而非常量成员函数无法在常量对象上直接调用。

举例说明:

class MyClass {
public:
    int getData() const {   // 声明为常量成员函数
        // 不修改对象的成员变量
        return data;
    }
    
    void setData(int newData) {   // 非常量成员函数
        // 修改对象的成员变量
        data = newData;
    }
private:
    int data;
};

在上述例子中,getData() 函数被声明为常量成员函数,而 setData() 函数没有被声明为常量成员函数。

  • 如果使用常量成员函数getData()调用对象,可以在常量对象 非常量对象 上直接调用 getData()
  • 如果使用非常量成员函数setData()调用对象,则只可以对非常量对象上调用setData(),不能在常量对象上调用 setData()
  • 这是因为常量成员函数承诺不会修改对象的状态,所以可以放心地在常量对象上调用。

如果没有使用 const 修饰符,则表示该函数可以修改对象的状态,并且无论是否是常量对象,都可以在其上调用该函数。这样可能导致在非常量对象上调用该函数时修改对象的状态,可能引入意外的副作用。总之,通过在函数后面使用 const 关键字修饰,可以明确函数的行为约定,并限制函数对对象的修改。

常量成员函数的重载:两个成员函数,名字和参数表都一样,但是一个是const,一个不是,那么是算是重载。

成员函数的重载是指在同一个类中创建多个同名函数,但它们的参数列表不同。在调用这些函数时,编译器会根据提供的参数来确定应该调用哪个函数。这使得我们可以根据不同的参数类型或数量来执行不同的操作。const成员函数与非const成员函数可以被重载。当我们在类中定义一个成员函数时,可以为同一个函数创建一个const版本和一个非const版本

class Sample
{
public:
    Sample() { m_value = 1; }
    int GetValue() const { return m_value; } // 常量成员函数
    int GetValue() { return 2*m_value; } // 普通成员函数
    int m_value;
};

int main()
{
    const Sample obj1;
    std::cout << "常量成员函数 " << obj1.GetValue() << std::endl;
    
    Sample obj2;
    std::cout << "普通成员函数 " << obj2.GetValue() << std::endl;
}

执行结果

常量成员函数 1
普通成员函数 2
<4> 常引用

引用前面可以加const关键字,成为常引用。不能通过常引用修改其引用的变量的,常引用(Constant Reference)可以让我们以只读方式访问对象的值

对象作为函数的参数时,生产该对象参数是需要调用复制构造函数的,这样效率就比较低。用指针作为参数,代码又不好看,如何解决呢?

可以用对象的引用作为参数,防止引发复制构造函数,如:

class Sample
{
    ...  
};

void Func(Sample & o) // 对象的引用作为参数
{
    ...
}

但是有个问题,对象引用作为函数的参数有一定的风险性,若函数中不小心修改了形参0,则实参也会跟着变,这可能不是我们想要的,如何避免呢?

可以用对象的常引用作为参数,如:

class Sample
{
    ...  
};

void Func(const Sample & o) // 对象的常引用作为参数
{
    ...
}

这样函数中就能确保不会出现无意中更改o值的语句了。

3. 哈希冲突的另一种解决方法----开散列

(1) 开散列概念

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
在这里插入图片描述
开散列中每个桶中放的都是发生哈希冲突的元素

(2) 开散列增容

<1> 何时增容

桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容

<2> 如何增容
  • 改变结点指向,不需要创造新节点
    在这里插入图片描述
  • 研究表明当表的长度为素数时,向表中插入新数据时冲突概率极小,那么在除留余数法中,最好模一个素数,如何每次快速取一个类似两倍关系的素数?
size_t GetNextPrime(size_t prime)
{
	const int PRIMECOUNT = 28;
	static const size_t primeList[PRIMECOUNT] =
	{
		53ul, 97ul, 193ul, 389ul, 769ul,
		1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
		49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
		1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
		50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,
		1610612741ul, 3221225473ul, 4294967291ul
	};
	size_t i = 0;
	for (; i < PRIMECOUNT; ++i)
	{
		if (primeList[i] > prime)
			return primeList[i];
	}
	return primeList[i];
}

4. 开散列与闭散列比较

应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上: 由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用开散列的效率比闭散列高。

所以实际中哈希桶的结构更加实用

  • 空间利用率高
  • 极端情况下有解决方案:在数据不多,负载因子低,但是大部分数据冲突时,可以将冲突数据多的这个桶改为红黑树结构。(在java中桶超过8个就好进行优化)要想变为红黑树那么哈希表中可以做一个联合union来减省空间。

5. 开散列实现

迭代器实现函数:

// 构造
// 拷贝构造

Self operator++()

Ref operator*()

Ptr operator->()

bool operator!=(const Self& s)

bool operator==(const Self& s)

哈希桶实现函数:

// 构造
HashTable() = default;
// 拷贝构造
HashTable(const HashTable& ht)
// 析构
~HashTable()
// 赋值重载
Self& operator=(const HashTable hf)

// 普通迭代器begin
iterator begin()
// const迭代器begin // 对begin的重载
const_iterator begin()const
// 普通迭代器end
iterator end()
// const迭代器end // 对end的重载
const_iterator end() const

size_t size()

size_t GetNextPrime(size_t prime)

iterator Find(const K& key)

pair<iterator,bool> Insert(const T& data)

bool Erase(const K& key)

具体实现:

#include <iostream>
#include <string>
#include <vector>
using namespace std;


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

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

	//注意仿函数定义为公有
    template<class K>
    class Hash
    {
        public:
        size_t operator()(const K& key)
        {
            return key;
        }
    };


    template<>
    class Hash<string>
    {
        public:
        size_t operator()(const string& str)
        {
            size_t val = 0;
            //const类型的str需要const类型的迭代器
            for(string::const_iterator it = str.cbegin(); it != str.cend(); it++)
            {
                val += *it;
                val *= 131;
            }
            return val;
        }
    };

    //前置声明
    template<class K,class T,class KeyofT,class HashFunc>
    class HashTable;

    template<class K,class T,class Ref,class Ptr,class KeyofT,class HashFunc = Hash<K>>
    class _HIterator
    {
        typedef _HIterator<K,T,Ref,Ptr,KeyofT,HashFunc> Self;
        typedef HashTable<K,T,KeyofT,HashFunc> HT;
        //********typedef HashNode<T>* pNode; //vector中数据的类型
        typedef HashNode<T> Node;

    public:
        _HIterator(Node* node = nullptr,const HT* ht=nullptr)
        {
            _node = node;
            _pht = ht;
        }
        
		__HashIterator(const Iterator& it)
			:_node(it._node)
			, _ht(it._ht)
		{}

        //----------------------------------------------------------------------------------
        //vector中存储的是结点的指针,而数据是存在每一个结点上的,我们不需要让迭代器在vector上行走
        //只需要让迭代器在每一个结点上行走,所以我们不需要Node**, 只需要Node*
        //vector只是让我们找到下一个结点(桶)的工具
        Self& operator++()
        {
            KeyofT kot;
            HashFunc hf;

            if(_node->_next == nullptr) //如果当前桶只有一个数据,就找表中的下一个位置
            {
                size_t index = hf(kot(_node->_data)) % (_pht->_table.size());
                ++index;//下一个桶的下标
                _node = _pht->_table[index];

                //如果下一个位置为空,就继续找洗一个,直到不为空
                while(_node==nullptr)
                {
                    index++;

                    if(index >= _pht->_table.size())
                    {
                        _node = nullptr;
                        return *this;
                    }

                    _node = _pht->_table[index];
                }
            }else
            {
                _node = _node->_next;
            }

            return *this;
        }

		//Self operator++(int)

		//T& operator*() 与下方的是相同的,引入ref是为了写const T&
        Ref operator*()
        {
            return _node->_data;
        }

		//T* operator->()
        Ptr operator->()
        {
            return &(_node->_data);
        }

        bool operator!=(const Self& it)const
        {
            return _node != it._node;
        }

        bool operator==(const Self& it)const
        {
            return _node == it._node;
        }

        //pNode* _pnode; //指向vector中数据的指针
        Node* _node;
        const HT* _pht;
    };




    template<class K,class T,class KeyofT,class HashFunc = Hash<K>>
    class HashTable
    {
        typedef HashNode<T> Node;
        typedef HashTable<K,T,KeyofT,HashFunc> Self;

        template<class K1,class T1,class Ref1,class Ptr1,class KeyofT1,class HashFunc1>
        friend class _HIterator;

    public:
        typedef _HIterator<K,T,T&,T*,KeyofT,HashFunc> iterator;
        typedef _HIterator<K,T,const T&,const T*,KeyofT,HashFunc> const_iterator;

        HashTable() = default;

        HashTable(const HashTable& ht)
        {
            _n = ht._n;
            _table.resize(ht.size());
            typename vector<Node*>::iterator it = _table.begin();
            typename vector<Node*>::const_iterator cit = ht._table.cbegin();
            while(cit != ht.end())
            {
                *it++ = *cit++;
            }
        }

		~HashTable()
		{
			for (auto& cur : _tables)
			{
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				cur = nullptr;
			}
		}

		Self& operator=(const HashTable hf)
        {
            _table.swap(hf._table);
            _n = hf._n;
            return *this;
        }

        //------------------------------

        iterator begin()
        {
            size_t index = 0;
            while(_table[index] == nullptr)
            {
                ++index;
                if(_table[index]!=nullptr)
                {
                    return iterator(_table[index],this);
                }
            }
            return end();
        }


		// const对象只能使用const迭代器
        const_iterator begin()const
        {
            size_t index = 0;
            while(_table[index] == nullptr)
            {
                ++index;
                if(_table[index]!=nullptr)
                {
                    return const_iterator(_table[index],this);
                }
            }
            return end();
        }


        iterator end()
        {
            return iterator(nullptr,this);
        }

		const_iterator end() const
		{
			return const_iterator(nullptr, this);
		}

        size_t size()
        {
            return _table.size();
        }

        //------------------------------

        size_t GetNextPrime(size_t prime)
        {
            const int PRIMECOUNT = 28;
            static const size_t primeList[PRIMECOUNT] =
            {
                53ul, 97ul, 193ul, 389ul, 769ul,
                1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
                49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
                1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
                50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,
                1610612741ul, 3221225473ul, 4294967291ul
            };
            size_t i = 0;
            for (; i < PRIMECOUNT; ++i)
            {
                if (primeList[i] > prime)
                    return primeList[i];
            }
            return primeList[i];
        }

        iterator Find(const K& key)
        {
            if(_table.size()==0)
                return end();

            KeyofT kot;
            HashFunc hf;
            //先找到桶的下标
            size_t index = hf(key) % _table.size();
            //if(_table[index]) //如果对应位置存在数据,就在这个桶中找
            Node* cur = _table[index]; // 桶中存放的是指针

            //如果桶中有数据就进入循环,否则返回end()
            //cur不是nullptr就表示该桶中有数据
            while(cur!=nullptr)
            {
                if(kot(cur->_data)==key)
                {
                    return iterator(cur,this);
                }else
                {
                	// 在这个桶中继续找下一个
                	cur = cur->_next;
                }
            }
            // 桶中没有数据了
            return end();
        }

        pair<iterator,bool> Insert(const T& data)
        {
            KeyofT kot;
            HashFunc hf;
            auto ret = Find(kot(data));
            // 找到了就表示可以插入
            // 没找到就表示K不合法,不可以插入
            if(ret!=end())
                return make_pair(ret,false);

            _n++;
            if(_table.size() == 0 || (double)_n / (double)_table.size() >= 1)
            {
                vector<Node*> newtable;
                newtable.resize(GetNextPrime(_table.size()));

                //将旧表中的结点移动到新表中
                size_t index = 0;
                for(;index < _table.size(); index++)
                {
                    //如果旧表中有数据的话
                    //如果当前桶有数据
                    if(_table[index])
                    {
                        Node* cur = _table[index];
                        // 直到当前桶没有数据
                        while(cur)
                        {
                        	// 先记录下下一个结点
                            Node* Next = cur->_next;
                            size_t newindex = hf(kot(_table[index]->_data)) % newtable.size();

                            //移动结点
                            //头插
                            //错误,这样写就把newtable中的新结点丢掉了 : newtable[newindex]->_next = cur;
                            cur->_next = newtable[newindex]; //让新的结点的后面连接以前的结点
                            newtable[newindex] = cur;

                            //这样写虽然旧表中依然指向那些结点但是最后把他们值null就好了

                            cur = Next; //移动到旧结点的cur的下一个结点
                        }
                    }
                    _table[index] = nullptr;
                }

                _table.swap(newtable);
            }


            size_t index = hf(kot(data)) % _table.size();

            Node* newNode = new Node(data);
            newNode->_next = _table[index];
            _table[index] = newNode;

            return make_pair(iterator(newNode,this),true);
        }

        bool Erase(const K& key)
        {
            HashFunc hf;
            KeyofT kot;
            size_t index = hf(key) % _table.size();
            Node* prev = nullptr;
            Node* cur = _table[index];

            while(cur)
            {
                if(kot(cur->_data)==key)
                {
                    //(1)头删除
                    if(_table[index]==cur)
                    {
                        _table[index] = cur->_next;
                    }
                    //(2)
                    else
                    {
                        prev->_next = cur->_next;
                    }

                    --_n;
                    delete cur;
                    return true;
                }else
                {
                    prev = cur;
                    cur = cur->_next;
                }
            }

            return false;
        }


    private:
        vector<Node*> _table;
        size_t _n = 0; //有效数据个数
    };
};

template<class K>
class KOT
{
    public:
    size_t operator()(const K& key)
    {
        return key;
    }
};

int test2()
{
    OpenHash::HashTable<int,int,KOT<int>> HT;
    HT.Insert(2);
    cout<<"ssss";
    auto ret = HT.Find(2);
    if(ret!=HT.end())
        cout<<"存在"<<'\n';

    cout<<HT.size()<<endl;
    HT.Erase(2);
    cout<<HT.size()<<endl;
    //cout<<HT._n<<endl;
    ret = HT.Find(2);
    if(ret==HT.end())
        cout<<"bu存在"<<'\n';

    HT.Insert(17);
    HT.Insert(12);
    HT.Insert(3);
    HT.Insert(8);
    HT.Insert(6);
    HT.Insert(24);
    HT.Insert(22);
    HT.Insert(45);
    HT.Insert(11);
    HT.Insert(10);
    HT.Insert(36);
    HT.Insert(62);
    HT.Insert(67);
    OpenHash::HashTable<int,int,KOT<int>>::iterator it = HT.begin();
    for(;it!=HT.end();++it)
    {
        cout<<*it<<" ";
    }
    //结果3 6 8 62 10 11 12 67 17 22 24 36 451是无序的
    return 0;
}

(1)难点:const_iterator的实现

注意:如果不引入RefPtr就会出现下面的情况,无法区分普通迭代器和const迭代器

typedef _HIterator<K,T,KeyofT,HashFunc> iterator;
typedef _HIterator<K,T,KeyofT,HashFunc> const_iterator;

(2)难点:插入算法优化

在扩容的时候,新创建节点,将新结点写入值并添加到新的哈希表中,实现如下:

pair<iterator, bool> Insert(const T& data)
{
	KeyOfT kot;
	HashFunc hf;
	iterator it = Find(kot(data));
	if (it != end())
	{
		return make_pair(it, false);
	}
	++_n;
	
	if (_table.size() == 0 || (double)_n / (double)_table.size() >= 1)
	{
		size_t newsize = GetNextPrime(_tables.size());
		HashTable<K,T,KeyofT,HashFunc> newht;
		newht._tables.resize(newsize);
		// 在旧表中循环循环每一个桶
		for (auto cur : _tables)
		{
			//如果该桶中指针不是空
			//就表示该桶有数据,循环把该桶中的数据插入到新表中

			//其中cur是_table中的每一个元素,即(Node*类型)的指针
		    while (cur)
			{
				newht.Insert(cur->_data);
				cur = cur->_next;
			}
		}
		_tables.swap(newht._tables);
	}
	

	size_t index = hf(kot(data)) % _tables.size();
	// 头插
	Node* newnode = new Node(data);
	newnode->_next = _tables[index];
	_tables[index] = newnode;

	return make_pair(iterator(newnode, this), true);;
}

如果这样写的话,原本hashtable._table中里面的节点链也需要进行释放,即析构;虽然vector在默认析构函数中会调用它默认的析构函数,但是vector中指针所指向的地址空间不会被释放/析构,所以在析构函数中需要额外操作:

~HashTable()
{
	for(auto& cur _table)
	{
		while(cur)
		{
			Node* next = cur->_next;
			delete cur;
			cur = next;
		}
		cut = nullptr;
	}
	_table.clear();
    _table.shrink_to_fit();
}

这样写由于每一次扩容的时候,都需要重新创建旧表中的所有结点,并然后将旧表中的结点全部释放,这样不如我们直接使用旧表中的结点,将旧表中的节点当作newnode头插入新表中,就如我们上方实现的那样


四、unorder_map 封装

#pragma once
#include "HashTable.h"

namespace LZH
{
	template<class K, class V, class Hash = HashFunc<K>>
	class unordered_map
	{
	public:
		struct MapKeyOfT
		{
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;
			}
		};
	public:
		//在C++中,如果 ​HashTable​是一个模板类而不是一个对象,那么它的内部类型应该使用双冒号 ​::​来访问,而不是点号 ​.​
		typedef typename OpenHash::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::iterator iterator;
		typedef typename OpenHash::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::const_iterator 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 pair<K, V>& kv)
		{
			return _ht.Insert(kv);
		}

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

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

		bool erase(const K& key)
		{
			return _ht.Erase(key);
		}

	private:
		OpenHash::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
	};
	
	//测试
	void test_unordered_map1()
	{
		unordered_map<int, int> m;
		m.insert(make_pair(1, 1));
		m.insert(make_pair(3, 3));
		m.insert(make_pair(2, 2));

		unordered_map<int, int>::iterator it = m.begin();
		while (it != m.end())
		{
			//it->first = 1;
			//it->second = 1;

			cout << it->first << ":" << it->second << endl;
			++it;
		}
		cout << endl;
	}

	void test_unordered_map2()
	{
		string arr[] = { "西瓜", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉", "梨" };
		unordered_map<string, int> countMap;
		for (auto& e : arr)
		{
			countMap[e]++;
		}

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

五、unorder_set 封装

#pragma once

#include "HashTable.h"

namespace LZH
{
	template<class K, class Hash = HashFunc<K>>
	class unordered_set
	{
	public:
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		//在C++中,如果 ​HashTable​是一个模板类而不是一个对象,那么它的内部类型应该使用双冒号 ​::​来访问,而不是点号 
		typedef typename OpenHash::HashTable<K, K, SetKeyOfT, Hash>::const_iterator iterator;
		typedef typename OpenHash::HashTable<K, K, SetKeyOfT, Hash>::const_iterator 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:
		OpenHash::HashTable<K, 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;
	}

	void test_unordered_set1()
	{
		int a[] = { 3, 33, 2, 13, 5, 12, 1002 };
		unordered_set<int> s;
		for (auto e : a)
		{
			s.insert(e);
		}

		s.insert(54);
		s.insert(107);


		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);
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值