目录(序号为leetcode题号)
73. 矩阵置零
给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。
解题思路:遍历数组,如果发现值为0,则将其行列首元素置为零,然后根据行列首元素,更新数组。需要使用一个标记,记录首行元素中是否有零,以免更新时候造成干扰。
public void setZeroes(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
boolean row = false;
// 记录首行元素中是否存在0
for(int i = 0;i < n;i++){
if(matrix[0][i] == 0){
row = true;
break;
}
}
// 遍历数组,将值为0的行列首元素置为0
for(int i = 1;i < m;i++){
for(int j = 0;j < n;j++){
if(matrix[i][j] == 0){
matrix[i][0] = matrix[0][j] = 0;
}
}
}
// 根据数组进行更新,注意不要干扰。
for(int j = n - 1;j >= 0; j--){
for(int i = 1;i < m; i++){
if(matrix[i][0] == 0 ||matrix[0][j] == 0){
matrix[i][j] = 0;
}
}
// 第一行中的元素要根据原本是否存在0而决定更新。
if(row){
matrix[0][j] = 0;
}
}
}
75. 颜色分类
给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
仅使用常数空间的一趟扫描算法
解题思路:与快速排序思路类似,将小于1的值0放在左边,大于1的值2放在右边。一次遍历搞定。
public void sortColors(int[] nums){
int left,right;
int i = 0;
left = 0;
right = nums.length;
while(i < right){
if(nums[i] == 0){
nums[i] = nums[left];
nums[left] = 0;
left++;
i++;
// 小于1的值只能是0,i直接++
}else if(nums[i] == 1){
i++;
}else{
nums[i] = nums[right-1];
nums[right-1] = 2;
right--;
//i值不变,因为小于2的数可能是1也可能是0,为了防止交换后的值为0,需要再次判断
}
}
}
76. 最小覆盖子串
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
注意:
对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
如果 s 中存在这样的子串,我们保证它是唯一的答案。
解题思路:使用滑动窗口,先使得窗口的右边界不断增加,直到子串中包含所有的t中的字符,然后不断使得左边界变小,并记录下最小的滑动窗口值。直到子串中不包含所有的t中字符,然后重复上一步,是得右边界不断增加,直到右边界到达s串的末尾。程序结束。
public String minWindow(String s, String t) {
if(s.length() < t.length()){
return "";
}
// 将s,t串转换为char数组,加速访问字符串中的元素
char[] chars = s.toCharArray();
char[] chart = t.toCharArray();
int slen = s.length();
int tlen = t.length();
// 使用数组来记录t串中各个字符出现的次数。
// 字符的ascll码值就对应下标。
int[] tnums = new int[128];
int[] snums = new int[128];
// nums记录滑动窗口中已包含t串字符的个数。
int nums = 0;
// 初始化数组,记录t串中各个字符出现的次数
for(char c: chart){
tnums[c]++;
}
// minlen记录滑动窗口的最小值,start记录窗口的起始位置
int minlen = slen + 1,start = 0;
// left滑动窗口的左右端点,滑动窗口的区间为 [left,right)
int left = 0,right = 0;
while(right < slen){
if(tnums[chars[right]] == 0){
right++;
continue;
}
if(snums[chars[right]] < tnums[chars[right]]){
nums++;
}
snums[chars[right]]++;
right++;
// 滑动窗口中已包含全部的 t中的字符,对left进行滑动
while(nums == tlen){
//将left向前滑动
if(tnums[chars[left]] == 0){
left++;
continue;
}
if(tnums[chars[left]] == snums[chars[left]]){
nums--;
// 更新最小子串
if(right-left < minlen){
minlen = right - left;
start = left;
}
}
snums[chars[left]]--;
left++;
}
}
// 未找到,返回空。
if(minlen == slen + 1){
return "";
}
// 找到,截取子串,返回。
return s.substring(start,start+minlen);
}
78. 子集
给你一个整数数组 nums ,数组中的元素互不相同 。返回该数组所有可能的子集(幂集)。
解集不能包含重复的子集。你可以按任意顺序返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
解题思路:每次从数组中加入一个新的元素,与前面所有的子集构成新的集合。
例子:数组中有1,2,3,4四个元素,依次从数组中取出新的值,如取出4,然后与已有的所以子集构成新的集合,*表示空集,最左边的数表示,前面已有的集合个数。
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
List<Integer> t,newList;
// 空集加入集合
t = new ArrayList<>();
result.add(t);
// 第一项加入集合
t = new ArrayList<>();
t.add(nums[0]);
result.add(t);
for(int i = 1;i < nums.length; i++){
for(int j = 0;j < Math.pow(2,i); j++){
t = result.get(j);
newList = new ArrayList<>(t);
newList.add(nums[i]);
result.add(newList);
}
}
return result;
}
79. 单词搜索
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
输出:true
解题思路:回溯,使用一个布尔数组,记录访问的情况
public boolean exist(char[][] board, String word) {
int wlen,m,n;
m = board.length;
n = board[0].length;
wlen = word.length();
char[] w = word.toCharArray();
boolean[][] flag =new boolean[board.length][board[0].length];
// 遍历网格,当网格中元素与字符串首字母相同,进行回溯。
for(int i = 0;i < m;i++){
for(int j = 0;j<n;j++){
// 首字母相同,回溯,并判断结果。
if(board[i][j] == w[0]){
flag[i][j] = true;
if(backtrack(board,m,n,i,j,w,wlen,flag,1))
return true;
flag[i][j] = false;
}
}
}
return false;
}
public boolean backtrack(char[][] board,int m,int n,int i, int j,char[] word,int wlen,boolean[][] flag,int num){
if(num == wlen){
return true;
}
// 上下左右四个方向寻找匹配的值,未访问过的,继续回溯。
// 上
if(i - 1>= 0 && word[num] == board[i-1][j] && !flag[i-1][j]){
flag[i-1][j] = true;
if(backtrack(board,m,n,i-1,j,word,wlen,flag,num+1))
return true;
flag[i-1][j] = false;
}
// 下
if(i+1<m && word[num] == board[i+1][j] && !flag[i+1][j]){
flag[i+1][j] = true;
if(backtrack(board,m,n,i+1,j,word,wlen,flag,num+1))
return true;
flag[i+1][j] = false;
}
//左
if(j-1>=0 && word[num] == board[i][j-1] && !flag[i][j-1]){
flag[i][j-1] = true;
if(backtrack(board,m,n,i,j-1,word,wlen,flag,num+1))
return true;
flag[i][j-1] = false;
}
//右
if(j+1<n && word[num] == board[i][j+1] && !flag[i][j+1]){
flag[i][j+1] = true;
if(backtrack(board,m,n,i,j+1,word,wlen,flag,num+1))
return true;
flag[i][j+1] = false;
}
return false;
}
84. 柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
示例:
输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大的矩形为图中红色区域,面积为 10
方法一:暴力解法
解题思路分析:面积为底乘高的值。对于本题中,最终最大面积的矩形的高度必定为数组中某一个元素的值,只需要以每个元素为中心,以该元素的值为高,向两边扩展即可,同时使用变量,记录最大的面积,数组等到全部遍历完成,最大面积就求出。
暴力解法在leetcode中是过不去的。只是为了引出第二种方法。
public int largestRectangleArea(int[] heights) {
int maxArea = 0;
for(int i=0;i<heights.length;i++){
int left = i - 1;
while(left>=0 && heights[left] >= heights[i]){
left--;
}
int right = i + 1;
while(right<heights.length && heights[right] >= heights[i]){
right++;
}
if(maxArea < (right-left-1)*heights[i]){
maxArea = (right-left-1)*heights[i];
}
}
return maxArea;
}
方法二:单调栈
在方法一,运用了暴力解法。
首先我们枚举某一根柱子 i 作为高 h = heights[i];随后我们需要进行向左右两边扩展,使得扩展到的柱子的高度均不小于 hh。
如果有两根柱子 j0和 j1,其中 j0在 j1 的左侧,并且 j0 的高度大于等于 j1,那么在后面的柱子 i 向左找小于其高度的柱子时,j1 会挡住 j0,j0 就不会作为答案了。
如果对数组从左向右进行遍历,同时维护一个「可能作为答案」的数据结构,其中按照从小到大的顺序存放了一些 j 值。根据上面的结论,如果我们存放了 j0, j1, …, js 。那么一定有 height[j0] < height[j1] < … < height[js],因为如果有两个相邻的 j 值对应的高度不满足 < 关系,那么后者会「挡住」前者,前者就不可能作为答案了。
可以使用栈来维护一个这样的数据结构,在程序中保证栈的单调性。
版本一
public int largestRectangleArea(int[] heights) {
int area = 0;
int width,height;
Stack<Integer> stack = new Stack<>();
for(int i = 0;i<heights.length;i++){
// 当前元素小于栈顶元素,栈顶元素出栈
while (!stack.empty() && heights[stack.peek()] > heights[i]){
// 栈中存储的是数组的下标
height = heights[stack.pop()];
// 栈中可能有值相同的元素,出栈
while(!stack.empty() && heights[stack.peek()] == height){
stack.pop();
}
// 栈为空,最后一个元素的宽度为到当前下标
if(stack.empty()){
width = i;
}else {
// 宽度为到当前下标与栈顶元素之间的值 - 1
width = i - stack.peek() - 1;
}
// 更新面积值
if(width*height > area){
area = width*height;
}
}
stack.push(i);
}
// 数组遍历完成之后,栈可能不为空。
while (!stack.empty()){
height = heights[stack.pop()];
while(!stack.empty() && heights[stack.peek()] == height){
stack.pop();
}
// 栈中的最后一个元素为数组的长度,因为是数组中最小的值。
if(stack.empty()){
width = heights.length;
}else {
width = heights.length - stack.peek() - 1;
}
if(width*height > area){
area = width*height;
}
}
return area;
}
版本二:使用哨兵,来使得代码更加简洁。
使用哨兵就不用考虑边界问题。
public int largestRectangleArea(int[] heights) {
//使用哨兵
int maxArea = 0;
int width,height;
int len;
// 哨兵元素为 0
int[] newHeights = new int[heights.length + 2];
for(int i = 0;i<heights.length; i++){
newHeights[i+1] = heights[i];
}
newHeights[heights.length + 1] = 0;
Stack<Integer> stack = new Stack<>();
// 提前将哨兵元素入栈
stack.push(0);
len = newHeights.length;
// 最后的哨兵元素为0,小于所有的元素,遍历完成之后,栈必不为空。
for(int i = 1; i < len; i++){
while(newHeights[stack.peek()] > newHeights[i]){
height = newHeights[stack.pop()];
while(newHeights[stack.peek()] == height){
stack.pop();
}
width = i - stack.peek() - 1;
maxArea = Math.max(maxArea,width*height);
}
stack.push(i);
}
return maxArea;
}
88. 合并两个有序数组
给你两个按非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
示例 1:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3] 和 [2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。
解题思路:双指针法,使用一个中间数组,每次将nums1与nums2较小的值放入中间数组。然后再将中间数组的值赋给nums1。
public void merge(int[] nums1, int m, int[] nums2, int n) {
int[] t = new int[m+n];
int i = 0,j = 0,k=0;
while(i< m && j < n){
if(nums1[i] < nums2[j]){
t[k++] = nums1[i++];
}else{
t[k++] = nums2[j++];
}
}
while(i < m){
t[k++] = nums1[i++];
}
while(j < n){
t[k++] = nums2[j++];
}
for(int l = 0; l<t.length;l++){
nums1[l] = t[l];
}
}
91. 解码方法
一条包含字母 A-Z 的消息通过以下映射进行了 编码 :
'A' -> 1
'B' -> 2
...
'Z' -> 26
要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,“11106” 可以映射为:
“AAJF” ,将消息分组为 (1 1 10 6)
“KJF” ,将消息分组为 (11 10 6)
注意,消息不能分组为 (1 11 06) ,因为 “06” 不能映射为 “F” ,这是由于 “6” 和 “06” 在映射中并不等价。
给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。
题目数据保证答案肯定是一个 32 位 的整数。
方法一:回溯
解题思路:回溯,字符的编码可能是一位,也可能是两位,将所有的可能遍历出来。
虽然思路正确,但是在leetcode中超时。
public int numDecodings(String s) {
// A-Z 1-26
int[] nums = new int[s.length()];
int[] num = new int[1];
for(int k = 0;k < nums.length; k++){
nums[k] = s.charAt(k)-'0';
}
backtrack(nums,nums.length,0,num);
return num[0];
}
public void backtrack(int[] s,int slen,int i,int[] num){
if(i == slen){
num[0]++;
return;
}
if(1 <= s[i] && s[i] <= 26){
backtrack(s,slen,i+1,num);
}
if(i+1 < slen && s[i] != 0 && 1 <= (s[i] * 10 + s[i+1]) && (s[i] * 10 + s[i+1]) <= 26){
backtrack(s,slen,i+2,num);
}
}
方法二:动态规划
考虑字符串中的第 i 个字符的状态,可以将第 i 个字符单独编码,也可以将 第 i 个字符与 i -1个字符组合起来编码(前提 在合法的字符之间 )。
对于 i 个字符的编码方式就是将单独编码和合起来编码两种情况加起来。
用 dp[i] 表示 0-i个字符的编码方式。
状态转移方程为:
动态规划的边界条件为:
dp[0] = 1
即空字符串可以有1种解码方法,解码出一个空字符串。
public int numDecodings(String s) {
if(s.charAt(0)-'0' == 0)
return 0;
int[] dp = new int[s.length() + 1];
// 设置边界为1
dp[0] = 1;
// 第一个字符不为0,编码方式为1
dp[1] = 1;
for(int i = 1; i<s.length(); i++){
if(s.charAt(i) != '0'){
// 数字0不能编码排除,1-9可以编码
dp[i+1] += dp[i];
}
if( s.charAt(i-1) != '0' && (s.charAt(i-1)-'0')*10 + (s.charAt(i)-'0') <=26){
// 第i个字符和第i-1个字符,可以一起编码
dp[i+1] += dp[i-1];
}
}
return dp[s.length()];
}
94. 二叉树的中序遍历
给定一个二叉树的根节点 root ,返回它的 中序 遍历。
示例:
输入:root = [1,null,2,3]
输出:[1,3,2]
方法一:递归
解题思路:树的中序遍历,访问顺序为,左儿子,父亲节点,右儿子,用递归函数进行中序遍历。
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if(root == null){
return result;
}
recursion(root,result);
return result;
}
public void recursion(TreeNode root,List<Integer> result){
if(root == null){
return;
}
//左儿子,父亲节点,右儿子
recursion(root.left,result);
result.add(root.val);
recursion(root.right,result);
}
方法二:栈
递归的时候,程序给我们维护了一个栈,非递归方式,需要自己维护栈。
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
while(root != null || !stack.empty()){
while(root != null){
stack.push(root);
root = root.left;
}
root = stack.pop();
result.add(root.val);
root = root.right;
}
return result;
}
98. 验证二叉搜索树
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
节点的左子树只包含 小于 当前节点的数。
节点的右子树只包含 大于 当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
示例:
输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4 。
方法一:递归
解题思路:递归遍历进行判断。对于任意的子树,必须都满足于二叉搜索树。对于左节点,不仅要小于父亲节点,还有大于一个下界。因为左节点的父亲节点,可能是祖父节点的右节点。
如下,4不仅仅要小于6,还要大于2。
同样,对于右节点,不仅仅要大于父亲节点,还有小于一个上界,因为右节点的父亲节点,可能是祖父节点的左节点。
如下:3不仅仅要大于2,还要小于5。
public boolean isValidBST(TreeNode root) {
// 对于根节点上下界为空
return recursion(root,null,null);
}
public boolean recursion(TreeNode root,Integer low,Integer up){
if(root == null){
return true;
}
// 当前节点值必须大于下界,小于上界,才能满足二叉搜索树
if(low != null && low >= root.val){
return false;
}
if(up!=null && up <= root.val){
return false;
}
// 对于左子树,上界就是根节点。
if(!recursion(root.left,low,root.val)){
return false;
}
// 对于右子树,下界就是根节点。
if(!recursion(root.right,root.val,up)){
return false;
}
return true;
}
方法二:中序遍历
解题思路:由于中序遍历的顺序为 左 根 右,对于二叉搜索树而已,前一个元素的值必定小于当前元素。只需要在中序遍历是,根据这个条件判断即可。
public boolean isValidBST(TreeNode root) {
//中序遍历 左 根 右
Stack<TreeNode> stack = new Stack<>();
Integer val = null;
while(root != null || !stack.empty()){
while(root != null){
stack.push(root);
// 先到最左边。
root = root.left;
}
// 根
root = stack.pop();
// 判断是否大于前一个元素。
if(val != null && root.val <= val){
return false;
}
val = root.val;
// 右
root = root.right;
}
return true;
}