LeetCode题号[100,199]刷题总结
- 前言
- 105. 从前序与中序遍历序列构造二叉树
- 114. 二叉树展开为链表
- 115. 不同的子序列(Hard)
- 116. 填充每个节点的下一个右侧节点指针
- 117. 填充每个节点的下一个右侧节点指针 II
- 123. 买卖股票的最佳时机 III(Hard)
- 124. 二叉树中的最大路径和(Hard)
- 128. 最长连续序列(Hard)
- 131. 分割回文串
- 132. 分割回文串 II(Hard)
- 134. 加油站
- 135. 分发糖果
- 137. 只出现一次的数字 II
- 138. 复制带随机指针的链表
- 141. 环形链表(Easy)
- 142. 环形链表 II
- 146. LRU缓存机制
- 151. 翻转字符串里的单词
- 152. 乘积最大子数组
- 153. 寻找旋转排序数组中的最小值
- 154. 寻找旋转排序数组中的最小值 II(Hard)
- 160. 相交链表(Easy)
- 162. 寻找峰值
- 思路
- 164. 最大间距(Hard)
- 166. 分数到小数
- 169. 多数元素
- 173. 二叉搜索树迭代器
- 174. 地下城游戏(Hard)
- 179. 最大数
- 187. 重复的DNA序列
- 188. 买卖股票的最佳时机 IV(Hard)
- 189. 旋转数组
- 190. 颠倒二进制位(Easy)
- 198. 打家劫舍(Easy)
- 已结束
前言
- 这里是题号[100,199]部分的题目
- 大部分Easy和其他较为简单的题目就跳过了
105. 从前序与中序遍历序列构造二叉树
根据一棵树的前序遍历与中序遍历构造二叉树。
注意:
你可以假设树中没有重复的元素。
思路
- 前序遍历序列:【根节点,{左子树},{右子树}】
- 中序遍历序列:【{左子树},根节点,{右子树}】
- 先通过前序遍历就找到根节点
- 再通过中序遍历获得左右子树长度
- 即可得到左右子树的序列
- 此后通过递归继续构造左右子树即可
public TreeNode buildTree(Map<Integer, Integer> map,int[] preorder, int[] inorder,int preStart,int preEnd,int inStart,int inEnd){
if (preStart > preEnd)
return null;
int pointRoot = map.get(preorder[preStart]);
int len = pointRoot - inStart;
TreeNode root = new TreeNode(preorder[preStart]);
root.left = buildTree(map,preorder,inorder,preStart+1,preStart+len,inStart,pointRoot-1);
root.right = buildTree(map,preorder,inorder,preStart+len+1,preEnd,pointRoot+1,inEnd);
return root;
}
public TreeNode buildTree(int[] preorder, int[] inorder) {
if (preorder.length == 0)
return null;
Map<Integer, Integer> map = new HashMap<>();
for(int i = 0;i < inorder.length;i++)
map.put(inorder[i],i);
return buildTree(map,preorder,inorder,0,preorder.length-1,0,inorder.length-1);
}
114. 二叉树展开为链表
给定一个二叉树,原地将它展开为一个单链表。
思路
- 前序遍历
public void flatten(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
TreeNode treeNode = new TreeNode(0);
if (root != null)
stack.push(root);
while(!stack.empty()){
TreeNode node = stack.pop();
treeNode.right = node;
treeNode = node;
if (node.right != null)
stack.push(node.right);
if (node.left != null)
stack.push(node.left);
node.left = node.right = null;
}
}
115. 不同的子序列(Hard)
给定一个字符串 S 和一个字符串 T,计算在 S 的子序列中 T 出现的个数。
一个字符串的一个子序列是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)
题目数据保证答案符合 32 位带符号整数范围。
思路
- 二维dp
- \(dp[i][j]\)表示s的前i个字符可以组成多少次t的前j个字符
- 那么,当$s[i-1] == t[j-1] $时,可以放弃或选择当前的两个字符
- 当放弃时,$ dp[i][j] = dp[i-1][j]$
- 当选择时,$ dp[i][j] = dp[i-1][j] + dp[i-1][j-1]$
public int numDistinct(String s, String t) {
int[][] dp = new int[s.length()+1][t.length()+1];//dp[s的前i个字符串][t的前i个字符串] = 次数
for(int i = 0;i <= s.length();i++)
dp[i][0] = 1;
for(int i = 1;i <= s.length();i++)
for(int j = 1;j <= t.length();j++)
dp[i][j] = dp[i-1][j]
+ (s.charAt(i-1) == t.charAt(j-1) ? dp[i-1][j-1] : 0);
return dp[s.length()][t.length()];
}
- 一维优化
- 很容易发现,我们每次只取\(dp[i-1]\)的结果,即前一行数组的结果
- 所以,可以将数组从二维优化到一维
public int numDistinct(String s, String t) {
int[] dp = new int[t.length()+1];
dp[0] = 1;
for(int i = 1;i <= s.length();i++)
for(int j = t.length();j >= 1;j--)//倒序保证此次操作不会对前一次结果造成影响
dp[j] += (s.charAt(i-1) == t.charAt(j-1) ? dp[j-1] : 0);
return dp[t.length()];
}
116. 填充每个节点的下一个右侧节点指针
给定一个完美二叉树,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。
提示:
- 你只能使用常量级额外空间。
- 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。
思路
- 难点在于只能使用常量级的格外空间,否则可以使用队列存储进行层序遍历即可
- 考虑使用递归,其实也就只有两种情况
- 【当前节点的左节点】 连接 【右节点】
- 【当前节点的右节点】 连接 【当前节点的next节点的左节点】
public void connect(Node root,Node next){
if (root != null){
root.next = next;
connect(root.left,root.right);
connect(root.right, root.next == null ? null : root.next.left);
}
}
public Node connect(Node root) {
connect(root,null);
return root;
}
117. 填充每个节点的下一个右侧节点指针 II
给定一个二叉树
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。
进阶:
你只能使用常量级额外空间。
使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。
思路
- 跟前一题的区别就是将【完全二叉树】变成了【二叉树】
- 所以需要考虑左右节点各为空的情况
public Node getRightNode(Node root){
while(root != null){
if (root.left != null)
return root.left;
if (root.right != null)
return root.right;
root = root.next;
}
return null;
}
public Node connect(Node root) {
if (root != null && (root.left != null || root.right != null)){
if (root.left != null && root.right != null){
root.left.next = root.right;
root.right.next = getRightNode(root.next);
}
else if (root.left == null){
root.right.next = getRightNode(root.next);
}
else{
root.left.next = getRightNode(root.next);
}
//先递归右子树,设置好右子树的next节点才能使用getRightNode获得next节点
connect(root.right);
connect(root.left);
}
return root;
}
123. 买卖股票的最佳时机 III(Hard)
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
思路
- 伪Hard级别,并不算难
- 这题完全可以当作第121题【买卖股票的最佳时机】做
- 第一次买卖股票前的初始利润为0,也就是第121题的情况
- 第二次买卖股票前的初始利润为第一次的最大利润,也就是第121题的答案
- 那么我们只要保存四个状态,就是第一二次买卖后的利润
- 关于第一次买卖的情况就很简单了,此时初始利润为0
- 那么第二次买卖呢?
- 由于我们之前已经保存了前面的第一次卖出后的最大利润,所以将初始利润设为第一次卖出的最大利润即可
- 接下来就与第一次买卖的情况基本一致了
public int maxProfit(int[] prices) {
int firstBuy = Integer.MIN_VALUE;//第一次买股票的利润
int firstSell = 0;//第一次卖股票的利润
int secondBuy = Integer.MIN_VALUE;//第二次买股票的利润
int secondSell = 0;//第二次买股票的利润
for(int price : prices){
firstBuy = Integer.max(firstBuy,0 - price);//第一次买前的初始利润是0
firstSell = Integer.max(firstSell,firstBuy + price);
secondBuy = Integer.max(secondBuy,firstSell - price);//第二次买前的初始利润是前面第一次卖出后的最大利润
secondSell = Integer.max(secondSell,secondBuy + price);
}
return secondSell;
}
124. 二叉树中的最大路径和(Hard)
给定一个非空二叉树,返回其最大路径和。
本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。
思路
- 递归遍历
- 当作为结果时,取左子树,右子树,根节点+左子树+右子树,根节点+max(左子树,右子树,0)中的最大值
- 当作为左右子树的结果返回时,取根节点+max(左子树,右子树,0)作为返回值(因为同时取左右子树时,就无法作为一条父节点的一条路径)
- 值得注意的是,当树全是负数时,需要特判
class Solution {
private int maxNode = Integer.MIN_VALUE;//树中的最大节点
private int ans = Integer.MIN_VALUE;
public int maxPathSum2(TreeNode root){
if (root == null)
return 0;
int leftSum = maxPathSum2(root.left);
int rightSum = maxPathSum2(root.right);
int value1 = root.val + Integer.max(0,Integer.max(leftSum,rightSum));//根节点+ 【左右子树中的最大值或0】
int value2 = root.val + Integer.max(0,leftSum) + Integer.max(0,rightSum);//根节点+ 左右子树(>0)
maxNode = Integer.max(maxNode,root.val);
ans = Integer.max(ans,Integer.max(Integer.max(value1,value2),Integer.max(leftSum,rightSum)));
return Integer.max(value1,0);
}
public int maxPathSum(TreeNode root) {
maxNode = ans = Integer.MIN_VALUE;
maxPathSum2(root);
return maxNode < 0 ? maxNode : ans ;
}
}
128. 最长连续序列(Hard)
给定一个未排序的整数数组,找出最长连续序列的长度。
要求算法的时间复杂度为 O(n)。
思路
- 伪Hard级别,其实并不算难,偏思维
- 使用一个HashSet存储元素,其查找的时间复杂度为\(O(1)\)
- 对于连续序列x,x+1,x+2....x+n来说,假设我们每个元素都从头到尾寻找其后面的连续序列,显然时间复杂度为\(O(n)\)
- 但是,有些遍历步骤完全可以省略,只需要从序列的第一个开始遍历即可
- 那么,对于任意一个元素y,我们就判断Set中是否存在y-1,如果存在,说明此时y不是序列的头一个元素,跳过,否则开始遍历
- 通过这种方式,任意一个元素最多只遍历过两次,所以时间复杂度为\(O(n)\)
public int longestConsecutive(int[] nums) {
int ans = 0;;
Set<Integer> set = new HashSet<>(nums.length);
for(int num : nums)
set.add(num);
for(int num : nums){
if (!set.contains(num - 1)){
int cur = num ;
while(set.contains(cur+1))
cur++;
ans = Integer.max(ans,cur-num+1);
}
}
return ans;
}
131. 分割回文串
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
思路
- 递归,回溯
class Solution {
private List<List<String>> ans;
public boolean check(String s,int start,int end){
while(start < end){
if (s.charAt(start++) != s.charAt(end--))
return false;
}
return true;
}
public void dfs(String s,List<String> list,int now){
if (now == s.length()){
ans.add(new LinkedList<>(list));
return ;
}
for(int i = now;i < s.length();i++)
if (check(s,now,i)){
list.add(s.substring(now,i+1));
dfs(s,list,i+1);
list.remove(list.size()-1);
}
}
public List<List<String>> partition(String s) {
ans = new LinkedList<List<String>>();
dfs(s,new LinkedList(),0);
return ans;
}
}
132. 分割回文串 II(Hard)
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回符合要求的最少分割次数。
思路
class Solution {
private int[] dp;//dp[i] = x 表示以0点为起点,以i点为终点的字符串的最小分割数为x
//中心拓展判断回文
public void check(String s,int left,int right){
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)){
if (left == 0){//当遍历到起点时,表示[0,right]为回文,所以不需要分割,dp = 0
dp[right] = 0;
break;
}
//将[0,right]分割为[0,left-1]与[left,right],此时[left,right]为回文,所以结果为[0,left-1]+1
if (dp[right] == -1 || dp[left-1]+1 < dp[right])
dp[right] = dp[left-1]+1;
left--;
right++;
}
}
public int minCut(String s) {
dp = new int[s.length()];
Arrays.fill(dp,-1);
for(int i = 0;i < s.length();i++){
check(s,i,i);//奇数回文串,以i点为中心点
check(s,i,i+1);//偶数回文串,以i,i+1为中心点
}
return dp[s.length() - 1];
}
}
134. 加油站
在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。
说明:
- 如果题目有解,该答案即为唯一答案。
- 输入数组均为非空数组,且长度相同。
- 输入数组中的元素均为非负数。
思路
public int canCompleteCircuit(int[] gas, int[] cost) {
int curSpare = 0;
int minSpare = Integer.MAX_VALUE;
int minIndex = 0;
for(int i = 0 ;i < gas.length;i++){
curSpare += gas[i] - cost[i];
if (curSpare < minSpare){
minSpare = curSpare;
minIndex = i;
}
}
return curSpare < 0 ? -1 : (minIndex+1)%gas.length;
}
135. 分发糖果
老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。
你需要按照以下要求,帮助老师给这些孩子分发糖果:
- 每个孩子至少分配到 1 个糖果。
- 相邻的孩子中,评分高的孩子必须获得更多的糖果。
那么这样下来,老师至少需要准备多少颗糖果呢?
思路
- 参考:https://leetcode-cn.com/problems/candy/solution/candy-cong-zuo-zhi-you-cong-you-zhi-zuo-qu-zui-da-/
public int candy(int[] ratings) {
int[] left = new int[ratings.length];
int[] right = new int[ratings.length];//这个数组可以优化掉
int ans = 0;
Arrays.fill(left,1);
Arrays.fill(right,1);
for(int i = 1;i < ratings.length;i++)
if (ratings[i] > ratings[i-1])
left[i] = left[i-1] + 1;
for(int i = ratings.length - 2;i >= 0;i--)
if (ratings[i] > ratings[i+1])
right[i] = right[i+1] + 1;
for(int i = 0;i < ratings.length;i++)
ans += Integer.max(left[i],right[i]);
return ans;
}
137. 只出现一次的数字 II
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
思路
- 位运算
- 由于每个元素都出现三次,所以其对应的二进制位 1 也出现了三次
- 因此,我们可以计算出每个二进制位出现的次数,num % 3 == 1 的时候就表示这个二进制位是只出现过一次的那个数的位
- 于是,可以考虑使用长度为32的数组来表示二进制位中 1 出现的次数
- 此外,也可以直接通过两个二进制来表示num % 3 的结果
public int singleNumber(int[] nums) {
/**
* 定义两个int 变量one two
* 其每个二进制位组合起来,显示为00,10,01
* 分别表示该二进制为1的数量 % 3 == 0,1,2
* 例如,010,100
* 从右往左数
* 第一个二进制位00,表示该位数量%3 == 0
* 第二个二进制位10,表示该位数量%3 == 1
* 第三个二进制位01,表示该位数量%3 == 2
*/
int one = 0;
int two = 0;
for(int num : nums){
/**
* one two的状态转变:
* 00 ---- num=1 --> 10 ---num=1 --> 01 -- num=1 -->00
* 当num = 1时,会向下一状态转变
* one会变成1时是以下两种情况,其余情况为0
* one = 0,num = 1,two = 0
* one = 1,num = 0,two = 0
* 此时one != num 且 two == 0
* 所以可以通过位运算 (one ^ num) & (~two) 得出结果
*
* 同理,two会变成1是以下两种情况:
* one = 1,num = 1,two = 0
* one = 0,num = 0,two = 1
* 此时 two != num && two != one
* 所以可以通过位运算 (two ^ num) & (two ^ one) 得出结果
* 但是,前面one已经计算成下一次状态的结果了
* 所以首先可以通过用临时变量temp存储没变化前的one,然而还有更加巧妙的方法
* 我们可以发现,two变成1的两种情况中,one都会变成0
* 所以可以直接通过变化后的one == 0来判断
* 此时 two != num && one == 0
* 所以可以通过位运算 (two ^ num) & (~one) 得出结果
*/
one = (one ^ num) & (~two);
two = (two ^ num) & (~one);
}
return one;
}
138. 复制带随机指针的链表
给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。
要求返回这个链表的 深拷贝。
我们用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:
val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。
思路
- 新链表交叉生成放入旧链表中,也就是旧节点后面接着该节点的拷贝节点
- 然后就可以很轻松得设置随机指针了,因为旧节点的随机指针指向的随机节点后面就是该随机节点的拷贝
- 最后拆分新旧链表即可
- 这种方法可以在\(O(1)\)的空间复杂度内完成
public Node copyRandomList(Node head) {
if (head == null)
return null;
Node curNode = head;
//先建一条链表,偶数节点为新链表
while(curNode != null){
Node nextNode = curNode.next;
curNode.next = new Node(curNode.val);
curNode = curNode.next;
curNode.next = nextNode;
curNode = nextNode;
}
//再设置新链表的随机指针
curNode = head;
while(curNode != null){
if (curNode.random != null){
curNode.next.random = curNode.random.next;
}
curNode = curNode.next.next;
}
//拆分开新旧链表
Node newHead = head.next;
curNode = head;
while(curNode != null){
Node nextNode = curNode.next;
Node nextNextNode = curNode.next.next;
curNode.next = nextNextNode;
nextNode.next = (nextNextNode == null ? null :nextNextNode.next);
curNode = nextNextNode;
}
return newHead;
}
141. 环形链表(Easy)
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
进阶:
你能用 O(1)(即,常量)内存解决此问题吗?
思路
- 双指针
- WXG面试手撕代码时出过这题
- 当时面试时没刷过这题,不过也不难,就是写的代码不怎么简洁
public boolean hasCycle(ListNode head) {
ListNode slowNode = head;
ListNode quickNode = head;
while (quickNode != null){
slowNode = slowNode.next;
quickNode = (quickNode.next == null ? null : quickNode.next.next) ;
if (quickNode == slowNode)
return quickNode != null;
}
return false;
}
142. 环形链表 II
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
说明:不允许修改给定的链表。
思路
- 双指针
public ListNode detectCycle(ListNode head) {
if (head == null)
return null;
ListNode slowNode = head;
ListNode quickNode = head;
//先让双指针碰撞
while (true){
slowNode = slowNode.next;
quickNode = (quickNode.next == null ? null : quickNode.next.next) ;
if (quickNode == null)
return null;
if (slowNode == quickNode)
break;
}
//再将快指针放回开头,两指针都只走一步,碰头地点即为链表环入口,数学证明自行百度
quickNode = head;
while(quickNode != slowNode){
quickNode = quickNode.next;
slowNode = slowNode.next;
}
return quickNode;
}
146. LRU缓存机制
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
- 获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
- 写入数据 put(key, value) - 如果密钥已经存在,则变更其数据值;如果密钥不存在,则插入该组「密钥/数据值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
- 时间复杂度:\(O(1)\)
思路
- LRU:最近最少使用策略
- 当未使用HashMap时,由于LRU采用的是链表形式,所以时间复杂度均为\(O(1)\)。
- 采用HashMap可以使得时间复杂度为\(O(1)\)
- 简单来说,就是将HashMap中插入的节点用一个链表来连接
- Java中本身自带了LinkedHashMap
class LRUCache {
class Node {
Integer key;
Integer value;
Node pre;
Node next;
Node(int key, int value) {
this.key = key;
this.value = value;
}
Node(){}
}
class LinkedNode {
private Node head;
private Node tail;
LinkedNode() {
head = new Node();
tail = new Node();
head.next = tail;
tail.pre = head;
}
public void put(Node node) {
node.pre = head;
node.next = head.next;
head.next = node;
node.next.pre = node;
}
public void remove(Node node) {
Node preNode = node.pre;
Node nextNode = node.next;
preNode.next = nextNode;
nextNode.pre = preNode;
}
public void toHead(Node node) {
remove(node);
put(node);
}
}
private int capacity;
private int size;
private Map<Integer, Node> map = new HashMap<>();
private LinkedNode linkedNode = new LinkedNode();
public LRUCache(int capacity) {
this.capacity = capacity;
size = 0;
}
public int get(int key) {
Node node = map.get(key);
if (node != null) {
linkedNode.toHead(node);
}
return node == null ? -1 : node.value;
}
public void put(int key, int value) {
if (get(key) != -1) {
map.get(key).value = value;
linkedNode.toHead(map.get(key));
} else if (size < capacity) {
Node node = new Node(key, value);
linkedNode.put(node);
map.put(key, node);
size++;
} else {
map.remove(linkedNode.tail.pre.key);
linkedNode.remove(linkedNode.tail.pre);
Node node = new Node(key, value);
map.put(key, node);
linkedNode.put(node);
}
}
}
151. 翻转字符串里的单词
给定一个字符串,逐个翻转字符串中的每个单词。
思路
- 时间复杂度\(O(n)\)
- 由于Java传入的参数是String,而String不可变,所以即使采用原地修改也没法使空间复杂度变为\(O(1)\)
- 但是如果传入参数是char的话,就能通过原地修改来使得空间复杂度\(O(1)\)
public String reverseWords(String s) {
/**
* 由于Java中的String是不可变的,所以只能将String变成char数组
* 如果传入的参数是char数组,就可以达到O(1)的空间复杂度了
* 思路比较简单:
* 1、先逆转整个数组
* 2、再逆转每个单词
* 但是有一个坑,就是原数组中两单词之间可能不止一个空格,而题目要求逆转后只能有一个
* 暴力方法就是删去多余的空格,然而数组删空格的时间复杂度O(n)
* 于是可以使用左右指针(具体可参考LC第80题)
* 左指针最后的位置就是逆转后的终点位置
* 右指针则是用来从头到尾遍历原数组
*/
char[] str = s.toCharArray();
int left = 0;
int right = 0;
reverse(str, 0, str.length - 1);
while (right < str.length) {
if (str[right] != ' ') {
if (left != 0) str[left++] = ' ';
int start = left;
int end = right;
while (right < str.length && str[right] != ' ') {
str[left++] = str[right++];
}
reverse(str, start, left - 1);
}
right++;
}
return String.copyValueOf(str, 0, left);
}
public void reverse(char[] str, int begin, int end) {
int left = begin, right = end;
while (left < right) {
char temp = str[left];
str[left] = str[right];
str[right] = temp;
left++;
right--;
}
}
- 也可以采用其方法,例如栈
- 将所有单词推入栈中,然后再全部取出来即可
- 这种方法更简单些,但是必须使用\(O(n)\)的空间复杂度
152. 乘积最大子数组
给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
思路
- 动态规划
- 记录前面的最小连续子数组(通常是负数),最大连续子数组,让后根据当前节点更新
- 由于需要的是连续子数组,所以状态转移方程每次都与当前节点相关
- ans保存期间最大的数值
int maxProduct(vector<int>& nums) {
int len = nums.size();
int maxn,minn,ans;
maxn = minn = ans = nums[0];
for(int i = 1;i < len;i++){
int temp_maxn = maxn;
int temp_minn = minn;
maxn = max(nums[i],max(temp_minn*nums[i],temp_maxn*nums[i]));//三个结果都与nums[i]相关
minn = min(nums[i],min(temp_minn*nums[i],temp_maxn*nums[i]));
ans = max(ans,maxn);
}
return ans;
}
153. 寻找旋转排序数组中的最小值
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
你可以假设数组中不存在重复元素。
思路
- emmmmmm,这种题好像做过很多次了,前面有道类似的貌似是找一个数,这次是找最小值
- 与前面那题大同小异吧
public int findMin(int[] nums) {
int left = 0,right = nums.length - 1;
while (left + 1 < right){
int mid = (left + right) >> 1;
if (nums[mid] >= nums[left]){
if (nums[right] <= nums[left])
left = mid + 1;
else
right = mid ;
}
else{
if (nums[mid] <= nums[right])
right = mid ;
else
left = mid;
}
}
return Integer.min(nums[left],nums[right]);
}
- 看了下评论,发现一种更短更妙的写法
public int findMin(int[] nums) {
int left = 0,right = nums.length - 1;
while (left < right){
int mid = (left + right) >> 1;
//当出现这种情况时,表示出现了逆序的情况,即最小值肯定在(mid,right]部分
if (nums[mid] > nums[right])
left = mid + 1;
//此时[mid,right]肯定是递增数组,因此可以直接排除掉(mid,right]
else
right = mid;
}
return nums[right];
}
154. 寻找旋转排序数组中的最小值 II(Hard)
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
注意数组中可能存在重复的元素。
思路
- 跟前面一题的唯一区别就是可能出现重复的元素
- 虽然可以使用二分,但在最坏的情况下时间复杂度为\(O(n)\)
- Why?
- 例如,{5,5,1,5,5,5,5,5,5}和
- 这种情况下nums[left],nums[right],nums[mid]一模一样,如何舍弃另一半进行二分?
- 此时只能舍弃一个元素
- 代码略
160. 相交链表(Easy)
编写一个程序,找到两个单链表相交的起始节点。
思路
- 使用双指针,两条链表同时前进
- 当一个指针到链表尾部时,就跳到另一个链表的头节点
- 最后两指针相交的地点即是链表的交点(若链表未相交,则此时指针同时为null)
- Why?
- 两个指针相交时,都遍历过两条链表未相交的地方一次,链表相交的地方一次
- 所以一定会相遇,且相遇点一定是相交点
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
//本来自己也写了个AC代码,但发现评论区有个更简洁的,就使用评论区的代码了
if (headA == null || headB == null)
return null;
ListNode curA = headA;
ListNode curB = headB;
while(curA != curB){
curA = (curA == null ? headB : curA.next);
curB = (curB == null ? headA : curB.next);
}
return curA;
}
162. 寻找峰值
峰值元素是指其值大于左右相邻值的元素。
给定一个输入数组 nums,其中 nums[i] ≠ nums[i+1],找到峰值元素并返回其索引。
数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。
你可以假设 \(nums[-1] = nums[n] = -∞\)。
思路
- 直接二分
- 当遇到\(nums[mid] > nums[mid+1]\) 时,直接抛弃右边的即可,因为可以保证左边一定有峰值!
- 为什么左边一定有峰值?
- 因为此时\(nums[mid]\) 右边的值小于当前值
- 若\(nums[mid - 1] < nums[mid]\),显然mid就是峰值
- 若\(nums[mid - 1] > nums[mid]\),则此时\(nums[mid - 1]\)右边的值小于当前值,继续判断$nums[mid - 2] $ 与\(nums[mid - 1]\)的关系
- 一直到最后还没找到峰值的话,当\(mid == 0\)时,\(nums[-1] == -∞\),此时峰值一定是\(nums[0]\)
public int findPeakElement(int[] nums) {
int left = 0,right = nums.length - 1;
while (left < right){
int mid = (left + right) >> 1;
if (nums[mid] > nums[mid+1])
right = mid;
else
left = mid + 1;
}
return left;
}
164. 最大间距(Hard)
给定一个无序的数组,找出数组在排序之后,相邻元素之间最大的差值。
如果数组元素个数小于 2,则返回 0。
说明:
- 你可以假设数组中所有元素都是非负整数,且数值在 32 位有符号整数范围内。
- 请尝试在线性时间复杂度和空间复杂度的条件下解决此问题。
思路
- 重点在于线性时间复杂度\(O(n)\),因为普通sort排序时间复杂度至少为\(O(nlogn)\),所以肯定不能用的
- 桶排序+鸽笼原理
- 最大间距就是找 max(相邻桶的最大值 减去 相邻桶的最小值)
- 注意:如果桶为空,则省略不计数,例如{1,null,3},则1,3相邻
- 为什么桶内的数字不存在最大间距?
- 桶的容量为\(bucketsSize = Integer.max(1,(max - min) / (nums.length - 1));\)
- 也就是桶内的最大差值就是\(bucketsSize\)
- 那么,我们转换下方程,可以得到
- \(bucketsSize * (nums.length - 1) = max - min\)
- 也就是说,桶容量就是整个数组的平均间距
- 那么,肯定存在最大间距,这个间距就是桶间的距离
class Bucket{
public int min;
public int max;
public Bucket(int min,int max){
this.min = min;
this.max = max;
}
public void refresh(int min,int max){
this.min = Integer.min(this.min,min);
this.max = Integer.max(this.max,max);
}
}
class Solution {
public int maximumGap(int[] nums) {
//获取最大最小值
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for(int num : nums){
min = Integer.min(min,num);
max = Integer.max(max,num);
}
if (max == min || nums.length < 2)
return 0;
//将数值放入桶中,保存桶中的最大最小值
int bucketsSize = Integer.max(1,(max - min) / (nums.length - 1));
Bucket[] buckets = new Bucket[(max - min) / bucketsSize + 1];
for(int num : nums){
int index= (num - min) / bucketsSize;
if (buckets[index] == null){
buckets[index] = new Bucket(num,num);
}
else{
buckets[index].refresh(num,num);
}
}
//获取最大间距
int ans = Integer.MIN_VALUE;
int preMax = Integer.MIN_VALUE;
for(int i = 0;i < buckets.length;i++){
if (buckets[i] != null && preMax == Integer.MIN_VALUE){
preMax = buckets[i].max;
}
else if (buckets[i] != null ){
ans = Integer.max(ans,buckets[i].min - preMax);
preMax = buckets[i].max;
}
}
return ans;
}
}
166. 分数到小数
给定两个整数,分别表示分数的分子 numerator 和分母 denominator,以字符串形式返回小数。
如果小数部分为循环小数,则将循环的部分括在括号内。
思路
- 模拟除法运算
- 当遇到相同的余数时,显然此时就遇到了循环节,此时在前面那个相同余数位置加"(",最后加")"即可
public String fractionToDecimal(int numerator, int denominator) {
if (numerator == 0)
return "0";
if (denominator == 0)
return "";
long numeratorLong = numerator;
long denominatorLong = denominator;
Map<Long,Integer> map = new HashMap<>();
StringBuilder ans = new StringBuilder("");
if ((numeratorLong > 0 ) != (denominatorLong > 0)){
ans.append("-");
numeratorLong = Math.abs(numeratorLong);
denominatorLong = Math.abs(denominatorLong);
}
ans.append(numeratorLong / denominatorLong);
if (numeratorLong % denominatorLong != 0){
ans.append(".");
long dec = (numeratorLong % denominatorLong )* 10;
while (dec != 0 && !map.containsKey(dec)){
map.put(dec,ans.length());
ans.append(dec / denominatorLong);
dec = (dec % denominatorLong) * 10;
}
if (dec != 0){
ans.insert(map.get(dec),"(");
ans.append(")");
}
}
return ans.toString();
}
169. 多数元素
给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
思路
-
Boyer-Moore 投票算法 (摘自官方题解)
- 如果我们把众数记为 +1,把其他数记为 -1,将它们全部加起来,显然和大于
0
,从结果本身我们可以看出众数比其他数多。
- 如果我们把众数记为 +1,把其他数记为 -1,将它们全部加起来,显然和大于
-
如何证明?
-
我想了下,cnt表示众数的净数量(众数数量 减去 其他数的数量)
-
遍历整个数组中,每经过一个元素,cnt要么加1,要么减1,分为两种情况:
-
1、当cnt中记录的ans是众数时,当前值等于众数,cnt++,否则cnt--,这个很好理解吧?
-
2、当cnt中记录的ans不是众数时,分为三种情况:
-
当前值等于众数,cnt--,需要相同数量的众数使得cnt减到0
-
当前值不等于众数且不等于ans,cnt--,等同于众数的净数量减少,没问题吧?
-
当前值不等于众数但等于ans,cnt++,此后遇到相同数量的众数使cnt减到0才行,等同于减少众数的净数量
-
-
-
那么整个过程中,由于众数数量多于 ⌊ n/2 ⌋,所以它的净数量一定大于0,因此最后表示的ans一定是众数
public int majorityElement(int[] nums) {
int cnt = 0,ans = 0;
for(int num : nums){
if (cnt == 0)
ans = num;
cnt += (ans == num ? 1 : -1);
}
return ans;
}
173. 二叉搜索树迭代器
实现一个二叉搜索树迭代器。你将使用二叉搜索树的根节点初始化迭代器。
调用
next()
将返回二叉搜索树中的下一个最小的数。
思路
- 中序遍历便是从小到大遍历,但是每次遍历的时间复杂度为\(O(h)\)(h为高度)
- 可以使用一个栈来替代递归,让时间复杂的变为\(O(1)\)
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class BSTIterator {
private Deque<TreeNode> deque = new LinkedList<>();
public BSTIterator(TreeNode root) {
deque.clear();
if (root != null)
deque.addLast(root);
}
/** @return the next smallest number */
public int next() {
TreeNode treeNode = null;
while ( (treeNode = deque.getLast()).left != null) {
deque.addLast(treeNode.left);
treeNode.left = null;
}
treeNode = deque.pollLast();
if (treeNode.right != null)
deque.addLast(treeNode.right);
return treeNode.val;
}
/** @return whether we have a next smallest number */
public boolean hasNext() {
return !deque.isEmpty();
}
}
/**
* Your BSTIterator object will be instantiated and called as such:
* BSTIterator obj = new BSTIterator(root);
* int param_1 = obj.next();
* boolean param_2 = obj.hasNext();
*/
174. 地下城游戏(Hard)
题目太长,略
思路
- 动态规划
- 一开始的想法是从左上角到右下角遍历,然后选择上边或左边,但是会出现一个问题:你到底是选择当前健康值最大的还是前面途中最小健康值最大的?
- 于是,我们可以转换思维,从右下角到左上角进行遍历
- 以右下角为起点,记录从右下角走到当前格子所需的最小健康值
- 那么,我们只需选择下边或右边中所需健康值最小的那一个即可
class Solution {
private int dp[][];
public int calculateMinimumHP(int[][] dungeon) {
dp = new int[dungeon.length+1][dungeon[0].length+1];
for(int i = dungeon.length - 1;i >= 0;i--){
for(int j = dungeon[0].length - 1;j >= 0;j--){
if (i == dungeon.length - 1 && j == dungeon[0].length - 1){
dp[i][j] = Integer.max(1,1 - dungeon[i][j]);
} else if (i == dungeon.length - 1){
dp[i][j] = Integer.max(1,dp[i][j+1] - dungeon[i][j]);
} else if (j == dungeon[0].length - 1){
dp[i][j] = Integer.max(1,dp[i+1][j] - dungeon[i][j]);
} else{
dp[i][j] = Integer.max(1,Integer.min(dp[i+1][j],dp[i][j+1]) - dungeon[i][j]);
}
}
}
return dp[0][0];
}
}
179. 最大数
给定一组非负整数,重新排列它们的顺序使之组成一个最大的整数。
思路
- 一开始的想法是根据数字的字典序进行排序,但是会出现一个问题
- 例如{123,12311}和{123,12356},字典序都是123小,但是前者组合成12312311更大,后者组合成12356123更大
- 所以,我们可以将两个数合并后再比较字典序
- 坑:多个前置0,例如[0,0,0]的话,就只能保留一个0
public String largestNumber(int[] nums) {
String strNums[] = new String[nums.length];
String ans = "";
for(int i = 0;i < nums.length ;i++){
strNums[i] = String.valueOf(nums[i]);
}
Arrays.sort(strNums,(a,b)->{
return (b+a).compareTo(a+b);
});
for(String strNum : strNums){
if (ans.equals("") && strNum.equals("0"))
continue;
ans += strNum;
}
return ans.equals("") ? "0" : ans;
}
187. 重复的DNA序列
所有 DNA 都由一系列缩写为 A,C,G 和 T 的核苷酸组成,例如:“ACGAATTCCG”。在研究 DNA 时,识别 DNA 中的重复序列有时会对研究非常有帮助。
编写一个函数来查找目标子串,目标子串的长度为 10,且在 DNA 字符串 s 中出现次数超过一次。
思路
- 将长度为10的子串都存到set中
public List<String> findRepeatedDnaSequences(String s) {
Set<String> set = new HashSet<>();
Set<String> ans = new HashSet<>();
for(int i = 0;i <s.length() - 9;i++){
String key = s.substring(i,i+10);
if (!set.contains(key)){
set.add(key);
} else if (!ans.contains(key)){
ans.add(key);
}
}
return new ArrayList<>(ans);
}
- 优化:将key变成整数,这样就只需要存储整数而不是String,优化了空间复杂度
- 由于只有四个字母,所以可以用两个二进制位表示,字串10个,所以只需要20位即可
public List<String> findRepeatedDnaSequences(String s) {
Set<Integer> set = new HashSet<>();
Map<Character,Integer> map = new HashMap<>();
List<String> ans = new ArrayList<>();
int key = 0;
map.put('A',0);
map.put('C',1);
map.put('G',2);
map.put('T',3);
for(int i = 0;i < s.length() ;i++){
key <<= 2;
key |= map.get(s.charAt(i));
key &= 0xfffff;
if (i < 9){
continue;
} else if (!set.contains(key)){
set.add(key);
} else if (!set.contains(key|(1<<21))){//1<<21表示答案是否已经记录过
ans.add(s.substring(i-9,i+1));
set.add(key|(1<<21));
}
}
return ans;
}
188. 买卖股票的最佳时机 IV(Hard)
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
思路
- 可参考第123题,123题是把k固定为2
- 买股票的利润等于max(前一笔卖股票的利润 - 当前股票价格)
- 卖股票的利润等于max(这一笔买股票的利润 + 当前股票价格)
- 坑点:k过大时会导致dp数组过大而内存超限,但是当k >= 天数一半时,就等同于无限次交易,可参考122题,就无需建dp数组了
class Solution {
public int maxProfit(int k, int[] prices) {
if (k >= prices.length >> 1){
int ans = 0;
for(int i = 1;i < prices.length;i++){
ans += prices[i] > prices[i-1] ? prices[i] - prices[i-1] : 0;
}
return ans;
}
int dp[][] = new int[k+1][2];
for(int i = 0;i <= k;i++){
dp[i][0] = Integer.MIN_VALUE;//第i次买股票的利润
dp[i][1] = 0;//第i次卖股票的利润
}
for(int price : prices){
for(int i = 1;i <= k;i++){
dp[i][0] = Integer.max(dp[i][0],dp[i-1][1] - price);
dp[i][1] = Integer.max(dp[i][1],dp[i][0] + price);
}
}
return dp[k][1];
}
}
189. 旋转数组
给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。
思路
- 转换思维,题目就是将后k个元素放到前面,可以采用反转的方法
- 首先将整个数组反转
- 然后前k个反转,后面的其他元素反转即可,这样就等同于将后k个元素放到前面
public void rotate(int[] nums, int k) {
k %= nums.length;
reverse(nums,0,nums.length - 1);
reverse(nums,0,k-1);
reverse(nums,k,nums.length-1);
}
public void reverse(int[] nums,int start,int end){
while (start < end){
int temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
start++;
end--;
}
}
190. 颠倒二进制位(Easy)
颠倒给定的 32 位无符号整数的二进制位。
思路
- 分治,依次逆转16位,8位,4位,2位,1位即可
public int reverseBits(int n) {
//Java中右移需要用>>>,因为如果用>>,则会用符号位的数进行替代,而不是0
n = (n >>> 16) | (n << 16);//先翻转前后16位,无需& 0xffff0000 和 & 0x0000ffff也行
n = ((n & 0xff00ff00) >>> 8) | ((n & 0x00ff00ff) << 8);//再翻转前后8位
n = ((n & 0xf0f0f0f0) >>> 4) | ((n & 0x0f0f0f0f) << 4);//4
n = ((n & 0xcccccccc) >>> 2) | ((n & 0x33333333) << 2);//c表示为1100,3表示为0011
n = ((n & 0xaaaaaaaa) >>> 1) | ((n & 0x55555555) << 1);//a表示为1010,5表示为0101
return n;
}
198. 打家劫舍(Easy)
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
思路
- 水题,每日一题中的题,就顺便敲一下
- 动态规划
int rob(vector<int>& nums) {
int dp[2] = {0,0};
for(int i = 0;i < nums.size();i++)
dp[i&1] = max(dp[i&1]+nums[i],dp[!(i&1)]);
return max(dp[0],dp[1]);
}