236.给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
dfs(后序遍历):
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null || root == p || root == q)
return root;//终止条件
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if(left == null && right == null)
return null; // 1.
if(left == null)
return right; // 3.
if(right == null)
return left; // 4.
return root; // 2. if(left != null and right != null)
}
}
存储父节点:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
//从根节点开始遍历整棵二叉树,用哈希表记录每个节点的父节点指针。从 p 节点开始不断往它的祖先移
//动,并用数据结构记录已经访问过的祖先节点。同样,我们再从 q 节点开始不断往它的祖先移动,如果有
//祖先已经被访问过,即意味着这是 p 和 q 的深度最深的公共祖先,即 LCA 节点。
class Solution {
//用哈希表记录每个节点的父节点指针
Map<Integer, TreeNode> parent = new HashMap<Integer, TreeNode>();
//记录已经访问过的祖先节点
Set<Integer> visited = new HashSet<Integer>();
public void dfs(TreeNode root) {
if (root.left != null) {
parent.put(root.left.val, root);
dfs(root.left);
}
if (root.right != null) {
parent.put(root.right.val, root);
dfs(root.right);
}
}
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
dfs(root);
while (p != null) {
visited.add(p.val);//p节点当前的值添加进去
p = parent.get(p.val);//向上移到父节点指针
}
while (q != null) {
//q节点开始不断往它的祖先移动,如果p祖先节点已经包含,即visited当中有
if (visited.contains(q.val)) {
return q;
}
q = parent.get(q.val);//向上移到父节点指针
}
return null;
}
}
238.给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。
题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。请不要使用除法,且在 O(n) 时间复杂度内完成此题。
左右乘积列表:
class Solution {
public int[] productExceptSelf(int[] nums) {
int length = nums.length;
// L 和 R 分别表示左右两侧的乘积列表
int[] L = new int[length];
int[] R = new int[length];
int[] answer = new int[length];
// L[i] 为索引 i 左侧所有元素的乘积
// 对于索引为 '0' 的元素,因为左侧没有元素,所以 L[0] = 1
L[0] = 1;
for (int i = 1; i < length; i++) {
L[i] = nums[i - 1] * L[i - 1];
}
// R[i] 为索引 i 右侧所有元素的乘积
// 对于索引为 'length-1' 的元素,因为右侧没有元素,所以 R[length-1] = 1
R[length - 1] = 1;
for (int i = length - 2; i >= 0; i--) {
R[i] = nums[i + 1] * R[i + 1];
}
// 对于索引 i,除 nums[i] 之外其余各元素的乘积就是左侧所有元素的乘积乘以右侧所有元素的乘积
for (int i = 0; i < length; i++) {
answer[i] = L[i] * R[i];
}
return answer;
}
}
优化:
class Solution {
public int[] productExceptSelf(int[] nums) {
int length = nums.length;
int[] answer = new int[length];
// answer[i] 表示索引 i 左侧所有元素的乘积
// 因为索引为 '0' 的元素左侧没有元素, 所以 answer[0] = 1
answer[0] = 1;
for (int i = 1; i < length; i++) {
answer[i] = nums[i - 1] * answer[i - 1];
}
// R 为右侧所有元素的乘积
// 刚开始右边没有元素,所以 R = 1
// 相当于刚才的两次遍历合在一起了,并且不再保存R[i],改为常数R
int R = 1;
for (int i = length - 1; i >= 0; i--) {
// 对于索引 i,左边的乘积为 answer[i],右边的乘积为 R
answer[i] = answer[i] * R;
// R 需要包含右边所有的乘积,所以计算下一个结果时需要将当前值乘到 R 上
R *= nums[i];
}
return answer;
}
}
239.给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
双向单调队列:
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums == null || nums.length < 2) return nums;
// 双向队列,保存当前窗口最大值的数组位置,保证队列中数组位置的数值按从大到小排序
LinkedList<Integer> queue = new LinkedList();
// 结果数组
int[] result = new int[nums.length-k+1];
// 遍历nums数组
for(int i = 0;i < nums.length;i++){
// 保证从大到小:如果前面数小则需要依次弹出,直至满足要求
while(!queue.isEmpty() && nums[queue.peekLast()] <= nums[i]){
queue.pollLast();
}
// 添加当前值对应的数组下标
queue.addLast(i);
// 判断当前队列中队首的值是否有效,否则检索并移除此列表的头元素(第一个元素)
if(queue.peek() <= i - k){
queue.poll();
}
// 当窗口长度为k时,及时更新保存当前窗口中最大值
if(i+1 >= k){
result[i+1-k] = nums[queue.peek()];
}
}
return result;
}
}
分块 + 预处理※:
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
int[] prefixMax = new int[n];
int[] suffixMax = new int[n];
for (int i = 0; i < n; ++i) {
if (i % k == 0) {
prefixMax[i] = nums[i];
}
else {
prefixMax[i] = Math.max(prefixMax[i - 1], nums[i]);
}
}
for (int i = n - 1; i >= 0; --i) {
if (i == n - 1 || (i + 1) % k == 0) {
suffixMax[i] = nums[i];
} else {
suffixMax[i] = Math.max(suffixMax[i + 1], nums[i]);
}
}
int[] ans = new int[n - k + 1];
for (int i = 0; i <= n - k; ++i) {
ans[i] = Math.max(suffixMax[i], prefixMax[i + k - 1]);
}
return ans;
}
}
240.编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:
每行的元素从左到右升序排列。
每列的元素从上到下升序排列。
二分查找:
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
if (matrix.length == 0 || matrix[0].length == 0) {
return false;
}
for (int i = 0; i < matrix.length; i++) {
//某一行的第一个元素大于了target ,当前行和后边的所有行都不用考虑了,直接返回 false。
if (matrix[i][0] > target) {
break;
}
//某一行的最后一个元素小于了target ,当前行就不用考虑了,换下一行。
if(matrix[i][matrix[i].length - 1] < target){
continue;
}
int col = binarySearch(matrix[i], target);
if (col != -1) {
return true;
}
}
return false;
}
//二分查找
private int binarySearch(int[] nums, int target) {
int start = 0;
int end = nums.length - 1;
while (start <= end) {
int mid = (start + end) >>> 1;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {//不是==所以为mid+1或mid-1
start = mid + 1;
} else {
end = mid - 1;
}
}
return -1;
}
}
从右上角出发,把 target 和当前值比较:
如果 target 的值大于当前值,那么就向下走。
如果 target 的值小于当前值,那么就向左走。
如果相等的话,直接返回 true 。
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
if (matrix.length == 0 || matrix[0].length == 0) {
return false;
}
//从右上角出发
int row = 0;
int col = matrix[0].length - 1;
while (row <= matrix.length - 1 && col >= 0) {
if (target > matrix[row][col]) {
row++;
} else if (target < matrix[row][col]) {
col--;
} else {
return true;
}
}
return false;
}
}
279.给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
动态规划:
class Solution {
public int numSquares(int n) {
int[] dp = new int[n + 1]; //默认初始化值都为0
for (int i = 1; i <= n; i++) {
dp[i] = i; //最坏的情况就是每次+1
for (int j = 1; i - j * j >= 0; j++) {
//动态转移方程(因为减了一个j*j,所以最少数量要加一)
dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
}
}
return dp[n];
}
}
283.给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
两次遍历:
class Solution {
public void moveZeroes(int[] nums) {
if(nums == null) {
return;
}
//第一次遍历的时候,j指针记录非0的个数,同时将非0的元素统统赋给nums[j]
int j = 0;
for(int i = 0; i < nums.length; ++i) {
if(nums[i] != 0) {
nums[j++] = nums[i];
}
}
//第二次遍历把下标为j及以后的元素都赋为0
for(int i = j; i < nums.length; ++i) {
nums[i] = 0;
}
}
}
一次遍历:
class Solution {
public void moveZeroes(int[] nums) {
if(nums == null) {
return;
}
//两个指针i和j
int j = 0;
for(int i = 0; i < nums.length; i++) {
//快排的思想,当前元素!=0就把其交换到左边,同时把前面的0交换到右边
if(nums[i] != 0) {
//nums[i]和nums[j]交换,i和j同时分别移到下一个位置
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
j++;
}
}
}
}
287.给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。
假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。
快慢指针:
转化为142.环形链表II⭐
class Solution {
public int findDuplicate(int[] nums) {
//首先进行思考,形成链表
//然后利用快慢指针,公式a+b+c+b=2(a+b)->c=a
int slow = 0;
int fast = 0;
slow = nums[slow];
fast = nums[nums[fast]];
while(slow != fast){
slow = nums[slow];
fast = nums[nums[fast]];
}
fast = 0;
while(fast != slow){
fast = nums[fast];
slow = nums[slow];
}
return fast;
}
}
二分查找:
二分法⭐结合抽屉原理
public class Solution {
public int findDuplicate(int[] nums) {
int len = nums.length;
int left = 1;
int right = len - 1;
while (left < right) {
int mid = left + (right - left) / 2;
int cnt = 0;
for (int num : nums) {
if (num <= mid) {
cnt += 1;
}
}
// 根据抽屉原理,小于等于 4 的个数如果严格大于 4 个,此时重复元素一定出现在 [1..4] 区间里
if (cnt > mid) {
// 重复元素位于区间 [left..mid]
right = mid;
} else {
// if分析正确了以后,else 搜索的区间就是if的反面区间 [mid + 1..right]
left = mid + 1;
}
}
return left;
}
}
297.序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。
请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
提示: 输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。
300.给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
动态规划(和279类似):
class Solution {
public int lengthOfLIS(int[] nums) {
if(nums.length == 0)
return 0;
int[] dp = new int[nums.length];
int res = 0;
Arrays.fill(dp, 1);
for(int i = 0; i < nums.length; i++) {
for(int j = 0; j < i; j++) {
if(nums[j] < nums[i])
dp[i] = Math.max(dp[i], dp[j] + 1);
}
res = Math.max(res, dp[i]);
}
return res;
}
}
贪心 + 二分查找:
class Solution {
public int lengthOfLIS(int[] nums) {
int len = 1, n = nums.length;//因为nums.length1>=1所以len最小为1
// 这里完全没有必要
// if (n == 0) {
// return 0;
// }
int[]d = new int[n + 1];
d[len] = nums[0];
for (int i = 1; i < n; i++) {
//如果nums[i]>d[len] ,则直接加入到d数组末尾,并更新len=len+1
if (nums[i] > d[len]) {
d[++len] = nums[i];
} else {
//否则,在d数组中二分查找,找到第一个比nums[i]小的数d[pos] ,并更新d[pos+1]=nums[i]
int l = 1, r = len, pos = 0;
while (l <= r) {
int mid = (l + r) >> 1;
if (d[mid] < nums[i]) {
pos = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
d[pos + 1] = nums[i];
}
}
return len;
}
}
301.给你一个由若干括号和字母组成的字符串 s ,删除最小数量的无效括号,使得输入的字符串有效。
返回所有可能的结果。答案可以按 任意顺序 返回。