链表
环形链表
题意: 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。力扣链接
主要思路:双指针快慢寻找
根据快慢指针的关系,得到入口位置x以及相遇节点到入口位置的距离z的关系
这个公式的启示是,如果一个指针从头出发,一个指针从相遇节点出发,他们最终会在入口处相遇。 做算法题不是求数学解,数学解可以给出启发,要明白公式的含义。
链表总结
基础知识
随机存储,指针链接
单链表 双链表 循环链表
增删改查
虚拟头节点
处理边界问题,可以避免判断终止条件
哈希表
两个数组的交集
题意:给定两个数组,编写一个函数来计算它们的交集。力扣链接
知识点:unordered_set底层逻辑是哈希表,适合快速取元素并去重,不适合遍历
empty,size
emplace(construct and insert), insert(insert)
erase(remove by position(iterator),value,range(iterator))
快乐数
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。
如果 n 是快乐数就返回 True ;不是,则返回 False 力扣链接
判断元素是否出现,用hash表,unordered_set
两数之和
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍力扣链接
unordered_map 哈希底层实现,可以存储相关信息,查找,删除,插入都是O(n) count,find,emplace,insert,erase,empty,size,at,contains emplace(a,b)or emplace(pair<T,T>(a,b))
四数之和
题意:给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。
注意:
答案中不可以包含重复的四元组。
示例: 给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。 满足要求的四元组集合为: [ [-1, 0, 0, 1], [-2, -1, 1, 2], [-2, 0, 0, 2]]
本题其实用双指针法比较合适,类似于三数之和,双指针法可以把复杂度从降到,五数之和类似。主要问题在于剪枝和边界判断。
一开始尝试用回溯法,较简单的例题可以通过,但是数组过长时则超出时间限制,但其剪枝思想是一样的。区别在于最后两层是否用双指针法降低了时间复杂度。这类问题之所以可以用双指针法是因为我们首先进行了排序而题目对顺序没有要求,从而可以移动指针。
题目:剑指Offer 05.替换空格
请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
示例 1: 输入:s = "We are happy."
输出:"We%20are%20happy."
本题使用了erase 和insert,erase是str.erase(pos,len) insert是在pos之前添加str.insert(pos,str)
字符串
题目:剑指Offer58-II.左旋转字符串
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
示例 1:
输入: s = "abcdefg", k = 2
输出: "cdefgab"
示例 2:
输入: s = "lrloseumgh", k = 6
输出: "umghlrlose"
限制:
1 <= k < s.length <= 10000
这道题第一遍做的时候,想的是怎么移动,怎么保存字串然后进行旋转,代码随想录的思路就非常巧妙,通过两次局部反转和一次整体反转就实现了该功能。可以这样理解,把字符串分为两部分,最后的目标是交换位置,交换位置的一个简易方法就是反转,但是反转后的字符串不符合要求,于是需要进一步反转,所以这个思路的核心在于位置的交换。
28. 实现 strStr()
实现 strStr() 函数。
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
示例 1: 输入: haystack = "hello", needle = "ll" 输出: 2
示例 2: 输入: haystack = "aaaaa", needle = "bba" 输出: -1
说明: 当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。 对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。
一个比较简单的实现方法就是两层循环暴力匹配,时间复杂度为O(mn),但是字符串匹配有一个经典算法KMP算法, 可以实现O(m+n)的时间复杂度。
该算法的核心是前缀表。前缀表存储的是最大相等前后缀的长度,其中前缀是包含首位不包含末尾的子串,后缀是包含末尾不包含首位的子串。通过记录最大相等前后缀,就可以不用回退,直接从前缀表记录的位置继续进行匹配,从而提升效率。因为如果当前位置要匹配成功,那么当前位置之前的字符串一定相同,所以只要找到相同前缀即可。
在匹配的过程中,如果相等则继续前移,如果不相等,则子串的匹配位置由next数组确定,直到第一位依旧不相同,则被匹配字符串推进下一位。
关于next数组的建立,可以用动态规划的方法,利用之前的next已经存储的信息进行更新,可以降低时间复杂度。
459.重复的子字符串
给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。
示例 1:
输入: "abab"
输出: True
解释: 可由子字符串 "ab" 重复两次构成。
示例 2:
输入: "aba"
输出: False
示例 3:
输入: "abcabcabcabc"
输出: True
解释: 可由子字符串 "abc" 重复四次构成。 (或者子字符串 "abcabc" 重复两次构成。
这个问题的暴力解法是求出所有能被整除的长度然后依次比较。
办法1是拼接字符串然后查看原字符串是否在拼接后的字符串里出现。
办法2是利用KMP算法自己进行比较。
1.求出next数组
2.找到最大相等前后缀的长度,至少要等于一半的长度才能保证是无间隔的repeat
3.没被包含在内的部分就是最小重复子串
4.长度要能整除该重复子串才能保证
栈和队列
232.用栈实现队列
使用栈实现队列的下列操作:
push(x) -- 将一个元素放入队列的尾部。
pop() -- 从队列首部移除元素。
peek() -- 返回队列首部的元素。
empty() -- 返回队列是否为空。
这道题考察关于栈和队列的认识,一个关键点在于定义两个栈stIn和stOut的目的是什么,要明确每一个变量的作用,核心思想是知道用到该命令时再进行操作,不要做太多无用功。
225. 用队列实现栈
使用队列实现栈的下列操作:
- push(x) -- 元素 x 入栈
- pop() -- 移除栈顶元素
- top() -- 获取栈顶元素
- empty() -- 返回栈是否为空
注意:
- 你只能使用队列的基本操作-- 也就是 push to back, peek/pop from front, size, 和 is empty 这些操作是合法的。
- 你所使用的语言也许不支持队列。 你可以使用 list 或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
- 你可以假设所有操作都是有效的(例如, 对一个空的栈不会调用 pop 或者 top 操作)。
这个问题不能简单的套用栈模拟队列,因为队列弹入弹出并不会改变顺序,实际上一个队列就可以了。要活用性质。
20. 有效的括号
给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 注意空字符串可被认为是有效字符串。
这个问题比较有意思,我最开始用switch case的方法每个类讨论虽然通过了但代码很丑陋,查看代码随想录的答案后,再入栈时不保存左括号而是右括号,这样在比较时就不需要分情况讨论,思路非常巧妙,这种小trick还是比较有用的。另外在开始写代码前最好把所有可能的情况全都讨论清楚这样才不会修修补补理不清楚。
1047. 删除字符串中的所有相邻重复项
给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例:
- 输入:"abbaca"
- 输出:"ca"
- 解释:例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
栈擅长处理匹配类问题!
150. 逆波兰表达式求值
根据 逆波兰表示法,求表达式的值。
有效的运算符包括 + , - , * , / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
说明:
整数除法只保留整数部分。 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
示例 1:
- 输入: ["2", "1", "+", "3", " * "]
- 输出: 9
- 解释: 该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例 2:
- 输入: ["4", "13", "5", "/", "+"]
- 输出: 6
- 解释: 该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
示例 3:
- 输入: ["10", "6", "9", "3", "+", "-11", " * ", "/", " * ", "17", "+", "5", "+"]
- 输出: 22
依旧是栈擅长处理的问题,这类问题的关键在于需要一直判断并进行删减的操作,或者说需要不断的回退。所以在做算法题时,选对数据结构很重要。只要知道用栈处理该问题,就很容易解决了。
239. 滑动窗口最大值
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
进阶:
你能在线性时间复杂度内解决此题吗?
单调队列这个问题非常重要,主要的难点在于线性时间复杂度。当遍历的时候,如果没有弹出最大值,则与最大值比较即可判断当前窗口内的最大值。但如果弹出了最大值,则剩下的窗口内的数字依旧要进行比较,就导致问题难以处理。这个时候,如果有个队列可以保持队头始终时最大值,则问题就可以简化。但并没有现成的数据结构,所以要 构造自己所需要的数据结构从而简化问题。
主要思路是构造一个单调队列,只保存从大到小或从小到大的顺序,当压入队列的数据不满足要求时则不断弹出直到符合要求。针对这个问题,在压入时就已经进行了排序,只需处理弹出时的匹配问题就可以轻松解决滑动窗口里最大值的问题。
347.前 K 个高频元素
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 1:
- 输入: nums = [1,1,1,2,2,3], k = 2
- 输出: [1,2]
示例 2:
- 输入: nums = [1], k = 1
- 输出: [1]
堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值
这题涉及了一个重要的概念优先级队列,优先级队列搜索最大数值的效率时O(1),大顶堆每次弹出最大值,小顶堆每次弹出最小值。插入和提取的效率时O(log(n)),定义如下
可以由自己定义的函数决定排列顺序,left>right是小堆栈。同样包含push,pop函数。
对于本题,只要先用map统计频率,然后就可以用该容器存储进而自动排列。之所以不用排序算法,是因为只需要维持k个元素的顺序,不需要排序整个数组。本体要使用小顶堆。
二叉树
二叉树前序遍历
给你二叉树的根节点 root ,返回它节点值的 前序遍历。
力扣
主要可以分为递归法和迭代法,递归法非常简单,只需模拟遍历顺序调用递归函数即可,迭代法主要用到栈进行保存。
这里主要有两个问题,一为什么递归法比迭代法简单很多,二为什么要用到栈。
对于第一个问题,需要明白递归法自身的性质以及适用的问题是什么样。 递归法是调用自身的函数,为什么需要调用自身?因为解题步骤中有一步或几步是和原问题要求相同,这就决定了它要调用自身。但是只调用自身如何终止呢?这就需要设置终止函数决定问题的边界或者说是原子。对于遍历问题,确实可以把原问题拆解为两个子问题,这就是递归法天然适用的原因。
对于第二个问题,为什么要用到栈。现在想一想遍历的过程,我先访问当前节点,然后遍历完左子树后再用相同的操作遍历右子树,那么重要的一点是遍历完左子树后,我怎么知道下一个要访问的点是根节点的右子树?而这个问题在遍历左子树的过程中一直存在,所以我每一步都要保存这个信息,保存后再取出还要保证顺序,那要看一看这个存和取的关系了。很容易看出,先存的节点反而是后取,显而易见要用到栈了。至此,该问题既可以迎刃而解了。
中序遍历
给定一个二叉树的根节点 root ,返回 它的 中序 遍历。
力扣
这个题非常有意思,递归的处理依旧简单,但迭代不是。在做完前序遍历后,想当然认为中序也很简单,但真正开始处理的时候发现非也。
前序遍历访问的时候就输出了因此不用考虑访问的元素。但是中序遍历访问之后并没有立刻输出而要等到左子树全部处理完才能输出,因此左中右的顺序,这样一来节点就要访问两次,第一次用来寻找子树,第二次用来输出。所以一个重要的思想是标记,用空指针进行标记,在第一次访问的时候不立刻输出而是加上一个空指针的标记,在第二次访问到空指针时就知道该节点已经访问过可以输出了。这是个比较有用的小trick。
后序遍历
给定一个二叉树的根节点 root ,返回 它的 后序 遍历。
力扣
后序的递归依旧简单,迭代法有两种思路,一种是利用标记法在第二次访问时输出。另一种比较有趣的地方在于利用前序的思想。注意到前序的输出顺序是中左右,后序的输出顺序是左右中。这样在便利的时候左右反序然后最后的输出结果再反序一样可以得到想要的结果,不得不说非常有创意。
层序遍历
给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
力扣
层序遍历,天然适合队列进行处理。先访问到的点先处理并且它的子节点也先存放。找到了合适的数据结果就迎刃而解了。对于它来说,迭代法反而容易些。
对于层序遍历的递归法,我没有想出来,查看答案发现并不是之前的思路,而是要用depth进行标记,然后依据depth保存节点值,并不需要自己处理保存的顺序,访问的顺序并不是按层访问,但是可以保证每层元素被访问到的顺序是由左往右就可以了。这也是一个有用的启发点。
226.翻转二叉树
翻转一棵二叉树。
这题的核心思想就是层序遍历,只要在遍历的时候更改左右子树即可,递归法和迭代法均可。
101. 对称二叉树
给定一个二叉树,检查它是否是镜像对称的。
这个题实质上是比较左子树和右子树的翻转是否相等,一开始产生的思路是先反转然后再用前序遍历比较两棵树,用迭代的方法。写之后发现其实可以不用反转,在遍历的时候就更改顺序直接进行比较,因此写出第一版的迭代版本。
在写完迭代版本后,发现实质上还是一个重复子问题的过程,因此尝试用递归的方法。只是递归传入的是左右子树的节点,然后在遍历时采用相反的顺序,这样每次取出来进行判断的值就是对称的。主要的突破就是传入节点不是一个而是两个,这不是一个判断自身的问题,而是两棵树比较的问题。
104.二叉树的最大深度
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例: 给定二叉树 [3,9,20,null,null,15,7],
只要在遍历的时候标记我们想要获取的信息深度,作为传入参数进行递归求解即可。相比来说,迭代法则使用层序遍历较为合理,但同时要相对复杂一些。
深度:根节点到该节点的最长路径。
高度:该节点到叶子节点的最长路径。
111.二叉树的最小深度
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7] 和二叉树最大深度的逻辑有类似,但是要确定是叶子节点而非单边。递归方法要求深度而非高度,这还是有区别的,对于求深度要用来求从叶子节点到当前节点的距离,所以叶子节点的深度从0开始不断往上加。
222.完全二叉树的节点个数
给出一个完全二叉树,求出该树的节点个数。
示例 1:
- 输入:root = [1,2,3,4,5,6]
- 输出:6
示例 2:
- 输入:root = []
- 输出:0
示例 3:
- 输入:root = [1]
- 输出:1
提示:
- 树中节点的数目范围是[0, 5 * 10^4]
- 0 <= Node.val <= 5 * 10^4
- 题目数据保证输入的树是完全二叉树
这道题其实遍历一遍就是O(n)的时间复杂度,如果不利用完全二叉树的性质也可以直接求解。
对于递归法只要左子树个数+右子树个数+1即可,对于迭代法,层序遍历统计个数即可。其实不用层序遍历只要完全遍历一遍统计个数便可以了。
下面讨论利用完全二叉树性质,对于完全二叉树,只要是满二叉树,直接利用深度根据公式即可求出个数,而对于一个完全二叉树而言,总有子树是满二叉树,满二叉树的条件便是最右侧与最左侧的深度一致,据此就可以用递归的方法进行求解,值得一提的是,时间复杂度为O(log n * log n)是小于O(log n)的。
110.平衡二叉树
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。
示例 1:
给定二叉树 [3,9,20,null,null,15,7]
这题一开始想到就是递归,不过我用了两个递归,第一个递归求高度第二个递归判断是否是平衡二叉树。但是实际上,再求高度的时候就可以进行判断,只不过是用一个特殊的数组-1进行标记,也算是一个小trick,核心思想就是左子树是平衡二叉树,右子树是平衡二叉树且左右子树的高度差不超过1即可。
257. 二叉树的所有路径
给定一个二叉树,返回所有从根节点到叶子节点的路径。
说明: 叶子节点是指没有子节点的节点。
这道题是典型的回溯算法,其实就是递归,所以关键还是确定终止条件,并且每次回退都要保证状态回退到前一个状态,本体用来保证的方法就是在传参的时候拼接这样就不用处理返回值之后回溯的步骤。
对该题,就是一路遍历一路凭借路径,到叶子节点时就可以把当前的路径放到结果中。
404.左叶子之和
计算给定二叉树的所有左叶子之和。
递归思想,多传一个参数用来判断当前节点是左节点还是右节点。
513.找树左下角的值
给定一个二叉树,在树的最后一行找到最左边的值。
只要是左优先遍历,当第一次发现更深的节点时,一定是那一层最左侧的节点。运用递归传入深度参数。
112. 路径总和
给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。
说明: 叶子节点是指没有子节点的节点。
这个题用递归就可以很快的解决,但是问题是如何选择终止条件以及每一层的逻辑如何处理。是当前层加上当前值,还是下一层加上当前值这个问题值得商榷。我一开始的逻辑是要进入下一层了,然后再加上当前层的值,但是问题是我在比较的时候并没有考虑这一点,所以一开始的时候总是有问题。另一个是叶子节点的判断条件是左右子节点全为空,而非当前为空,因为有可能另一个节点不为空。所以写代码的时候,还是要清楚的知道每一行代码代表的意义以及是否符合定义。
一个小trick,直接使用目标值减去当前值,这样可以少定义一个参数。
106.从中序与后序遍历序列构造二叉树
根据一棵树的中序遍历与后序遍历构造二叉树。
注意: 你可以假设树中没有重复的元素。
例如,给出
- 中序遍历 inorder = [9,3,15,20,7]
- 后序遍历 postorder = [9,15,7,20,3] 返回如下的二叉树
这题的关键是选择边界以及判断合适的终止条件。另外,做本题时竟然卡在创建结构体的指针上,可见时常复习的重要性。
TreeNode* root = new TreeNode(val,left,right) 首先是根据后续遍历的最后一个元素确定当前节点的值,然后在中序遍历中进行查找从而分割左右子树。 接着是递归的思想重复操作左右子树,所以就要把原来的两个数组都要进行分割分成需要的左右子树从而作为参数传参。
其实简单一些的方法是直接创建子数组,没有那么多参数需要考虑但是这需要额外空间。所以本题我采用了下标标记的方法,将中序后序的开始结束的坐标作为参数传递。这就需要明确是开集还是闭集以及如何更新下一次的边界问题。另外终止条件的确定也与边界的选取有关,总之是比较细节的问题。
654.最大二叉树
给定一个不含重复元素的整数数组。一个以此数组构建的最大二叉树定义如下:
- 二叉树的根是数组中的最大元素。
- 左子树是通过数组中最大值左边部分构造出的最大二叉树。
- 右子树是通过数组中最大值右边部分构造出的最大二叉树。
通过给定的数组构建最大二叉树,并且输出这个树的根节点
这题和上一题构造二叉树其实一样,而且更简单,只需要找出最大值然后左右分割即可,递归的方法很容易就能解决。
617.合并二叉树
给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。
你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。
这题我并没有快速解决,反而是参考了自己直接的答案,归根结底是突然传入两个数做为参数,一下不知道如何处理了。老是觉得要另起一个递归函数处理,实则不用。
其实可能出现的情况我是知道的,也很容易罗列出来,但是针对每个情况如何处理我并没有好好思考,比如有一个为空时直接返回另一个就可以了,但我却尝试分许多类别讨论。此外,如何连接子问题我也没有考虑清楚,子问题其实就是同时传root1和root2相同的左右节点然后进行判断,这明明很简单我却没能思考出来。这表明我思维中还是有问题。已经能知道可能出现的情况却不肯仔细思考如何处理,面对涉及两个参数的情况就有些不知所措,还是迁移能力不足。
98.验证二叉搜索树
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
- 节点的左子树只包含小于当前节点的数。
- 节点的右子树只包含大于当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
二叉搜索树最重要的一个性质,中序遍历下为递增序列,知道这个性质后,只需要中序遍历保存下来就可以了,先用递归法保存,然后遍历序列,时间复杂度为O(n)。
关于本题,我自己的做法是用了两次递归函数,第一个递归函数用来判断当前节点值是否大于左边所有节点且小于右边所有节点,第二个递归就是判断左右子树是否也满足条件。这个方法直观,但是时间复杂度远大于上一个方法,因为要遍历不止一次。
可见,熟悉所要研究物体的性质并加以利用可以事半功倍。
530.二叉搜索树的最小绝对差
给你一棵所有节点为非负值的二叉搜索树,请你计算树中任意两节点的差的绝对值的最小值。
这题险些和上一题一样,试图遍历出左右子树的极值然后比较。但是这是二叉搜索树,只要中序遍历就可以了。
然而这题还有一个改进之处,就是既然保存的时候已经是按照顺序保存了,那为什么不在遍历的时候就直接计算并比较呢?主要的思想就是要保存前一个节点,其实就是用一个全局变量在每次取值的时候保存下来就可以了。所以一个问题多思考还是有不少可改进之处的。
501.二叉搜索树中的众数
给定一个有相同值的二叉搜索树(BST),找出 BST 中的所有众数(出现频率最高的元素)。
假定 BST 有如下定义:
- 结点左子树中所含结点的值小于等于当前结点的值
- 结点右子树中所含结点的值大于等于当前结点的值
- 左子树和右子树都是二叉搜索树
这题和上题类似,利用二叉搜索数中序遍历有序的性质,比较当前值和前一个值是否相等并统计次数,可以在遍历的过程中就完成筛选无需统计后再筛选。
一个关键是记录前一个节点,因为一开始是空指针所以要进行判断,在这里我犯了一个错误把赋值前一个节点写在判断里面了,导致永远无法进入判断所以还是要谨慎。其他的代码顺序也很重要,如果不用if else的话,就要仔细考虑两个条件哪一个放在前面,否则前一个条件判断执行后很有可能影响下一个条件判断。
236. 二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
这题是比较有意思的,乍一看没有思路,其实关键问题在于递归时怎么判断该节点是否为公共节点,当当前节点为空节点时返回空节点没有问题,但是当当前节点不为空时该怎么判断呢?如何将当前节点与要比较的节点结合起来呢?
所以,如果当前节点是要比较的节点的其中之一,这说明什么?说明至少目标之一在该子树上,如果我返回这个节点,层层回溯,该怎么判断?
有两种情况,如果当前节点的左右字节点各返回非空节点,说明该节点就是最近公共祖先,可如果只有一个节点返回,说明该节点就不是最小公共祖先,其左右节点必有一个为最小公共祖先,或目标中一个节点可能为另一个节点的子节点,这时返回的节点就会一直是最小公共祖先。在回溯的时候只可能是这两种情况。
235. 二叉搜索树的最近公共祖先
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]
这个题其实和上一题一样甚至比上题简单很多,利用二叉搜索树的性质,可以根据当前值进行判断,如果当前值比较大的值还大,那么说明两个节点都在当前节点左子树,反之亦然。如果当前值位于中间,说明一个在左侧一个在右侧,这时的节点就是最小公共祖先了。
看了示例代码后发现写的还是比我要简洁不少,不需要判断哪个值大,两个数一起比较就好了。写的还更简洁。
701.二叉搜索树中的插入操作
给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据保证,新值和原始二叉搜索树中的任意节点值都不同。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回任意有效的结果。
这题其实比较简单,只要利用二叉搜索树的性质,把节点插入每个空节点,就不需要更改树的结构。一个注意的点时要标记父节点以便后续的插入。
450.删除二叉搜索树中的节点
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
首先找到需要删除的节点; 如果找到了,删除它。 说明: 要求算法时间复杂度为 ,h 为树的高度。
这道题还是有点意思的,主要是讨论清楚所有可能出现的情况
1.没找到
2.左右子节点有一个为空
3.左右子节点都为空
4左右子节点都不为空
其实还有个情况是如果要找的节点是根节点的话,还是要稍微处理一下。其余情况都比较容易,当左右不为空时,一个简单的方法就是把右子树放在左子树的最右边。
基本上大致思路就是如此,这题可以分为迭代法和递归法,我是用的迭代法,如何把代码写的简洁清晰也是很重要的,关键是复用部分,例题是写了一个删除节点的函数,这样就用通用性,而我针对每种情况具体写就显得有一些紊乱。把一部分操作集合到一个函数中是基本操作,要注意使用。
669. 修剪二叉搜索树
给定一个二叉搜索树,同时给定最小边界L 和最大边界 R。通过修剪二叉搜索树,使得所有节点的值在[L, R]中 (R>=L) 。你可能需要改变树的根节点,所以结果应当返回修剪好的二叉搜索树的新的根节点。
这题用递归法很容易就解决掉了,利用二叉搜索树的性质进行判断,如果当前值小于范围,则左子树一定也小于,只需处理右子树,反之亦然。如果当前值符合范围则保留并同时处理左右子树。
108.将有序数组转换为二叉搜索树
将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。
本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
这题再次体现了递归的威力。因为是要求高度平衡,所以当左右子树的个数相同或只差一个时是没有问题的。所以利用有序数组的性质,每次选取中间的值作为当前的根节点,这样左侧为左子树,右侧为右子树满足二叉搜索树的要求。如此一来,只需要每次更新数组的起始和终止值即可。
538.把二叉搜索树转换为累加树
给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。
提醒一下,二叉搜索树满足下列约束条件:
节点的左子树仅包含键 小于 节点键的节点。 节点的右子树仅包含键 大于 节点键的节点。 左右子树也必须是二叉搜索树。
这个题同样很简单,只要从大到小遍历二叉搜索树,用一个指针记录前一个节点就可以不断累加了。这是利用二叉搜索数中序遍历有序的性质,不过这里要先遍历右子树再遍历左子树才能保证其为从大到小的遍历。
回溯
接下来进入回溯部分的刷题了。回溯其实就是暴力破解法,但是暴力破解法也不是说就用一堆for循环。如果需要循环100次还怎么写循环呢?这时候就要用到回溯了,回溯其实也是递归的一种形式,它最大的特点是要不断的放入再取出。
回溯法解决的问题都可以抽象为树形结构,是的,我指的是所有回溯法的问题都可以抽象为树形结构!
因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。
递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。
图片暂时无法插入,给出链接回溯树形图解
第77题. 组合
给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
这题是回溯法的一个简单应用,因为有过一遍地刷题经验,所以很快就写出来了,但是实际上我希望自己是靠思考而非记忆写出本题。所以利用树结构的回溯进行一下分析。
这个本身就是要找出集合的子集,所以根节点是集合,子节点是从根节点选出一个节点后所有剩余的所有元素,以此类推知道叶子节点。这时就需要判断一下何为叶子节点,根据题意,叶子节点是当子集大小为k时的结果。所以判断终止的条件就显而易见了。
这个问题有个剪枝的部分可以优化,就是如果当前剩余的元素数量已经不足以达到子集个数的要求,则下面其实就不必遍历了。对于这个边界问题的判断也很细节,我自己瞎想一会儿也没直接想出是否要加一,然后根据现在的理解n-(k-path.size())+1的含义其实是
k-path.size() 是还需要多少个元素,n-(k-path.size())就是把需要的个数减去,但是我当前的遍历其实是还没算进去的,所以终止的位置要加一。比如我还差两个元素,那么其实倒数第二个元素也应该能取到。因为取到之后还差一个元素,而正好也剩了一个元素。
216.组合总和III
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
- 所有数字都是正整数。
- 解集不能包含重复的组合。
示例 1: 输入: k = 3, n = 7 输出: [[1,2,4]]
示例 2: 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]
这题也是一个组合问题,类似于上一题,套路都一样,主要是剪枝的判断。当和大于目标时就跳出。 当子集元素个数等于要求个数且和为目标值时,即达到叶子节点返回。
39. 组合总和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
- 所有数字(包括 target)都是正整数。
- 解集不能包含重复的组合。
示例 1:
- 输入:candidates = [2,3,6,7], target = 7,
- 所求解集为: [ [7], [2,2,3] ]
示例 2:
- 输入:candidates = [2,3,5], target = 8,
- 所求解集为: [ [2,2,2,2], [2,3,3], [3,5] ]
从集合中挑选满足符合特定条件的子集,剪枝的代码顺序也很重要,如果代码放错了就有可能缺少回溯的步骤导致错误。
40.组合总和II
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明: 所有数字(包括目标数)都是正整数。解集不能包含重复的组合。
- 示例 1:
- 输入: candidates = [10,1,2,7,6,1,5], target = 8,
- 所求解集为:
这题很重要!!!
关键在于当在树同一层时不能使用相同的元素因为要避免重复,但是在不同层时可以使用相同数值的元素,所以剪枝的关键在于如何标记当前是否是在同一层。
我尝试了用map结构来处理,但发现无论如何都无法解决。理想情况是递归回到当前层时标记数组要保持原样然后将使用过的数给标记起来,但是这又是不可能实现的。而且我是针对数值进行标记而不是元素进行标记,似乎有些偏离。
答案中给出的解法是对每一个元素进行标记,这里的used我感觉是用来判断是否是新一层而不是使用过,如果当前是新的一层就可以使用重复数值,如果当前在同一层那么就跳过该步。我觉得这个命名used有误导的嫌疑。
先将数组进行排序,然后针对每个元素进行标记,进入下一层前标记为true,这样下一层在遍历时如果当前元素等于前一个元素,就会依据前一个元素是否为true来判断是否是不同层,如果是不同层则可以继续回溯,反之跳过。过于used数组的标记,当然也要回溯,如果前一个元素不是在当前元素的上一层,那就不能使用相同的值。
131.分割回文串
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
示例: 输入: "aab" 输出: [ ["aa","b"], ["a","a","b"] ]
这题很重要
老实说这题还是有点难的,一开始知道要用回溯算法但是却无从下手,一个关键的点是不知道该如何切割,具体就是如果判断当前的字符串为回文串,理论上应该要存放该结果,但是依旧要判断他的子串是否为回文,而且如果判断子串的时候,存放的结果应该与当前的结果并列,但是要用递归的话理论上应该是下一层出现矛盾难以解决。
在思考之后,觉得这个题其实还是一个组合问题,就是找出所有可能的组合判断每个组合是否为回文串即可,可这依旧比较模糊。回溯的核心思想是每一层都要存放一个结果然后递归,所以我应该是每一个循环内都要放入,这是想到这题不是完全的组合,因为子串必须是相邻的,如果我将当前串分为头部和尾部,头部不需要再次切割直接判断,尾部则需要进行递归就可以满足条件,而且因为递归的原因,所有可能的子串组合都可以遍历到,到这其实问题就比较明朗了。
接下来就是终止条件,因为是不断移动尾部子串的起始点,所以当尾部子串的起始点大于当前串的大小时,说明已经遍历完所有情况可以存放结果了。
至于判断回文串,我使用了双指针法,值得一提的是可以用for循环两个变量这样比while更简洁。答案中给出用动态规划的方法提前判断所有子串是否为回文串的方法然后查表可以优化时间。
93.复原IP地址
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。
例如:"0.1.2.201" 和 "192.168.1.1" 是 有效的 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效的 IP 地址
这题也比较有些意思,有几个点。一个是用字符串的insert和erase直接在原字符串上操作,不过要仔细判断参数的含义以及位置是否准确。
另一个是判断字符串是否有效时,我把'0'写成了0,debug了好久。
再一个就是终止条件,我选择的是遍历到最后一个点然后再判断点的个数,但是参考里面直接判断点的个数然后判断最后一个子串是否满足条件,似乎可以节省不少。这里比较重要的就是标记添加的点的个数了。
再一个就是关于剪枝,一开始是判断当前字符串个数大于3就终止本层,但是参考里使用的是如果不满足条件就直接跳出,明显对性质的理解更深刻。确实,如果不满足条件,即使后面再增添数字也不会满足条件了。
78.子集
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例: 输入: nums = [1,2,3] 输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]
这个题其实比较简单,但是正因为简单所以一开始反而没思路,加了很多没用的东西。经过思考发现,不需要判断条件每次递归都要把结果保存起来,每一层的遍历顺序其实是改变以哪个数为起点的所有子集。比如1,2,3,4。第一次是包含1的所有子集,递归不断把后面的数字放入。然后把1弹出把2放入。
90.子集II
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
- 输入: [1,2,2]
- 输出: [ [2], [1], [1,2,2], [2,2], [1,2], [] ]
这题还是要判断前一个数与当前的数是否是在同一层,所以要用used数组进行标记,如果在同一层则跳过否则就继续操作。在传入之前要把原数组排序,利用有序的信息直接与前一个数比较。
看过参考后发现也可以不用used数组直接比较是否与前一个数字相同,如果相同则跳过。40组合总和不同不对!!!其实一样。我尝试了40题发现这样也可以。i>0改成了i>start因为这本来就不是从头开始遍历的而是从start开始,所以i=start说明是新的一层不需要判断是否使用过,但是当i>start说明是同一层的事情了,这个时候如果和前一个元素相同就可以直接跳过了,这还是比较巧妙的。
491.递增子序列
给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。
示例:
- 输入: [4, 6, 7, 7]
- 输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]
说明:
- 给定数组的长度不会超过15。
- 数组中的整数范围是 [-100,100]。
- 给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。
这题很重要!!!
这题也比较有趣因为不能排序了,需要保持原顺序来记录非减子序列,如果不能排序的话又该如何标记一个数字是否在当层使用过呢?办法是利用set,每一层都创建一个新的set,如果使用过了就跳出这次循环,否则要在set中保存。除此之外就是要筛选非减的元素,当然是和最后一个元素进行比较,这里有个问题是当path为0时是无法比较的,所以当path为0时直接保存,另一个判断条件是子序列的个数至少要大于等于2,这个也可以很容易判断。
值得一提的是,i>start的条件,结果出错,后发现这样一来第一个元素不会进行大小判断,那么随着递归的深入,第一个元素就会无条件加入进去。所以还是要细心啊。
46.全排列
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
- 输入: [1,2,3]
- 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
这题很重要!!! 这一题也比较有意思,算是第一个排列题,和之前的组合有所不同,一个关键的区别就是起始点不是startindex而是每次都要从第一个元素开始找。
我一开始是通过删除插入元素进行操作,但其实这样效率比较低,参考里面用的是used数组标记已经使用过的元素,效率相对会高一些。
47.全排列 II
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
- 输入:nums = [1,1,2]
- 输出: [[1,1,2], [1,2,1], [2,1,1]]
示例 2:
- 输入:nums = [1,2,3]
- 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
这题我用了一个used数组标记当前路径上该元素是否使用过,用一个level set标记当前层前一个元素是否使用过首先是在该层如果当前元素可以在level里找到说明已经使用过了,直接跳过否则回溯。进入下一层之后,利用used判断当前元素是否已经使用过。
看了参考之后,发现一个used数组就足够了,i>0&&nums[i]==nums[i-1]&&used[i-1]==false意味着如果前一个元素是false说明没有进入新的一层也就是说同层根据遍历顺序前面一个元素必定已经遍历过了,这时候直接跳过本次循环就可以了。然后used[i]==true,如果为true,说明这个元素在之前的路径上已经使用过了,之前跳过即可。
332.重新安排行程
给定一个机票的字符串二维数组 [from, to],子数组中的两个成员分别表示飞机出发和降落的机场地点,对该行程进行重新规划排序。所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。
提示:
- 如果存在多种有效的行程,请你按字符自然排序返回最小的行程组合。例如,行程 ["JFK", "LGA"] 与 ["JFK", "LGB"] 相比就更小,排序更靠前
- 所有的机场都用三个大写字母表示(机场代码)。
- 假定所有机票至少存在一种合理的行程。
- 所有的机票必须都用一次 且 只能用一次。
示例 1:
- 输入:[["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]]
- 输出:["JFK", "MUC", "LHR", "SFO", "SJC"]
示例 2:
- 输入:[["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]
- 输出:["JFK","ATL","JFK","SFO","ATL","SFO"]
- 解释:另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"]。但是它自然排序更大更靠后。
这题很重要这是道hard的题。我自己的解法是使用一个used数组标记当前元素是否使用过,如果使用过就跳过否则继续,另一个判断条件是当前元素的出发位置是否是路径最后的到达位置,如不是则跳过。
一个问题在于可能有多种解,如何找到字母顺序最靠前的解?我用的办法是自己写一个比较函数,按照到达点的字母顺序进行排序,这里有两点需要标记:
1.comp参数一个是左参数一个是右参数,所以当做参数小于右参数返回真则按照从小到大进行排序,反之从大到小。
2.作为一个类的成员函数,comp要加static表示全局函数,否则不能使用。
在排序之后,第一次遍历到的结果就是顺序最靠前的结果,这里回溯函数返回值不为空而是要返回布尔值表示找到了所求行程一路返回到根节点不再进行遍历。
参考里给出的比较巧妙,使用了unordered_map<string,map<string,int>> 这样一个数据结构来进行求解,因为map在保存时就已经按顺序排好所以不需要额外排序了。同时如果使用过则+1从而避免了used数组额外进行判断。这样,一个数据结构就同时解决了两个问题,不得不说用好数据结构还是很有作用的。
另外还有一个点是起始位置必须是"JFK"我一开始想要在回溯里面分类处理结果写出的代码非常冗余,思考之后,在传入参数之前就直接先把起点放入result中代码形式就得到了统一,变得简洁了不少。所以一点小小的改动就可以让问题的解决方法变得优雅。
这题其实应该学习的是如何选择容器像我的解法用了简单容器代码就要变得复杂,如果选择合适的容器就会变得轻松。本题选择的标准是出发点和目的地之间要有映射关系这样unordered_map就比较好,而除此之外呢,目的地有多种选择是要依赖顺序进行选择,所以要用有序的容器map。
一个小trick是用数字统计是否使用过,压入的时候用--,弹出的时候用++,和常用的逻辑是反过来的,但是就会使代码变得容易。可以说这题虽然长度不长,但是内含很多的思考,每一行代码都有设计。
51. N皇后
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
这题很重要,是道hard题我在写完之后的感觉就是圆满完美。
首先要单独写一个函数判断新加的皇后棋是否与棋盘原有的棋冲突,这里其实直接遍历整个棋盘判断同行同列同斜线是否冲突。但实际上依据我回溯的顺序,行的遍历到当前加入行就可以截止了,不需要继续遍历。这里算是一个剪枝操作。
然后就是初始化棋盘,按照行的顺序进行回溯,即每一层的循环是在列上进行的。判断当前加入的棋盘是否可行然后回溯直到遍历完最后一行就可以了。这个题写的非常舒服,仔细研究也有一些小trick比如剪枝,比如利用行数判断终止条件而非单独再一个变量统计皇后棋的个数。
37. 解数独
编写一个程序,通过填充空格来解决数独问题。
一个数独的解法需遵循如下规则: 数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现一次。 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。 空白格用 '.' 表示。
这题我的解法同样和参考不一样,我是每次回溯的时候按先列后行的顺序寻找下一个点然后直到最后,这样如果最后的没有空点了说明遍历结束返回答案。至于判断当前加入的点是否合适,思路和步骤与参考都是一样的。
但是从参考来说,使用的是所谓的二维递归,每一次遍历都是从头开始遍历,然后尝试所有可能的值如果不对返回false,这样一来不需要再向我一样找下一个点。和我的思路比起来有点像广度优先与深度优先的区别。
贪心算法
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
说实话贪心算法并没有固定的套路。
贪心算法建议回头再刷一遍
所以唯一的难点就是如何通过局部最优,推出整体最优。
贪心算法一般分为如下四步:
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
455.分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
这题是贪心算法的第一题,其实比较简单,只要两个数组排序然后优先把大的饼干给胃口大的孩子就可以了。但是我在写的时候还是提交了好几次,很多细节问题没处理好,不如该如何循环,要先排序,判断终止条件等,简单题也要细心啊!
376. 摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
这题很重要这个题虽然利用贪心求解,但其实仔细考虑的时候有很多种情况,我就是一遍又一遍提交然后打补丁最后拼拼凑凑把结果写出来但最后写的代码极其丑陋。
主要的思想就是当出现峰值或低谷时就统计一下,但是这里包含的情况有,
1.上下坡有平坡
2.数组首尾两端
3.单调坡有平坡
用两个变量分别记录前一个的差和当前的差,进行比较。对于一个的情况单独处理。
53. 最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
- 输入: [-2,1,-3,4,-1,2,1,-5,4]
- 输出: 6
- 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
这题是比较典型的贪心算法的应用。当当前子序列的值小于0是立即放弃因为加上它只会让答案更小,遍历一遍后返回最大值。
值得一提的是当所有的值都为负数时怎么办?所以result的初始值不能设置为0。且要先比较当前值与最大值的大小关系再判断当前和是否小于0,否则就会得到错误的答案。
122.买卖股票的最佳时机II
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
- 输入: [7,1,5,3,6,4]
- 输出: 7
- 解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4。随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
这题相对来说比较简单,因为没有连续的要求,所以把所有利润大于零的结果加起来就是最大利润了。
55. 跳跃游戏
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个位置。
这题也比较简单,就是不断的扩大所能到达的最大范围然后在最大范围内遍历直到超出最后一个点或者说最大范围不再变化位置,,经过精简可以写出比较优雅的代码。
45.跳跃游戏II
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
这题做是做出来了但是看了参考之后觉得写的还是太垃圾了。我用了while循环时刻判断最大范围是否超出数组长度,在while循环里又用for循环遍历当前最大范围内的所有点,写的十分冗余。
看了参考后,只用了一个for循环,不断更新下一跳的最大范围,当到达当前最大范围后步数加一,十分巧妙。
135. 分发糖果
老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。
你需要按照以下要求,帮助老师给这些孩子分发糖果:
- 每个孩子至少分配到 1 个糖果。
- 相邻的孩子中,评分高的孩子必须获得更多的糖果。
那么这样下来,老师至少需要准备多少颗糖果呢?
这题很重要!这题很重要!这题很重要! 老实说,做贪心算法的题没有之前做回溯来的得心应手,究其原因还是回溯有固定的套路,给了明确的思考方向,而且对回溯的理解也到位一些。但是对于贪心算法,我至今没有很好的理解,很多时候做题就是靠直觉,没有所谓的方法论。
对于本题,我也思考了很久,想找出峰值和低谷然后求峰值与两侧低谷的距离,但是要处理的情况实在有点多也很麻烦,最后不了了之。看了参考后发现写的很优雅。
用两次贪心遍历,第一次从前向后遍历,如果右侧值比左侧值高就加一。第二次从后向前遍历,如果左侧值比右侧值高就取当前值和右侧值加一之后的较大值。
第一次遍历之后可以把所有上坡的情况给记录下来,第二次从后向前遍历的原因是下坡是从右侧的低谷向左侧的峰值,所以第二次遍历的时候把下坡的相对高度给求出来,但是因为我们要的是绝对高度,所以要与当前的高度相比,如果当前高度大于下坡的高度,说明峰值左侧要高一些当前值取决于左侧,反之取决于右侧。
之所以能这样分开两次累加是因为谷底的值始终为1不会计算两个谷底的相对高度,但是峰值的值是要取决于两侧高度的。
这是贪心算法的一个重要应用,应该细细体会。
134. 加油站
在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。
这题老实说我也没有很好地做出来,贪心算法没有固定套路,只能算是一个猜想,一个较好地答案是使用两个变量一个统计总和一个统计当前和,如果最后总和小于0说明是无法到达目的地的,如果综合大于0,说明最终可以到达。在统计的时候,如果当前和小于0,说明这段路是没办法到达的,无论从这段路的哪里开始都跨不过当前的点。所以从下一个点开始计算,这样遍历到最后就可以把起始点给找出来。
1005.K次取反后最大化的数组和
给定一个整数数组 A,我们只能用以下方法修改该数组:我们选择某个索引 i 并将 A[i] 替换为 -A[i],然后总共重复这个过程 K 次。(我们可以多次选择同一个索引 i。)
以这种方式修改数组后,返回数组可能的最大和。
这题勉强算是写出来了,但是很不优雅,代码写的很丑陋。虽然想清楚了可能的情况,但是具体写的时候对于各变量的使用很不好,参考的思路是先排序,然后全部遍历一遍,如果还有反转的机会且当前值小于0那么反转,机会减一否则不予操作,这样在遍历完一遍后要么是全反转了还有剩余,要么是还没反转完,如果还有剩余就计算剩余的机会是否为偶数,如果不是把当前值取反。
思路很清晰,还是要学习啊!
860.柠檬水找零
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。
顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
这题相对简单,逻辑也比较固定,优先找10然后再找5,如果不满足就退出。
406.根据身高重建队列
假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。
请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。
这题自己做东拼西凑算是做出来了,但是代码极其丑陋打各种补丁处理边界条件,看了参考后发现一如既往的优雅。其实参考的思路我也试着写了一下,但是参考用的是插入,我是直接赋值这就导致逻辑变复杂最后我没弄出来。
参考的思路是按照身高从大到小排列,插入的时候直接按照k值就可以了,因为后面的身高始终小于前面的身高所以无论怎么插入都不会有影响,和糖果题一样,贪心的时候要考虑两次分开考虑,如果想要同时兼顾就会顾此失彼,所以遇到的问题还是要吸取经验教训否则就无意义了。
重要!!! 有size和capacity两个属性,一个是用户看到的一个是底层数组的实际大小,当使用insert方法时,如果capacity不够会成倍扩容然后复制过去,所以实际效率很低不如 list结构。
452. 用最少数量的箭引爆气球
在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。
一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。
这题解决的还是挺快的,有了之前的教训,不同时考虑两个情况,只先考虑一种情况就是按照末端的位置排序,末端小的排在前面,这样在遍历的时候,只要后面遍历的气球起始位置小于当前末端的位置那么一定能覆盖当前气球的末端所以可以一次射中。这样分开考虑之后问题就变得简单了。
435. 无重叠区间
给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意: 可以认为区间的终点总是大于它的起点。 区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
这个题依旧贯彻分类贪心的思想,优先按照末尾的位置进行排序,如果相同的话,那么起始位置靠前的排在前面,这样在遍历的时候,如果这个点与前面的末尾相交了那就删除,继续遍历后面的点。如果当前点没有与前面的点相交说明不用删除,这时更新末尾位置为当前点的末尾。因为已经排过序,所以这个末尾一定是最靠前的最不会影响到其余点的末尾,如此直到结束遍历。
763.划分字母区间
字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。
这题的关键事项想清楚终止条件,就是这个区间内的最远的点是否和当前位置相等,如果相等说明所有的点都出现在了这段区间内,所以要求出每个字母的最远位置,这就需要遍历一遍不断更新,对于这种问题用vector就可表示26个字母了,不用再使用map了。一边遍历一边更新,遍历的技巧也比较重要,我一开始是用一个变量标记长度一个遍量标记最远距离,参考里是用一个变量标记起始位置,是先更新最远距离再进行比较这样就可以处理最后一个的边界问题不至于要单独处理。
56. 合并区间
给出一个区间的集合,请合并所有重叠的区间。
示例 1:
- 输入: intervals = [[1,3],[2,6],[8,10],[15,18]]
- 输出: [[1,6],[8,10],[15,18]]
- 解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
- 输入: intervals = [[1,4],[4,5]]
- 输出: [[1,5]]
- 解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。
- 注意:输入类型已于2019年4月15日更改。 请重置默认代码定义以获取新方法签名。
这题的思路和之前的类似,依照起始位置进行排序,只要遍历的这个点的起始位置小于最大范围就依旧属于同一区间,然后更新最大范围,不同之处在于边界的处理。老实说我又写的很不优雅,最后还要单独拎出来处理最后一个点,看了参考后不得不说还是优秀,每次都先超出之前的范围后都直接压入下一个点然后不断更新末尾范围就可以了,这样最后一个点的情况也能处理,而不用额外考虑了,不得不说还是要动脑好好思考,有解题思路是一回事,怎么优雅的写出来又是另一回事。
738.单调递增的数字
给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。
当且仅当每个相邻位数上的数字x和y满足x小于等于y时,我们称这个整数是单调递增的。
这题没得说,合理利用贪心的思想从后向前遍历,如果不符合条件了,那么后面的所有数全部置9然后当前值减一就是最大的非减数。这次的写法也比较优雅,利用数字转字符可以处理少一位数的情况。
968.监控二叉树
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
这题是到困难题,但是经过我的思考后最后还是解决掉了。首先肯定要遍历树,但是采用什么遍历顺序好呢?我一开始采用中序遍历,结果发现这样一来就无法考虑右侧节点的情况,因此改用后序遍历,这样每次处理节点时左右节点就都已经处理过了。
然后就是核心部分的逻辑,如果当前节点有一个子节点为相机则当前节点就可以被监控到用数字2表示,但是这样有可能出现右侧节点没有处理的情况,所以第二个条件是如果当前节点至少有一个子节点为0,则该节点要置-1设置为相机否则那个子节点永远也无法监控到了。后面一个条件的优先级高于前面一个所以要放在后面,这样就可以处理漏洞。
经过遍历后,还有个问题就是根节点,如果根节点左右两个节点都能被检测到,那么它其实是在等它上面的节点来监控,可它没有上面的节点所以最后遍历完要查看根节点的状态,如果是0,那么要多加一个相机监控。
写完又看了参考,果然比我的代码要好,参考利用了递归算法的返回值来标记当节点的状态,1为摄像头,0为未覆盖,2为已覆盖,左右节点都已覆盖时,当前节点为未覆盖状态,有一个未覆盖,当前节点要设置为相机,有一个为相机时,当前节点为已覆盖。值得一提的是,所有空姐点都默认为已经覆盖的情况。最后再单独处理根节点即可。