- 写于2019年6月7日 - 6月9日
文章目录
- [114. 二叉树展开为链表](https://leetcode.com/problems/flatten-binary-tree-to-linked-list/)
- [121. 买卖股票的最佳时机](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/)
- [124. 二叉树中的最大路径和](https://leetcode.com/problems/binary-tree-maximum-path-sum/)
- [128. 最长连续序列](https://leetcode.com/problems/longest-consecutive-sequence/)
- [136. 只出现一次的数字](https://leetcode.com/problems/single-number/)
114. 二叉树展开为链表
① 题目描述
- 给定一个二叉树,原地将它展开为链表。
- 例如,给定二叉树
- 将其展开为:
② 自己的笨办法 —— 先序遍历再构造二叉树
- 发现展开后的二叉树是原二叉树先序遍历后,只添加右孩子构成。
- 先序遍历获得所有的节点的val,再在以前root的基础上不停地添加右孩子。
- 注意: 方法返回为void,所以直接在root根节点的基础上构造右孩子。
- 代码如下,但没有达到原地构造的要求:
public void flatten(TreeNode root) {
if (root == null) {
return;
}
List<Integer> list = new ArrayList<>();
PreOrder(root, list);
TreeNode p = root;
for (int i = 1; i < list.size(); i++) {
p.left=null;
p.right = new TreeNode(list.get(i));
p = p.right;
}
}
public void PreOrder(TreeNode root, List<Integer> list) {
if (root == null) {
return;
}
list.add(root.val);
PreOrder(root.left, list);
PreOrder(root.right, list);
}
③ 迭代
- 获取root的左子树的最右节点,将其指向root的右子树。
- 将root的左子树移到右边,成为右子树。
- 将根节点的右节点作为当前的根节点,循环进行展开。
- while循环的条件是,只要左右子树有一个为空,则继续循环,以下两种情况都要进行调整操作。
- 代码如下:
public void flatten(TreeNode root) {
if (root == null) {
return;
}
while (root.right != null || root.left != null) {
if (root.left != null) {
TreeNode node = root.left;
while (node.right != null) {
node = node.right;// 找到左子树的最右节点
}
node.right = root.right;// 左子树的最右节点指向root的右子树
root.right = root.left;// root的右子树指向其左子树
root.left = null;
}
root = root.right;// 指向root的右子树,对右子树构造平摊子树
}
}
121. 买卖股票的最佳时机
① 题目描述
- 给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
- 如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
- 注意你不能在买入股票前卖出股票。
- 示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意:利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
- 示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
② 暴力法
- 由于股票的买入和卖出有时间先后顺序,第i天买入的股票,一定在第i天以后才能卖出。
- 使用暴力法,查找第i天和第i天以后的价格差,取最大价格差。
- 代码如下,花了
192ms
,效果并不是很好:
public int maxProfit(int[] prices) {
int profit = 0;
for (int i = 0; i < prices.length - 1; i++) {
for (int j = i + 1; j < prices.length; j++) {
int temp = prices[j] - prices[i];
if (temp > profit) {
profit = temp;
}
}
}
return profit;
}
- 时间复杂度: O ( n 2 ) O(n^2) O(n2),循环运行 n ( n − 1 ) 2 \dfrac{n (n-1)}{2} 2n(n−1)次。
- 空间复杂度: O ( 1 ) O(1) O(1)。只使用了两个变量 —— maxprofit \text{maxprofit} maxprofit和 profit \text{profit} profit。
③ 一次遍历 —— 查找波谷和最大利润
- 将
[7, 1, 5, 3, 6, 4]
绘制在图表上,我们将会得到:
- 可以发现: 最大利润一定出现在
最低波谷
之后的最高波峰
处,因此我们需要找到最小的谷之后的最大的峰。 我们可以维持两个变量——minprice
和maxprofit
,它们分别对应迄今为止所得到的最小的谷值和最大的利润(最低波谷之后的最高波峰与最低波谷之间的差值)。 - 代码如下,运行时间只花费了
1ms
:
public int maxProfit(int[] prices) {
int profit = 0;
int minPrice = Integer.MAX_VALUE;
for (int i = 0; i < prices.length; i++) {
if (prices[i] < minPrice) {// 寻找最低波谷
minPrice = prices[i];
} else if (prices[i] - minPrice > profit) {// 寻找最低波谷后的最高波峰,计算出二者的差值便是利润
profit = prices[i] - minPrice;
}
}
return profit;
}
124. 二叉树中的最大路径和
① 题目描述
- 给定一个非空二叉树,返回其最大路径和。
- 本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。
- 示例 1:
- 示例 2:
② 递归
- 给定一个非空节点,最终路径经过这个节点有4种情况:
- 只有该节点本身(左右子树的路径和都是负数);
- 该节点+左子树路径;
- 该节点+右子树路径;
- 该节点+左子树路径+右子树路径。`
其中1,2,3都可以作为子树路径和向上延伸,而4则不行。
- 在上图中,
20,15,7
是完整路径。如果向上连接父节点,加上-10
,整个路径就出现了分叉,不再是唯一路径。因此第4中情况不能向上发展。 - 用
temp
记录可以向上发展父节点的三种情况的最大值,用curMax
记录四种情况中的最大值,递归过程中返回可以向上发展父节点的temp
值。 - 上面的情况,就返回
20 + 15 = 35
作为左子树,向上发展父节点。 - 代码如下:
int maxSum = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
getMaxPathSum(root);
return maxSum;
}
public int getMaxPathSum(TreeNode root) {
if (root == null) {// 当前节点为空,不可以向上连接父节点,直接返回0
return 0;
}
int left = getMaxPathSum(root.left);// 获取左子树的最大路径和
int right = getMaxPathSum(root.right);// 获取右子树的最大路径和
int temp = Math.max(Math.max(left + root.val, right + root.val), root.val);// 可以向上发展父节点的三种情况里面的最大路径和
int curMax=Math.max(temp,root.val+left+right);// 四种情况的最大路径和
if (maxSum<curMax){// 更新最大路径和
maxSum=curMax;
}
return temp;// 返回可以向上连接父节点的三种情况的最大路径和
}
128. 最长连续序列
① 题目描述
- 给定一个未排序的整数数组,找出最长连续序列的长度。
- 要求算法的时间复杂度为 O(n)。
- 示例:
输入: [100, 4, 200, 1, 3, 2]
输出: 4
解释: 最长连续序列是 [1, 2, 3, 4]。它的长度为 4。
② 自己的笨办法
- 主要思想:先使用
Arrays.sort(nums)
对数组进行排序,然后使用start
和end
双指针,如果nums[end] = nums[end - 1] + 1
,说明是连续的,end
指针后移;否则更新连续序列的长度maxLen = Math.max(maxLen, end - start);
。 - 问题1: 如果nums的长度为1,只在循环中更新了
maxLen
。没有考虑到循环到end = nums.length
退出循环时,也需要更新maxLen
。 - 问题2:没有考虑到,排序后的数组可能为
[1, 2, 2, 3, 5]
,可以认定最长连续序列为1, 2, 3
,不考虑重复的数字。因此,需要将排序后的nums
数组转化为List
,进行去重。 - 问题3:去重时,使用
list.get(i + 1) == (list.get(i))
,这种判断方式貌似对负数并不友好,最后的case中,-999, -999, -999, -998, -998, -997 ...
并没有达到去重的目的,导致结果错误。需要使用list.get(i + 1).equals(list.get(i))
进行相等判断,才能达到去重的目的。 - 问题4:要考虑到nums的长度为0或者为null的情况。
- 代码如下,运行花费
33ms
:
public int longestConsecutive(int[] nums) {
if (nums.length == 0 || nums == null) {
return 0;
}
Arrays.sort(nums);
List<Integer> list = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
list.add(nums[i]);
}
for (int i = 0; i < list.size() - 1; i++) {// 去掉重复元素
if (list.get(i + 1).equals(list.get(i))) {
list.remove(i + 1);
i--;
}
}
int start = 0;
int end = 1;
int maxLen = 0;
for (; end < list.size(); end++) {
if (list.get(end) != (list.get(end - 1) + 1)) {
maxLen = Math.max(maxLen, end - start);
start = end;
}
}
maxLen = Math.max(maxLen, end - start);
return maxLen;
}
- 时间复杂度: O ( 3 n ) O(3n) O(3n),一次用于将nums转为List,一次用于List去重,一次用于查找最长连续序列。
- 空间复杂度: O ( n ) O(n) O(n),需要开辟新的空间,存储List。
③ 哈希表和线性空间的构造
- 时间复杂度:
O
(
n
)
O(n)
O(n)
尽管在 for 循环中嵌套了一个 while 循环,时间复杂度看起来像是二次方级别的。但其实它是线性的算法。因为只有当currentNum
遇到了一个序列的开始, while 循环才会被执行(也就是currentNum-1
不在数组nums
里)。 while 循环在整个运行过程中只会被迭代n
次。这意味着尽管看起来时间复杂度为 O ( n ⋅ n ) O(n \cdot n) O(n⋅n),实际这个嵌套循环只会运行 O ( n + n ) = O ( n ) O(n + n) = O(n) O(n+n)=O(n) 次。所有的计算都是线性时间的,所以总的时间复杂度是 O ( n ) O(n) O(n)的。 - 空间复杂度: O ( n ) O(n) O(n)。为了实现 O ( 1 ) O(1) O(1) 的查询,我们对哈希表分配线性空间,以保存 nums 数组中的 O ( n ) O(n) O(n)个数字。除此以外,所需空间与暴力解法一致。
- 主要思想:
① 将nums转化为HashSet,可以去掉重复。解决了自己构造List并去重的问题。
② 查找连续序列时,使用!set.contains(num - 1)
作为条件,使得每次while循环,都是从从新序列开始。与自己之前更新start = end
有异曲同工之处。 - 代码如下,花费了
6ms
,比自己的快。
public int longestConsecutive(int[] nums) {
if (nums.length == 0 || nums == null) {
return 0;
}
int maxLen = 0;
Set<Integer> set = new HashSet<>();
for (int num : nums) {
set.add(num);
}
for (int num : set) {
if (!set.contains(num - 1)) {// 不包含当前数-1,说明当前数是新序列的起点,避免了重复查找当前数已经存在于其他序列的情况
int len = 1;
int curNum = num;
while (set.contains(curNum + 1)) {// 查找以当前数为序列起点的新序列
len++;
curNum++;
}
maxLen = Math.max(maxLen, len);
}
}
return maxLen;
}
136. 只出现一次的数字
① 题目描述
- 给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
- 说明: 你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
- 示例 1:
输入: [2,2,1]
输出: 1
- 示例 2:
输入: [4,1,2,1,2]
输出: 4
② 暴力法
- 查找
nums[i]
是否出现两次,不经要向前查找,还要向后查找,因为可能越往前,很多的数的第二个数早已经出现过。于是第二层循环j = 0
。如果没有找到nums[i]
对应的第二个数,直接nums[i]
;找到了,跳出循环,继续查找nums[i + 1]
。 - 代码如下,运行时间
154ms
:
public int singleNumber(int[] nums) {
for (int i = 0; i < nums.length; i++) {
boolean flag = false;
for (int j = 0; j < nums.length; j++) {// 以nums[i]为中心,向前或向后查找
if (i != j && nums[i] == nums[j]){
flag=true;
break;
}
}
if (!flag){
return nums[i];
}
}
return -1;
}
③ 使用HashMap,存储每个数字出现的次数
- 遍历
nums数组
,使用HashMap
存储每个数字出现的次数。然后遍历HashMap
,查找出现次数为1的数字。 - 代码如下,花费了
10ms
:
public int singleNumber(int[] nums) {
HashMap<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
for (Integer key : map.keySet()) {
if (map.get(key) == 1) {
return key;
}
}
return -1;
}
④ 位操作
- 如果我们对 0 和二进制位做 XOR 运算,得到的仍然是这个二进制位
a ⊕ 0 = a a \oplus 0 = a a⊕0=a - 如果我们对相同的二进制位做 XOR 运算,返回的结果是 0
a ⊕ a = 0 a \oplus a = 0 a⊕a=0 - XOR 满足交换律和结合律
a ⊕ b ⊕ a = ( a ⊕ a ) ⊕ b = 0 ⊕ b = b a \oplus b \oplus a = (a \oplus a) \oplus b = 0 \oplus b = b a⊕b⊕a=(a⊕a)⊕b=0⊕b=b - 只需要对所有的数字进行异或操作,最后的结果便是
single number
- 代码如下,所用时间
0ms
,超级高效
。
public int singleNumber(int[] nums) {
int result = nums[0];
for (int i = 1; i < nums.length; i++) {
result = result ^ nums[i];
}
return result;
}