HashTable哈希表/散列表(线性探测和二次探测)

本文介绍了哈希表的基本概念及其实现方式,包括直接定址法和除留余数法,并探讨了如何使用哈希表优化特定问题的时间复杂度。此外,还详细解释了哈希冲突的概念及解决方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

HashTable的简单介绍

HashTable是根据关键字直接访问在内存存储的数据结构。
HashTable叫哈希表或者散列表。
它通过一个关键值的函数将所需的数据直接映射到表中的位置来访问数据,这个映射函数叫散列函数(哈希函数),存放记录的数组叫散列表(哈希表)。

比如:
给定一字符串“abckhdgubhkacdggk”找出第一次只出现一次的字符。
对于这个问题,用指针解决,用两个指针,一个指针cur指向当前字符,另一个指针next向后遍历,如果找到cur与next相同,cur指向下一个字符,next再进行遍历,直到next遍历完成所有字符,都与cur不同,则此时的cur指向的字符就是第一个只出现一次的字符。时间复杂度O(n^2)。

这种方法可以解决但是时间复杂度比较高,优化一下,在查找时红黑树和堆排序的查找时间复杂度会低一点,建红黑树然后再根据关键字进行查找。时间复杂度O(nlogn)。

能不能再优化一下?
那就是运用到哈希表,所有字符总共256个,我们可以利用字符对应的ASCII值为下标建立数组。遍历一遍字符串,将每个字符对应的下标ASCII处加1,遍历完成后就从头往后进行查找,查找时直接能找到对应的位置。所以这个时间复杂度就为O(n)。

这就是哈希表的直接定址法。

当对一组10以内的数进行排序,也可以用哈希表。比如:1、3、5、2、5、3、5、8,建立一个数组a[10],将对应的数存放在对应的下标处,出现一次加一下,打印时如果下标对应的是0就不打印,对应1就打印1次,对应的下标里存几就打印几。如下:
这里写图片描述
但是并不是所有的数都适合这种排序,如:1000、1002、1000、1005、1000、1008。最大数是1008,开1008大小的数组前1000个就浪费了,我们只需开(1008 - 1000)的大小就好,对应的下标就是(1008-1000)/1000。
这就是哈希表的除留余数法。

除此外,构造哈希表的方法还有:平方取中法、折叠法、随机数法、数学分析法


哈希冲突(哈希碰撞)

不同的关键字通过相同的哈希函数处理后可能产生相同的值哈希地址。我们称这种情况为哈希冲突或哈希碰撞。(任意的散列函数都不能避免产生冲突)
散列表的载荷因子定义:α = 填入表中的元素 / 表的长度

解决哈希冲突的方法:
1. 闭散列方法-开放定址法
2. 开链法/拉链法


线性探测法和二次探测法

闭散列方法-开方地址法有线性探测法和二次探测法

线性探测法:
这里写图片描述
Hash(key) = key % 10(表长);
89放入9的位置;18放入8的位置;49与89冲突,往后加到尾了就再回到头,0的位置为空放入;58与18冲突,往后加有89,再加有49,再加就放入1的位置;9与89冲突一直加到2的位置放入。
Hash(key) + 0,Hash(key)+1,……

线性探测的增删查:

#pragma once

#include <vector>

enum State
{
    EXIST,
    EMPTY,
    DELETE,
};

template <class K,class V>
struct HashNode
{
    pair<K, V> _kv;
    State _s;//状态
};

template <class K,class V,class HushFunc>
class HashTable
{
    typedef HashNode<K, V> Node;
public:
    HashTable()
        :_size(0)
    {}

    HashTable(size_t n)
        :_size(0)
    {
        _tables.resize(n);
    }

    Node* Find(const K& key)//查找
    {
        size_t index = _HashFunc(key);
        while (_tables[index]._s != EMPTY)
        {
            if (_tables[index]._kv.first == key)
            {
                if (_tables[index]._s == EXIST)
                    return &_tables[index];
                else
                    return NULL;
            }
            index += 1;
            if (index == _tables.size())
            {
                index = 0;
            }
        }
        return NULL;
    }

    bool Insert(const pair<K, V>& kv)//插入
    {
        _Check();//增容

        size_t index = _HashFunc(kv.first);
        while (_tables[index]._s == EXIST)
        {
            if (_tables[index]._kv.first == kv.first)//有key,直接返回
                return false;
            index += 1;
            if (index == _tables.size())//到尾了,就从开头找
                index = 0;
        }
        _tables[index]._kv = kv;
        _tables[index]._s = EXIST;
        ++_size;
        return true;
    }

    bool Remove(const K& key)//删除
    {
        Node* cur = Find(key);//找到了key
        if (cur)
        {
            cur->_s = DELETE;
            --_size;
            return true;
        }
        else
            return false;
    }

protected:
    void _Check()//增容
    {
        if (_tables.size() == 0)
            _tables.resize(10);
        if ((_size * 10) / (_tables.size()) == 7)
        {
            size_t newSize = _tables.size() * 2;
            HashTable<K, V, HushFunc> newHashTable(newSize);
            for (size_t i = 0; i < _tables.size(); ++i)
            {
                if ((_tables[i]._s) == EXIST)
                {
                    newHashTable.Insert(_tables[i]._kv);
                }
            }
            Swap(newHashTable);
        }
    }

    void Swap(HashTable<K, V,HushFunc> &ht)//交换
    {
        swap(_size, ht._size);
        _tables.swap(ht._tables);
    }

    size_t _HashFunc(const K& key)//
    {
        return key % _tables.size();
    }
protected:
    vector<Node> _tables;
    size_t _size;//哈希表实际存放的个数
};

void TestHashTable()
{
    int a[] = { 89, 18, 49, 58, 9 };
    HashTable<int, int,int> ht1;
    for (size_t i = 0; i < sizeof(a) / sizeof(a[0]); ++i)
    {
        ht1.Insert(make_pair(a[i], i));
    }
    ht1.Insert(make_pair(10, 1));
    ht1.Insert(make_pair(11, 1));
    ht1.Insert(make_pair(12, 1));
    ht1.Insert(make_pair(13, 1));
    ht1.Insert(make_pair(14, 1));

    ht1.Find(10);
    ht1.Remove(10);
    ht1.Find(10);
    ht1.Remove(10);
}

二次探测
这里写图片描述
Hash(key) = key % 10(表长);
89放入9的位置;18放入8的位置;49与89冲突,加1^2,到0的位置;58与18冲突,加1^2,到了9的位置,有数继续加2^2到2的位置;9与89冲突,加1^2,在0的位置,继续加2^2,到3的位置放入。
Hash(key)+ i^2(i= 1、2、3……不算第一放入的数)

线性探测和二次探测必须考虑载荷因子,超过0.7-0.8就增容,增大效率,(以空间换时间)
其中删除是惰性删除,也就是只标记删除记号,实际操作则待表格重新整理时再进行,因为HashTable中的每一个元素不仅代表自己,也关系到其他元素。

### 哈希表散列表)的数据结构原理 哈希表是一种支持快速查找操作的数据结构,在理想条件下,其时间复杂度为 O(1)[^2]。这种高效性能源于哈希表内部采用了一种称为哈希函数的技术来映射键到数组中的位置。 #### 哈希表的概念 哈希表利用一种特殊的方式存储数据——不是按照顺序排列而是依据特定算法计算得到的位置放置元素。这种方式使得即使面对大量数据也能保持高效的存取速度[^1]。 #### 哈希函数设计原则 为了使哈希表能够正常工作并发挥最佳效能,需要精心设计哈希函数: - **均匀分布**:好的哈希函数应该尽可能让不同的输入产生不相同的输出值; - **简单快捷**:由于每次访问都要执行该函数,因此它应当具有较高的运算效率; - **减少冲突**:尽管完全消除碰撞是不可能的任务,但仍需努力降低发生概率; 一些常见的哈希函数构建方式包括但不限于直接定址法、除留余数法、平方取中法以及基数转换法等[^3]。 ```cpp // C++ 中简单的哈希函数示例 (除留余数法) int hashFunction(int key, int tableSize){ return key % tableSize; } ``` #### 处理哈希冲突方法 当两个不同关键字经过相同哈希函数变换后指向同一索引时就会引发所谓的“哈希冲突”。针对这种情况有两种主要解决方案: ##### 开放地址法 这种方法是在遇到冲突时不创建新的节点链接起来形成链表,而是在原数组里寻找下一个可用槽位继续尝试插入直到成功为止。具体策略有线性探测二次探查等多种变体形式。 ##### 链地址法(拉链法) 每当检测到已有记录占据目标位置,则新建一个单向链表连接这些溢出项。此方案相对容易实现且易于维护,但可能增加额外空间消耗。 ```cpp struct ListNode { int val; struct ListNode *next; }; class MyHashTable { public: vector<ListNode*> buckets; void insert(int value); }; ``` 对于 Java 的 `HashMap` 类而言,除了上述提到的基础机制外还涉及到了对象比较逻辑。每当涉及到新 Entry 加入或者旧 Entry 查找更新的时候都会先调用 `hashCode()` 方法获取对应 bucket 序号再进一步确认是否真的存在重复 Key 则会触发 `equals()` 进行最终判断[^4]。 ### 实际应用案例分析 在实际编程实践中,开发者可以根据需求选择合适的库或框架所提供的现成容器类型如 STL 中的 unordered_map 或者手写简易版自定义 HashTable 来满足项目所需功能特性。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值