C++哈希表

目录

  • 介绍
    • 哈希概念
    • 哈希冲突
    • 哈希函数
    • 解决哈希冲突
  • 闭散列
    • 介绍
      • 线性探测
      • 二次探测
      • 负载因子
    • 实现
      • 哈希表结构
      • 哈希函数
      • 元素查找
      • 插入元素
      • 删除元素
  • 开散列
    • 介绍
    • 实现
      • 哈希表结构
      • 元素查找
      • 插入元素
      • 删除元素
      • 析构函数

介绍

哈希概念

    了解过搜索二叉树与红黑树后,它们的结构特点主要是为了进行快速查找, O ( l o g 2 N ) O(log_2N) O(log2N)的时间复杂度,通常在几次关键码的比较后就能找到目标元素。但是最最理想的搜索,是能够像数组那样,知道元素的下标,直接就可以访问,时间复杂度达到 O ( 1 ) O(1) O(1)

    其实哈希结构就是与数组类似的,通过元素的关键码计算出下标(哈希映射)。通过这一步计算,可以将元素存储到对于位置,同样也可以在对应位置访问该元素。

示例:将6个数字存储到哈希结构中
C++哈希表_第1张图片

哈希冲突

对于两个数据元素i和j,其中关键码 k i ≠ k j k_i \ne k_j ki=kj,但是hash( k i k_i ki) = hash( k j k_j kj)

即:不同关键字通过相同哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

当插入数字9时,进行哈希计算后下标为1,但是该位置以及存在其他元素。
C++哈希表_第2张图片

哈希函数

引起哈希冲突的一个原因:哈希函数不够合理

哈希函数:能将任意长度的关键码映射成固定长度的数据(下标)

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值
    域必须在0到m-1之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该较简单

常见的哈希函数(了解):

  • 直接定址法

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

  • 除留余数法

散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数
H a s h ( k e y ) = k e y   m o d   p ( p < = m ) Hash(key) = key \bmod p (p <= m) Hash(key)=keymodp(p<=m)
选择质数作为除数:由于质数本身不存在多余的因子,所以与其他数做取模运算的结果更加分散

  • 平方取中法

例如:关键字为1234,平方为1522756,选取中间3位:277作为哈希地址

  • 折叠法

折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这
几部分叠加求和,并按散列表表长,取后几位作为散列地址

  • 随机数法

H a s h ( k e y ) = r a n d o m ( k e y ) Hash(key) = random(key) Hash(key)=random(key)

解决哈希冲突

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

闭散列

介绍

闭散列也叫做开放定址法

    当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中下一个空位置中

线性探测

当发生哈希冲突时,从发生冲突的位置开始,依次向后探测,直到找到下一个空位置为止

int index = Hash(key);
while(hashTable[index].state == EXITS)
{
    ++index;//向后遍历
}

对于发生冲突的情况,向后遍历寻找最近的一个空位置—线性探测
C++哈希表_第3张图片

线性探测的缺陷是产生冲突的数据会堆积在一起 ,寻找某个关键码可能需要多次比较,导致效率降低。

二次探测

寻找下一个空位置时,不再挨着逐个去找,选择逐步跳跃式的去找,避免冲突堆积

int index = Hash(key);
int start = index, i = 0;
while(hashTable[index].state == EXITS)
{
    index = start + i * i;
    ++i;
}

插入数字16时,发生冲突,选择二次探测的方式查找空位置
C++哈希表_第4张图片

负载因子

随着插入数据的增多, 插入的数据产生冲突的概率也增加了。冲突的增加,在查找时的效率也会降低。

装载因子(load factor) = 表中有效数据个数 / 空间大小

  • 装载因子越大,数据越多,冲突的可能性越大
  • 装载因子越小,数据越少,冲突的可能性越小

通常来说,当哈希表的装载因子超过0.7时就需要考虑进行扩容,否则可能会导致插入/查找操作的效率大幅下降。

实现

哈希表结构

enum State
{
	EMPTY,	//空状态
	EXITS,	//使用状态
	DELETE,	//删除状态
};

template
struct HashDate
{
	pair _kv; // 存储数据的键值对
	State _state;   // 标记状态
};

template>
class HashTable
{
private:
	vector> _table; // 哈希数组
	size_t _n = 0;  // 有效数据个数,来计算装载因子
};

哈希函数

将k类型的关键码转换为整型

template
    struct HashFunc
    {
        size_t operator()(const K& key)
        {
            return (size_t)key;
        }
    };
// 特化  20:12继续
template<>
struct HashFunc
{
    // BKDR哈希算法
    size_t operator()(const string& key)
    {
        size_t val = 0;
        for (auto ch : key)
        {
            val *= 131;
            val += ch;
        }

        return val;
    }
};

BKDR哈希算法是由徐盛忠教授于1992年提出的一种哈希函数,它是通过将字符串视为整数来计算哈希值的。其基本思想是使用一个较小的质数seed(比如31、131、1313、13131等),将字符串中各个字符的ASCII码转化为整数后,与seed相乘并求和得到一个哈希值

元素查找

HashDate* find(const K& key)
{
    if (_table.size() == 0)//没有元素
    {
        return nullptr;
    }


    HashFunc hf;	//通过仿函数将关键码转换为整型
    size_t index = hf(key) % _table.size();//除留余数法

    // 线性探测
    while (_table[index]._state != EMPTY)
    {
        // 找到待查找元素,并返回它的地址
        if (_table[index]._state == EXITS && 
            _table[index]._kv.first == key)
        {
            return &_table[index];
        }

        ++index;
        index %= _table.size();

    }
    return nullptr;// 不存在此元素
}

插入元素

bool insert(const pair& kv)
{
    if (find(kv.first))
    {
        return false;	// 元素已存在
    }


    if (_table.size() == 0)
    {
        _table.resize(10); //设置哈希表初始空间大小
    }
    // 装载因子超过阈值0.7就进行扩容
    else if( _n * 1.0 /_table.size() > 0.7)
    {
        //创建一个新的哈希表,大小为原哈希表的2倍
        HashTable newHT;
        newHT._table.resize(2 * _table.size());
        //将原数据插入到新哈希表
        for (auto& e : _table)
        {
            if (e._state == EXIST)
            {
                newHT.Insert(e._kv);
            }
        }
        //交换两个哈希表
        _table.swap(newHT._table);
    }

    // 用哈希函数算出哈希表中的映射位置
    HashFunc hf;
    size_t index = hf(kv.first) % _table.size();

    // 线性探测
    while (_table[index]._state == EXITS)
    {
        ++index;
        index %= _table.size(); // 防止下标越界
    }
    //插入元素
    _table[index]._kv = kv;
    _table[index]._state = EXITS;
    ++_n;

    return true;
}

删除元素

bool erase(const K& key)
{
    //查找该元素
    HashDate* ret = find(key);
    if (ret == nullptr)
        return false;	//不存在
    else // 找到
    {
        ret->_state = DELETE;//删除
        --_n;
        return true;
    }
}

开散列

介绍

开散列也叫做链地址法

    首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

C++哈希表_第5张图片
产生哈希冲突的元素会通过链表组织起来,形成一个子集合-----哈希桶

实现

哈希表结构

template
struct HashNode
{
	pair _kv;
	HashNode* _next;

	HashNode(const pair& kv)
		:_kv(kv)
		, _next(nullptr)
	{}
};
template>
class HashTable
{
	typedef HashNode Node;
private:
	vector _tables;//哈希表
	size_t _size = 0; // 存储有效数据个数
};

元素查找

Node* find(const K& key)
{
    if (_table.size() == 0)
        return nullptr;	//哈希表为空

    //哈希映射
    hash hash;
    size_t index = hash(key) % _table.size();	
    Node* cur = _table[index];
    while (cur != nullptr)
    {
        if (cur->_kv.first == key)
            return cur;
        else cur = cur->_next;
    }
    return nullptr;
}

插入元素

bool insert(const pair& kv)
{
    // 去重
    if (Find(kv.first)) return false;

    if (_table.size() == 0)
    {
        _table.resize(53); //设置哈希表初始空间大小
    }
    // 装载因子超过阈值0.7就进行扩容
    else if (_n * 1.0 / _table.size() > 0.7)
    {
        //创建一个新的哈希表,大小为原哈希表的2倍
        vector newTables;
        newTables.resize(__stl_next_prime(_tables.size()), nullptr);
        // 旧表中节点移动映射新表
        Hash hash;
        for (size_t i = 0; i < _tables.size(); ++i)
        {
            Node* cur = _tables[i];//首节点
            while (cur != nullptr)
            {
                Node* next = cur->_next;

                size_t hashi = hash(cur->_kv.first) % newTables.size();
                //头插
                cur->_next = newTables[hashi];
                newTables[hashi] = cur;

                cur = next;
            }

            _tables[i] = nullptr;
        }
        //交换两个哈希表
        _tables.swap(newTables);
    }

    Hash hash;
    size_t hashi = hash(kv.first) % _tables.size();
    // 头插
    Node* newnode = new Node(kv);
    newnode->_next = _tables[hashi];
    _tables[hashi] = newnode;

    ++_size;

    return true;
}
//返回质数
size_t __stl_next_prime(size_t n)
{
    static const size_t __stl_num_primes = 28;
    static const size_t __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
    };

    for (size_t i = 0; i < __stl_num_primes; ++i)
    {
        if (__stl_prime_list[i] > n)
        {
            return __stl_prime_list[i];
        }
    }

    return -1;
}

使用质数作为除数

删除元素

bool erase(const K& key)
{
    //哈希表为空
    if (_tables.size() == 0) return nullptr;

    //find
    Hash hash;
    size_t hashi = hash(key) % _tables.size();
    Node* prev = nullptr;
    Node* cur = _tables[hashi];
    while (cur)
    {
        if (cur->_kv.first == key)
        {
            if (prev == nullptr) //头删
            {
                _tables[hashi] = cur->_next;
            }
            else	//中间删
            {
                prev->_next = cur->_next;
            }

            delete cur;
            --_size;

            return true;
        }

        prev = cur;
        cur = cur->_next;
    }

    return false;
}

析构函数

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

    观看~~

你可能感兴趣的:(一块来学C++,散列表,c++,哈希算法)