算法--数据结构+STL

本节内容

在这里插入图片描述

链表与邻接表

链表

主要思想

在这里插入图片描述
因为用真正的链表来写算法的话 会new很多东西 而new的过程是非常慢的

所以我们要用数组来写链表
如上图 链表分为单链表和双链表 单链表主要是邻接表 主要作用是存储图和树

e[]数组用来存储各个结点的val值 而ne[]数组用来存储各个结点的next指针 而表示在数组里就是存储下一个结点的数组下标
最后的空地址用-1表示
head表示指向头结点的指针 实际上就是头结点的下标

!!所有的结点都在e[]数组里 但是他们的排序不是按照e[]的下标按照顺序连续排序的 他们的排序是一个链表 存在ne[]数组里
e[k] 是指下标为k的e[]数组中的值 也就是在e[]数组中 下标为k的值
ne[k] 是指在e[]数组中下标为k的结点 下一个结点在e[]数组中的位置(下标)
在这里插入图片描述
同时会定义一个int变量idx 存储当前操作的点的下标 实质上发挥着指针的作用
他会指向一个e[]数组中 最新的可用的位置 用来新建一个结点

链表操作

初始化+在head结点后面插入

在这里插入图片描述
初始化就是将head改为-1 意思是head指向-1位置的空间 也就是空指针
然后idx可以更新为0 因为e[]数组中第一个可用位置就是下标为0的位置

插入
首先新建结点 e[idx]=x

之后next域指向头结点 ne[idx]=head
因为head存储的就是第一个数据的下标

之后将head指向新插入的结点 head=idx

最后idx后移 更新最新的可操作的位置的下标

普通插入

在这里插入图片描述
将x插入到下标是k的结点的后面

删除操作

在这里插入图片描述

在这里插入图片描述
注意中括号里就是存储的下标

!!所有的结点都在e[]数组里 但是他们的排序不是按照e[]的下标按照顺序连续排序的 他们的排序是一个链表 存在ne[]数组里
注意 k结点的下一个结点的下标不一定是k+1 而是ne[k] 这就是k结点的下一个结点在e[]数组中的下标

而e[]数组中的元素的位置排序 是按照生成的时间来排序的 因为生成一个结点就是在e[]数组中新建一个数据
比如 第k个插入的点 他的下标是k-1 这里说的就是在e[]数组中的下标

例子

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
注意光标所在行给了特判

双链表(双向循环链表)

主要思想

在这里插入图片描述
在单链表的基础上 加了两个指针域函数 一个是l[ ] 一个是r[ ]
在这里插入图片描述
因为是双向循环链表 可以让0号位置的空间是 头结点
1号位置是尾结点
这样的话 head的值就是0 tail(指向尾结点的指针)的值就是1

操作

初始化+双向插入

在这里插入图片描述
初始化 因为是循环链表 头结点和尾结点也是双向链接 所以 r[0]=1 l[1]=0;
同时 因为0 1 的位置被占用了 所以idx初始化是2

插入:
在下标为k的节点后面插入在这里插入图片描述
先新建结点 e[idx]=x
首先更新新节点的r 和 l
然后 更新两端结点的指针数组
先更新 右端点的 l[ ]数组 再更新左端点的 r[ ] 数组

不然如果先更新r 那么就找不到右端点的下标了 也就是找不到右端点了

在下标为k的点的左边插入
在这里插入图片描述
在k左边插入 实际上就是k的前一个结点的右边插入 所以改一下函数输入就行 add(l[k],x)

删除第k个点

在这里插入图片描述
将左边点的r 更新为 右边点的下标
将右边点的l 更新为 左边点的下标
在这里插入图片描述

邻接表

主要思想

在这里插入图片描述
就是将所有节点的邻节点用链表存了起来

栈和队列

主要思想

先进后出

主要操作

在这里插入图片描述
tt是栈顶指针 也就是栈顶下标 初始值为0(主要目的是好判断是不是空 因为初始化为0之后 一旦有元素插入 那么tt>0)

队列

主要思想

先进先出

操作

在这里插入图片描述
hh表示队头下标 tt表示队尾下标
tt初始化为-1)(这里不用再向栈一样用tt>0来判断是不是为空了 直接用hh<=tt 来判断是不是有元素 所以tt就可以初始化为-1 这样插入元素的时候 直接从下标0开始)
hh初始化为0
(不进行初始化 默认是0)

单调栈与单调队列

在这里插入图片描述

单调栈

主要思想

在这里插入图片描述
单调栈的题型:
在i的位置插入一个数x
找到在该序列中 左边离x最近的且小于x的数

单调栈 主要就是将一个序列 找到他的性质 将其单调化 如下:
在这里插入图片描述
假设我们要找一个数x的左边离x最近的且小于x的数,那么在一个栈中,如果这个数x左边有如上图这样的数的关系的话,即ax >= ay 且ax在ay的左边,即x <= y,那么这个ax首先不是最小的(因为ay比ax小),其次他的位置也不是离x最近的,所以,有这样的关系的话,ax永远不会是答案,所以可以将栈中这样的元素直接删掉,而我们可以用栈顶元素来当那个参照点ay

在这里插入图片描述
我们可以把i左边的数全部加入栈中
然后 对该栈进行一些处理 把那些相对位置在左边的且较大的数弹出 这样 整个栈就变成了一个单调递增的栈 与新插入进来的数进行比较的时候就比较方便 (进行该步时 先暂时不考虑是否相等的问题 因为现在先处理的是研究点x放入之前的状态)

当x插入之后 将x栈顶元素(stk[tt])与x比较 当栈顶元素比x大的时候 就出栈(tt–)
(因为如果栈顶元素大于新插入的点,那么上述那种关系又产生了,所以,就可以将其出栈)
直到找到某个元素小于x 这样即可

例题

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
使用 cin.tie(0)
ios::sync_with_stdio(false)

可以使输入输出提高效率

循环之前的tt以及循环完之后的tt,都是目前单调栈中的栈顶位置(tt永远是栈顶元素的位置),或者可以说是即将要压栈的x元素的前一位置

要牢记:while循环就是用来进行无用元素的删除的,可以看到主要是对tt操作

单调队列

主要思想

在这里插入图片描述
从原序列中 找到一个性质 可以让序列变单调

例如,本题要寻找每次窗口内的最小值:在每一次的窗口中,如果该窗口,即该队列队尾前面有些元素大于队尾,那么就可以将大于队尾的元素删去了,因为只要队尾元素比他们小而且比他们存在时间长,所以,只要有队尾元素的存在,那么前面那些大于队尾元素的元素就永远不可能是最小值,所以他们相当于是无用元素,直接删去(即弹出队列),这样,在每个窗口之内都是单调递增的。那么每次更新窗口之后,进行无用元素的删除,就可以得到一个窗口的单调性,有了该窗口的单调性,就可以在该窗口内进行取值和其他优化了

例题

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
n是数据的数量,k是窗口的大小
a数组存储原始数据,q数组是一个队列,他存储一个队列元素中各个元素在a数组中的下标,之所以不存储数据是因为根据题目要求,数据已经存在了a数组里,之后我们并不知道数据是什么,我们只知道窗口在a数组内滑动,而a数组的数据固定,所以存储元素的下标是一个较为清晰的选择(q[hh]就是当前hh所指向元素的下标,q[tt]就是当前队尾所指向的元素的下标)

首先在main函数中输入原始数据
之后初始化hh=0,tt=-1
for循环 i ,i 表示窗口的右端点下标
之后,判断窗口是否划过了a数组的第一个元素,因为如果没划过,那么hh不动,如果划出了a数组的第一个元素,那么hh每次都要进行更新,所以,用一个if放在一个for循环中,i-k+1,表示左端点,当他>q[hh],时(因为此时q[hh]=0),hh ++
之后一个while循环,与单调栈一样,while循环是用来清除无用元素的,所以当队列不空且a[q[tt]] <= a[i]时,(其中a[q[tt]]是队尾元素)就将其删去,因为此时a[i]还未加入,所以直接出队(但是是从队尾出队,即操作tt),tt–
循环结束后,无用元素被排除干净了,然后将新下标加入到队列,q[ ++ tt] = i;
最后当 i - k + 1 的值大于等于0时,说明窗口正式满员,输出开始,输出队头元素就是最小元素a[q[hh]]

之后对于最大值,将while循环改一下即可,最大值就是队头,因为a数组全程没修改,只是在用q数组,所以初始化hh 和 tt 到最原始状态,就可以再次进行最大值的输出了

总结

单调栈和单调队列都是在其中一次栈或者一次队列中,进行无用元素的删除,从而达到目前栈或者队列的单调性,就可以进行取值或者其他优化了

KMP

算法思想

在这里插入图片描述
KMP是字符子串匹配,即给定一个子串,找到原始字符串(主串)中与该子串匹配的位置

主串和子串的字符都是从下标为1开始存储

先看第一部分,当红线(子串)匹配的过程中,遇到某个点出现了不匹配的情况,那么按照朴素思想,我们应该将整个子串向后移动一个,然后从子串开头开始匹配,但是这样效率显然很慢
我们对子串的回溯位置做优化:
我们可以看子串从断点(即不匹配的点)前一个点开始,向前到达某个位置,该段的子串与从1开始长度一样的子串相等,即后缀==前缀,因为我们知道后缀与主串后面的部分相等,而现在前缀又等于后缀,所以,前缀就等于主串后面的部分,也就是这段已经匹配成功了,无需再匹配了,(即以 i - 1 对应的子串的位置为终点,向前回溯长度最长的,满足该条件的子串,我们记长度为 j ), 我们可以将这样的位置预处理在一个ne数组中,ne[ j ] (因为主串中的 i 点是断点,对应着子串中的 j + 1 点,所以也可以说 j + 1 是断点,所以,每次更新的是断点之前的点,即如果子串在 j + 1 点失配了,那么更新断点之前的点,ne[ j ]表示在j + 1 失配后, j 要被更新到的位置)

且后缀越长 那么已经确定的就越多 待确定的就越少,那么就是j越大越好

这样的话,下一次如果在 j + 1 断开了,那么直接从ne[ j ] + 1 的位置开始接着后续的匹配

例子+题解

在这里插入图片描述
在这里插入图片描述

数据分析:n是子串(模式串)的大小,m是主串大小
p数组是存储子串,s数组存储主串(修正:类型应该是char)
ne数组存预处理出来的 j 位置回溯到的位置(大小与子串的大小相等)

但是注意,子串和主串都是从下标为1的位置开始存字符串的(之后i表示主串的下标,j表示子串的下标,他们作用于不同的数组,即作用于不同的字符串)

之后先看匹配过程,
for循环,使用 i 进行大循环,i 从 1 开始,j 从 0 开始(因为 j 都要比 i 慢一步,j 的对应位置是 i - 1 );i <= m; i ++
之后一个while循环,条件是(j && s[i] != p[ j + 1 ] ),意为当主串的 i 与子串的 j + 1 失配,并且 j 没有到0的位置(因为 j 到0的话就退无可退了) 就将 j 进行回溯,j = ne[ j ]。那么将j 回退成ne(j),但是如果回退了还是失配,那么就还需要回退,所以是循环
之后因为while的停止条件有俩,所以,如果是第二个条件,即第一个if判断,如果s[i] 与 p[j+1] 匹配了,那么 j 就正常递增,j++
最后判断如果j == n,那么就是匹配成功

预处理ne数组的过程
在这里插入图片描述
本质上,我们是想求子串每个位置 i 的 ne[ i ],也就是找到每个以 i 为后缀的某个段(也就是以j+1为后缀的终点,这是与之前kmp不同的地方),与其子串的某个等长的前缀的段相同,
所以我们可以效仿kmp匹配的过程,
我们复制一份子串,然后将其中一个子串固定,当成主串,另一个子串进行匹配,一旦拷贝出来的那个子串有某个位置 + 1,可以与 i 进行配对,那么我们就找到了找到了以j+1为终点的后缀 = =等长的前缀,也就是 i 位置的“后缀==前缀”这个条件(因为以j为终点的子串前缀在任何时候都与主串的后部分相等,所以主要就是寻找能满足 j+1 与 i 匹配的 j 点)
设置 i 为断点,(也就是j + 1 为断点),找到某个 j + 1 能与 i 匹配,这时,这个 j 就是ne[i] 了
在这里插入图片描述
所以,我们对子串的长度进行枚举,i 从2开始,因为1的时候就是0,j仍然等于0,i <= n(子串长度); i ++
之后while循环,条件是j没有退到0,且p[i] != p[j+1],那么这个时候还没成功,就继续更新 j = ne[ j ]
然后如果 p[ i ] == p[j + 1],也就是满足了我们要找的特殊条件,则 j ++
这个时候,j 就是我们要的ne[ i ]
这样就预处理出来了所有的ne[ i ]

之后完善一下匹配成功之后的操作:
在这里插入图片描述
因为这个时候,j先++了,i 还没来得及++,所以,i 这个时候 等于 j ,想要得到匹配成功的起始位置,那么就是 i - n + 1(如下图),但是如果题目要求下标从0开始,那么就是将我们得到的结果再减一即可,即 i - n,
在这里插入图片描述

!!!
我们还看到在匹配成功后,又进行了一步 j = ne [ j ],这个主要用于我们还需要得到第二次匹配成功,因为此时匹配成功了,j 已经到了子串的最后一个位置,无法继续向下移动了,所以,我们要将j回退,继续下一个匹配的寻找,如下图解
在这里插入图片描述

我们要继续寻找下一次匹配成功,那么这时 i更新了,向后移动一位 要与子串的 j + 1 进行匹配,但是因为 j 已经是子串的末尾了,也就无法进行匹配了,所以,要把 j 回溯,继续进行第二次的匹配
但是注意:这个只是用在要在主串中多次进行子串的匹配时才会用到,如果一旦找到子串就拿信息然后退出的话,就没必要再做这一步了

Trie树

思想

在这里插入图片描述
他的作用是高效的存储和查找字符串
在这里插入图片描述
存储字符串的时候 每个字符串的结尾字符要打上一个标记 表明有字符串以该位置结束
在这里插入图片描述
当查找时 哪怕每个字符都能找到 但是结尾没有打标记 那么也是无法找到该字符

例子+模板

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
son[N][26]用来存储某个点的所有儿子 N是指整个树一共的结点个数
cnt[N] 表示整个树中某个结点作为结尾出现了几次
idx表示当前的最新的可以使用的无重复的编号
在这里插入图片描述
idx每次的值只会递增 给每一个节点都赋上一个特有的值,如下图:
在这里插入图片描述
插入两个字符串,他们的结点都被附上了不同的值,因为idx是全局变量,不会被重置为0
注意根结点会被赋值为0 以及空节点(也就是没有儿子的点)会被赋值为0

p从根结点开始 每次更新为当前的操作结点

u是字母映射到数字
son[p][u]:下标为p的点的其中一个儿子是26单词表中第u+1个字母

插入:
首先传入要插入的字符串 之后定义p等于0 意味着从根结点开始遍历 然后for循环利用字符串最后以\0结尾进行循环 同时将每个字母映射
判断如果当前节点的孩子中没有该字母 那么就让该位置等于 ++idx 这样该位置就是不是0 意味着是该子串的第一个字母被加入了结点(也可以理解为编号,每个字符的编号不一样,都是独一无二的) 最后更新p为son的值,也就是当前节点不当儿子了,当父亲,从而看他的儿子该是谁,p永远是最后一个结点
循环结束后 更新cnt[p] 令其++,记录树的分支中“通过特定的字符串序列,生成以编号p的字符为结尾的字符串”的数量,因为每个结点的编号不一样,所以每个p都不一样,哪怕对应的字符一样,其通过不同的字符串也会有不同的p。(但如果字符串内容一样,那么就是同一个p编号,因为在if后面会无条件执行p=son[p][u],如果是不同的字符串,那么会将新产生的son赋值给p,但如果是相同的字符串,那么每次if判断都为假,不会执行if语句,每次都在更新p为当前节点已经存储的儿子结点,相当于顺了下去,没有任何改变,最终顺到结尾,cnt++)

查询:
跟插入是一样的操作 操作指针p从根结点0 开始 之后循环判断字母是否存在 如果有一处不存在 直接返回0
每次循环不管存在与否 ,也就是如果if没有执行,那么就是可以查到,那么就更新p为最新的操作结点 p=son[p][u]; 相当于顺了下去,没有任何改变,最终顺到结尾,这样循环结束之后 就可以找到传进来的字符串str[]

同时 p也指向了尾巴结点 该结点有一个数组信息 保存着该结点为结尾的字符串的数量
若要查询某个字符串出现的次数 那么要根据该特定的字符串遍历下来,才能找到某个字符的末尾 也就是拿到末尾的信息 返回cnt[p]即可
以上是插入和查询操作
在这里插入图片描述
加上上面这个之后 就是整个题的题解

补充

二维的话用bitset容器,#include bitset<一维大小> b[二维大小]
将该容器当成bool来用,节省32倍空间

并查集

思想

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
并查集的优化 :路径压缩
在第一次遍历后 从某个点一直向上查找 找到了根节点之后 将路径上的点全都指向根结点 这样 只需要一次遍历 就一劳永逸了 时间复杂度近乎 O(1)

例子+模板

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
N的作用只是用来开辟一块空间
p[x] 表示某个结点的父节点,x可以是整个树任意一个结点的下标

在main函数中 首先输入 n m
对n个数 分别创建n个根结点 如上图for循环 并且对n个结点都赋予编号 依次是从1到n(因为题目要求每个结点刚开始都各自在一个集合中,而如果题目不做要求,也要这样初始化,每个节点各自在一个集合)

之后对于m个查询 要输入操作 M 1 2 或者Q 3 4

这里注意 读取char类型时 用字符串的方式来读入 且定义字符数组时 要定义一个以上(给字符串后面的\0留出空间)读取用%s 后面对应数组名 这样在读取时可以去除空格或者回车 不然要是用%c读入 会读进来空格或者回车(因为空格和回车都是字符,也就是我们常说的转义字符,所以会被%c读入)

之后,合并集合时:
直接将p[find(a)] = find(b)
可能会有一个疑问,如果我们先输入了 2 4(假设2 和 4 原先都是集合的头结点),那么p[find(a)] = find(b),会将2从属到4麾下
那么如果我们又输入 4 2,那么p[find(4)] = find(2),下意识意为将4从属到2麾下,但是实际上并不这样,实际上find(4)还是4,find(2)就不再是2了,而是4,所以,实际上是p[4] = 4,不改变任何状态,相当于没操作,所以该式子会自动忽略相同的两点并集的操作,且哪怕有许多个连接组合,最终如果并成一个集合的话,也是所有的点只有共同的一个祖宗结点,这是通过大量实验得到的结论,所以该式放心用即可(但是如果遇到计数的话,还是老老实实进行判断是否p[find(a)] == p[find(b)],将这些没做出实际改变的行为排除掉)

上面的find函数 是并查集最核心的操作
采用了递归+路径压缩
如果p[x]不是根结点 那么p[x]=find(p[x]);
最后return p[x];
在这里插入图片描述
如上图 这样不仅可以返回到c的根结点的编号 还可以把路径上的每个节点的父节点数组都更新为根结点

补充:小tips
在这里插入图片描述
这里注意 读取char类型时 用字符串的方式来读入 且定义字符数组时 要定义一个以上(给字符串后面的\0留出空间)读取用%s 后面对应数组名 这样在读取时可以去除空格或者回车 不然要是用%c读入 会读进来空格或者回车(因为空格和回车都是字符,也就是我们常说的转义字符,所以会被%c读入)

补充:
注意!!
在这里插入图片描述
创建结点时循环从i=1开始 最后小于等于n结束
输出时注意回车的输出

其他应用

在这里插入图片描述
在这里插入图片描述
相对于上面的题 这里多定义了一个size[N]数组 用来维护某个根结点的集合中的元素个数 因为题目后续要问

之后在for循环里 对size[]进行初始化 直接全部等于1即可
在这里插入图片描述
当执行创建一条边的操作时 也就是跟上面的将两个集合合并的操作是一样的 但是这里要对size[]进行维护 也就是进行加和 size[find(b)] += size[find(a)] 便于后续查询某个根结点所在集合的元素个数
如第三个if就用到了size[] 要查询某个结点所在区块的结点个数 那么先将该节点的祖宗节点找到 之后返回所维护的该节点的size大小

这个continue,不仅仅去除了一些重复的连线操作,还把题目中提到的两个点可能相等这个情况排除掉了

堆(手写数组堆 而不是STL,可以支持一些STL中无法直接提供的操作)

思想

在这里插入图片描述
这五个操作是手写堆的操作 其中第四第五个操作是STL中的堆所无法直接实现的

对于堆的介绍:
在这里插入图片描述
堆是一种完全二叉树 也就是最后一层之上全是满的 最后一层从左到右依次排列
同时 根结点小于左右儿子 所以 整棵树从上到下每层依次是从小到大排列 但是每个层层内的大小关系不确定 这种关系俗称小根堆

可以用一维数组来存储堆 下标从1开始 1号是根结点 假设某点下标是x 那么该点的左儿子下标是 2x 右儿子是 2x+1
且这样排号,反映到树上就是从上到下从左到右依次按序号存储
1
2 3
4 5 6 7

基本操作

堆的基本操作

down(x):
在这里插入图片描述
在这里插入图片描述
当替换一个更大的数 那么就要将其向下移动 确保最小值在上层
所以 将x与下层的min进行交换 之后循环判断交换 直到满足最小的在上层

up(x):
在这里插入图片描述
在这里插入图片描述
在最底层加入一个数之后 还是要确保最小的在上层 但是由于之前已经按照规则排序好了 所以只需要考虑该节点与其父节点 二者进行比较 满足下面的大于上面的 就进行交换 循环判断执行 直到满足小根堆的性质

由down和up可以组合起来完成上面的五个操作
在这里插入图片描述
1.对于插入一个数 首先要维护一个size 用来记录当前的heap数组的大小 同时也是最新的可以插入的数组的位置 由于之前已经看到 树中的序号从上到下从左到右是从1开始的有序排列 所以 新插入一个数 就会跟着接着排序 也就是heap一维数组中的最新的空位置 所以heap[++size]
=x;之后up(size) 将该数up一遍 确保是最小值在上面
2.对于求最小值 直接获取根结点即可
3.对于删除最小值 因为在数组中 删除最后一个点好删 但是删除第一个节点 是不太容易的 所以就把最后一个点代替掉根结点 然后将新的根结点上的值 down一遍 确保最小值在上面 如下图
在这里插入图片描述

在这里插入图片描述
对于第四第五 直接进行替换 之后down一遍 然后up一遍 二者只会执行一个

例题+模板

(实现down以及up操作)
down
在这里插入图片描述
在这里插入图片描述
对于down操作 首先用t来表示三个点的最小值的坐标 首先将t赋值成参数u
之后两个if 第一个部分判断是否存在左儿子 以及 左儿子比根结点小 那么就更新记录最小值的下标的变量t

最后判断是否t被更新了 如果被更新 那么再将h[t]与h[u]进行交换 最后在if函数里递归执行 down(t)

对于创建一个堆 首先将n个数依次输入到h[i]中 然后利用一个for循环 i从n/2开始 当i不为0 i–
之后在for循环 down(i);

up:
在这里插入图片描述
直接进入一个循环 循环条件:当u/2不为0 那就是父节点存在(因为左儿子是2x,所以如果当前下标是左儿子的下标,那么该下标除以2就是其父节点的下标,而右儿子是2x+1,他是一个奇数,因为整除的话,会舍弃掉余数,所以他的下标除以2也是x) 同时 h[u/2] > h[u] 父节点大于该结点 那么就没有形成堆 那么就进行交换 同时更新u = u/2;
在这里插入图片描述
因为是整形运算 所以会丢掉余数 所以不管是左儿子还是右儿子 除以2 都是父节点的下标

哈希表

基本思想

在这里插入图片描述
哈希表基本思想就是 将一个范围特别大数 映射到一个范围较小的数

而将x哈希成某个值的函数叫做哈希函数(一般是对哈希数组的长度取模运算) 但是由于等待被哈希的数范围很大 范围被压缩之后 难免会有某几个值的哈希值相同 这样就产生了冲突 而解决冲突的方式 分成了开放寻址法和拉链法

补充:
在这里插入图片描述
第一步进行取模映射时 最好那个基数是大于哈希数组长度并且是一个质数 可以用程序跑一下看看大于数组长度的最小的质数是几 如下:
在这里插入图片描述

解决冲突:

拉链法:

在这里插入图片描述
将冲突的数 用一个链表 接在数组的节点上
在这里插入图片描述
删除:
先找到要删除的数 然后不会进行真正意义上的删除 而是打一个标记 表示该数被删除了
在这里插入图片描述
数据分析:
N的作用是表示哈希数组的大小 且被处理成大于题目所要求的哈希数组大小的第一个质数

h[N] 哈希数组 每个数的哈希值与数组下标对应 例如 某个数哈希值是1 那么就在h[1]的位置加一个节点 但是哈希数组内存储的值 是链表的第一个节点的下标
e[N] 链表的值域 存储节点的值
ne[N] 表示在链表中存储节点的下一个节点在e数组中的下标
idx 表示最新的e[]数组中可用的空间的下标

例子+模板

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
首先包含一个cstring头文件

之后是插入和查询操作

对于插入:
首先将传入的x哈希值求出来
x % N 但是由于数据范围有负数 c++中 负数余上一个数还是负数 所以要+N之后再% N

然后就是将x传入e[]数组 也就是开辟新节点
之后将新节点插入到链表头 现将整个链表的第一个节点的下标传递给新节点的ne数组 然后更新h[k]数组的内容 更新为新节点的下标 这样就把新节点插入到链表头了

对于查询:
首先是一个bool类型的函数
第一步仍然是把传入进来的x哈希值求出来 这样就找到了哈希值为下标的数组元素 该数组元素存储着链表的第一个节点的下标 通过h[k]的值 就可以找到所有映射到该下标的原始值 之后遍历链表 判断是否是x即可
-1表示已经到了链表的末尾(因为初始化时 就是使用-1来初始化的 然后一旦插入了链表 -1就到了链表的末尾)
在这里插入图片描述
在main函数里 加入了一个memset函数 参数列表是 哈希数组数组名,-1,哈希数组的长度
作用是初始化哈希数组为-1 需要加入头文件
这里的-1就是表示地址,表示一个空地址

开放寻址法:

在这里插入图片描述
实际上就是找x的哈希值k
然后x存放在h[k]
如果h[k]已经存放了一个x的话
那么如果有冲突 就往后排队存放

在这里插入图片描述
首先对于开放寻址法 哈希数组的长度要开数据范围的2到3倍 同时要找到大于该数的第一个质数 作为N的值 也就是哈希数组的大小

之后主要是利用find(x)函数来进行所有的操作

该函数的功能是找到原始值x的在哈希数组中的位置 如果存在 那么返回该位置下标 如果查不到 那么返回的就是该原始值x“将”要在哈希数组中的位置

注意:该算法中,h【x】存储的是映射值为x的原始值

对于find(x)函数{
首先将x的哈希值找到 之后在哈希数组中 该x在哈希数组中的位置就是从k下标开始 往后排队的
所以有一个循环 如果h[k]!=null && h[k] !=x 那么就说明是还在查找中
循环函数中:
首先让k++
之后if( k==N ) k=0; 意为当遍历到数组的最后一位还是没有找到的话 就从数组的第一位开始找前面的位置

return k;
}

这里注意 设null=0x3f3f3f3f 设置初始值用比数据范围要大的数来设置 该数比1e9还要大 所以一般情况下可以放心使用
之后初始化数组 memset(h , 0x3f,sizeof h)
因为memset第二个参数只能取四位 所以是0x3f 但是会把该位*4 所以最后仍然是0x3f 直接记就好 最后初始化的结果仍然是每一位都是0x3f3f3f3f

对于0 和 -1 直接就是0 和 -1就好 因为很巧这两个的四分之一还是他们自己
所以直接:
memset(h , 0,sizeof h)
memset(h , -1,sizeof h)

在main函数中
插入时 直接返回k=find(x) 这时拿到的是该原始值x“将”要在哈希数组中的位置 所以直接h[k]=x;
查询时 拿到k=find(x)之后 这时应该是有上面所说的两种情况 所以判断h[k] !=null 那么就是有
如果h[k] !=null 那么就是没有

字符串哈希

思想

在这里插入图片描述
字符串哈希 就是可以将字符串的前i个字母组成的子串 映射成一个独有的哈希值

我们可以将一个子串转化成一个p进制的数 因为进制数都是该数的权重*该数的值 之后求和
所以映射成上图的最后一行的数 然后要将该数对Q取模

!! 注意:h[x]的值,就是前x个字符的hash值,也就是处理完之后的值

这里有一组经验值 就是当p取131 或者13331 并且Q=2的64次方 时 这样的规则映射下是不会有冲突的

这里注意
在这里插入图片描述
不能映射成0 这样一定会产生冲突
在这里插入图片描述
利用哈希值 可以求得任意一个子串的哈希值 如下图
在这里插入图片描述
最终的公式就是 h[R]-h[L-1]*p的R-L+1次方(即R-(L-1))
该公式的含义就是从L到R这一段子串的哈希值
在这里插入图片描述
上面说要求得加权和之后 去模 2的64次方
这里有个技巧 就是用unsigned long long 来存储h[] 这样溢出就相当于取模 就不需要取模了 子串的哈希值永远都是0~2的64次方-1 因为unsigned long long 的最大范围就是2的64次方(因为对于ull类型的数组,他的元素都是ull类型的元素,而当我们给一个ull类型的数赋值,不在他的范围的时候,他就会进行取模运算)(具体见c++primer关于变量一章)

补充:
这是对一个字符串进行预处理 将其映射成哈希数组
在这里插入图片描述
h[i],表示前i个字符组成的字符串的hash值
p,表示进制,一般用131、13331
str[],是一个字符数组,存储着原始字符串,他在运算时会提升为int
在这里插入图片描述
每个子串的哈希值 在0~2的64次方-1 这就是取模的效果

这就体现了哈希的特点 从大向小映射

例子+模板

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
用处:快速判断两个字符串是否相等

!!! 注意点:一定要注意区分小p数组:p[ ] 和 大p进制数:P

首先 对ull 重命名为ULL
设置大P为131 大P就是P进制

之后创建字符串char str[N]
以及定义ULL类型的哈希数组h[N]
以及p[N] 表示大P的某次方(表示当前操作位的权重)(p[2],就是p的2次方,p[100],就是p的100次方,这个主要用于计算某段子串的hash值,其大小与h数组大小同步)
h[ ]数组表示给定的字符串的前几个字符组成的字符串的hash值

首先定义p[0]=1 因为任何数的0次方都是1
之后一个for循环从1开始 到小于等于n 但是注意输入字符串时 要从数组名+1开始
之后预处理p[ ] 以及 h[ ]

P的i次方就是前一个次方*P
h[i]=h[ i -1 ]*P + str[i];

之后看上面的get函数 返回值是ULL 参数是两个整数 表示字符串str中的下标 会返回下标是l 到 r的子串的哈希值
用公式 return h[r]-h[l-1]*p[r-l+1]

小技巧 输出字符串的时候 可以用puts (“字符串”)

STL补充

c++11标准下 迭代器支持随机访问的容器

(c++20标准也是这样)

支持随机访问 那就意味着迭代器可以+n 同时也可以 ++ –
不支持的话 只能++ –

支持随机访问:
vector
string
deque

不支持随机访问:
set
map
list

没有迭代器:
queue
stack
C ++迭代器用于对数据结构中的元素进行顺序访问或随机访问。因此,对于根据定义不允许顺序或随机访问的数据结构,迭代器没有任何意义。这就是堆栈和队列没有迭代器的原因。另一方面,向量和列表允许对元素进行顺序和/或随机访问,因此迭代器对于导航这些数据结构是有意义的。

快速上手网站
https://www.dotcpp.com/course/111

补充:倒序迭代器
在这里插入图片描述
只有vector、deque、list、set、multiset、map、multimap可以使用

vector

在这里插入图片描述
1.构造:
在这里插入图片描述
在vector容器对象后面直接加一个数字 表示直接定义这个vector的容量
在这里插入图片描述
这时一种新的较为方便的遍历方式 直接for(auto x :a ) cout<< x <endl;
2.size empty方法
在这里插入图片描述
这两种方法在所有容器中都存在

3.倍增思想:
在这里插入图片描述
在c++中 系统做某一操作所用的时间 与空间大小无关 与申请次数有关 例如 申请一块1000的空间与申请一块1的空间所用的时间一样
在这里插入图片描述
这样就可以采用倍增思想 首先定义32的空间 之后不够用了 就开一块2*32的空间 并把原数据拷贝下去
在这里插入图片描述
这样倍增思想可以减少运行时间 效率提高
4.三种遍历方式
在这里插入图片描述
第三种代码简洁 且效率最高
在这里插入图片描述
迭代器可以改为auto 自动推导i的类型

5.支持比较运算
运算法则是字典运算 如下
在这里插入图片描述
7.front()和back()
在这里插入图片描述
他们返回的是第一个元素的引用以及最后一个元素的引用 可以拿一个int类型的引用变量来接 或者直接输出 可以输出变量的值

pair

在这里插入图片描述
构造 可以用make_pair()
或者大括号
在这里插入图片描述

string

在这里插入图片描述
1.size()
2.empty()
3.clear()
4.字符串拼接
在这里插入图片描述
最后a=yxcdefc

5.截取子串
在这里插入图片描述
第一个参数是字符串开始下标 第二个参数是从这里开始的多少个元素 在这里插入图片描述
当第二个没有参数的时候 就从该位置一直到最后
在这里插入图片描述
如果第二个参数大于后面的数量 那么也会全部输出

6.c_str()返回字符串头指针
在这里插入图片描述
而字符串输出也有一种方式 就是用%s输出字符串的第一个指针 就会输出整个字符串

queue队列

在这里插入图片描述

clear实现
在这里插入图片描述
queue 是没有clear函数的
如果想清空队列 可以直接开一个空队列赋值过去
q = queue ()

stack优先队列

在这里插入图片描述
修改大根堆变成小根堆
一下两种方式:
在这里插入图片描述
在这里插入图片描述

deque

在这里插入图片描述

该容器效率极其慢

set/map

绝大部分操作时间复杂度是o(log n)
在这里插入图片描述

在这里插入图片描述

map注意点

与makepair配合

在这里插入图片描述
map与make_pair配合

用数组的方式加入和访问元素

在这里插入图片描述
只不过这里不是下标,而是键,插入和访问都是键来索引

排序是依据键的大小进行排序

在这里插入图片描述
输出是
1 1
3 6
5 4

map和set一样,不可以元素不可以重复,实际上是键不可以重复

在这里插入图片描述
如果使用make_pair插入(1,2)的话,是不允许的,插入是无效的,因为键“1”已经存在了
而如果使用m[1] = 2,则可以进行修改,他的意思是修改值,而不是加入新的键值对

unordered_set and unordered_map

在这里插入图片描述
与上面的操作一致 但是要除去有关排序的操作 因为这是无序的

在这里插入图片描述

bitset

在这里插入图片描述
一个布尔值占用一个字节
1024个字节=1KB
一个字节是8位

压位就是将一个字节压缩到一个位 那么1个字节就变成了一个位
所以原本需要1024B的空间 现在只需要开128B
压位可以节省8倍的空间
在这里插入图片描述

访问方式总结以及插入弹出方式总结

vector

顺序表,
随机访问,当成数组,可以迭代器(且支持随机访问),可用数组中括号
拿到对应的值进行改变的话会影响原vector

push_back(x)
pop_back()

list

链表,使用迭代器(不支持随机访问)
拿到对应的值进行改变的话会影响原vector

push_back()、push_front(x)
pop_back()、pop_front()

stack

栈,没有迭代器,top(),返回栈顶元素
拿到对应的值进行改变的话会影响原vector

push():入栈
pop():出栈

queue

队列,没有迭代器,front(),返回头元素,back(),返回尾元素
拿到对应的值进行改变的话会影响原vector

push():队头出队
pop():队尾入队

set

红黑树集合
使用迭代器(不支持随机访问),find(val)查找某个值,返回该值的迭代器
拿到对应的值进行改变的话会影响原vector

insert(x)
自动排序,不可用重复

map

键值对集合
使用迭代器(不支持随机访问),find(val)查找某个键值对的键,返回该键值对的迭代器,返回该值的迭代器
或者m[键],就可以拿到值
拿到对应的值进行改变的话会影响原vector

insert(make_pair(x,y))
m[新键] = 新值

总结

1、使用任何方式获取容器的某个元素,修改之后,都会影响原容器
2、除了stack、queue,其他都有迭代器
3、压入元素是push,弹出是pop,只不过vector只能从back,list有back也有front,set、map是insert

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值