| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 数据结构与算法 -> 链表与邻接表、栈与队列、单调栈、单调队列、kmp 算法 -> 正文阅读 |
|
[数据结构与算法]链表与邻接表、栈与队列、单调栈、单调队列、kmp 算法 |
链表可以用指针加结构体的实现方式 ?? 动态链表 在面试题中用得比较多,在笔试题中不常见 栈、队列都可以直接使用 stl 中的容器 但是如何用数组模拟链表、栈与队列?静态链表 为什么要用数组模拟呢? 效率问题:如果用指针加结构体的方式来实现链表的话,每次创建一个新的节点的时候,就要调用 new Node(),每次 new 一个新的节点。但是这个操作是非常慢的,笔试中一般不采用这种动态链表的方式(笔试中链表的大小一般为 10 w 级别 ~ 100 w 级别,单单把这 10 w 个节点 new 出来都会导致超时) 用数组模拟单链表单链表最初的状态:有一个头节点,头节点最一开始是指向空节点,每次往里面插入一个新的元素 单链表在笔试题最大的用途是用来写邻接表( n 个链表):用于存储树 (最小生成树问题) 和图 (最短路问题) 单链表的长相:每一个点都会存储两个值:一个 val(当前点表示的值是多少)、一个 next 指针(下一个点的位置在什么地方),链式的形状 用数组来模拟需要定义的东西如下: ①需要定义每个节点的 val ②每个点的 next 指针 用 e[ N ] 来表示某个点的值是多少,用 ne[ N ] 来表示某个点的 next 指针是多少,e[ N ] 和 ne[ N ] 都是整型数组 e[ N ] 和 ne[ N ] 是怎么关联起来的呢? e[ N ] 和 ne[ N ] 是用下标关联起来的,下图就是链表在数组中的表现方式:每个节点有两个值,e[ N ] 和 ne[ N ] 把每一个节点都存储到数组中,这些节点都有一个编号,假设第一个节点编号是 0,第二个节点编号是 1. . .空节点的编号用 -1 来表示 单链表的性质:可以用 O(1) 的时间直接找到下一个点的位置,但是找不到上一个点的位置 单链表是只往后看,不往前看的 如果想找到当前点的前面那个点(找 1 号点前面那个点),只能从头开始遍历 如果想遍历整个链表的话,可以沿着这条边,把整个链表遍历一遍 链表最一开始为空,需要给它不断分配新的点,不断往里面插入新的点,idx 相当于是一个指针,一开始总共分配了 n 个点的位置,idx 是一个指针,从第 1 个点开始,当我们需要分配一个新的点的时候,就把当前 idx 这个指针指向的节点分配给它,然后把 idx 指针往后移动一位 单链表可以在任意位置插入,但是如果想在 O(1)的时间复杂度之内插入的话,就只能在某一个点后面一个点插入 题目保证所有操作都是合法的,不用担心删除 一 个已经删除的节点 或者说,在第 k 个插入点后面插入一个数的话,第 k 个插入点一定是存在的 示例: 最一开始 head 指向空 ①H 9 在头节点的位置插入 9 ②在第 一 个插入点 (下标是 0 的点) 后面插入 一 个新的点 ③删除第一个插入的点 (删除编号是 0 的点) ④当 k 等于 0 时,表示删除头节点,整个链表都被删除了 ⑤H 6 在头节点的位置插入 6 (注意:这是第 3 个插入点,下标 idx 是 2) ⑥在第 3 个插入点后面插入一个新的点 6 ⑦在第 4 个插入点后面插入一个新的点 5 ⑧再在第 4 个插入点后面插入一个新的点 5 ⑨在第 3 个插入点后面插入一个新的点 4 ⑩删除第 6 个插入点 (把下标 idx 是 5 的点删除) 如何往链表中插入一个点? 1.将一个新节点 x 插到头节点的位置 2.将一个新节点 x 插入到下标为 k 的点后面? 删除一个点,idx 不变 (删除的点当作它不存在,一般不需要考虑,不需要考虑是否会浪费内存空间、是否会出现内存泄漏的问题) ? 题目要求实现一个单链表,head 一 开始指向空,需要支持三种不同的操作,最后输出整个链表 删除第 k 个插入的数后面的数:删除下标是 k - 1 的点的后面一个点 idx 一 开始等于 0,每次插入一个新的点之后,idx ++,因此第一个插入点的下标是 0,第二个插入点的下标是 1,第三个插入点的下标是 2. . .第 k 个插入点的下标是 k - 1 在第 k 个插入的数后插入一 个数:在下标是 k - 1 的点后面插入一个新的点 第 k 个插入的数其实就是下标是 k - 1 的点
用数组模拟双链表用于优化某些问题 单链表的每一个节点只有一个指向后面的指针?? 双链表的每一个节点有两个指针,一个指向前,一个指向后 int l[ N ] 存储每一个点左边的点是什么,int r[ N ] 存储每一个点右边的点是什么 这里不定义头节点和尾节点,直接让下标是 0 的点是左边的点 head、下标是 1 的点是最右边的点 (最后一个点) tail 最一开始的状态:0 号点的右边等于 1 号点,1 号点的左边等于 0 号点, 0 号点和 1 号点其实是两个边界 如何插入一个点? 有两种选择,可以插到某个点的左边,也可以插在某个点的右边,假设想在第 k 个点的右边插入一个点 x,一共要改变四条边,有四个指针操作 先建立下图的两个红色指针 ? 在 k 的左边插入一个新的点,相当于在 l[k] 的右边插入一个新的点 (在 k 的左边这个点的右边插入 x 与 在 k 的左边插入时一样的) ??? 删除第 k 个点,只有两个指针的操作
邻接表开了 n 个单链表,把每个点的所有临边全部存储下来 head[ i ] 用链表存储第 i 的点所有的临边 用数组模拟栈什么是栈? 先进后出,后放进去的元素先拿出来 下标是从 0 开始或者从 -1 开始都可以
用数组模拟队列什么是队列? 先进先出,先放进去的元素先拿出来 在队尾插入元素,在队头弹出元素
单调栈给定一个序列,求一下在这个序列当中的每一个数 左 / 右边离它最近的、且比它小 / 大的数 是什么,如果不存在返回 -1 ①3 不存在,返回 -1 ②4 左边离它最近,且比它小的数是 3,返回 3 ③2 左边所有数都比它大,返回 -1 ④7 左边离它最近,且比它小的数是 2,返回 2 ⑤5 左边离它最近,且比它小的数是 2,返回 2 单调栈的考虑方式和双指针类似,先想暴力做法,再挖掘一些性质,把目光集中到比较少的状态中,从而起到把一个问题的时间复杂度降低的效果 暴力做法如下:求每个数 左边离它最近且比它小的这个数 的位置 两重循环,第一重循环枚举每一个数,第二重循环从 i - 1 开始枚举(从 i 左边第一个数开始枚举,一直往左走,直到找到第一个比 i 小的数为止,这个数就是 i 左边离它最近且比它小的数) 发掘暴力做法中的性质:随着 i 往右走的过程当中,可以用一个栈来存储 i 左边所有的元素(最一开始栈为空,i 指针每往右边移动一个位置,就往栈里面加入一个新的数) 每一次找是从栈顶往后找,找到第一个比 i 小的数为止就停下来 x 轴是下标,y 轴是它们的值 由于第 2 点比第 1 个点小,而且第 2 个点在第 1 个点的右边,因此第 1 个点就可以被删掉了,同理把所有逆序的点全部删掉,最后剩下的所有点一定是一个单调上升的序列 会不会有些元素永远都不会作为答案输出呢? 如果 a3 ≥ a5,a3 永远都不会作为答案输出,由于 a5 在 a3 的右边,而且 a5 比 a3 小(或相等),在寻找的过程中,如果 a3 能够作为目标值(a3 < ai),那么 a5 一定会更好(由于a5 < a3,因此 a5 < ai,并且要找离 i 最近的比 i 最小的数,所以 a3 一定不会被用到) 只要以下逆序的关系,前面的数 ax 就会被删掉 假设当前栈中元素已经单调上升,到达 i 的位置,想找到左边第 1 个比 i 小的数在什么地方,可以从栈顶 stk[ tt ] 开始找,只要栈顶大于等于 ai ( ai 需要插入到栈中,ai 是比栈顶要小的,而且 ai 在更右边,因此 ai 比当前栈顶更好) ,栈顶就永远不会被当成答案,需要删除栈顶. . .一直做删除,直到删除到一个小于 ai 的为止,此时的 stk[ tt ] 就是要找的 ai 的左边离它最近且比它小的数,最后把 ai 插入到栈中 每一个元素只会进栈一次,而且每个元素最多只会出栈一次,最多也有 2n 次操作,整个算法的时间复杂度为 O( n ) 输入数据比较多,读入比较慢,需要优化: cin 比 scanf 慢了10倍左右,cin 加 ios::sync_with_stdio(false) 会快约 200 ms,cin.tie(0)
单调队列求滑动窗口中的最大值 / 最小值,一共有 n 个数,要求把长度为 3 的滑动窗口中的最大值和最小值都输出出来 每次把窗口往右滑动一位,第一遍把滑动窗口中的最小值输出出来,第二遍把滑动窗口中的最大值输出出来 在问题中抽象出模型,用单调队列来优化 ? 先来想一下暴力怎么做,把没有用的元素删除,得到单调性,有单调性再求极值,可以直接拿第一个点或者最后一个点,把原来有枚举一遍的时间复杂度变成 O( 1 )? 可以用队列来维护窗口,窗口一开始长度是 3,保证队列中时时刻刻存储的都是当前窗口中的所有元素,每次移动分两步来操作: ①把新元素插到队尾 ②把滑出去的元素从队头弹出去 每次求极值的时候,暴力做法:直接遍历队列中的所有的元素,时间复杂度为 O( k )(假设窗口中有 k 个元素) 整个算法时间复杂度为 O( nk ) 优化:看队列中是否有些元素是没有用的,把没有用的元素删掉,看是否能够得到单调性 当 - 3 进到队列后,第 1 个 3 就一定没有用了(每次求的是队列中的最小值),- 3 是小于第 1 个 3 的,而且第 1个 3 在 - 3 的左边,第 1 个 3 会被先弹出去, - 1 也是一样,- 3 会在 -1 后面被弹出去, - 3 ≤ - 1,- 1 也不会被当作最小值输出 只要前面有一个数比后面的数大,前面的点就一定没有用:由于后面的点会被后弹出,而且比前面的数小 只要以下逆序的关系,就把大的点删掉,把所有的逆序点删掉后,最后会变成严格单调上升的队列 最终要求单调上升队列中的最小值就是队头(端点) q[ hh ] 找值:二分 队头元素什么时候出队? 已知当前枚举的右端点 i 和 区间的长度 k,队列里存储的不是值,存储的是下标,可以直接判断队头的下标是否超出了从 i - k + 1 到 i 这个范围,如果超出的话就把队头删掉 将队列置空,队尾进元素,队首出元素。当每个节点入队列时,判断队列尾部元素和该元素的大小,如果当前元素小于队列尾部元素,则将队列尾部元素出栈,再逐一比较,直到遇到小于当前元素的队列尾部元素,或者将队列置空( while(hh <= tt && a[ q[tt] ] >= a[i] ) tt–;) 一定需要注意的是 要注意头 k-1 个插入的元素不输出最小值或者最大值,因为窗口还没满 if(i >= k-1) 我们用 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}。 那么,我们对单调队列的基本操作已经分析完毕。因为单调队列中元素大小单调递(增 / 减 / 自定义比较),因此,队首元素必定是最值。按题意输出即可。 [AcWing]154. 滑动窗口(C++实现)单调队列模板题_Cloudeeeee的博客-CSDN博客 Acwing算法基础课学习笔记(四)--数据结构之单链表&&双链表&&模拟栈&&模拟队列&&单调栈&&单调队列&&KMP_nullwh的博客-CSDN博客
kmp 算法先想一下暴力做法?如何去优化? 算法篇 --- BF算法(暴力匹配算法)_菜徐鸭的博客-CSDN博客 ?s[ N ]:比较长的串,要匹配的串 p[ M ]:比较短的串,模板串 蓝色表示模板串 红色表示要匹配的串 kmp 的下标习惯从 1 开始? 第一重循环 i 枚举当前起点(假设从 s[i] 开始匹配 p,能不能匹配成功),第二重循环从前往后枚举整个长度,flag 一 开始为 true 表示成功的状态,匹配过程中,如果有不匹配的情况出现(s[ i ] != p[ j ]),flag = false 假设匹配到中间的某个位置之前都是完全一样的,匹配到下一个字母就不一样了,暴力做法需要把整个模板串往后移动一位,重新开始匹配 其中隐含了一些额外的信息,如果可以利用这些额外信息,就可以帮助我们少枚举一些东西(从而不需要重新匹配) 假设匹配到中间的某一个位置失败了,需要重新开始匹配的模板串 最少往后移动到什么时候,可以继续匹配 图中两段绿色的线长度应该是一样长的,发现最少往后移动多少只和模板串有关,只要预处理出来前缀和后缀相等的最大值就是答案(相等的值越大,意味着往后移动的距离越少) 需要对模板串数组的每一个点预处理出前缀和后缀相等的长度,相等的前缀和后缀长度最大值是多少→ next 数组 next[ i ] 表示以 i 为中点的后缀和从 1 开始的前缀相等,而且后缀的长度最长 next 数组的含义: 匹配的过程: 假设从前往后匹配的时候,在 i 之前都是匹配的(si - 1 = pj),从第 i 个点开始出现不匹配的情况:si != pj+1,此时需要把红色的线从前往后移动,看最少移动多少(直接调用 next[ j ],最少移动的距离就是最大的后缀) 找到以当前点为终点的后缀和前缀最长是多少 移动之后的下标就是 next[ j ],再看模式串的下一个点是否和 s[ i ] 匹配,如果不匹配,重复此过程,如果匹配就继续往下走 i 遍历 s 所有的字母, j 从 0 开始,每次试图往前移动,看能不能完全匹配,判断 j 能不能往下走:如果 j 不能往下走,j 就往前退一步 ( j = ne[ j ] ),如果 j 已经退无可退了还是没办法走,就看 si 的下一个位置;如果退到某一步之后下一步可以走了,就继续往下走. . . 求 next[ i ] 过程: next数组的含义是:一个固定字符串的最长前缀和最长后缀相同的长度,并且长度小于(不能等于)固定字符串本身,使得当匹配失败我们让子串向后移动最少的距离使模板串可以继续匹配,也就是说在某个字符失配时,该字符对应的 next 值会告诉你下一步匹配中,子串应该跳到哪个位置 和匹配过程类似,需要求 next 数组的串为? p[ N ],对于 i 而言,可以匹配的最大后缀和前缀是什么 j = ne[ j ],假设到 i 这个点已经匹配成功了,当我们想匹配下一个位置的时候,j 不能再往后走了(停止了),想看 j 下一次再重新匹配的时候,可以把 j 最少往后移动多少 (使得最大后缀和前缀相等),就是 ne[ j ] next[ 1 ] = 0,a 匹配失败应该从 0 开始,所以 i 应该从 2 开始,整个时间复杂度为 O( n ) KMP详细解释及KMP算法模板_zhbbbb!的博客-CSDN博客_kmp算法模板
|
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/26 3:38:38- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |