算法基础的数据结构模板

文章目录

    • 一、链表
      • 1.单链表
      • 2.双链表
    • 二、堆栈
      • 1.栈
        • (1)基本特点
        • (2)单调栈
      • 2.队列
        • (1)基本特点
        • (2)单调队列
    • 三、KMP字符匹配,Trie
      • 1.KMP字符匹配
      • 2.Trie
    • 四、并查集
    • 五、堆
    • 六、散列表
      • 1.模拟散列表
      • 2.字符串哈希

一、链表

由于动态链表new一个新空间的时候耗时较长,因此写算法题时,尽量用数组模拟链表,即静态链表,不需要new,更快速,可以节约很多时间;

1.单链表

const int N=1e8;    
int e[N];   //存储节点的值
int ne[N];  //存储节点的next指针
int idx;    //表示当前用到了哪个节点
int head;   //head存储链表头
void init(void)     //初始化
{
    idx=0;
    head=-1;
}
void insert(int x)   //在链表头插入数x(即在链表的最开头插入x)
{
    e[idx] = x;
    ne[idx] = head;
    head = idx++;
}
void add(int k,int x)   //在节点后插入数x
{
    e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx++;
}
void del(int k)     //删除第k个节点后的结点
{
    ne[k] = ne[ne[k]];
}
void remove(void)   //将当前头结点删除
{
    head = ne[head];
}

模板题

2.双链表

使单个节点具有左右两个next指针;
记得main函数中引入初始化!!!

const int N=1e5+10;
int e[N];
int l[N];   //当前节点的左next指针
int r[N];   //当前节点的右next指针
int idx;
void init(void)
{
    l[1] = 0;   //初始时使得0号节点为头节点,1号节点为尾节点,都不存数值,只是作为链表的起点和终点形式
    r[0] = 1;
    idx = 2;
}
void insert(int k,int x)    //在k号节点右边插上数x
{
    e[idx] = x;
    l[idx] = k;     //建立插入节点与左右节点关系
    r[idx] = r[k];
    l[r[k]] = idx;  //修改左右节点指向,与下一行顺序不能反,否则l[r[k]]会变化
    r[k] = idx++;
}
void remove(int k)      //删除k号节点
{
    r[l[k]] = r[k];
    l[r[k]] = l[k];
}

模板题

二、堆栈

1.栈

(1)基本特点

一种后进先出的数据结构;
数组模拟栈:需要一个数组和一个栈顶top指针;

int st[N];
int top=0;    //栈顶指针
st[++top] = x;	//入栈
--top;  //出栈
if(!top) empty;	//判断是否为空
	else not_empty;
st[top];	//访问栈顶元素

模板题

(2)单调栈

栈中元素大小如下图呈现单调增加或单调减少的趋势;
应用时先用栈按照暴力模拟做法解出题目,然后去掉其中无用的元素,再分析其中的单调性,若剩下元素有单调性,则进行优化;优化后取极值或最值即取两边端点值,查找值可用二分;
算法基础的数据结构模板_第1张图片
一般需要用单调栈的问题就是求一个数左边第一个比它小的数,或者右边第一个比它大的数这种类似的问题;

给定一个长度为N的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出-1。
输入格式
第一行包含整数N,表示数列长度。
第二行包含N个整数,表示整数数列。
输出格式
共一行,包含N个整数,其中第i个数表示第i个数的左边第一个比它小的数,如果不存在则输出-1。
数据范围
1≤N≤105
1≤数列中元素≤109
输入样例:
5
3 4 2 7 5
输出样例:
-1 3 -1 2 2

题目链接

#include 
using namespace std;
const int N=1e5+10;
int s[N];
int top;
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);    //加速cin,cout读入输出
    int n;
    cin >> n;
    int x;
    for(int i=0;i<n;++i){
        cin >> x;   //每读入一个数,就处理一次
        while(top && s[top] >= x){  //如果在s[top]前面有比s[top]更大的数s[y],则s[y]不可能会被作为答案,因为s[top]离x更近,所以s[y]可以被清除
            --top;      //该循环后得到的结果是栈为空,或者x前面的数都比x小;而最后得到的结果是栈内的数呈现单调递增的趋势
        }
        if(!top)    //如果栈是空的,说明x前面没有比它小的数了
            cout << "-1 ";
        else
            cout << s[top] << " ";
        s[++top] = x;   //将当前的x入栈
    }
    cout << endl;
    return 0;
}

2.队列

(1)基本特点

一种先进先出的数据结构;
数组模拟队列:需要一个数组和两个指针,其中一个是队头指针,另一个是队尾指针;

int q[N];
int head;   //队头指针
int tail;   //队尾指针
q[tail ++] = x;	//入队,队尾插入
++ head;		//出队,队头弹出
if(head == tail) empty;		//若首尾指针相遇,则队列为空
	else	not_empty;
q[head];	//队头元素
(2)单调队列

队列中元素大小规律与单调栈类似;
应用时先用队列按照暴力模拟做法解出题目,然后去掉其中无用的元素,再分析其中的单调性,若剩下元素有单调性,则进行优化;优化后取极值或最值即取两边端点值,查找值可用二分;
一般用单调队列的就是窗口滑动类似的问题;
典型问题:窗口滑动

给定一个大小为 n ≤ 1 0 6 n≤10^6 n106的数组。 有一个大小为k的滑动窗口,它从数组的最左边移动到最右边。 您只能在窗口中看到k个数字。
每次滑动窗口向右移动一个位置。 您的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
输入格式 输入包含两行。
第一行包含两个整数n和k,分别代表数组长度和滑动窗口的长度。
第二行有n个整数,代表数组的具体数值。
同行数据之间用空格隔开。
输出格式 输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
算法基础的数据结构模板_第2张图片

题目链接

参考题目解析:

以样例为例;
我们用q来表示单调队列,p来表示其所对应的在原列表里的序号。
由于此时队中没有一个元素,我们直接令1进队。此时,q={1},p={1}。
现在3面临着抉择。下面基于这样一个思想:假如把3放进去,如果后面2个数都比它大,那么3在其有生之年就有可能成为最小的。此时,q={1,3},p={1,2}
下面出现了-1。队尾元素3比-1大,那么意味着只要-1进队,那么3在其有生之年必定成为不了最小值,原因很明显:因为当下面3被框起来,那么-1也一定被框起来,所以3永远不能当最小值。所以,3从队尾出队。同理,1从队尾出队。最后-1进队,此时q={-1},p={3}
出现-3,同上面分析,-1>-3,-1从队尾出队,-3从队尾进队。q={-3},p={4}。
出现5,因为5>-3,同第二条分析,5在有生之年还是有希望的,所以5进队。此时,q={-3,5},p={4,5}
出现3。3先与队尾的5比较,3<5,按照第3条的分析,5从队尾出队。3再与-3比较,同第二条分析,3进队。此时,q={-3,3},p={4,6}
出现6。6与3比较,因为3<6,所以3不必出队。由于3以前元素都<3,所以不必再比较,6进队。因为-3此时已经在滑动窗口之外,所以-3从队首出队。此时,q={3,6},p={6,7}
出现7。队尾元素6小于7,7进队。此时,q={3,6,7},p={6,7,8}。
那么,我们对单调队列的基本操作已经分析完毕。因为单调队列中元素大小单调递*(增/减/自定义比较),
因此,队首元素必定是最值。按题意输出即可。
作者:Tyouchie
链接:https://www.acwing.com/solution/content/2499/

#include 
using namespace std;
const int N=1e6+10;
int q[N];   //单调队列,但储存的并不是数,而是数组下标,即位置,再通过a[q[i]]得到值
int a[N];   //储存读入的数
int main()
{
    ios::sync_with_stdio(false);    //加速读入
    cin.tie(0);
    cout.tie(0);
    int n,k;
    cin >> n >> k;
    for(int i = 0; i < n; ++ i)  //滑动窗口,单调队列
        cin >> a[i];
    int head = 0,tail = -1;     //head要设为窗口最开始即为0吧,且为了保持统一,不特判,因为后面有元素入队操作q[++tail] = i,所以tail开始置为-1,++之后就到窗口最开始即0了吧
    //类比单调栈,那里是top为0为空栈,而在这里是head>tail为空队列,head<=tail则队列中有元素,开始时head为0,那么tail就会被置为-1
    for(int i=0; i<n; ++i){     //全部读入后一个个对数组元素进行处理
        if(head <= tail && i-q[head]+1 > k)     //当队列不为空即head<=tail 且当前队列中的长度(i为当前元素,即滑动窗口的最右端,q[head]为滑动窗口的最左端,窗口长度为i-q[head]+1)大于题目中给出的窗口长度k时
            ++head;     //队头元素(窗口最左端元素)出队
        while(head <= tail && a[q[tail]] >= a[i])   //当队列不为空,且当前处理元素a[i]更小时,因为a[i]左边比a[i]更大的值a[max](即a[tail]) 与a[i]在同一个窗口时,(且a[i]在a[tail]右侧,所以a[i]存在时间比a[tail]的更长) a[tail]不可能会被选中,所以去除a[tail],即使得a[tail]出队
            -- tail;     //队尾元素(窗口最右端元素)出队,特殊情况,非正常队列出队
        q[++ tail] = i;  //将当前处理元素i(a[i])入队
        if(i+1 >= k)    //因为队列中的元素是一个个增加,逐个处理的,而很明显只有当滑动窗口内(不算上已清除的)元素个数满足k时,才会输出最小值,比如总共8个元素,k=3,所以会输出8-3+1=6个元素,就是因为刚开始时处理窗口元素个数过少,不会输出元素
            cout << a[q[head]] << " ";  //由于队列(即滑动窗口)中元素为单调递增的(单调队列+处理的性质),所以head,即窗口最左端元素为最小值
    }
    cout << endl;
    head = 0,tail = -1;     //输出最大值,与上述类似
    for(int i = 0; i < n; ++i){
        if(head <= tail && i-q[head]+1 > k)
            ++head;
        while(head <= tail && a[q[tail]] <= a[i])   //这里改为若队列元素小于等于当前处理元素就出队
            --tail;
        q[++ tail] = i;
        if(i + 1 >= k)
            cout << a[q[head]] << endl;
    }
    cout << endl;
    return 0;
}

三、KMP字符匹配,Trie

1.KMP字符匹配

KMP算法真的难,多思考思考吧;
可求循环节;

关键是模板串p与模板串s匹配的时候,p的移动并不是暴力做法那样一位位移动,而是跳着移动,即利用已经匹配好的部分same,在s后面未匹配的部分中寻找与same相同的部分,再跳到该部分,如p=abc,s=abdabfabdabc,此时p与s中ab是相同的,即相同部分same=ab,所以在s中找ab即可,具体实现是由next数组完成,而next数组是通过p与自身匹配后得出的,实际上next数组的得出与kmp匹配类似,不同的是前者为p与p匹配,后者p与s匹配;匹配过程中s与p中指针j的物理位置是不变的,j= next[j],实际上是整个模板串p的向右移动,移动到具有s中相同部分same处,而这样相对来看就是j向左移动了。且实际上j只能在p上面移动,即j的移动范围是0~n,(初始为0,n为p的长度),且j之前的都是匹配成功的,所以匹配失败j就向前移动,若无法匹配则j=0,匹配成功j就在p上++,即向右移动,若j移动到p末尾n的位置,就说明整个模板串p都成功匹配了;

但是强也是真的强,时间复杂度为O(n+m),即O(n);

给定一个模式串S,以及一个模板串P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模板串P在模式串S中多次作为子串出现。
求出模板串P在模式串S中所有出现的位置的起始下标。
输入格式
第一行输入整数N,表示字符串P的长度。
第二行输入字符串P。
第三行输入整数M,表示字符串S的长度。
第四行输入字符串S。
输出格式
共一行,输出所有出现位置的起始下标(下标从0开始计数),整数之间用空格隔开。
数据范围
1≤N≤105
1≤M≤106
输入样例:
3
aba
5
ababa
输出样例:
0 2

#include 
using namespace std;
const int N = 1e5+10;
const int M = 1e6+10;
char s[M];  //模式串
char p[N];  //模板串
int ne[N];  //next有时会与头文件冲突,所以用ne更好,next[i]表示子串s[1,2,,,,,,i-1,i]的最长相等前后缀的前缀最后一位下标,或者说是子串的最长相等前后缀的长度,因为我们是从下标1开始的,这也体现出了从1开始的好处
int main()
{
    ios::sync_with_stdio(false);    //cin,cout加速
    cin.tie(0);//    **i一直向右**移动,作为**后缀**;而j经常会跳回去,即**j回退**,很容易回退到0即从头开始匹配**前缀**,但如果p前面出现abab···,当p[1~4]与s[1~4]满足匹配,j=4,但p[4+1]不满足匹配时,j回退,但此时不必回退到0,回退到p[2]即可,因为p[1],p[2]分别与p[3],p[4]相等,又因为匹配时是j+1进行匹配(p[j+1]=p[i]),所以p从第2个a开始匹配前缀就可以了,即j=next[j]
    cout.tie(0);
    int n,m;
    int pos;
    cin >> n >> p+1 >> m >> s+1;    //直接用cin读入字符串p和s,由于p和s都是从1开始的,所以是p+1和s+1
    for(int i = 2,j = 0; i <= n; ++i){  //求ne数组,本质是模板串p与自身的匹配,因为ne[1] = 0,所以从2开始,其中p[i]相当于p的后缀,p[j+1]相当于p的前缀,最好是debug一遍就容易理解了,p:abcdeabcgabcd
        while(j && p[i] != p[j+1])      //匹配失败,j回退,即将模板串向右移动
            j = ne[j];
        if(p[i] == p[j+1])  //如果该位匹配成功,j就向前移动一位即使得j指向该位置
            ++j;
        ne[i] = j;  //更新next,每次移动i前,将已经匹配成功的长度记录到next中,由于j为模板串中的1~j的最后一位,所以j即为最大相同后缀(或前缀)长度,再把j与下标i对应
    }
    for(int i = 1,j = 0; i <= m; ++i){  //由于是s[i] == p[j+1],s和p都是从1开始,所以int i = 1,j = 0
        while(j && s[i] != p[j+1])  //不匹配则回退,即将模板串p向右移动,可以看成是s和j指针的物理位置都不变化,只有p向右移动j-ne[j]+1位
            j = ne[j];
        if(s[i] == p[j+1])  //匹配成功则j往前走一步,注意j是在p上面移动的,j从0开始走,直到走到模板串p的末尾,即匹配成功,j移动的位置即为成功匹配的位置
            ++j;
        if(j == n){ //因为j在p上移动的位置就是p与s成功匹配的位置,所以当j移动到n时,即整个模板串p都已经成功匹配了
            pos = i - n;    //i为当前p匹配成功时,s的对应元素位置,又因为模板串p长度为n,且答案要求数组s是从0开始计算匹配位置,所以起点为(i-n+1)-1=i-n
            cout << pos << " ";     //输出成功匹配后s对应元素的起点位置
            j = ne[j];  //继续移动j或者说模板串p,看之后的s中还有没有与p完全匹配的
        }
    }
    cout << endl;
    return 0;
}

模板题链接

解释一下下标从1开始的好处,首先KMP算法的核心就是要理解next数组的含义。
当下标从0开始时,next[i]表示子串s[0,i]的最长相等前后缀的前缀最后一位的下标,如果我们要求出这个子串的最长相等前后缀的长度时,需要next[i]+1;
当下标从1开始时,next[i]依然表示子串s[1,i]的最长相等前后缀的前缀最后一位的下标,而且next[i]就是这个子串的最长相等前后缀的长度(这也是next[i]数组的另一层含义),不需要我们再去人为加1了。
从另一个角度来讲,下标从0开始时,next[i]=-1,表示我们找不到相等的前后缀。如果下标从1开始,next[i]=0,表示最长相等前后缀的长度为0,也就是说没有相等的前后缀,显然后者更符合我们的一般思路。
所以推荐大家最好从下标为1开始输入。
参考文献 胡凡-算法笔记

时间复杂度分析:

首先,在每次i++的过程中,j最多只会增加一次,因此j总共最多增加M次,而while循环中j总共的回退次数不可能超过它增加的次数,因此while循环中j最多回退M次,所以 KMP 匹配过程时间复杂度为 O(M)。Next[ ]求解时间复杂度为 O(N)。
因此,整个算法的总时间复杂度为 O(M+N)

参考题解1
参考题解2
参考题解3

2.Trie

Trie是一种高效地存储和查找字符串集合的数据结构(一颗树);
算法基础的数据结构模板_第3张图片
模板题链接

#include 
using namespace std;
const int N = 1e5 + 10;
int cnt[N]; //记录字符串出现的次数
int son[N][26];     //表示当前节点的儿子,且一个节点最多有26个子节点,son[1][0]=2表示1号节点的值为a的子节点为2号节点,son[x][0]为x的第一个儿子,son[x][1]为x的第二个儿子
int idx;    //现在是第几个节点,idx=0表示根节点或空节点
char str[N];
void insert(char str[])
{
    int p = 0;  //初始化,开始是根节点
    for(int i = 0; str[i] ; ++i){
        int u = str[i] - 'a';   //将a~z映射成0~25
        if(!son[p][u])  //如果当前无该字母,即对应的son[p][u]=0,则把该字母安照编号次序加进去
            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];  //返回该字符串出现次数
}
int main()
{
    // ios::sync_with_stdio(false); //加了个这东西反而错了???????
    // cin.tie(0);
    // cout.tie(0);
    char op;
    int n;
    cin >> n;
    while(n--){
        cin >> op >> str;
        if(op == 'I')
            insert(str);
        else
            cout << query(str) << endl;
    }
    return 0;
}

数值型Trie数题

#include 
#include 
using namespace std;
const int N = 1e5 + 10 , M = 31 * N;
int a[N];
int idx;    //节点次序
int son[M][2];   //整数个数不大于1e5,每个整数占据31个点,所以总共的点数需要31*N个
void insert(int a)
{
    int p = 0;  //开始时p指针指向根节点
    int k;  //每个节点最多有0或1两个子节点而一条路径上恰好有31个节点,即根节点沿着一条路径出发到叶节点,其中有31个节点
    for(int i = 30; i >= 0; --i){   //由于是从高位向低位(位数越高优先级越高,因为求得的值会最大)依次取31位上对应的0或1,所以开始时i = 30
        k = a >> i & 1; //取二进制0或1操作
        if(!son[p][k])  //如果当前无该二进制数分支
            son[p][k] = ++idx;  //则新建一个子节点
        p = son[p][k];  //p指针指向下一个节点
    }
}
int query(int a)
{
    int p = 0;
    int k;
    int res = 0;
    for(int i = 30; i >= 0; --i){
        k = a >> i & 1;
        if(son[p][!k]){ //如果存在于a[i]这一位不同的,就沿着这往下走,这样异或一定是较大的
            p = son[p][!k]; //p指针下移
            res = (res << 1) + 1;//答案res此时左移一位,再加上该第i位对应的1,;用<<或>>的时候打括号,不然会发生优先级的错误
        }
        else {
            p = son[p][k];
            res = (res << 1) + 0;//没有!a[i]也要左移一位
        }
    }
    return res;
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin >> n;
    for(int i = 0; i < n; ++i){ //先将所有数 分解为二进制形式后存入Trie树中
        cin >> a[i];
        insert(a[i]);
    }
    int res = 0;
    for(int i = 0; i < n; ++i){
        res = max(res , query(a[i]));   //依次开始查询
    }
    cout << res << endl;
    return 0;
}

四、并查集

作用:
1.合并两个集合;
2.询问两个元素是否在同一个集合当中;

基本原理:
1.用一棵树来表示一个集合;
2.树根的编号就是整个集合的编号;
3.每个节点存储它的父节点,p[x]就是x的父节点,根节点的父节点就是它本身;

实现细节:

if(p[x] == x) True;	//1.判断树根
int find(int x)//2.求x的集合编号,即通过递归结合路径压缩求对应根节点
{
	if(p[x] != x)
		p[x] = find(p[x]);
	return p[x];
}

	
p[y] = x;//3.合并两个集合,如让集合y成为集合x的子树

并查集优化方法: 路径压缩

还可通过添加其他变量来维护集合某些性质,如添加一个数组记录集合中节点数量可以维护集合内部节点数;
模板题

#include 
using namespace std;
const int N = 1e5 + 10;
int cnt[N]; //动态统计连通块中的节点的数量,开始用size[N]报错???可能和next一样
int p[N];   //父节点
int find(int x)     //寻找根节点
{
    if(p[x] != x)   //根节点特点是p[x] == x
        p[x] = find(p[x]);  //关键步骤,寻找根节点+路径压缩,递归实现,若没找到则一直令p[x] = find(p[x])即一直向上找
    return p[x];//直到找到根节点,且由于p为全局数组,当第一遍递归实现后,一个集合中所有子节点都直接与根节点相连(即路径压缩),这样的话之后就不用再递归寻找根节点了,可以以O(1)的时间找到根节点
}
int main()
{
    int n,m;
    char op[2];
    int a,b;
    scanf("%d%d",&n,&m);
    for(int i = 1; i <= n; ++i) {//初始化p和cnt,开始时每个节点都为根节点,每个集合里只有一个元素,1到n
        p[i] = i;
        cnt[i] = 1;
    }
    while(m--){
        scanf("%s",op);
        if(op[0] == 'C'){
            scanf("%d%d",&a,&b);
            a = find(a),b = find(b);//寻找a,b各自根节点
            if(a != b) {    //如果a,b不在同一个集合中
                p[a] = b;   //合并a,b
                cnt[b] += cnt[a];   //将a的数量添加到b中
            }
        }
        else if(op[1] == '1'){
            scanf("%d%d",&a,&b);
            if(find(a) == find(b))  //根据所属集合编号即所属根节点来判断是否在同一个集合中
                puts("Yes");
            else
                puts("No");
        }
        else{
            scanf("%d",&a);
            printf("%d\n",cnt[find(a)]);
        }
    }
    return 0;
}

较复杂模板题链接

五、堆

功能:
1.插入一个数;
2.求集合当中的最值(大根堆,小根堆);
3.删除最值;
4.删除任意一个元素;//4,5在STL中只能间接实现,不能直接实现
5.修改任意一个元素;

建堆的时间复杂度是O(n),
算法基础的数据结构模板_第4张图片
堆排序模板题

简单无映射堆:

#include 
#include 
using namespace std;
const int N = 1e5 + 10;     //堆的话下标从1开始,方便处理
int heap[N];    //堆数组
int n,m;
int size;   //堆大小
void down(int u)
{
    int t = u;
    if(2*u <= size && heap[2*u] < heap[t])  //当左儿子存在且更小
        t = 2*u;    //更新当前t
    if(2*u+1 <= size && heap[2*u+1] < heap[t])  //当右儿子存在且更小
        t =2*u+1;
    if(t != u){     //若出现儿子值更小的情况
        swap(heap[t],heap[u]);  //更新交换两节点
        down(t);    //继续看下一层儿子节点是不是比当层节点更小,一直递归
    }
}
void up(int u)
{
    while(u/2 && heap[u/2] > heap[u]){  //看父节点是否存在且更小
        swap(heap[u],heap[u/2]);    //更新
        u /= 2;
    }
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin >> n >> m;
    size = n;
    for(int i = 1; i <= n; ++i)
        cin >> heap[i];
    for(int i = n/2; i >=1; --i){   //从n/2即最后一个有子节点的节点开始建堆,向左向上依次down
        down(i);
    }
    while(m--){//堆排序
        cout << heap[1] << " ";     //根节点即为最小值
        heap[1] = heap[size--];     //直接用最后一个节点的值覆盖根节点的,再size--
        down(1);    //重新down,调整堆
    }
    cout << endl;
    return 0;
}

较复杂有映射堆
模板题链接

若全局数组值作为函数参数,可能会在函数中把本来不该改变的全局数组值给修改掉,因此不能直接传全局数组值,因此提前设置一个变量,使其代替该全局数组值;

#include 
#include //在有映射的堆中,调整节点位置都是通过swap_heap完成的,因为要考虑到hp和ph的映射关系
using namespace std;
const int N = 1e5 + 10;  //堆的话下标从1开始,方便处理
int heap[N];    //堆数组
int n;
int cnt;    //记录插入次序
int hp[N];  //heap point 由堆中节点对应的插入第k次次序,由于题目涉及第k次插入的数的求解,所以需要用两个数组记录第k次插入次序以及对应的在堆中节点的下标
int ph[N];  //point heap 第k次插入对应在堆中的节点下标,ph与hp互为反函数,即ph[k] = x,hp[x] = k
int length;   //堆大小
void swap_heap(int a,int b)//给的a,b是堆中节点下标,hp,ph都为堆节点下标与次序的映射
{//不仅要交换值,也要调整映射关系,否则会出现混乱
    swap(ph[hp[a]],ph[hp[b]]); //由于ph中参数为次序,所以先要把下标a,b转换成hp[a],hp[b]
    swap(hp[a],hp[b]); //双向都要交换,上面交换下标,这里交换次序
    swap(heap[a],heap[b]);//交换值
}
void down(int u)    //up和down参数对象都是堆中元素下标
{
    int t = u;
    if(2*u <= length && heap[2*u] < heap[t])  //当左儿子存在且更小
        t = 2*u;    //更新当前t
    if(2*u+1 <= length && heap[2*u+1] < heap[t])  //当右儿子存在且更小
        t =2*u+1;
    if(t != u){     //若出现儿子值更小的情况
        swap_heap(t,u);  //更新交换两节点
        down(t);    //继续看下一层儿子节点是不是比当层节点更小,一直递归
    }
}
void up(int u)
{
    int t = u;
    while(t/2 && heap[t/2] > heap[t]){  //看父节点是否存在且更小
        swap_heap(t,t/2);    //更新
        t >>= 1;
    }
}
int main()
{
    scanf("%d",&n);
    string op;
    int k,x;
    while(n--){
        cin >> op;
        if(op == "I"){//插入一个数
            scanf("%d",&x);
            ++cnt,++length; //插入次序和下标都++
            heap[length] = x;
            ph[cnt] = length;   //修改双向的映射关系
            hp[length] = cnt;
            up(length);     //插入的话是插到最后面,所以是up
        }
        else if(op == "PM"){//输出最小值
            printf("%d\n",heap[1]);
        }
        else if(op == "DM"){//删除最小值
            swap_heap(1,length--);
            down(1);
        }
        else if(op == "D"){//删除第k个插入的值
            scanf("%d",&k);
            k = ph[k];  //若全局数组值作为函数参数,可能会在函数中把本来不该改变的全局数组值给修改掉,因此不能直接传全局数组值,因此提前设置一个变量,使其代替该全局数组值
            swap_heap(k,length--);  //交换两个节点或者说一个节点覆盖另一个节点的操作由swap_heap函数来完成
            up(k); //在down和op中最多只会执行一个操作
            down(k);//为了方便up和down都写一遍
        }
        else{//修改第k个插入的值
            scanf("%d%d",&k,&x);
            k = ph[k];
            heap[k] = x;
            up(k);
            down(k);
        }
    }
    return 0;
}

六、散列表

1.模拟散列表

有两种创建散列的方法:
一是开放寻址法,即如果插入元素与表内元素发生位置上的冲突,插入元素就会移动到空位置再插入;
二是拉链法,用个数组表示每个hash后的值,然后数组的每个槽上连一条单链表;
散列表的操作一般只有插入和查找两种操作,删除的话并不是直接删除元素,而是在对应位置做个bool标记;
散列表模板题

(1)开放寻址法

#include  //开放寻址法模拟散列表
#include 
using namespace std;
const int N = 2e5 + 3;  //N一般是大于数据量两倍的一个质数
int h[N];   //散列表数组
const int null = 0x3f3f3f3f;    //null为大于题目中最大数据1e9的数,类似于inf,若数组位置内为这个数,则说明该位置为空,int为0x3f3f3f3f,long long为0x3f3f3f3f3f3f3f3f
int find(int x)//返回情况有两种,一种是找到空位置即返回null,一种是找到了x
{
    int k = (x % N + N) % N;    //hash操作
    while(h[k] != null && h[k] != x){//如果既非空又不等于x就一直向右寻找
        ++k;
        if(k == N)  //如果到了表的尽头就从头开始
            k = 0;
    }
    return k;
}
int main()
{
    memset(h,0x3f,sizeof(h));   //把h都初始化成null(inf),memset按字节初始化,int一个字节8位,即0x3f
    string op;
    int n;
    int x;
    scanf("%d",&n);
    while(n--){
        cin >> op;
        scanf("%d",&x);
        int t = find(x);
        if(op == "I")
            h[t] = x;   //不管是空还是x,都可以直接插入
        else{
            if(h[t] == null)
                puts("No");
            else
                puts("Yes");
        }
    }
    return 0;
}

(2)拉链法

#include  //拉链法模拟散列表
#include 
using namespace std;
const int N = 1e5 + 3;//N为大于1e5的质数
int ne[N];  //存储节点指向的下一个位置
int e[N];   //存储节点值
int h[N];   //槽,即数组每个位置对应的单链表的第一个位置
int idx;    //标号,其中标号是所有单链表共用的,如idx=1可能是第一条单链表里的,而idx=2可能是最后一条单链表里的
void insert(int x)
{
    int k = (x % N + N) % N;    //hash函数,这样取防止有负数,得到的结果一定为0~N的正数
    e[idx] = x; //和链表一样,先存值
    ne[idx] = h[k]; //类似于插到头结点,即插到链表的第一个位置
    h[k] = idx++;   //更新头结点
}
int find(int x)
{
    int k = (x % N + N) % N;
    for(int i = h[k]; i != -1; i = ne[i]){  //从h[k]开始寻找,按链表向下寻找,直到遇到-1
        if(e[i] == x)
            return true;
    }
    return false;
}
int main()
{
    int n;
    int x;
    memset(h,-1,sizeof(h)); //开始时数组槽由于没有链表,全部初始化成-1
    string op;
    scanf("%d",&n);
    while(n--){
        cin >> op;
        scanf("%d",&x);
        if(op == "I")
            insert(x);
        else {
            if(find(x))
                printf("Yes\n");
            else
                printf("No\n");
        }
    }
    return 0;
}

2.字符串哈希

字符串前缀哈希法
h[n] = 前n个字符的hash值(数值)
hash过程:
(1)将字符串str看成p进制的数,位数为str.size;
(2)计算值,转化为10进制数,如str = “abc”,则 n u m = a ∗ p 2 + a ∗ p 1 + c num = a*p^2+a*p^1+c num=ap2+ap1+c
(3)将值取模q,即将hash值映射到0~q-1,num = num % q;

这样可以将任意字符串映射到0~q-1;

注意事项:
(1)不能将某个字符映射成0,如h(a) = 0,则h(aa)= 0也成立,这样就会有冲突;
(2)p =131或13331,且 q = 2 64 q = 2^{64} q=264,这样几乎不会发生hash冲突;

若已知h[R]和h[L-1](R>L,h[R]是前R个字符的hash值)
h [ R − L ] = h [ R ] − h [ L − 1 ] ∗ p R − L + 1 h[R-L] = h[R] - h[L-1]*p^{R-L+1} h[RL]=h[R]h[L1]pRL+1;(先让h[L-1]乘上p的R-L+1次方,使得其与h[R]对齐,然后相减即得结果)
预处理:(类似前缀和)
h [ i ] = h [ i − 1 ] + s t r [ i ] h[i] = h[i-1] + str[i] h[i]=h[i1]+str[i]

字符串hash模板题

#include 
using namespace std;
const int N = 1e5 + 10,P = 131;
typedef unsigned long long ull; //一般用unsigned long long ,这样溢出的话就相当于取模了
ull p[N];   //存p的n次方,因为是p进制数,所以是乘p
ull h[N];   //前n个字符的hash值
char str[N];
ull get(int l,int r)    //计算l~r的hash值,类似前缀和的公式
{
    return h[r] - h[l-1] * p[r-l+1];
}
int main()
{
    p[0] = 1;   //初始化幂方为1
    int n, m;
    int l1, r1, l2, r2;
    scanf("%d%d%s", &n, &m, str+1); //类似前缀和,下标从1开始
    for(int i = 1; i <= n; ++i){    //初始化,类似于求前缀和
        p[i] = p[i - 1] * P;    //顺便计算p的n次方
        h[i] = h[i - 1] * P + str[i];   //h[i]是前i个字符的hash值
    }
    while(m--){
        scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
        if(get(l1, r1) == get(l2, r2))
            puts("Yes");
        else
            puts("No");
    }
    return 0;
}

你可能感兴趣的:(算法基础,数据结构)