【数据结构c++】(学习笔记)哈希表与大数据处理(线性探测哈希表、链式哈希表、大数据查重、topk问题、位图、布隆过滤器、快排分割)

学习笔记,部分内容参考网络教学视频,侵删

哈希表

哈希冲突

哈希表本身搜索的复杂度应该是 O ( 1 ) O(1) O(1),但由于哈希冲突的存在,我们通过线型探测法解决哈希冲突时,其复杂度会趋近于 O ( n ) O(n) O(n)

除留余数法

把数据存放在数组中下标为其余数的位置。

为了减小哈希冲突,哈希表的长度一般取素数

哈希表的装载因子loadfactor

已占用的桶的个数 / 桶的个数,一般当装载因子达到0.75时,认为哈希表需要扩容。

以除留余数法为例,哈希表扩容的造价很大,因为原来哈希表中的元素放到新的表中需要重新哈希

线性探测哈希表的均摊时间复杂度 O ( 1 ) O(1) O(1)

线性探测哈希表

增加:

  • 通过哈希函数计算数据存放的位置
    • 该位置空闲,直接储存元素,完成
    • 该位置被占用(哈希冲突),从当前位置向后找空闲的位置,存放该元素

查询:

  • 通过哈希函数计算数据存放的位置,从该位置取值
    • 该值 == 要查询元素的值,找到了!
    • 该值 != 要查询元素的值(存放时发生了哈希冲突),继续向后遍历寻找该元素
      判空
      • 这个位置一直是空的,没放过元素 -> 不需要继续向后搜索了
      • 这个位置以前放过元素,后来这个元素被删除了导致空位 -> 继续向后搜索
        可以使用枚举enum来列举状态

删除:

  • 通过哈希函数计算数据存放的位置,从该位置取值,判断状态
    • 该值 == 要删除的值,直接修改当前位置的状态
    • 该值 != 要删除的值,继续向后遍历寻找该元素,修改元素,直到遍历到一直为空的位置

代码实现

#include <iostream>
using namespace std;

// 定义桶的状态
enum State
{
    STATE_UNUSE, // 从未使用过的
    STATE_USING, // 正在使用的
    STATE_DEL,   // 被删除的
};

// 定义桶的类型
struct Bucket
{
    // 初始化构造函数
    Bucket(int key = 0, State state = STATE_UNUSE) : key_(key),
                                                     state_(state)
    {
    }

    int key_;     // 存储的数据
    State state_; // 桶的当前状态
};

// 线性探测哈希表类型
class HashTable
{
public:
    HashTable(int size = primes_[0], double loadfactor = 0.75) : useBucketNum_(0),
                                                                 loadfactor_(loadfactor),
                                                                 primeIdx_(0)
    {
        if (size != primes_[0])
        {
            for (; primeIdx_ < prime_size; primeIdx_++)
            {
                if (primes_[primeIdx_] > size)
                {
                    break;
                }
            }
            // 越界情况处理
            if (primeIdx_ == prime_size)
            {
                // 只能调整为最大的那个素数
                primeIdx_--;
            }
        }
        tableSize_ = primes_[primeIdx_];
        table_ = new Bucket[tableSize_];
    }

    ~HashTable()
    {
        delete[] table_;
        table_ = nullptr;
    }

public:
    bool insert(int key)
    {
        // 考虑扩容
        double factor = 1.0 * useBucketNum_ / tableSize_;
        cout << "factor: " << factor << endl;
        if (factor > loadfactor_)
        {
            // 需要扩容
            expand();
        }

        int idx = key % tableSize_;

        // // 位置没有占有
        // if (table_[idx].state_ != STATE_USING)
        // {
        //     table_[idx].state_ = STATE_USING;
        //     table_[idx].key_ = key;
        //     return true;
        // }

        // // 当前位置占用
        // for (int i = (idx + 1) % tableSize_; i != idx; i = (i + 1) % tableSize_) // 环形实现
        // {
        //     if (table_[i].state_ != STATE_USING)
        //     {
        //         table_[i].state_ = STATE_USING;
        //         table_[i].key_ = key;
        //         return true;
        //     }
        // }

        // 上面内容可以使用do-while循环实现
        int i = idx;
        do
        {
            if (table_[i].state_ != STATE_USING)
            {
                table_[i].state_ = STATE_USING;
                table_[i].key_ = key;
                useBucketNum_++;
                return true;
            }
            i = (i + 1) % tableSize_;
        } while (i != idx);

        return false;
    }

    bool erase(int key)
    {
        int idx = key % tableSize_;

        int i = idx;
        do
        {
            if (table_[i].state_ == STATE_USING && table_[i].key_ == key)
            {
                table_[i].state_ = STATE_DEL;
                useBucketNum_--;
            }
            i = (i + 1) % tableSize_;
        } while (i != idx && table_[i].state_ != STATE_UNUSE);

        return true;
    }

    bool find(int key)
    {
        int idx = key % tableSize_;

        int i = idx;
        do
        {
            if (table_[i].state_ == STATE_USING && table_[i].key_ == key)
            {
                return true;
            }
            i = (i + 1) % tableSize_;
        } while (i != idx && table_[i].state_ != STATE_UNUSE);

        return false;
    }

private:
    void expand()
    {
        // 边界检查
        ++primeIdx_;
        if (primeIdx_ == prime_size)
        {
            throw "Hashtable is too large!";
        }

        Bucket *newTable = new Bucket[primes_[primeIdx_]];
        // 遍历旧的哈希表
        for (int i = 0; i < tableSize_; i++)
        {
            if (table_[i].state_ == STATE_USING)
            {
                // 对存在的数据重新哈希
                int idx = table_[i].key_ % primes_[primeIdx_];

                int k = idx;
                do
                {
                    if (newTable[k].state_ != STATE_USING)
                    {
                        newTable[k].state_ = STATE_USING;
                        newTable[k].key_ = table_[i].key_;
                        break;
                    }
                    k = (k + 1) % primes_[primeIdx_];
                } while (k != idx);
            }
        }

        delete[] table_;
        table_ = newTable;
        tableSize_ = primes_[primeIdx_];
    }

private:
    Bucket *table_;     // 指向动态开辟的哈希表
    int tableSize_;     // 哈希表当前长度
    int useBucketNum_;  // 已经使用的桶的个数
    double loadfactor_; // 哈希表装载因子

    static const int prime_size = 10; // 定义素数表大小
    static int primes_[prime_size];   // 注意静态变量不能直接在类内赋值
    int primeIdx_;                    // 当前使用的素数在素数表的下标
};

int HashTable::primes_[prime_size] = {3, 7, 23, 47, 97, 251, 443, 911, 1471, 42773}; // 通过类名访问静态变量

int main()
{
    HashTable htable;
    htable.insert(21);
    htable.insert(32);
    htable.insert(14);
    htable.insert(15);

    // 验证扩容
    htable.insert(22);

    // 验证查找和删除
    cout << htable.find(14) << endl;
    htable.erase(14);
    cout << htable.find(14) << endl;

    return 0;
}

链式哈希表

线性探测哈希表的缺点:

  1. 哈希冲突过多时,复杂度趋近于 O ( n ) O(n) O(n)
  2. 多线程环境中,不能对同一个数组进行操作,除非使用互斥锁

链式哈希表采取数组+链表的方式,这里依旧以“除留余数法”为例:

  • 数组的每个位置挂链表
  • 用除留余数法计算数据挂在数组的哪一格
  • 发生哈希冲突的数据往后串

在这里插入图片描述
用容器可以大致表示为vector<list<int>> table

缺点:

  • 每个桶的链表越长,链表搜索花费的时间就越长,时间复杂度趋近于 O ( n ) O(n) O(n)

优化点:

  1. 当链表长度大于8(或者其他较大的数字)把桶里面这个链表转化为红黑树 O ( n log ⁡ n ) O(n\log n) O(nlogn)
  2. 给每个桶创建一个分段互斥锁,不同桶中的链表操作可以并发执行

代码实现

#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
using namespace std;

class HashTable
{
private:
    vector<list<int>> table_; // 哈希表的数据结构
    int useBucketNum_;        // 记录桶的个数
    double loadFactor_;       // 记录哈希表装载因子

    static const int prime_size = 10; // 定义素数表大小
    static int primes_[prime_size];   // 注意静态变量不能直接在类内赋值
    int primeIdx_;                    // 当前使用的素数在素数表的下标

public:
    HashTable(int size = primes_[0], double loadFactor = 0.75) : useBucketNum_(0),
                                                                 loadFactor_(loadFactor),
                                                                 primeIdx_(0)
    {
        if (size != primes_[0])
        {
            for (; primeIdx_ < prime_size; primeIdx_++)
            {
                if (primes_[primeIdx_] >= size)
                {
                    break;
                }
            }

            if (primeIdx_ == prime_size)
            {
                primeIdx_--; // 指向最后一个素数
            }
        }
        table_.resize(primes_[primeIdx_]);
    }

    // 由于容器自带的析构效果,这个类不需要写析构函数

public:
    // 增加元素
    // 附加要求:不能重复插入key
    void insert(int key)
    {
        // 判断扩容
        double factor = 1.0 * useBucketNum_ / table_.size();
        cout << "factor: " << factor << endl;
        if (factor > loadFactor_)
        {
            expand();
        }

        int idx = key % table_.size();
        if (table_[idx].empty())
        {
            // 只有在空桶添加元素时需要增加占用桶数
            useBucketNum_++;
            table_[idx].emplace_front(key);
        }
        else
        {
            // 需要注意不能重复插入
            // 为避免重名,在前面加::表示调用全局的泛型算法而不是调用自己的成员函数
            auto it = ::find(table_[idx].begin(), table_[idx].end(), key);

            if (it == table_[idx].end()) // 不存在
            {
                table_[idx].emplace_front(key);
            }
        }
    }

    // 删除元素
    void erase(int key)
    {
        int idx = key % table_.size();
        auto it = ::find(table_[idx].begin(), table_[idx].end(), key);

        if (it != table_[idx].end()) // 找到了
        {
            table_[idx].erase(it);
            if (table_[idx].empty()) // 桶被删空了
            {
                useBucketNum_--;
            }
        }
    }

    // 搜索元素
    bool find(int key)
    {
        int idx = key % table_.size();
        auto it = ::find(table_[idx].begin(), table_[idx].end(), key);

        return it != table_[idx].end();
    }

private:
    void expand()
    {
        if (primeIdx_ + 1 == prime_size) // 已经达到素数表最大值
        {
            throw "HashTable cannot expand any more!";
        }

        primeIdx_++;
        // 重新哈希时,useBucketNum_可能会改变
        useBucketNum_ = 0;

        vector<list<int>> oldTable;
        table_.swap(oldTable); // 空间配置器一样时,swap只交换成员变量,效率非常高
        // 交换之后所有数据在oldTable,原来的table_成了空表
        table_.resize(primes_[primeIdx_]);

        for (auto list : oldTable)
        {
            for (auto key : list)
            {
                int idx = key % table_.size();
                if (table_[idx].empty())
                {
                    // 只有在空桶添加元素时需要增加占用桶数
                    useBucketNum_++;
                }
                table_[idx].emplace_front(key);
            }
        }
    }
};

int HashTable::primes_[prime_size] = {3, 7, 23, 47, 97, 251, 443, 911, 1471, 42773}; // 通过类名访问静态变量

int main()
{
    HashTable htable;
    htable.insert(21);
    htable.insert(32);
    htable.insert(14);
    htable.insert(15);

    // 验证扩容
    htable.insert(22);
    htable.insert(67);

    // 验证查找和删除
    cout << htable.find(14) << endl;
    htable.erase(14);
    cout << htable.find(14) << endl;

    return 0;
}

大数据处理

查重

哈希表

查重或者统计重复的次数。查询的效率高但是占用的内存空间较大

预备知识:哈希表STL实现

unordered_setunordered_map 是 C++ 标准库(STL)中非常常用的无序容器,它们的底层都基于哈希表实现,核心优势是插入、删除、查找的平均时间复杂度为 O (1)

  1. unordered_set(无序集合)
  • 本质:存储唯一的元素(不允许重复),且元素无序(插入顺序与存储顺序无关)。
  • 用途:适合需要快速去重、查找元素是否存在的场景(如 “判断某个值是否出现过”)。
  1. unordered_map(无序映射)
  • 本质:存储键值对(key-value),其中键(key)唯一,值(value)可重复,整体无序
    我的理解:哈希表的每个元素都是一个二元组
  • 用途:适合需要通过键快速查找对应值的场景(如 “字典查询”“缓存映射”)。
代码实现
#include <iostream>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <stdlib.h>
#include <time.h>

using namespace std;

int main()
{
    // 模拟问题,vector中存放原始数据
    // 有一个数组放了一万个数字
    vector<int> vec;
    srand(time(NULL));
    for (int i = 0; i < 10000; i++)
    {
        vec.push_back(rand() % 10000);
    }

    // 找第一个重复的数字
    // 找所有重复的数字
    unordered_set<int> s1;
    for(auto key : vec) // 遍历提供的数字
    {
        auto it = s1.find(key); // 在哈希表里面找
        if(it == s1.end()) // 如果没找到
        {
            s1.insert(key); // 存入哈希表
        }
        else // 找到了
        {
            cout << "key:" << key << endl;
            break;
            // 找所有重复的数字,去掉break即可
        }
    }

    // 统计重复数字以及重复次数
    unordered_map<int,int> m1; // 存储键值对
    for(int key : vec)
    {
        auto it = m1.find(key);
        if(it == m1.end())
        {
            m1.emplace(key,1); 
            // 插入键值对时,比insert更加高效(省内存)
        }
        else
        {
            it->second ++;
        }

        // 以上 if -else 结构可以通过一句话实现
        // m1[key]++;

        //原理:
        /*
        检查 key 是否存在于 m1 中:
        如果 key 不存在(第一次出现):
        unordered_map 会自动插入一个新的键值对 (key, 0)(int 类型的默认值是 0)

        如果 key 已存在(非第一次出现):直接找到对应的键值对。

        返回 value 的引用:
        m1[key] 会返回该 key 对应 value 的引用(可以理解为 “直接拿到值的变量”)。
        */
    }

    for(auto pair : m1)
    {
        if(pair.second > 1)
        {
            cout << "key:" << pair.first << " cnt:" << pair.second << endl;
        }
    }

    // 去重
    // unordered_set元素不允许重复
    // 依此特性进行去重
    unordered_set<int> s2;
    for(auto key : vec)
    {
        s2.emplace(key);
    }
    cout << "去重后的元素:" << endl;
    for (auto val : s2) {  // 范围 for 循环遍历 s2 中的每个元素
        cout << val << endl;  // 打印当前元素
    }

    return 0;
}
找第一个没有重复出现的字符
#include <iostream>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <stdlib.h>
#include <time.h>

using namespace std;

int main()
{
    string src = "jjhfgiyurtytrs";
    unordered_map<char,int> m;

    for(char ch : src)
    {
        m[ch]++;
    }

    for(char ch : src)
    {
        if(m[ch] == 1)
        {
            cout << "第一个没有重复的字符是:" << ch << endl;
            return 0;
        }
    }

    cout << "所有字符都有重复出现过" << endl;
    return 0;
}

位图

用一个为(0或1)储存数据的状态,比较适合状态简单、数据量大,要求内存使用率低的场景

没有办法表示重复了多少次

在这里插入图片描述
左移操作:1左移n位得到第n位为1,低位全是0的数字

下面用1 << n表示1左移n位

按位与:&1 & 1 = 1,0 & 1 = 0,0 & 0 = 0

按位或:|1 | 1 = 1,1 | 1 = 1,0 | 0 = 0

bitmap数组中,

取得该位的值:bitmap[index] & (1 << offset)

将该为置为1:bitmap[index] = bitmap[index] | (1 << offset)

位图有缺陷的场景:

数据相差过大:1,3,1000000000,非常浪费空间

代码实现:

#include <iostream>
#include <vector>
#include <stdlib.h>
#include <time.h>
#include <memory> // 智能指针

using namespace std;
/*
有一亿个整数,最大值不超过一亿
问:
哪些元素重复了?
谁是第一个重复的?
谁是第一个不重复的?
限制内存100M
*/

int main()
{
    vector<int> vec{12, 78, 90, 78, 123, 8, 9, 12};

    // 定义位图数组
    int max = vec[0];
    for (size_t i = 1; i < vec.size(); i++)
    {
        if (vec[i] > max)
        {
            max = vec[i];
        }
    }
    cout << max << endl;

    int *bitmap = new int[max / 32 + 1](); // 初始化为全0
    unique_ptr<int> ptr(bitmap);           // 智能指针,自动析构

    // 找第一个重复出现的数字
    for (auto key : vec)
    {
        int index = key / 32;
        int offset = key % 32;

        // 取对应的位的值
        if (0 == (bitmap[index] & (1 << offset))) // 该位是0
        {
            // 表示key没有出现过
            bitmap[index] |= (1 << offset);
        }
        else // 出现过一次
        {
            // cout << key << " first appeared again!" << endl;
            // return 0;

            // 所有重复
            cout << key << " appeared again!" << endl;
        }
    }

    return 0;
}

对于 谁是第一个不重复的? 可以使用两个位记录信息,00表示未出现过,01表示出现过一次,10表示出现过两次及以上

可以借用STL中的bitset实现

借鉴其他帖子

template <size_t N>
class twobitset
{
public:
	void set(size_t x)
	{
		bool inset1 = _bs1.test(x);
		bool inset2 = _bs2.test(x);
		//00
		if (inset1 == false && inset2 == false)
		{
			//->01
			_bs2.set(x);
		}
		else if (inset1 == false && inset2 == true)
		{
			//->10
			_bs1.set(x);
			_bs2.reset(x);
		}
	}
		void print_once_num()
		{
			for (int i = 0; i < N; i++)
			{
				//筛选出两个位图为01的数
				if (_bs1[i] == false && _bs2[i] == true)
				{
					cout << i << endl;
				}
			}
		}
private:
	bitset<N> _bs1;
	bitset<N> _bs2;
};
void test_oncenum()
{
	int a[] = { 1,1,2,3,4,5,5,5,6,6,6,6,7,9,22 };
	twobitset<100> bs;
	for (auto e : a)
	{
		bs.set(e);
	}
	bs.print_once_num();
}

布隆过滤器 Bloom Filter

bloom filter是通过一个位数组和k个哈希函数构成的

在这里插入图片描述
增加一个元素:

  1. 经过k个哈希函数计算,得到位数组里面的一组位的序号
  2. 把相应的位置成1

搜索一个元素:

  1. 经过k个哈希函数计算,得到位数组里面的一组位的序号
  2. 如果有一个是0,则证明key不在bloom filter中

注意:

  • bloom filter无法提供删除操作
  • 查出来每一位都是1,不一定存在(例如上图中哈希后位4,5,6的key并不存在)
  • 查出来有是0的位,那么数据一定不存在

求top k问题

大小根堆

例如:求出序列中值最小的3个元素

算法:使用大根堆(含有3个元素),不断用更小的值替换堆顶的大值,最后剩下的即为最小的3个元素

代码实现:

#include <iostream>
#include <vector>
#include <stdlib.h>
#include <time.h>
#include <queue>
#include <functional>
using namespace std;

int main()
{
    vector<int> vec;
    srand(time(NULL));

    for (int i = 0; i < 1000; i++)
    {
        vec.push_back(rand() % 10000);
    }

#if 0
    // 求vec中值最小的前5个元素
    priority_queue<int> maxheap;
    int k = 5;

    // 前五个元素构建大根堆
    for (int i = 0; i < k; i++)
    {
        maxheap.push(vec[i]);
    }

    // 遍历
    for (int i = k; i < vec.size(); i++)
    {
        if (maxheap.top() > vec[i])
        {
            maxheap.pop();
            maxheap.push(vec[i]);
        }
    }

    // 输出
    while (!maxheap.empty())
    {
        cout << maxheap.top() << endl;
        maxheap.pop();
    }

    cout << endl;

#endif

// 求vec中值最大的前5个元素
    priority_queue<int,vector<int>,greater<int>> minheap;
    int k = 5;

    // 前五个元素构建大根堆
    for (int i = 0; i < k; i++)
    {
        minheap.push(vec[i]);
    }

    // 遍历
    for (int i = k; i < vec.size(); i++)
    {
        if (minheap.top() < vec[i])
        {
            minheap.pop();
            minheap.push(vec[i]);
        }
    }

    // 输出
    while (!minheap.empty())
    {
        cout << minheap.top() << endl;
        minheap.pop();
    }

    cout << endl;


    return 0;
}

快排分割

快速排序会首先选择一个基准,然后调整至“左边小于基准,右边大于基准

第一轮调整后,设求最小的k个数字,基准左边有m个数字,右边有n个数字

若k<=m+1,则继续在左边分割;若k>m+1,则在右边分割

代码实现:

#include <iostream>
#include <algorithm>
using namespace std;

int partitionfg(int arr[], int begin, int end)
{
    int pivot = arr[end];
    int i = begin;

    // 遍历数组,将小于基准的元素放到左侧(i的位置)
    for (int j = begin; j < end; j++)
    {
        if (arr[j] <= pivot)
        {
            swap(arr[i], arr[j]);
            i++; // 只有交换后才移动i,保证i左侧都是小于基准的元素
        }
    }

    // 将基准元素放到i的位置(此时i左侧均小于基准,右侧均>=基准)
    swap(arr[i], arr[end]);
    return i; // 返回基准元素的最终位置
}

void SelectTopK(int arr[], int begin, int end, int k)
{
    if (begin < end)
    {
        int pivotPos = partitionfg(arr, begin, end);

        // 计算当前基准位置到起始位置的元素个数(即前pivotPos-begin+1个元素)
        int currentK = pivotPos - begin + 1;

        if (currentK == k)
        {
            return; // 已找到前k小元素
        }
        else if (currentK > k)
        {
            // 前k小元素在左侧,缩小范围继续查找
            SelectTopK(arr, begin, pivotPos - 1, k);
        }
        else
        {
            // 左侧元素均为前k小的一部分,继续在右侧查找剩余元素
            SelectTopK(arr, pivotPos + 1, end, k - currentK);
        }
    }
}

int main()
{
    int arr[] = {64, 45, 52, 80, 66, 68, 0, 2, 18, 75};
    int size = sizeof(arr) / sizeof(arr[0]);
    int k = 3;

    SelectTopK(arr, 0, size - 1, k);

    // 输出前3小元素
    for (int i = 0; i < k; i++)
    {
        cout << arr[i] << " "; // 结果应为:0 2 18
    }

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值