C++ 算法基础课 05 —— 数据结构_Trie树/并查集/堆/Hash表/STL使用技巧

本文详细介绍了数据结构中的Trie树(字符串统计、合并操作)、并查集(查询元素、合并集合、连通块中点数量)、堆(堆排序、模拟堆)、一般哈希(拉链法、开放寻址法)以及字符串哈希的模板和习题解析,同时涵盖了STL中的vector、pair、string、queue、priority_queue、stack、deque、set、map等使用技巧。
摘要由CSDN通过智能技术生成

1 Trie树

  • 使用Trie树的题目会限制字母的种类和数量,一般是26或者52个
  • 高效存储和查找字符串集合的数据结构
  • 字符串最后都会被打上标记,表示以当前这个字母结尾是有个单词
    在这里插入图片描述
  • 比如查找abcd,按照路线可以找到abcd字符串,但是d没有被标记,所以不存在abcd这个字符串
    在这里插入图片描述

1.1 模板

int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点
// cnt[]存储以每个节点结尾的单词数量

// 插入一个字符串
void insert(char *str)
{
    int p = 0;
    for (int i = 0; str[i]; i ++ )
    {
        int u = str[i] - 'a';
        if (!son[p][u]) son[p][u] = ++ idx;
        p = son[p][u];
    }
    cnt[p] ++ ;
}

// 查询字符串出现的次数
int query(char *str)
{
    int p = 0;
    for (int i = 0; str[i]; i ++ )
    {
        int u = str[i] - 'a';
        if (!son[p][u]) return 0;
        p = son[p][u];
    }
    return cnt[p];
}
  • 使用数组模拟指针,下标是x的点,这个节点所有的儿子,son[x][0]表示第1个儿子。
#include<iostream>
using namespace std;

const int N = 100010;
int son[N][26];// 每个节点的子节点个数最多是26,每个节点最多向外连26条边
int cnt[N];// 以当前点结尾的单词有多少个,存储以每个节点结尾的单词数量
int idx = 0;// 表示当前用到哪个下标(节点),下标为0的点,既是根节点,又是空节点

// 存储插入
void insert(char str[])
{
    int p = 0;// 从根节点开始,p的范围是0~9999,因为字符串总长度不超过100000
    for(int i = 0; str[i]; i++)// C++字符串结尾是\0, str[]判断是否走到结尾
    {
        int u = str[i] - 'a';// 把字母a~z映射成数字0~25
        if(!son[p][u]) son[p][u] = ++idx;// p这个节点不存在u这个儿子的话,就创建出来
        p = son[p][u];// 走到下一个点,因为son[p][u]的值会一直+1
    }
    cnt[p] ++;// 以这点为结尾的单词数量多了一个
}

// 查询字符串出现多少次
int query(char str[])
{
    int p = 0;
    for(int i = 0; str[i]; i++)
    {
        int u = str[i] - 'a';// 得到当前查询字母子节点的编号
        if(!son[p][u]) return 0;// 如果不存在当前字母,直接return 0
        p = son[p][u];//存在当前字母就走下去
    }
    return cnt[p];//返回以p结尾的单词数量
}

1.2 习题1 —— 835.Trie字符串统计

Acwing 835.Trie字符串统计

#include<iostream>
using namespace std;

const int N = 100010;
int son[N][26];// 每个节点的子节点个数最多是26,每个节点最多向外连26条边
int cnt[N];// 以当前点结尾的单词有多少个
int idx = 0;// 表示当前用到哪个下标(节点),下标为0的点,既是根节点,又是空节点
char str[N];// 每次输入的字符串

// 存储插入
void insert(char str[])
{
    int p = 0;// 从根节点开始,p的范围是0~9999,因为字符串总长度不超过100000
    for(int i = 0; str[i]; i++)// C++字符串结尾是\0,str[]判断是否走到结尾
    {
        int u = str[i] - 'a';// 把字母a~z映射成数字0~25
        if(!son[p][u]) son[p][u] = ++idx;// p这个节点不存在u这个儿子的话,就创建出来
        p = son[p][u];//走到下一个点
    }
    cnt[p] ++;// 以这点为结尾的单词数量多了一个
}

// 查询字符串出现多少次
int query(char str[])
{
    int p = 0;
    for(int i = 0; str[i]; i++)
    {
        int u = str[i] - 'a';// 得到当前查询字母子节点的编号
        if(!son[p][u]) return 0;// 如果不存在当前字母,就是son[p][u] == 0, 直接return 0
        p = son[p][u];//存在当前字母就走下去
    }
    return cnt[p];//返回以p结尾的单词数量
}

int main()
{
    int n;// 输入n次
    scanf("%d",&n);
    while(n--)
    {
        char op[2];
        scanf("%s%s",op,str);// 先读入类型,再读入操作的字符串
        if(op[0] == 'I') insert(str);
        else printf("%d\n",query(str));
    }
    return 0;
}

1.3 习题2 —— 143.最大异域对

2 并查集(常用)

在这里插入图片描述

  • 优化问题2,在x向上遍历的过程中,搜过路径的每一个节点最后都指向根节点,可以提升速度
  • 优化方法叫:路径压缩
    在这里插入图片描述

2.1 查询元素

  • 作用1:询问两个元素是否在同一个集合中
  • 根节点的标号就是当前集合的编号,每个点都存储它的父节点是谁。
  • 求当前点的编号就不断向上找父节点,直到找到根节点为止,则当前节点的编号就是根节点的编号。这样可以快速找到哪个元素属于哪个集合。
    在这里插入图片描述
  • p[x]表示当前点x的父节点
  • 除了根节点p[x] == x之外,其他的点的父节点p[x]和x都不相等
  • 不断往上判断,x = p[x],父节点赋值给子节点,就是父节点一步一步变成子节点

2.2 集合合并

  • 作用2:将两个集合合并
  • px是x的集合编号,py是y的集合编号,p[x] = y,就是直接将x的集合插入到y里面,就是说将y看做是x的父节点
    在这里插入图片描述

2.3 模板

  • 只要记住例题的find函数模板

2.3.1 朴素并查集

	int p[N]; //存储每个点的祖宗节点

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ ) p[i] = i;

    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);

2.3.2 维护size的并查集

	int p[N], size[N];
    //p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        size[i] = 1;
    }

    // 合并a和b所在的两个集合:
    size[find(b)] += size[find(a)];
    p[find(a)] = find(b);

2.3.3 维护到祖宗节点距离的并查集

    int p[N], d[N];
    //p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x)
        {
            int u = find(p[x]);
            d[x] += d[p[x]];
            p[x] = u;
        }
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        d[i] = 0;
    }

    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);
    d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量

2.4 习题1 —— 836.合并集合(模板例题)

Acwing 836.合并集合
并查集总结

  • p[find(a)] = find(b): p[x] = y,find(a)是a的祖宗节点,find(b)是b的祖宗节点。让a的祖宗节点的父亲等于b的祖宗节点

在这里插入图片描述

// M 1,2最初1和2分别在两个集合,所以需要将1和2合并到一个集合。M 3,4同理
// 这里先进行合并,再进行查询
#include<iostream>
using namespace std;

const int N = 100010;

int n,m;// 读入数
int p[N];// 每个元素的父节点

//find函数和下面p[i] = i一起看(这段重点)
int find(int x)// 返回x的祖宗节点,就是x所在集合的编号 + 路径压缩优化
{
    if(p[x] != x) p[x] = find(p[x]);// 如果x不是根节点, 让左边的父节点等于右边的祖宗节点。求集合编号x = p[x]
    return p[x];// 返回父节点,p[x]里面已经有被赋的值(p[i] = i)
}

int main()
{
    scanf("%d%d", &n, &m);
    
    // 最开始每个数都是自己单独一个为集合,就是树根都是自己
    for(int i = 0; i < n; i++) p[i] = i;// p[x] == x时候,x就是树根,将所有的p值赋成自己。
    
    while(m--)
    {
        char op[2];// 有M和Q两种指令,sacnf读入字符串(%s)比字符(%c)更好用。无论读字符还是字符串,都用字符串来读
        int a, b;// 表示指令后面的两个数字
        scanf("%s%d%d", op, &a, &b);// 字符串数组名字本身就是数组首地址
        
        // 合并操作,p[x] = y,find(a)是a的祖宗节点,find(b)是b的祖宗节点。让a的祖宗节点的父亲等于b的祖宗节点
        if(op[0] == 'M') p[find(a)] = find(b);
        
        else    // 判断两个节点是不是在一个集合里面,此时op[0] = 'I'
        {
            if(find(a) == find(b)) puts("Yes");
            else puts("No");
        }
    }
    return 0;
}

2.5 习题2 —— 837.连通块中点的数量

Acwing 837.连通块中点的数量

  • a, b可能相等,相等就在a上面画一个圈
  • C a b:在点 a 和点 b之间连一条边,a 和 b可能相等;
  • Q1 a b:询问点 a 和点 b 是否在同一个连通块中,a 和 b可能相等
  • Q a b:询问点 a所在连通块中点的数量
    在这里插入图片描述
  • 在两个连通块之间加一条线,就是相当于进行集合合并。a和b之间拉一条线就是进行合并
  • 在集合中,只保证根节点的size是有意义的
    在这里插入图片描述
  • 合并之后,求集合中所有点的size,只需要size(a) + size(b),a和b都是根节点
    在这里插入图片描述
#include<iostream>
using namespace std;

const int N = 100010;

int n, m;
int p[N];


// 查找节点x的祖宗节点
int find(int x)
{
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main()
{
    scanf("%d%d",&n, &m);
    
    // size表示每一个集合的大小,就是每个集合里面点的数量。只保证祖宗节点(根节点)的size是有意义的
    int size[N];
    for(int i = 0; i < n; i++) 
    {
        p[i] = i;
        size[i] = 1;// 最开始每个集合数量为1
    }
    
    while(m--)
    {
        char op[5];
        int a, b;
        scanf("%s", op);// 数组首字母就是数组首地址
        
        if(op[0] == 'C') // 第1个操作是进行合并
        {
            scanf("%d%d",&a, &b);
            if(find(a) == find(b)) continue;// 如果a和b已经在同一个集合里,就直接跳过
            size[find(b)] += size[find(a)];// a的根节点合并到b的根节点之后,size就进行相加
            p[find(a)] = find(b);
        }
        else if(op[1] == '1')// 第2个操作是Q1,判断第二个字符是1即可
        {
            scanf("%d%d",&a, &b);
            if(find(a) == find(b)) puts("Yes");
            else puts("No");
        }
        else
        {
            // 求某一个点所在集合中点的数量(连通快中点的数量)
            scanf("%d", &a);
            printf("%d\n",size[find(a)]);// 只需要返回点a的根节点的size
        }
    }
    return 0;
}

3 堆(完全二叉树)

  • STL可以实现前三个操作,STL里面的堆就是优先队列,priority_queue
  • 基本操作1:down(x)
  • 基本操作2:up(x)
  • heap[size]代表树的最后一个数,heap[1]代表树的第1个数
  • 插入一个数heap[++size] = x; up(size); 在树的最后插入一个数,将最后一个数size然后不断往上移动
  • 求集合中的最小值heap[1]; 根节点是最小值
  • 删除最小值heap[1] = heap[size]; size–; down(1); 把最后一个元素覆盖到堆顶区,再把最后一个元素删除,最后将堆顶元素不断往下down。
    在这里插入图片描述
  • 删除任意一个元素heap[k] = heap[size]; size–;down[k];up[k]; 用最后一个数覆盖掉需要删除的数,根据大小来判断,是down还是up,只会执行一个
  • 修改任意一个元素:**heap[k] = x; down[k];up[k];**根据大小来判断,是down还是up,只会执行一个
    在这里插入图片描述
  • 小根堆,每个点的左右两个子节点的值都大于父节点,所以小根堆的最小值是根节点
    在这里插入图片描述
  • 堆状数据结构,就是完全二叉树都用一维数组来存。第1个位置是根节点,第x个结点的左儿子位置是2x,x的右儿子是2x+1
    在这里插入图片描述

3.1 模板

// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
// ph[k]存储第k个插入的点在堆中的位置
// hp[k]存储堆中下标是k的点是第几个插入的
int h[N], ph[N], hp[N], size;

// 交换两个点,及其映射关系
void heap_swap(int a, int b)
{
    swap(ph[hp[a]],ph[hp[b]]);
    swap(hp[a], hp[b]);
    swap(h[a], h[b]);
}

void down(int u)
{
    int t = u;
    if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2;
    if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
    if (u != t)
    {
        heap_swap(u, t);
        down(t);
    }
}

void up(int u)
{
    while (u / 2 && h[u] < h[u / 2])
    {
        heap_swap(u, u / 2);
        u >>= 1;
    }
}

// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);

3.2 习题1 —— 838.堆排序

Acwing 838.堆排序
down 从n/2开始的分析

  • 这里只需要操作2和操作3
  • 求集合中的最小值heap[1]; 根节点是最小值
  • 删除最小值heap[1] = heap[size]; size–; down(1); 把最后一个元素覆盖到堆顶区,再把最后一个元素删除,最后将堆顶元素不断往下down。
// 就是要求最小值,并且不断删除最小值,再求最小值,删除
#include<iostream>
#include<algorithm>// swap函数
using namespace std;

const int N = 100010;
int n, m;
int h[N], mySize;// h就是heap, size表示存在当前heap里面的元素

// down函数
void down(int u)// 求父节点和两个子节点中最小值,u表示数组下标编号
{
    int t = u;// t表示三个点里面的最小值的位置标号
    // 第1个位置是根节点,第u个结点的左儿子位置是2u,右儿子位置是2u+1
    if( u * 2 <= mySize && h[u * 2] < h[t]) t = u * 2;// 判断节点u的左儿子是否比点u大,小的话将最小值位置坐标赋给t
    if( u * 2 + 1 <= mySize && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
    if(u != t)// 表示当前根节点不是最小值
    {
        swap(h[u],h[t]);// 将最小值节点和当前值节点交换
        down(t);// 再将t结点down
    }
}

// up函数
void up(int u)
{
    while(u / 2 > 0 && h[u / 2] > h[u])// u是子节点,u/2表示父节点,h[u/2]表示父节点的值,h[u]表示子节点的值
    swap(h[u/2],h[u]);
    u /= 2;// u = u/2表示将当前的父节点再赋值为子节点,一步一步向上
}

int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++) scanf("%d",&h[i]);
    mySize = n;// 开始size的值就是n
    
    for(int i = n/2; i; i--) down(i);// 首先进行堆排序,序号从1开始
    while(m--)
    {
        printf("%d ",h[1]);// 打印出堆顶元素,就是最小值
        h[1] = h[mySize];// 把最后一个元素赋给堆顶元素
        mySize --;// 这三句话表示删除堆顶元素(最小值)
        down(1);// 把堆顶元素按照大小down下来
    }
    return 0;
}

3.3 习题2 —— 839.模拟堆

Acwing 839.模拟堆

  • 难点在于删除第k个插入的数(而不是第k个数)
  • ph[k]存的是第k个插入的数在堆里面的下标是什么
  • hp[k]存的是堆里面第k个点是第几个插入的点
    在这里插入图片描述
  • 两个数交换后
  • ph[j] = k 表示第 j 个插入的数在堆里面的下标是 k
  • hp[k] = j 表示堆里面下标为 k 的数是第 j 个插入的数
  • 需要从第几个插入的元素找队里的元素,有需要从堆里面的元素确定是第几个插入的
    在这里插入图片描述
swap(ph[hp[a]], ph[hp[b]]);// 绿色部分
swap(hp[a],hp[b]);// 红色部分
swap(h[a],h[b]);

在这里插入图片描述

// 老师讲解中所有的size都不能用,用其他代替
#include<iostream>
#include<algorithm>//  swap函数
#include<cstring>
#include<stdio.h>// C的头文件
using namespace std;

const int N = 100010;
int h[N], asize = 0;// heap[]数组
int ph[N], hp[N];// ph[k]表示第k个插入的数在堆里面的下标是什么,hp[k]表示堆里面第k个点是第几个插入的点

// 交换,进行存储映射(ph[i]和ph[j]所指的数)
void heap_swap(int a, int b)// a和b均表示堆里面元素的下标
{
    // 如上图存在 ph[j] = k, hp[k] = j的映射关系
    // 这里是hp[a] = i, ph[i] = a,hp[b] = j, ph[j] = b,所以交换ph[]所指的方向
    swap(ph[hp[a]], ph[hp[b]]);
    swap(hp[a],hp[b]);
    swap(h[a],h[b]);
}

// down函数
void down(int u)
{
    int t = u;// 先假设父节点为最小值,u表示下标标号
    if(u * 2 <= asize && h[u * 2] < h[t]) t = u * 2;// 左儿子小于父节点,则最小值t为左儿子
    if(u * 2 + 1 <= asize && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
    if(u != t)
    {
        heap_swap(u, t);// 这里u是父节点,t是子节点,不等于的话说明子节点比父节点值小,所以要交换
        down(t);
    }
}

void up(int u)
{
    while(u / 2 && h[u / 2] > h[u])// 父节点大于子节点,就交换
    {
        heap_swap(u / 2, u);
        u = u / 2;
    }
}

int main()
{
    int n, m = 0;// m表示当前第几个插入的数
    scanf("%d",&n);
    while(n --)
    {
        char op[10];
        int x, k;
        scanf("%s", op);
        
        // 操作1: 将输入的数插入到堆的末尾,再进行up操作
        if(!strcmp(op, "I"))// 字符串比较,strcmp(a, b),比较两个字符串的大小,a < b 返回-1,a == b 返回0,a > b返回1 
        {
           scanf("%d",&x);// 把插入的数读进来
           asize ++;// 表示堆里面增加一个元素
           m ++;// 当前的x是第m个插入的数,  size和m都是先+1,再进行下面操作,因为m和size都要从1开始
           ph[m] = asize, hp[asize] = m;// 二者进行映射
           h[asize] = x;
           up(asize);
        }
        
        // 操作2: 输出当前集合的最小值
        else if(!strcmp(op,"PM")) printf("%d\n",h[1]);
        
        // 操作3: 删除当前集合中的最小值
        else if(!strcmp(op, "DM"))
        {
            heap_swap(1,asize);// 堆顶元素换成堆的末尾元素,直接将最后一个数和第1个交换
            asize--;
            down(1);// 将堆顶元素down下来
        }
        
        // 操作4: 删除第 k 个插入的数
        else if(!strcmp(op, "D"))
        {
            scanf("%d",&k);// 第k个插入的数
            int a;
            a = ph[k];// ph[k]表示第k个插入的数在堆的下标位置
            heap_swap(a, asize);// 删除就是将第k个数和末尾元素进行交换
            asize--;
            down(a),up(a);//  将位置a的元素down下来,保证堆顶元素是最小值,down和up函数只会执行一个
        }
        
        // 操作5: 修改第 k 个插入的数,将其变为 x
        else if(!strcmp(op,"C"))// 这边是两个字符串作比较
        {
            scanf("%d%d", &k, &x);
            int b;
            b = ph[k];
            h[b] = x;
            down(b), up(b);
        }
    }
    return 0;
}

4 一般哈希

  • 将一个-10e9到10e9之间的数映射到0到10e5
  • 离散化是特殊的哈希方式(需要排序单调递增),这里讲的是一般哈希
  • 哈希表有两个操作:插入数字,查找数字
    在这里插入图片描述

4.1 模板

4.1.1 拉链法

  • mod后面的数尽量取质数,这样发生冲突的概率会小
    在这里插入图片描述
// x % N可能是正数也可能是负数,取决于x正负,再加上N就一定是正数。N表示质数
int k = (x % N + N) % N;// 为了让哈希值k变成正数,k也是头结点下标
e[idx] = x;// 将k赋值给链表
ne[idx] = h[k];// k指向下一个点变成新插入的点指向下一个点
h[k] = idx;

在这里插入图片描述

int h[N], e[N], ne[N], idx;

// 向哈希表中插入一个数
void insert(int x)
{
    int k = (x % N + N) % N;
    e[idx] = x;
    ne[idx] = h[k];
    h[k] = idx ++ ;
}
// 在哈希表中查询某个数是否存在
bool find(int x)
{
    int k = (x % N + N) % N;
    for (int i = h[k]; i != -1; i = ne[i])
        if (e[i] == x)
            return true;

    return false;
}
  • 类似单链表插入
    在这里插入图片描述
// 将x值插到头结点(头插法)
void add_to_head(int x)// idx表示插入点的下标值,插入的第1个点idx = 0
{
	// head 表示头结点的下标,存储链表头,head的存储值时0,head——>0
	// ne[ ]存储节点的next指针,就是ne[k]里面存的是k+1,就是ne[k] = k + 1
    e[idx] = x;// 将x的值存下来
    ne[idx] = head;// 红色指针1指向之前head存的,之前是head指向节点0,现在换成ne[idx]指向节点0, 这里的ne[idx]存储的就是idx+1
    head = idx;// 将head指向红色指针2,相当于 head ——> idx,idx = 0
    idx ++;
}

4.1.2 开放寻址法

在这里插入图片描述

int h[N];

// 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置
int find(int x)
{
    int t = (x % N + N) % N;
    while (h[t] != null && h[t] != x)
    {
        t ++ ;
        if (t == N) t = 0;
    }
    return t;
}

4.2 习题1 —— 840.模拟散链表

Acwing 840.模散链表

4.2.1 拉链法

  • 使用拉链法,首先要求大于100000的最小质数,质数求解方法如下
    在这里插入图片描述
  • 首先求大于100001的质数
// 求大于100000的最小质数
for(int i = 100000; ; i++)
{
    bool flag = true;// 先假设i是质数,不是则为false
    
    // 判断不是质数
    for(int j = 2; j * j <= i; j++)// 针对25,49这样的数字,才会用到j * j = i情况,但i不是质数
    {
        if(i % j == 0)
        {
            flag = false;
            break;
        }
    }
    
    if(flag)// 是质数
    {
        cout << i << endl;// 结果是100003
        break;
    }
}
  • 题目的完整程序
#include<iostream>
#include<cstring>
using namespace std;

const int N = 100003;// N是大于100000的最小质数
int h[N];// 开N个槽
int e[N], ne[N];// 每个槽后面都是链表,e[N]存的是当前链表的数,ne[N]存的是下一个链表位置
int idx;// 表示当前用到哪一个位置

void insert(int x)// 用一个函数将数字x(范围是−109≤x≤109)映射到下标为1到105的链表中
{
    // h[k]表示链表的头结点,相当于head,存储指向第1个结点的指针域(就是结点下标)
    // x % N可能是正数也可能是负数,取决于x正负,再加上N就一定是正数
    int k = (x % N + N) % N;// 为了让哈希值k变成正数,k也是头结点下标
    e[idx] = x;// x赋值给以h[k]为头结点的链表
    ne[idx] = h[k];// k指向下一个点变成新插入的点指向下一个点
    h[k] = idx ++;// 插入的方法是头插法,刚开始idx = 0
}

bool find(int x)
{
    int k = (x % N + N) % N;
    for(int i = h[k]; i != -1; i = ne[i])// i不指向空节点
    {
        if(e[i] == x) return true;
    }
    return false;
}

int main()
{
    int n;
    scanf("%d",&n);
    
    memset(h, -1 , sizeof(h));// 清空槽,空指针用-1表示。在cstring库中
    
    while(n--)
    {
        int op[2];// scanf读入的话,最好用字符串,因为scanf会自动把制表符、回车、空格忽略掉。不要用scanf读入字符
        int x;
        scanf("%s%d",op,&x);
        if(op[0] == 'I') insert(x);
        else
        {
            if(find(x)) puts("Yes");
            else puts("No");
        }
    }
    
    return 0;
}

4.2.2 开放寻址法

  • 一般开两倍,100000的两倍是200000,首先找到大于200000的最小质数
// 求大于200000的最小质数
for(int i = 200000; ; i++)
{
    bool flag = true;// 先假设i是质数,不是则为false
    
    // 判断不是质数
    for(int j = 2; j * j <= i; j++)// 针对25,49这样的数字,才会用到j * j = i情况,但i不是质数
    {
        if(i % j == 0)
        {
            flag = false;
            break;
        }
    }
    
    if(flag)// 是质数
    {
        cout << i << endl;// 结果是100003
        break;
    }
}
  • 题目程序如下,核心是find函数find主要用来找位置
#include<iostream>
#include<cstring>
using namespace std;

// 大于200000(两倍于100000)的最小质数,0x3f3f3f3f不在10e9范围内
const int N = 200003, null = 0x3f3f3f3f;
int h[N];// 开N个槽

// 核心操作是find函数,如果x在哈希表已经存在的话,就返回所在的位置。如果x在哈希表不存在的话,返回的是x应该存储的位置
int find(int x)
{
    int k = (x % N + N) % N;
    while(h[k] != null && h[k] != x)// h[k]不为空且不等于x
    {
        k ++; // 往后看下一个位置
        if(k == N)  k = 0;// 说明已经看完最后一个位置,循环第1个位置
    }
    return k;// 如果x在哈希表,k就是x的下标。如果x不在表中,k就是x应该存储的位置
}

int main()
{
    int n;
    scanf("%d",&n);
    
     // memset是对字节操作,以字节为单位对内存进行初始化,h是int数组,有四个字节,每个字节都是0x3f,则最后就是0x3f3f3f3f
    memset(h, 0x3f , sizeof(h));// 将h[N]初始化为0x3f3f3f3f
    // 常用的是0和1,如果每个字节都为0则数字就死0,-1用每个字节都是1表示,这样结果也是-1  
    
    while(n--)
    {
        int op[2];// scanf读入的话,最好用字符串,因为scanf会自动把制表符、回车、空格忽略掉。不要用scanf读入字符
        int x;
        scanf("%s%d",op,&x);
        
        int k = find(x);
        if(op[0] == 'I') 
        {
            h[k] = x;// 赋值插入
        }
        else
        {
            if(h[k] != null) puts("Yes");
            else puts("No");
        }
    }
    
    return 0;
}

5 字符串哈希

  • 哈希最核心的是将一个字符串看成K进制的数字
  • 不需要考虑冲突
    在这里插入图片描述

5.1 模板

  • 将任何一个数映射到0 ~ Q-1之间的数
    在这里插入图片描述
  • 不能映射成0, P 取131或者13331,Q取2^64(经验值)
    在这里插入图片描述
  • 使用unsigned long long 来存储,因为如果溢出的话,就相当于取模2的64次方
    在这里插入图片描述
  • 右下部分就是哈希值L —> R 部分的计算公式h[R] - h[L - 1] * P ^(R - L + 1)。乘以P的R - L +1 次方,说明1 ~ L - 1要与L ~ R 对齐
    在这里插入图片描述
  • h[i] = h[i - 1] * P + str[i]:第 i 位的哈希值
    在这里插入图片描述
核心思想:将字符串看成P进制数,P的经验值是13113331,取这两个值的冲突概率低
小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果

typedef unsigned long long ULL;
ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64

// 初始化
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
    h[i] = h[i - 1] * P + str[i];
    p[i] = p[i - 1] * P;
}

// 计算子串 str[l ~ r] 的哈希值
ULL get(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];
}

5.2 习题1 —— 841.字符串哈希

Acwing 841.字符串哈希

#include<iostream>
using namespace std;

typedef unsigned long long ULL;// 用ULL表示前面unsigned long long

const int N = 100010;
int P = 131;

int n, m;
char str[N];
// h数组表示某一个前缀的哈希值,h[i]表示前i个字母的哈希值
int h[N], p[N];// 这里的p[]存储多少次方, 用的p进制

ULL get(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];// 区间[l, r]之间的哈希值
}

int main()
{
    scanf("%d%d%s", &n, &m, str + 1);
    
    // 核心代码
    p[0] = 1;
    for(int i = 1; i <= n; i++)
    {
        p[i] = p[i - 1] * P;// p[1] = P, P[2] = P^2, p[3] = P^3
        // h[0] = 0, h[1] = h[0] * P + str[1] = str[1], h[2] = h[1] * P + str[2], str[i]自动变成数字
        h[i] = h[i - 1] * P + str[i];
    }
    
    while(m--)
    {
        int l1,r1,l2,r2;
        scanf("%d%d%d%d",&l1, &r1, &l2, &r2);
        
        if(get(l1,r1) == get(l2, r2)) puts("Yes");
        else puts("No");
    }
    return 0;
}

6 STL使用技巧

6.1 模板

6.1.1 vector 数组

vector, 变长数组,倍增的思想
    size()  返回元素个数
    empty()  返回是否为空
    clear()  清空
    front()/back()
    push_back()/pop_back()
    begin()/end()
    []
    支持比较运算,按字典序
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<vector>
using namepspace std;

int main()
{
	vector<int>a;
	for(int i = 0; i < 10; i++) a.push_back(i);
	
	// 方式1
	for(int i = 0; i < a.size(); i++) cout << a[i] << endl;
	
	// 方式2 迭代器
	for(vector<int>::iterator i = begin(); i != end(); i++)cout << *i << endl;
	for(auto i = begin(); i != end(); i++)cout << *i << endl;// auto方式
	// a.begin() 就是a[0],a.end()就是a[a.size()]最后一个数的后面一位
	
	// 方式3
	for(auto x : a) cout << x << endl;
}

6.1.2 pair

pair<int, int>
    first, 第一个元素
    second, 第二个元素
    支持比较运算,以first为第一关键字,以second为第二关键字(字典序)
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<vector>
using namepspace std;

int main()
{
	pair<int, string> p;
	// p.first
	// p.second
	p = make_pair(10, "nnn");
	p = (10, "nnn");// C++11标准
 	
 	pair<int, pair<int, int>> p;// 存储三种类型数据
}

6.1.3 string 字符串(重点!)

string,字符串
    size()/length()  返回字符串长度
    empty()
    clear()
    substr(起始下标,(子串长度))  返回子串
    c_str()  返回字符串所在字符数组的起始地址
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<vector>
using namepspace std;

int main()
{
	string a = "123";
	a += "nnnn";// 添加字符串
	a += 'b';// 添加字符
	cout << a << endl;

	cout << a.substr(1,2) << endl;// 下标从1开始(起始下标为0),长度为2。结果是"23"
	cout << a.substr(1,20) << endl;// 长度超过字符串总长度,输出到最后一个字母为止 

	printf("%s\n", a.c_str());// 使用printf函数输出字符串,c_str()函数输出字符数组的起始地址
}

6.1.4 queue队列

  • 没有 clear 函数
queue, 队列
    size()
    empty()
    push()  向队尾插入一个元素
    front()  返回队头元素
    back()  返回队尾元素
    pop()  弹出队头元素
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<queue>
using namepspace std;

int main()
{
	queue <int> p;
}

6.1.5 priority_queue 优先队列(堆实现)

  • 无 clear 函数
priority_queue, 优先队列,默认是大根堆
    size()
    empty()
    push()  插入一个元素
    top()  返回堆顶元素
    pop()  弹出堆顶元素
    定义成小根堆的方式:priority_queue<int, vector<int>, greater<int>> q;
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<queue>
using namepspace std;

int main()
{
	priority_queue <int> heap;
	heap.push(-x);// 把-x 从大到小排序,实现小根堆
}

6.1.6 stack 栈

stack,size()
    empty()
    push()  向栈顶插入一个元素
    top()  返回栈顶元素
    pop()  弹出栈顶元素

6.1.7 deque 双端队列(加强vector)

  • 效率低
#include<deque>
deque, 双端队列
    size()
    empty()
    clear()
    front()/back()
    push_back()/pop_back() 队尾插入和弹出
    push_front()/pop_front() 队尾插入和弹出
    begin()/end()
    [] // 随机选取

6.1.8 set & map & multiset & multimap

set, map, multiset, multimap, 基于平衡二叉树(红黑树),动态维护有序序列
    size()
    empty()
    clear()
    begin()/end()
    ++, -- 返回前驱和后继,时间复杂度 O(logn)

    set/multiset // set里面没有重复元素,multiset里面可以有重复元素
        insert()  插入一个数
        find()  查找一个数
        count()  返回某一个数的个数
        erase()
            (1) 输入一个数 x,删除所有 x   O(k + logn),k是x的个数
            (2) 输入一个迭代器,删除这个迭代器
        lower_bound()/upper_bound() // 核心操作
            lower_bound(x)  返回大于等于x的最小的数的迭代器
            upper_bound(x)  返回大于x的最小的数的迭代器
    map/multimap // 映射,从A到B
        insert()  插入的数是一个pair
        erase()  输入的参数是pair或者迭代器
        find()
        []  // 核心操作,注意multimap不支持此操作。 时间复杂度是 O(logn)
        lower_bound()/upper_bound() 
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<set>
using namepspace std;

int main()
{
	map<string, int> a;
	a["nnn"] = 1;// 像数组一样使用map, multimap不支持
	cout << a["nnn"] << endl;
}

6.1.9 unordered_set & unordered_map & unordered_multiset & unordered_multimap

unordered_set, unordered_map, unordered_multiset, unordered_multimap, 基于哈希表实现
    和上面类似,增删改查的时间复杂度是 O(1)
    不支持 lower_bound()/upper_bound(), 迭代器的++--

6.1.10 bitset 位存储

bitset, 圧位
    bitset<10000> s;
    ~, &, |, ^
    >>, <<
    ==, !=
    []

    count()  返回有多少个1

    any()  判断是否至少有一个1
    none()  判断是否全为0

    set()  把所有位置成1
    set(k, v)  将第k位变成v
    reset()  把所有位变成0
    flip()  等价于~
    flip(k) 把第k位取反
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

2021 Nqq

你的鼓励是我学习的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值