算法题整理(蓝桥 & leetcode)(待更新)
Note:根据 CSDN算法技能树 整理的算法题,这里仅收录一些有意思的题目。可以填空作答,阅读并理解算法。关于算法的时间复杂度计算参考 算法时间复杂度计算
文章目录
一、蓝桥杯(基础)
- 最大公约数(m)和最小公倍数(q)(ab = mq),可参考辗转相除法求最大公约数,短除法(可求最大公因数 和 最小公倍数,但是不适合计算机求解)
- 蛇形填数(每次完成2斜线操作,注意边界判断)
- 门牌制作
- 微生物增殖
- 日期计算:
- 合并检测
- 排它平方数
- 四平方和(二分查找)
- 大数乘法(分块乘法,题目中所画的图不太对,并没有两个m1,m2,m3,注意进位处理)
二、蓝桥杯(简单)
- k倍区间(累加和 + 数学性质,若 ( sum [j] -sum[i-1] )%K= 0,则[i, j]区间为K倍区间 ),该题目答案不是那么好理解,参考python蓝桥杯 k倍区间
- 乘积尾零 (分别累加整除2,5的数的个数)
- 猜字母(字符数组 或者使用迭代器 - 边删除边遍历)
- 次数差(注意检查数组是否越界)
- 交换瓶子 (可利用索引与数值关系 或 依次遍历最小替换当前,也就是寻找交换次数非遍历次数)
- 递增三元组(设3个指针同时移动)
- 比酒量(约束条件:初始不少于20人,每次少掉k人)
- 素数等差数列,可参考蓝桥杯第二题素数等差数列(暴力算法,第一层for初始素数,第二层for控制增量,第三层while判断序列)
- 错误票据(排序 或 计数非排序,主要在条件判断上),strtok按分隔符拆分字符串,atoi,atol字符串转整型,使用atoi时报错
- 神奇算式(用一个数组维护map,用频数2来判断,每次memset重新分配数组)
三、蓝桥杯(字符串)
-
黄金连分数(大数除法),可参考什么是连分数,使用fabonacci计算黄金连分数(错误答案),大数除法(超长整数运算除法器)详解,黄金连分数C++解题(使用大数除法)
Note:- 被除数很大超过了
long long
或者unsigned long long
,就需要使用大数除法。 - 由于double精度只有16,在使用递归计算黄金分割数保留位数不够。
- 当N>100时,fabonnaci计算的值会溢出,如果使用N=50的fabonacci数值,相除也会出现溢出问题。
- 被除数很大超过了
-
尾数加一(利用位运算>>和 & 将十进制打印成二进制,利用 原十进制数 和 加1的十进制数 按位与 得到问题的解),可参考位运算(a << b 左移,末尾添b个0;a >> b 右移,去掉末b位;&按位与)
四、蓝桥杯(递归)
- 抽签(理解循环中的递归:我国派出k人,剩余n-k人你们其他国自己决定,到时通知我就行;每次通知到我国,该回合结束;)
Note:- 该题以队伍人数已满作为约束条件,如果本次递归过程中没有可行解(队伍未满),则回溯到前一层的状态;递归操作由栈实现,栈中保留着上一层状态的变量信息,因此递归可以天然实现回溯思想,但是不能把递归和回溯划等号。
- 一般情况下当递归里有循环,循环里套递归,才会使用回溯法,其搜索空间为树状结构,这样才能回溯到上一个状态,并重新作出选择;如果只有选择判断,则使用分治法,搜索空间为链式结构。
因此如果对于一个问题可以用树形结构来描述,则可以使用回溯法。参考七段码 - 回溯法和分治法并不冲突,在回溯法中,每个大回合的求解也可以使用分治思想;从根节点开始,树状结构的每个分支为链式结构,参考全排列
- 对于扑克序列,它每一次递归过程会通过字符交换产生一个候选解,接着通过各种约束条件,判断候选解是否为可行解,如果解不可行,则回溯到上一层状态;由于有字符交换操作,要完全回溯到上一层的状态,需要手动恢复,即把字符交换回来。
- 打印图形
- 等差数列(先找公差,通过最大值/公差,计算实际数列长度;分治法,具体见题解)
Note:- 大部分递归实现的背后,是分治的思想,即一个问题可以划分为若干子问题,而若干子问题又可以划分成若若干的子问题(大事化小,小事化了),划分的这些子问题通常是相互独立的,典型算法有快速排序,参考快速排序(Java版)。
- 动态规划则与分治法不同,如果待求解问题可以分解成若干个相互重叠的子问题(即分解后的子问题通常是不互相独立的),一般来说,子问题的重叠关系表现在对给定问题求解的递推关系(也就是动态规划函数)中,将子问题的解求解一次并填入表中,当需要再次求解此子问题时,可以通过查表获得该子问题的解而不用再次求解,从而避免了大量重复计算。参考分治法,动态规划区别
- 如果递归函数的内部套着循环,循环里又套着递归函数,则这些递归实现的背后,是回溯法的思想(回溯算法需要状态恢复,递归可以自动实现状态恢复,比如抽签;如果在某个阶段手动修改了数组变量的值,则在回溯过程中需要手动恢复,比如扑克序列)。回溯法每次只构建可能解的一部分,然后评估这个部分解,如果这个部分有可能导致一个完全解,对其进一步搜索,否则,就不必继续构造这部分的解了,回溯法常常可以避免搜索所有可能的解,所以,它适用于求解组合数组较大的问题。可参考分治法,动态规划法,贪心法,回溯法,分支限界法的区别和联系以及适用情况
- 深度优先搜索采用了回溯法思想,一般可采用递归实现;而广度优先搜索采用了分支限界法的思想,一般采用队列实现。参考广搜与深搜算法
- 取位数(从前往后数k位,子问题容易定义,递归 / 循环都可)
- 波兰表达式(前缀表达式,子问题每次返回计算得到的操作数 以及 处理到的字符串索引号,见题解)
- 扑克序列(轮到我的时候,我只进行当前位置和其他位置的交换,其他人想怎么交换字符,我一概不知;待该回合结束之后,我(k = 0)需手动重置回字符交换之前的状态,这时字符串恢复为起始的字符串)
- 振兴中华(路径个数搜索,分治法: 由于格子内容限制,每次只能向下或向右移动)
- 递归实现全排列,可参考c++实现全排列的三种方式 ,全排列 leetcode,递归实现全排列
- 带分数,可参考【递归】带分数
- 斐波那契(递归容易超时,使用非递归写法)
- JZ55 二叉树的深度(递归实现分治思想)
- 方格填数(使用回溯法,注意每回合重置方格状态,见题解)
- BM57 岛屿数量(只有四个方向,斜方向为1不作为同一个岛屿,采用回溯法进行4个方向的感染:1表示未感染,>1表示已感染)
- 不同的二叉搜索树 II(分治回溯思想:根据
i
节点,将begin~end
序列分成左右两份,分别收集i
的左右孩子集合,通过两层for
将i
节点的左右孩子组合起来),参考Leetcode–不同的二叉搜索树
五、蓝桥杯(堆栈/队列/链表)
- 堆的计数(考查动态规划,注意完全二叉树中父节点(
i
i
i)的左(索引为
2
∗
(
i
+
1
)
−
1
2 *(i + 1) - 1
2∗(i+1)−1 )右(索引为
2
∗
(
i
+
1
)
2 * (i + 1)
2∗(i+1))孩子下标与数N的关系),可参考蓝桥杯第九届javaB组–第十题–堆的计数问题–动态规划,乘法逆元求组合数,乘法逆元概念 & 4种解法,【洛谷日报#205】在取模下的乘法逆元
Note:- 使用动态规划可以将堆的计数问题划分成多个重叠的子问题,在使用动态规划时要考虑完全二叉树的性质:根节点确定,左子树节点个数
lsize
= N − 2 f l o o r ( l o g 2 N ) − 1 N - 2^{floor(log_{2}N) - 1} N−2floor(log2N)−1,比如数组长度为5时,根节点的左子树lsize = 5 - 2 = 3
;当数组长度为9时,根节点左子树lsize = 9 - 4 = 5
。 - 假设
d[i]
是以完全二叉树i
号位置为根结点的二叉子堆个数,我们需要从n-1
个节点中选出lsize
个节点放入左子树,选法一共组合数C(n-1,lsize)
种,剩余的放在右子树中,所以d[i]=C(n-1,lsize)*d[i的左儿子]*d[i的右儿子]
; - 百度百科中给出了乘法逆元的定义:逆元素是指一个可以取消另一给定元素运算的元素。使用快速幂法可以求解乘法逆元。
- 比如要求 2 2 2关于模 7 7 7下的乘法逆元, 2 2 2与 7 7 7的最大公因数为 1 1 1则有解;接着 2 7 − 1 ≡ 1 ( % 7 ) 2^{7 - 1} \equiv 1(\%7) 27−1≡1(%7),则 2 ∗ 2 5 ≡ 1 ( % 7 ) 2 * 2^5 \equiv 1(\%7) 2∗25≡1(%7),则 2 2 2关于模 7 7 7下的乘法逆元为 2 5 = 32 2^5 = 32 25=32,检验 64 % 7 ≡ 1 64 \% 7 \equiv 1 64%7≡1。
- 比如要求 3 3 3关于模 7 7 7下的乘法逆元, 3 3 3与 7 7 7的最大公因数为 1 1 1则有解;接着 3 7 − 1 ≡ 1 ( % 7 ) 3^{7 - 1} \equiv 1(\%7) 37−1≡1(%7),则 3 ∗ 3 5 ≡ 1 ( % 7 ) 3 * 3^5 \equiv 1(\%7) 3∗35≡1(%7),则 3 3 3关于模 7 7 7下的乘法逆元为 3 5 = 243 3^5 = 243 35=243,检验 749 % 7 = 1 749 \% 7 =1 749%7=1。
- 通过乘法逆元求解带模的组合数的公式为
C(n,m)=n!*inv[m!]*inv[(n-m)!]
(其中:inv表示逆元)。假设组合数 18 2 \frac{18}{2} 218关于模 7 7 7的表示为 18 2 % 7 = 2 \frac{18}{2} \% 7 = 2 218%7=2,则通过求解分母 2 2 2的乘法逆元 32 32 32,我们可以将上式子等价为 18 ∗ 32 % 7 = 576 % 7 = 2 18 * 32 \% 7 = 576 \% 7 = 2 18∗32%7=576%7=2。
- 使用动态规划可以将堆的计数问题划分成多个重叠的子问题,在使用动态规划时要考虑完全二叉树的性质:根节点确定,左子树节点个数
- JZ23 链表中环的入口结点(有环)
Note:- 设置快慢指针,两指针同时从头指针出发,慢指针步长为1,快指针步长为2,找到第一次相遇的节点(第一次相遇的节点并不一定是入口节点)
- 由于慢指针走了
X+Y
步,快指针走了2(X+Y)
步,而快指针在环路内重复走了两次Y
,因此可以设置两个指针,分别从第一次相遇的节点和头指针同步出发,再次相遇的节点为入口节点。 - 注意考虑链表无环的用例。
- JZ77 按之字形顺序打印二叉树(看题解)
Note:- 使用队列依次存储第
i
层的节点; - 在插入第
i
层某节点的左右孩子节点之前,先将队列中的节点转移至栈中。 - 弹出栈顶节点,根据
height = i + 1
,决定是先插入左子树还是右子树。 - 遍历结束条件为队列为空。
- 使用队列依次存储第
- JZ54 二叉搜索树的第k个节点(使用递归或非递归(栈)对二叉搜索树进行中序遍历)
- 幸运数(注意第一个幸运数都是从1开始的),可参考蓝桥杯 幸运数(Java)
Note:- 假设
(m = 1,n = 9)
:1,2,3,4,5,6,7,8,9
1)第一个幸运数为1,以2开始整除索引,得到1,3,5,7,9
2)第2个幸运数为3,以3开始整除索引,得到1,3,7,9
3)第3个幸运数为7,以7开始整除索引,数列不变则结束
因此m = 1 ~ n = 9
之间有2个幸运数 - 整体思路是第一层循环用于判断幸运数是否大于数列长度,第二层循环用于将删除掉的索引进行赋值
-1
占位(省去了数组删除操作带来的开销)
- 假设
六、蓝桥杯(模拟)
- 方格填数(使用回溯法,注意每回合重置方格状态,见题解)
- 承压计算(把承压图形看成直角三角形即可,用相似三角形求解关于min=2086458231的最大值)
- 7段码(只有连通二极管发光才能算一个字符,考查图的邻接矩阵表示和回溯思想,在结束条件下使用并查集的
find()
检查是否满足约束),参考E题.七段码 - dfs+并查集,七段码,【算法与数据结构】— 并查集,百度百科 - 并查集
Note:- 并查集是一种树型的数据结构,可以解决图论中判断两个点是否在同一个连通子图中的问题。
- 并查集主要构成:
并查集主要由一个整型数组pre[]
和两个函数find()、join()
构成。 数组pre[]
记录了每个点的前驱节点是谁,函数find(x)
用于查找指定节点x
属于哪个集合,函数join(x,y)
用于合并两个节点x
和y
。 - 并查集主要操作(参考例题百度百科:并查集,每次通过边的关系,更新这个全局集合,这个全局集合可以简化成并查集数组
pre[]
):- 初始化:把每个点所在集合初始化为其自身。通常来说,这个步骤在每次使用该数据结构时只需要执行一次,无论何种实现方式,时间复杂度均为 O ( n ) O(n) O(n)。
- 查找:查找元素所在的集合,即根节点。
- 合并:将两个元素所在的集合合并为一个集合。通常来说,合并之前,应先判断两个元素是否属于同一集合,这可用上面的“查找”操作实现。
- 并查集的主要作用:求连通分支数(如果一个图中所有点都存在可达关系(直接或间接相连),则此图的连通分支数为1;如果此图有两大子图各自全部可达,则此图的连通分支数为2……)
- 压缩变换(考查区间树和分治思想),参考蓝桥杯:压缩变换,数据结构—线段树(区间树),区间树 Interval Tree,【数据结构】之线段树(区间树),【递归 & 分治】压缩变换题解
Note:- 可以把区间树看作是一种二叉搜索树(平衡二叉树),它是根据数组索引(不是数组数值)建立起来的;对于索引值具有二叉搜索树的性质:即左子树各节点值(
[0,1]
)均小于根节点([0,3]
),右子树各节点值([2,3]
)均不小于根节点([0,3]
)。 - 该数据结构主要解决区间计算问题,叶子节点保存不可分割的最小区间的值,而非叶子节点保存的是当前区间的状态,例如求区间最大值、最小值、求和等等;线段树不是完全二叉树,但线段树是一颗平衡二叉树。
- 构建区间树:区间树的构建采用递归方式,当前区间是单元区间时直接赋值给节点,否则,首先递归构建左右子树,然后将左右子树的最小值赋值给当前结点。其复杂度是 O ( N ) O(N) O(N)。
- 可以把区间树看作是一种二叉搜索树(平衡二叉树),它是根据数组索引(不是数组数值)建立起来的;对于索引值具有二叉搜索树的性质:即左子树各节点值(
七、蓝桥杯(搜索)
八、蓝桥杯(动态规划)
九、leetcode(数组)
十、leetcode(链表)
- 删除排序链表中的重复元素 II
Note:- 创建一个新的头结点指向当前头部,定义
pre
和cur
双指针 - 比较
cur.val
和cur.next.val
是否相等,cur
遍历直到cur.val != cur.next.val
pre.next = cur.next
完成重复元素的去除。
- 创建一个新的头结点指向当前头部,定义
- 删除链表的倒数第 N 个结点(创建一个新的头结点及其指向它的
pre
指针,设置slow
,fast
指针,fast
先走N步,待fast
到尾,对slow
节点删除操作即可,也可以仅使用slow
和fast
双指针) - 删除排序链表中的重复元素(同删除排序链表中的重复元素 II,只不过
pre.next = cur
) - K 个一组翻转链表(设置两指针
ptr
,ptr1
分别指向第一段和第二段子链表的头结点,接着对每一段进行reverse
再拼接即可),参考JZ6 从尾到头打印链表,题解 - 分隔链表(将链表划分成小于
x
的分区和不小于x
的分区)
Note:- 设置3个指针:
ptr
用于遍历;ptr1
指向小于x
的节点,遍历过程中每找到一个新的小于x
的节点,则ptr1
指向该节点;ptr2
指向第一个大于等于x
的节点 - 当
ptr2
不为null
时,才需要将小于x
节点移动值ptr1
后,ptr2
前。
- 设置3个指针:
- 合并K个升序链表(递归分治,合并的链表可以出现重复数字)
Note:- 对于
K
条链表,list[i]
和list[i+1]
两两进行比较,并让后一条链表指针list[i+1]
指向合并好后的新链表。 - 对于两链表合并操作,可以使用分治法:假设
list[i]
第一个节点为最小的节点,要完成升序,则要求对list[i]->next
子链表和list[i+1]
进行合并,list[i] -> next
指向合并好后的链表。 - 如果题目中说明链表带空值头指针,则需要对所有链表的指针进行后移操作。
- 对于
- 反转链表 II(对区间内的子链表进行反转)
Note:- 法1:先找到
left
,right
两个节点的指针,接着使用辅助指针来反转; - 法2:先找到
left
节点指针,接着让n-m
递减到0,找到right
节点指针即,时间复杂度比法1低。
- 法1:先找到
- 两数相加(每个节点只存储 一位数字,每位数字逆序存储,思路:从个位开始相加计算,每次保留一位并向前一位进位)
- 两两交换链表中的节点(不单纯的改变节点内部的值,而是实际进行节点交换)
- 合并两个有序链表(合并的链表可以出现重复数字,可以使用递归分治)
十一、leetcode(字符串)
十二、leetcode(栈与队列)
十三、leetcode(排序算法)
十四、leetcode(双指针)
十五、leetcode(树)
- 不同的二叉搜索树 II,参考Leetcode–不同的二叉搜索树
Note:- 分治 + 回溯思想:根据
i
节点,将begin~end
序列分成左右两份,分别收集i
的左右孩子集合,通过两层for
将i
节点的左右孩子组合起来 - 注意这里输入的是一个数字
n
,代表1~n
的整数序列
- 分治 + 回溯思想:根据
- 相同的树(比求解子树结构简单,只需从根节点同步遍历两棵树,判断结构是否一致),参考JZ26树的子结构
- 二叉树中的最大路径和(在回溯时,返回
max(左,右,左+右+root)
即可) - 验证二叉搜索树
Note:- 通过中序遍历比较数列是否升序即可(递归 或者 栈基本操作 实现,二叉搜索树可以通过中序遍历进行节点升序)
- 在使用递归实现中序遍历时,要标记当前中序遍历下前一个节点的值,这样才能和
cur->val
进行比较(指针或者引用传递) - 二叉搜索树通常两个考点:中序遍历有序;可以进行二分查找。
- 不同的二叉搜索树 I(分治法),参考Leetcode–不同的二叉搜索树
- 恢复二叉搜索树(先通过中序遍历找到不满足升序的第一个节点
left
(a[i] > a[i+1]
)和第二个节点right
(a[i] > a[i+1]
),进行两值交换) - 二叉树的中序遍历(栈实现非递归)
Note:- 用栈实现非递归:先从根节点出发,依次遍历左节点并全部压入,再弹出栈顶节点后,再压入该节点的右孩子。
- 对称二叉树(使用双队列)
Note:- 对于根节点,左子树和右子树分别用两个队列维护(其实用双栈也行,只不过无法像队列逐层插入节点):左子树先压右再压左;右子树先压左再压右。
同时弹出队首元素并进行比较,如果相等,则按照规则,将弹出节点的左右子树分别插入到两个队列中。 - 也可以使用单个栈实现,只不过每次弹出栈顶两元素进行比较。
- 对于根节点,左子树和右子树分别用两个队列维护(其实用双栈也行,只不过无法像队列逐层插入节点):左子树先压右再压左;右子树先压左再压右。
- 二叉树的层序遍历(使用队列的
pop
和push_back
,实现levelOrder)
Note:参考关于C++ STL 之 push()、pop()、offer()、poll()等- 栈进和取 分别为:
push
(链表尾部插入)和pop
(链表尾部读取)。(栈只有链表尾部,竖着看) - 队列
vector
进和取 分别为:offer
(链表尾部插入)和poll
(链表头部读取),或者push_back
(链表头部插入)和pop
(链表尾部读取)。(队列尾部进,头部出,横着看) - 双端队列
deque
进和取 分别为:pop_front
(链表头部弹出)和pop_back
(链表尾部弹出),push_back
(链表尾部插入)和push_front
(链表头部插入)。(双端队列两头,两尾都可进出,横着看)
- 栈进和取 分别为:
- 二叉树的锯齿形层序遍历,参考JZ77 按之字形顺序打印二叉树