【数据结构】C++实现哈希表

news2025/1/9 1:03:23

闭散列哈希表

哈希表的结构

在闭散列的哈希表中,哈希表每个位置除了存储所给数据之外,还应该存储该位置当前的状态,哈希表中每个位置的可能状态如下:

  1. EMPTY(无数据的空位置)。
  2. EXIST(已存储数据)。
  3. DELETE(原本有数据,但现在被删除了)。

我们可以用枚举定义这三个状态。

// 闭散列哈希表
enum State {
    EMPTY,// 哈希表位置为NULL
    EXITS,// 哈希表位置有值了
    DELETE// 哈希表位置为删除标志
};

为什么需要标识哈希表中每个位置的状态?

若是不设置哈希表中每个位置的状态,那么在哈希表中查找数据的时候可能是这样的。以除留余数法的线性探测为例,我们若是要判断下面这个哈希表是否存在元素40,步骤如下:

  1. 通过除留余数法求得元素40在该哈希表中的哈希地址是0。
  2. 从0下标开始向后进行查找,若找到了40则说明存在。

但是我们在寻找元素40时,不可能从0下标开始将整个哈希表全部遍历一次,这样就失去了哈希的意义。我们只需要从0下标开始往后查找,直到找到元素40判定为存在,或是找到一个空位置判定为不存在即可。

在这里插入图片描述

因为线性探测在为冲突元素寻找下一个位置时是依次往后寻找的,既然我们已经找到了一个空位置,那就说明这个空位置的后面不会再有从下标0位置开始冲突的元素了。比如我们要判断该哈希表中是否存在元素90,步骤如下:

  1. 通过除留余数法求得元素90在该哈希表中的哈希地址是0。
  2. 从0下标开始向后进行查找,直到找到下标为5的空位置,停止查找,判定元素90不存在。

但这种方式是不可行的,原因如下:

  1. 如何标识一个空位置?用数字0吗?那如果我们要存储的元素就是0怎么办?因此我们必须要单独给每个位置设置一个状态字段。
  2. 如果只给哈希表中的每个位置设置存在和不存在两种状态,那么当遇到下面这种情况时就会出现错误。

我们先将上述哈希表当中的元素1000找到,并将其删除,此时我们要判断当前哈希表当中是否存在元素40,当我们从0下标开始往后找到2下标(空位置)时,我们就应该停下来,此时并没有找到元素40,但是元素40却在哈希表中存在。

在这里插入图片描述

因此我们必须为哈希表中的每一个位置设置一个状态,并且每个位置的状态应该有三种可能,当哈希表中的一个元素被删除后,我们不应该简单的将该位置的状态设置为EMPTY,而是应该将该位置的状态设置为DELETE。

这样一来,当我们在哈希表中查找元素的过程中,若当前位置的元素与待查找的元素不匹配,但是当前位置的状态是EXIST或是DELETE,那么我们都应该继续往后进行查找,而当我们插入元素的时候,可以将元素插入到状态为EMPTY或是DELETE的位置。

因此,闭散列的哈希表中的每个位置存储的结构,应该包括所给数据和该位置的当前状态。

template<class K, class V>
struct HashData {
    pair<K, V> _kv;
    State _state = EMPTY;//状态初始化为空
};

而为了在插入元素时好计算当前哈希表的负载因子,我们还应该时刻存储整个哈希表中的有效元素个数,当负载因子过大时就应该进行哈希表的增容。

//哈希表
template<class K, class V>
class HashTable{
public:
	//...
private:
    vector<HashData<K, V>> _tables;// 将Hash值存放在vector中
    size_t _n = 0;                 // 存储的数据个数
};

哈希表的查找

在哈希表中查找数据的步骤如下:

  1. 先判断哈希表的大小是否为0,若为0则查找失败。
  2. 通过哈希函数计算出对应的哈希地址。
  3. 从哈希地址处开始,采用线性探测向后向后进行数据的查找,直到找到待查找的元素判定为查找成功,或找到一个状态为EMPTY的位置判定为查找失败。

注意: 在查找过程中,必须找到位置状态为EXIST,并且key值匹配的元素,才算查找成功。若仅仅是key值匹配,但该位置当前状态为DELETE,则还需继续进行查找,因为该位置的元素已经被删除了。

HashData<K, V> *Find(const K &key) {
    //哈希表大小为0,表示哈希表为空,返回nullptr
    if (this->_tables.size() == 0) {
        return nullptr;
    }

    //哈希函数
    size_t hashi = key % this->_tables.size();
    size_t i = 1;
    size_t index = hashi;// index是插入的位置
    // 当哈希状态为EXITS,说明表中位置已经有值,那么就继续查找
    while (this->_tables[index]._state != EMPTY) {
        // 当表中值跟key相等,并且状态为存在时才返回,因为可能值的状态被改为了delete说明刚刚被删除,不可以返回
        if (this->_tables[index]._kv.first == key && this->_tables[index]._state == EXITS) {
            return &this->_tables[index];
        }
        index = hashi + i;  //线性探测
        //index = hashi + i * i;  //二次探测
        index %= this->_tables.size();// 防止index越界,绕回去
        i++;

        // 这里的_state可能都是存在或者删除,那么程序就可能陷入死循环,所以需要给定条件退出
        // 如果已经查找一圈,那么说明全是存在+删除
        if (index == hashi) {
            break;
        }
    }

    return nullptr;
}

哈希表的插入

向哈希表中插入数据的步骤如下:

  1. 查看哈希表中是否存在该键值的键值对,若已存在则插入失败。
  2. 判断是否需要调整哈希表的大小,若哈希表的大小为0,或负载因子过大都需要对哈希表的大小进行调整。
  3. 将键值对插入哈希表。
  4. 哈希表中的有效元素个数加一。

其中,哈希表的调整方式如下:

  • 若哈希表的大小为0,则将哈希表的初始大小设置为10。
  • 若哈希表的负载因子大于0.7,则先创建一个新的哈希表,该哈希表的大小为原哈希表的两倍,之后遍历原哈希表,将原哈希表中的数据插入到新哈希表,最后将原哈希表与新哈希表交换即可。

注意: 在将原哈希表的数据插入到新哈希表的过程中,不能只是简单的将原哈希表中的数据对应的挪到新哈希表中,而是需要根据新哈希表的大小重新计算每个数据在新哈希表中的位置,然后再进行插入。

将键值对插入哈希表的具体步骤如下:

  1. 通过哈希函数计算出对应的哈希地址。
  2. 若产生哈希冲突,则从哈希地址处开始,采用线性探测向后寻找一个状态为EMPTY或DELETE的位置。
  3. 将键值对插入到该位置,并将该位置的状态设置为EXIST。

注意: 产生哈希冲突向后进行探测时,一定会找到一个合适位置进行插入,因为哈希表的负载因子是控制在0.7以下的,也就是说哈希表永远都不会被装满。

bool Insert(const pair<K, V> &kv) {
    //1.查找值
    if (Find(kv.first)) {
        return false;
    }

    // 当我们的哈希表是空或者负载因子大于0.7的时候,我们需要给将哈希表增容
    // 负载因子 = 表中有效数据个数 / 空间的大小
    if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7) {
        //为空的时候,给初始10,负载因子大于0.7就扩容两倍
        size_t newsize = this->_tables.size() == 0 ? 10 : this->_tables.size() * 2;
        HashTable<K, V> newHashTable;// 重新创建一个HashTable类
        newHashTable._tables.resize(newsize);

        // 遍历旧表,重新映射到新表
        for (auto &data: this->_tables) {// data是_table中的类型,对应的HashData
            if (data._state == EXITS) {
                newHashTable.Insert(data._kv);// 将旧的kv插入到新的类对象中
            }
        }

        //交换
        this->_tables.swap(newHashTable._tables);
    }

    //哈希函数
    size_t hashi = kv.first % this->_tables.size();

    // 线形探测
    size_t i = 1;
    size_t index = hashi;// index是最后要插入的位置
    // 当哈希状态为EXITS,说明表中位置已经有值,那么就继续查找
    while (this->_tables[index]._state == EXITS) {
        index = hashi + i;
        //index = hashi + i * i;  //二次线性探测
        index %= this->_tables.size();// 防止index越界,绕回去
        i++;
    }

    this->_tables[index]._kv = kv;
    this->_tables[index]._state = EXITS;
    this->_n++;// 存储的数据个数+1

    return true;
}

哈希表的删除

删除哈希表中的元素非常简单,我们只需要进行伪删除即可,也就是将待删除元素所在位置的状态设置为DELETE。

在哈希表中删除数据的步骤如下:

  1. 查看哈希表中是否存在该键值的键值对,若不存在则删除失败。
  2. 若存在,则将该键值对所在位置的状态改为DELETE即可。
  3. 哈希表中的有效元素个数减一。

注意: 虽然删除元素时没有将该位置的数据清0,只是将该元素所在状态设为了DELETE,但是并不会造成空间的浪费,因为我们在插入数据时是可以将数据插入到状态为DELETE的位置的,此时插入的数据就会把该数据覆盖。

bool Erase(const K &key) {
    HashData<K, V> *ret = Find(key);
    if (ret) {
        ret->_state = DELETE;
        this->_n--;
        return true;
    } else {
        return false;
    }
}

完整代码

#pragma once
#include <iostream>
#include <vector>
using namespace std;
// 闭散列哈希表
enum State {
    EMPTY,// 哈希表位置为NULL
    EXITS,// 哈希表位置有值了
    DELETE// 哈希表位置为删除标志
};

template<class K, class V>
struct HashData {
    pair<K, V> _kv;
    State _state = EMPTY;//状态初始化为空
};

template<class K, class V>
class HashTable {
public:
    HashData<K, V> *Find(const K &key) {
        //哈希表大小为0,表示哈希表为空,返回nullptr
        if (this->_tables.size() == 0) {
            return nullptr;
        }

        //哈希函数
        size_t hashi = key % this->_tables.size();
        size_t i = 1;
        size_t index = hashi;// index是插入的位置
        // 当哈希状态为EXITS,说明表中位置已经有值,那么就继续查找
        while (this->_tables[index]._state != EMPTY) {
            // 当表中值跟key相等,并且状态为存在时才返回,因为可能值的状态被改为了delete说明刚刚被删除,不可以返回
            if (this->_tables[index]._kv.first == key && this->_tables[index]._state == EXITS) {
                return &this->_tables[index];
            }
            index = hashi + i;//线性探测
            //index = hashi + i * i;  //二次探测
            index %= this->_tables.size();// 防止index越界,绕回去
            i++;

            // 这里的_state可能都是存在或者删除,那么程序就可能陷入死循环,所以需要给定条件退出
            // 如果已经查找一圈,那么说明全是存在+删除
            if (index == hashi) {
                break;
            }
        }

        return nullptr;
    }
    bool Insert(const pair<K, V> &kv) {
        //1.查找值
        if (Find(kv.first)) {
            return false;
        }

        // 当我们的哈希表是空或者负载因子大于0.7的时候,我们需要给将哈希表增容
        // 负载因子 = 表中有效数据个数 / 空间的大小
        if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7) {
            //为空的时候,给初始10,负载因子大于0.7就扩容两倍
            size_t newsize = this->_tables.size() == 0 ? 10 : this->_tables.size() * 2;
            HashTable<K, V> newHashTable;// 重新创建一个HashTable类
            newHashTable._tables.resize(newsize);

            // 遍历旧表,重新映射到新表
            for (auto &data: this->_tables) {// data是_table中的类型,对应的HashData
                if (data._state == EXITS) {
                    newHashTable.Insert(data._kv);// 将旧的kv插入到新的类对象中
                }
            }

            //交换
            this->_tables.swap(newHashTable._tables);
        }

        //哈希函数
        size_t hashi = kv.first % this->_tables.size();

        // 线形探测
        size_t i = 1;
        size_t index = hashi;// index是最后要插入的位置
        // 当哈希状态为EXITS,说明表中位置已经有值,那么就继续查找
        while (this->_tables[index]._state == EXITS) {
            index = hashi + i;
            //index = hashi + i * i;  //二次线性探测
            index %= this->_tables.size();// 防止index越界,绕回去
            i++;
        }

        this->_tables[index]._kv = kv;
        this->_tables[index]._state = EXITS;
        this->_n++;// 存储的数据个数+1

        return true;
    }
    bool Erase(const K &key) {
        HashData<K, V> *ret = Find(key);
        if (ret) {
            ret->_state = DELETE;
            this->_n--;
            return true;
        } else {
            return false;
        }
    }

private:
    vector<HashData<K, V>> _tables;// 将Hash值存放在vector中
    size_t _n = 0;                 // 存储的数据个数
};

开散列哈希表(哈希桶)

哈希表的结构

在开散列的哈希表中,哈希表的每个位置存储的实际上是某个单链表的头结点,即每个哈希桶中存储的数据实际上是一个结点类型,该结点类型除了存储所给数据之外,还需要存储一个结点指针用于指向下一个结点。

template<class K, class V>
struct HashNode {
    HashNode<K, V> *_next;
    pair<K, V> _kv;

    HashNode(const pair<K, V> &kv)
        : _kv(kv), _next(nullptr) {}
};

与闭散列的哈希表不同的是,在实现开散列的哈希表时,我们不用为哈希表中的每个位置设置一个状态字段,因为在开散列的哈希表中,我们将哈希地址相同的元素都放到了同一个哈希桶中,并不需要经过探测寻找所谓的“下一个位置”。

哈希表的开散列实现方式,在插入数据时也需要根据负载因子判断是否需要增容,所以我们也应该时刻存储整个哈希表中的有效元素个数,当负载因子过大时就应该进行哈希表的增容。

//哈希表
template<class K, class V>
class HashTable{
public:
	//...
private:
    vector<Node *> _tables;
    size_t n = 0;// 存储有效数据的个数
};

只能存储key为整形的元素,其他类型怎么解决?

使用模板特化编写仿函数

template<class K>
struct HashFunc {
    size_t operator()(const K &key) {
        return key;
    }
};

// 特化模板,传string的话,就走这个
template<>
struct HashFunc<string> {
    size_t operator()(const string &s) {
        size_t hash = 0;
        for (auto ch: s) {
            hash += ch;
            hash *= 31;
        }
        return hash;
    }
};

这样我们的结构就变成了:

template<class K, class V, class Hash = HashFunc<K>>// Hash用于将key转换成可以取模的类型
class HashTable {
public:
	//
private:
    vector<Node *> _tables;
    size_t n = 0;// 存储有效数据的个数
};

哈希表的查找

在哈希表中查找数据的步骤如下:

  1. 先判断哈希表的大小是否为0,若为0则查找失败。
  2. 通过哈希函数计算出对应的哈希地址。
  3. 通过哈希地址找到对应的哈希桶中的单链表,遍历单链表进行查找即可。
Node *Find(const K &key) {
    if (this->_tables.size() == 0) {
        return nullptr;
    }

    Hash hash;    //用于处理各种类型的仿函数
    size_t hashi = hash(key) % this->_tables.size();
    Node *cur = this->_tables[hashi];
    while (cur) {
        if (cur->_kv.first == key) {
            return cur;
        }
        cur = cur->_next;
    }
    return nullptr;
}

哈希表的插入

向哈希表中插入数据的步骤如下:

  1. 查看哈希表中是否存在该键值的键值对,若已存在则插入失败。
  2. 判断是否需要调整哈希表的大小,若哈希表的大小为0,或负载因子过大都需要对哈希表的大小进行调整。
  3. 将键值对插入哈希表。
  4. 哈希表中的有效元素个数加一。

其中,哈希表的调整方式如下:

  • 若哈希表的大小为0,则将哈希表的初始大小设置为10。
  • 若哈希表的负载因子已经等于1了,则先创建一个新的哈希表,该哈希表的大小为原哈希表的两倍,之后遍历原哈希表,将原哈希表中的数据插入到新哈希表,最后将原哈希表与新哈希表交换即可。

重点: 在将原哈希表的数据插入到新哈希表的过程中,不要通过复用插入函数将原哈希表中的数据插入到新哈希表,因为在这个过程中我们需要创建相同数据的结点插入到新哈希表,在插入完毕后还需要将原哈希表中的结点进行释放,多此一举。

实际上,我们只需要遍历原哈希表的每个哈希桶,通过哈希函数将每个哈希桶中的结点重新找到对应位置插入到新哈希表即可,不用进行结点的创建与释放。

说明一下: 下面代码中为了降低时间复杂度,在增容时取结点都是从单链表的表头开始向后依次取的

将键值对插入哈希表的具体步骤如下:

  1. 通过哈希函数计算出对应的哈希地址。
  2. 若产生哈希冲突,则直接将该结点头插到对应单链表即可。
bool Insert(const pair<K, V> &kv) {
    Hash hash;// 仿函数用于不能取模的值
    // 已经有这个数,就不用插入了
    if (Find(kv.first)) {
        return false;
    }

    // 负载因子 == 1时扩容
    if (this->n == this->_tables.size()) {
        // size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
        size_t newsize = this->GetNextPrime(_tables.size());
        vector<Node *> newtables(newsize, nullptr);
        for (auto &cur: this->_tables) {// cur是Node*
            while (cur) {
                // 保存下一个
                Node *next = cur->_next;
                // 头插到新表
                size_t hashi = hash(cur->_kv.first) % newtables.size();
                cur->_next = newtables[hashi];
                newtables[hashi] = cur;

                cur = next;
            }
        }
        _tables.swap(newtables);
    }

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

    return true;
}

哈希表的删除

在哈希表中删除数据的步骤如下:

  1. 通过哈希函数计算出对应的哈希桶编号。
  2. 遍历对应的哈希桶,寻找待删除结点。
  3. 若找到了待删除结点,则将该结点从单链表中移除并释放。
  4. 删除结点后,将哈希表中的有效元素个数减一。

注意: 不要先调用查找函数判断待删除结点是否存在,这样做如果待删除不在哈希表中那还好,但如果待删除结点在哈希表,那我们还需要重新在哈希表中找到该结点并删除,还不如一开始就直接在哈希表中找,找到了就删除。

bool Erase(const K &key) {
    Hash hash;
    size_t hashi = hash(key) % this->_tables.size();
    //删除的时候需要找到前一个节点和后一个节点进行链接
    Node *prev = nullptr;
    Node *cur = this->_tables[hashi];//cur初始为头结点
    //遍历单链表
    while (cur) {
        if (cur->_kv.first == key) {
            if (prev == nullptr) {
                //要找的结点就是头结点则直接更新头
                this->_tables[hashi] = cur->_next;
            } else {
                //链接
                prev->_next = cur->_next;
            }
            delete cur;
            return true;
        } else {
            //更新prev和cur
            prev = cur;
            cur = cur->_next;
        }
    }
    return false;
}

扩容优化

在哈希表中,使用素数作为表的大小可以有效地减少哈希冲突。这主要基于以下两点:

  1. 哈希函数的设计:哈希函数的目的是将键均匀地散列在哈希表中,以尽可能减少哈希冲突。许多哈希函数都会利用取模操作来计算元素在表中的位置,例如hash(key) = key % table_size。在这种情况下,如果table_size是素数,那么哈希函数就能够更好地将不同的键散列在表的不同位置,从而减少哈希冲突。
  2. 避免周期性模式:如果我们使用的哈希表大小不是一个素数,特别是如果它有多个不同的因子,那么可能会产生周期性的模式,同样的键可能会被映射到同一个位置。这是因为两个数字如果它们的差是哈希表大小的因子,那么它们的哈希值将会相同。使用素数作为表的大小可以避免这种情况,因为素数只有两个因子,1和它自己。

因此,当哈希表需要扩容时,通常选择下一个较大的素数作为新的表的大小,以优化哈希表的性能。

// 扩容优化,使用素数扩容
size_t GetNextPrime(size_t prime) {
    // SGI
    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};

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

    return __stl_prime_list[i];
}

完整代码

#pragma once
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <vector>
using namespace std;

template<class K, class V>
struct HashNode {
    HashNode<K, V> *_next;
    pair<K, V> _kv;

    HashNode(const pair<K, V> &kv)
        : _kv(kv), _next(nullptr) {}
};

template<class K>
struct HashFunc {
    size_t operator()(const K &key) {
        return key;
    }
};

// 特化模板,传string的话,就走这个
template<>
struct HashFunc<string> {
    size_t operator()(const string &s) {
        size_t hash = 0;
        for (auto ch: s) {
            hash += ch;
            hash *= 31;
        }
        return hash;
    }
};

template<class K, class V, class Hash = HashFunc<K>>// Hash用于将key转换成可以取模的类型
class HashTable {
    typedef HashNode<K, V> Node;

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

    Node *Find(const K &key) {
        if (this->_tables.size() == 0) {
            return nullptr;
        }

        Hash hash;
        size_t hashi = hash(key) % this->_tables.size();
        Node *cur = this->_tables[hashi];
        while (cur) {
            if (cur->_kv.first == key) {
                return cur;
            }
            cur = cur->_next;
        }
        return nullptr;
    }

    bool Erase(const K &key) {
        Hash hash;
        size_t hashi = hash(key) % this->_tables.size();
        //删除的时候需要找到前一个节点和后一个节点进行链接
        Node *prev = nullptr;
        Node *cur = this->_tables[hashi];//cur初始为头结点
        //遍历单链表
        while (cur) {
            if (cur->_kv.first == key) {
                if (prev == nullptr) {
                    //要找的结点就是头结点则直接更新头
                    this->_tables[hashi] = cur->_next;
                } else {
                    //链接
                    prev->_next = cur->_next;
                }
                delete cur;
                return true;
            } else {
                //更新prev和cur
                prev = cur;
                cur = cur->_next;
            }
        }
        return false;
    }

    // 扩容优化,使用素数扩容
    size_t GetNextPrime(size_t prime) {
        // SGI
        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};

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

        return __stl_prime_list[i];
    }

    bool Insert(const pair<K, V> &kv) {
        Hash hash;// 仿函数用于不能取模的值
        // 已经有这个数,就不用插入了
        if (Find(kv.first)) {
            return false;
        }

        // 负载因子 == 1时扩容
        if (this->n == this->_tables.size()) {
            // size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
            size_t newsize = this->GetNextPrime(_tables.size());
            vector<Node *> newtables(newsize, nullptr);
            for (auto &cur: this->_tables) {// cur是Node*
                while (cur) {
                    // 保存下一个
                    Node *next = cur->_next;
                    // 头插到新表
                    size_t hashi = hash(cur->_kv.first) % newtables.size();
                    cur->_next = newtables[hashi];
                    newtables[hashi] = cur;

                    cur = next;
                }
            }
            _tables.swap(newtables);
        }

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

        return true;
    }

    // 获取哈希表索引最大长度(哈希桶长度)
    size_t MaxBucketSize() {
        size_t max = 0;
        for (int i = 0; i < _tables.size(); ++i) {
            auto cur = _tables[i];
            size_t size = 0;
            while (cur) {
                ++size;
                cur = cur->_next;
            }

            printf("[%d]->%d\n", i, size);

            if (size > max) {
                max = size;
            }
            if (max == 5121) {
                printf("%d", i);
                break;
            }
        }

        return max;
    }

private:
    vector<Node *> _tables;
    size_t n = 0;// 存储有效数据的个数
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1037840.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Zabbix介绍与安装

目录 一、概述 二、zabbix的主要功能 三、zabbix监控原理 四、Zabbix 监控模式 五、zabbix的架构 server-client server-proxy-client master-node-client 六、zabbix的安装 安装zabbix服务端 安装zabbix客户端 测试zabbix 1、在 Web 页面中添加 agent 主机点击左…

SystemC入门学习-第3章 数据类型

本章将详细的描述SystemC的数据类型&#xff0c;并介绍这些类型的数据可以进行哪些操作。比如值保持器&#xff08;value holder&#xff09;就是一种特殊的类型。在所有的类型中&#xff0c;最重要的是bool和sc_uint两种类型 3.1 值保持器 值保持器有三种&#xff1a; 变量…

如何管理销售团队?

本文将为大家讲解&#xff1a;如何管理销售团队&#xff1f; 销售团队的管理是企业成功的关键因素之一。一个高效、协同的销售团队可以推动企业的增长&#xff0c;增强市场竞争力。然而&#xff0c;销售团队的管理并不是一件容易的事情&#xff0c;它涉及多个方面的协调和优化…

Verilog开源项目——百兆以太网交换机(二)AES加解密模块设计

Verilog开源项目——百兆以太网交换机&#xff08;二&#xff09;AES加解密模块设计 &#x1f508;声明&#xff1a;未经作者允许&#xff0c;禁止转载 &#x1f603;博主主页&#xff1a;王_嘻嘻的CSDN主页 &#x1f511;全新原创以太网交换机项目&#xff0c;Blog内容将聚焦整…

基于微信小程序的投票系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言用户微信小程序的主要功能有&#xff1a;管理员的主要功能有&#xff1a;具体实现截图论文参考为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,C…

Vue.js路由及Node.js的入门使用---超详细

一&#xff0c;Vue路由 1.1 路由是什么 路由是用来管理应用程序中不同页面之间导航的概念。Vue Router是Vue.js官方提供的路由管理器&#xff0c;它允许我们通过定义路由规则和视图组件来配置路由 1.2 路由给我们带来的好处有哪些&#xff1f; 单页应用&#xff08;Single Pag…

Springboot整合jdbc和Mybatis

目录 整合jdbc 1. 新建项目 2. 编写yaml配置文件连接数据库 3. 测试类 使用原生的jdbcTemplate进行访问测试 使用Druid连接池 1. 添加类型 2. 初始化连接池 3. 编写config类 配置Druid数据源监视 整合Mybatis 1. 导入依赖 2. 编写mapper接口 3. 编写实体类 4. 编…

GLTF编辑器的另一个作用

1、GLB模型介绍 GLB&#xff08;GLTF Binary&#xff09;是一种用于表示三维模型和场景的文件格式。GLTF是"GL Transmission Format"的缩写&#xff0c;是一种开放的、跨平台的标准&#xff0c;旨在在各种3D图形应用程序和引擎之间进行交换和共享。 GLB文件是GLTF文件…

MySQL数据库详解 二:数据库的高级语句(高级查询语句)

文章目录 1. 克隆表 ---- 将数据表的数据记录生成到新的表中1.1 方式一&#xff1a;先创建新表&#xff0c;再导入数据1.2 方式二&#xff1a;创建的时候同时导入 2. 清空表 ---- 删除表内的所有数据2.1 delete删除2.2 truncate删除&#xff08;重新记录&#xff09;2.3 创建临…

别着急,解决不了的问题,就请交给时间吧

转眼间我走出社会已过去四年之久&#xff0c;但很多事依旧历历在目&#xff0c;就好像昨天发生的一样。 我小时候&#xff0c;因为一场医学事故患有先天性白内障&#xff0c;真的是连黑板的看不清&#xff0c;当时自己也不太懂事&#xff0c;上课对我来说就是画画以及一切能够消…

保姆级 Keras 实现 Faster R-CNN 十三 (训练)

保姆级 Keras 实现 Faster R-CNN 十三 训练 一. 将 Faster R-CNN 包装成一个类二. 修改模型结构1. 修改 input_reader 函数2. 增加 RoiLabelLayer 层 三. 损失函数1. 自定义损失函数2. 自定义精度评价函数 四. 模型编译五. 模型训练六. 预训练模型七. 保存模型与参数八. 代码下…

更新、修改

MySQL从小白到总裁完整教程目录:https://blog.csdn.net/weixin_67859959/article/details/129334507?spm1001.2014.3001.5502 语法: update 表名 列名该列新值, 列名该列新值, ... where 记录匹配条件; 说明&#xff1a;update 更新、修改 set 设置 …

通讯网关软件011——利用CommGate X2ODBC实现DDE数据转入ODBC

本文介绍利用CommGate X2ODBC实将DDE数据源中的数据转入到ODBC数据源。CommGate X2ODBC是宁波科安网信开发的网关软件&#xff0c;软件可以登录到网信智汇(http://wangxinzhihui.com)下载。 【案例】如下图所示&#xff0c;将DDE数据源&#xff08;如Excel&#xff09;的数据写…

【软件设计师-从小白到大牛】下午题基础篇:第一章 数据流图(DFD)

文章目录 前言章节提要一、数据流图基本概念二、数据流图的分层&#xff08;DFD&#xff09;三、数据字典四、数据流图平衡原则五、答题技巧问题一问题二问题三问题四 六、案例分析1、案例12、案例2 前言 ​ 本系列文章为观看b站视频以及b站up主zst_2001系列视频所做的笔记&…

DAZ To UMA⭐一.DAZ简单使用教程

文章目录 &#x1f7e5; DAZ快捷键&#x1f7e7; DAZ界面介绍 &#x1f7e5; DAZ快捷键 移动物体:ctrlalt鼠标左键 旋转物体:ctrlalt鼠标右键 导入模型:双击左侧模型UI &#x1f7e7; DAZ界面介绍 Files:显示全部文件 Products:显示全部产品 Figures:安装的全部人物 Wardrobe…

Floyd算法基础

弗洛伊德算法(Floyd) 之前介绍了迪杰斯特拉算法(Dijkstra)。具体请看&#xff1a;最短路径算法——简单明了的迪杰斯特拉算法(Dijkstra)。Dijkstra适用于非负权图&#xff0c;并且一次只能从网络中找源点到任何一个节点的最短路径&#xff0c;而Floyd算法的应用更加广泛&#…

基于vue的黑马前端项目小兔鲜

目录 项目学习 初始化项目 建立项目 引入elementplus elementPlus主题设置 配置axios 路由 引入静态资源 自动导入scss变量 Layout页 组件结构快速搭建 字体图标渲染 一级导航渲染 吸顶导航交互实现 Pinia优化重复请求 Home页 分类实现 banner轮播图 …

vue变量赋值中文,但是输出为乱码,解决办法

很奇怪&#xff0c;展示出来为乱码 来看代码输出 控制台的输出也是乱码 这是因为文件编码问题&#xff0c;可以看到我使用的编码不对 更改一下编码 要选择UTF-8 保存之后重新运行&#xff0c;就可以看到正确显示啦&#xff01;

JavaScript学习笔记05

JavaScript笔记05 操作 BOM 对象&#xff08;重点&#xff09; 什么是 BOM BOM&#xff08;Browser Object Model&#xff09;是指浏览器对象模型&#xff0c;是用于描述这种对象与对象之间层次关系的模型。浏览器对象模型&#xff08;BOM&#xff09;提供了独立于内容的、可…