文章目录
- 32、最长有效括号(栈)
- 33、搜索旋转排序数组(二分查找)
- 34、在排序数组中查找元素的第一个和最后一个位置(二分查找)
- 35、搜索插入位置(二分查找)
- 36、有效的数独(HashSet)
- 37、解数独(递归回溯算法)
- 39、组合总和I(递归回溯)
- 40、组合总数II(递归回溯)
- 42、接雨水(双指针!)
- 43、字符串相乘(双指针)
- 44、 通配符匹配(双指针!)
- 55、跳跃游戏I(贪心算法)
- 45、跳跃游戏II(贪心算法)
- 46、全排列I(递归回溯算法)
- 47、全排列II(递归回溯算法)
- 48、旋转图像(二维数组)
- 49、字母异位词分组(hash)
- 50、Pow(x, n)(分治算法)
- 53、最长子序和(动态规划!)
- 54、螺旋矩阵(二维数组)
- 55、跳跃游戏(贪心算法)
- 56、合并区间(重要!!)
- 57、插入区间(重要!!)
- 58、最后一个单词的长度
- 59、螺旋矩阵II(二维数组)
32、最长有效括号(栈)
**题目:**给定一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长的包含有效括号的子串的长度。
示例 1:
输入: "(()"
输出: 2
解释: 最长有效括号子串为 "()"
示例 2:
输入: ")()())"
输出: 4
解释: 最长有效括号子串为 "()()"
代码:
/**
time:O(n)
space:O(n)
*/
public class Solution {
public int longestValidParentheses(String s) {
Stack<Integer> stack = new Stack<>();
int res = 0;
int start = -1;
for(int i=0;i<s.length();i++){
//如果为左括号,将索引下标放入栈内存中
if(s.charAt(i)=='('){
stack.push(i);
}else{
//如果栈为空说明栈中没有'('
if(stack.isEmpty()){
//记录有效括号的初始下标
start = i;
}else{
//如果栈不为空,说明栈中有'(',将左括号弹出
stack.pop();
//如果弹出之后栈为空,说明栈中已经没有'('
if(stack.isEmpty()){
//计算有效括号的长度
res = Math.max(res,i-start);
}else{
//如果弹出后栈中还有左括号,计算有效括号的长度
res = Math.max(res,i-stack.peek());
}
}
}
}
return res;
}
}
33、搜索旋转排序数组(二分查找)
**题目:**假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
你可以假设数组中不存在重复的元素。你的算法时间复杂度必须是 O(log n) 级别。
示例 1:
输入: nums = [4,5,6,7,0,1,2], target = 0
输出: 4
示例 2:
输入: nums = [4,5,6,7,0,1,2], target = 3
输出: -1
代码:
/**
time:O(logn)
space:O(1)
*/
public class Solution {
public int search(int[] nums, int target) {
//判断边界条件
if(nums.length==0 || nums==null){
return -1;
}
//使用二分查找法
int start = 0;
int end = nums.length-1;
while (start+1<end){
int mid = (end-start)/2+start;
if(nums[mid]==target) return mid;
//如果前半段为递增的
if(nums[start]<=nums[mid]){
//判断target的位置
if(nums[start]<=target && target<=nums[mid]){
end = mid;
//如果上述条件不满足
}else{
start = mid;
}
//如果后半段为递增的
}else{
//判断target的位置
if(nums[mid]<=target && target<=nums[end]){
start=mid;
//如果上述条件不满足
}else{
end=mid;
}
}
}
if(nums[start]==target) return start;
if(nums[end]==target) return end;
return -1;
}
}
34、在排序数组中查找元素的第一个和最后一个位置(二分查找)
**题目:**给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。你的算法时间复杂度必须是 O(log n) 级别。如果数组中不存在目标值,返回 [-1, -1]。
示例 1:
输入: nums = [5,7,7,8,8,10], target = 8
输出: [3,4]
示例 2:
输入: nums = [5,7,7,8,8,10], target = 6
输出: [-1,-1]
代码:
/**
* time:O(n)
* space:O(1)
*/
public class Solution {
public int[] searchRange(int[] nums, int target) {
//二分查找算法
if(nums.length==0 || nums==null) return new int[]{-1,-1};
//找出初始索引
int start = findFirst(nums, target);
if(start==-1) return new int[]{-1,-1};
//找出最后索引
int end = findLast(nums, target);
return new int[]{start,end};
}
private int findFirst(int[] nums, int target) {
//二分查找
int start = 0;
int end = nums.length-1;
while (start+1<end){
//二分的mid
int mid = (end-start)/2+start;
//如果target在mid的右边
if(target>nums[mid]){
start = mid;
}else{
end = mid;
}
}
//先返回start,再返回end
if(nums[start]==target) return start;
if(nums[end]==target) return end;
return -1;
}
private int findLast(int[] nums, int target) {
int start = 0;
int end = nums.length-1;
while (start+1<end){
int mid = (end-start)/2+start;
//如果target在nums的左边
if(target<nums[mid]){
end = mid;
}else{
start = mid;
}
}
//先返回end,再返回start
if(nums[end]==target) return end;
if(nums[start]==target) return start;
return -1;
}
}
35、搜索插入位置(二分查找)
**题目:**给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。你可以假设数组中无重复元素。
示例 1:
输入: [1,3,5,6], 5
输出: 2
示例 2:
输入: [1,3,5,6], 2
输出: 1
示例 3:
输入: [1,3,5,6], 7
输出: 4
示例 4:
输入: [1,3,5,6], 0
输出: 0
代码:
/**
* time:O(logn)
* space:O(1)
*/
public class Solution {
public int searchInsert(int[] nums, int target) {
//二分查找
int start = 0;
int end = nums.length-1;
while (start+1<end){
int mid = (end-start)/2+start;
if(nums[mid]==target) return mid;
else if(target<nums[mid]) end = mid;
else start = mid;
}
//如果数组中不存在target,就判断target和nums[start]与nums[end]的大小,进而插入到相应位置
if(target<=nums[start]){//1、target<=nums[start]
return start;
}else if(target<=nums[end]){//2、nums[start]<target<=nums[end]
return end;
}else { //3、target>nums[end]
return end+1;
}
}
}
36、有效的数独(HashSet)
**题目:**判断一个 9x9 的数独是否有效。只需要根据以下规则,验证已经填入的数字是否有效即可。
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
上图是一个部分填充的有效的数独。
数独部分空格内已填入了数字,空白格用 ‘.’ 表示。
示例 1:
输入:
[
["5","3",".",".","7",".",".",".","."],
["6",".",".","1","9","5",".",".","."],
[".","9","8",".",".",".",".","6","."],
["8",".",".",".","6",".",".",".","3"],
["4",".",".","8",".","3",".",".","1"],
["7",".",".",".","2",".",".",".","6"],
[".","6",".",".",".",".","2","8","."],
[".",".",".","4","1","9",".",".","5"],
[".",".",".",".","8",".",".","7","9"]
]
输出: true
示例 2:
输入:
[
["8","3",".",".","7",".",".",".","."],
["6",".",".","1","9","5",".",".","."],
[".","9","8",".",".",".",".","6","."],
["8",".",".",".","6",".",".",".","3"],
["4",".",".","8",".","3",".",".","1"],
["7",".",".",".","2",".",".",".","6"],
[".","6",".",".",".",".","2","8","."],
[".",".",".","4","1","9",".",".","5"],
[".",".",".",".","8",".",".","7","9"]
]
输出: false
解释: 除了第一行的第一个数字从 5 改为 8 以外,空格内其他数字均与 示例1 相同。
但由于位于左上角的 3x3 宫内有两个 8 存在, 因此这个数独是无效的。
说明:
一个有效的数独(部分已被填充)不一定是可解的。
只需要根据以上规则,验证已经填入的数字是否有效即可。
给定数独序列只包含数字 1-9 和字符 '.' 。
给定数独永远是 9x9 形式的。
思路:
把这个数独当成一个二维数组来遍历,利用HashSet取出重复元素
如何取出每三宫格的重复元素?顺序是从左到右进行每一个三宫格的去重:
第一个3宫格: 第二个3宫格: 第三个3宫格:
(0,0) (0,1) (0,2) (0,3) (0,4) (0,5) (0,6) (0,7) (0,8)
(1,0) (1,1) (1,2) (1,3) (1,4) (1,5) (1,6) (1,7) (1,8)
(2,0) (2,1) (2,2) (2,3) (2,4) (2,5) (2,6) (2,7) (2,8)
代码:
/**
* time:o(n^2)
* space:O(n)
*/
class Solution {
public boolean isValidSudoku(char[][] board) {
//遍历这个二维数组
for(int i=0;i<board.length;i++){
//去除每一行的重复数据
HashSet<Character> rows = new HashSet<>();
//去除每一列的重复数据
HashSet<Character> cols = new HashSet<>();
//去除每个三宫格的重复数据
HashSet<Character> cub = new HashSet<>();
for(int j=0;j<board[0].length;j++){
//去除每一行的重复数据
if(board[i][j]!='.' && !rows.add(board[i][j])) return false;
//去除每一列的重复数据
if(board[j][i]!='.' && !cols.add(board[j][i])) return false;
//从左到右,去除每个三宫格的重复数据
int rowIndex = (i/3)*3;
int colIndex = (i%3)*3;
if(board[rowIndex+j/3][colIndex+j%3]!='.' && !cub.add(board[rowIndex+j/3][colIndex+j%3])) return false;
}
}
return true;
}
}
37、解数独(递归回溯算法)
**题目:**编写一个程序,通过已填充的空格来解决数独问题。
一个数独的解法需遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
空白格用 ‘.’ 表示。
答案被标成红色。Note:
给定的数独序列只包含数字 1-9 和字符 '.' 。
你可以假设给定的数独只有唯一解。
给定数独永远是 9x9 形式的。
思路:
递归回溯算法:深度优先搜索,在每个空格内填入数据,使其满足数独的条件
代码:
public class Solution {
public void solveSudoku(char[][] board) {
if(board.length==0 || board==null) return;
solve(board);
}
public boolean solve(char[][] board){
//深度优先搜索,递归回溯算法
for(int i=0;i<board.length;i++){
for(int j=0;j<board[0].length;j++){
//说明这个位置需要填入一个数字
if(board[i][j]=='.'){
//填入的数字可能为1-9中的一个
for(char c='1';c<='9';c++){
//该位置插入c后是否满足数独的条件,c=1-9
if(isValid(board,i,j,c)){
board[i][j] = c;
//插入成功后,继续深度搜索
//判断下一个位置能否插入成功,如果成功返回true
if(solve(board)) return true;
//如果失败,返回上一层,变为初始值
else board[i][j] = '.';
}
}
//这一层循环完毕后,1-9都不能插入,返回false
return false;
}
}
}
return true;
}
private boolean isValid(char[][] board, int row, int col, char c) {
for (int i=0;i<9;i++){
//判断每一行有没有和要插入的c重复的
if(board[row][i] != '.' && board[row][i]==c) return false;
//判断每一列有没有和要插入的c重复的
if(board[i][col] != '.' && board[i][col]==c) return false;
/**
* 假如说(3,4):row=3,col=4
* (3,3)、 (3,4)、 (3,5)
* (4,3)、 (4,4)、 (4,5)
* (5,3)、 (5,4)、 (5,5)
*/
if(board[(row/3)*3+i/3][(col/3)*3+i%3] !='.' && board[(row/3)*3+i/3][(col/3)*3+i%3]==c) return false;
}
return true;
}
}
39、组合总和I(递归回溯)
**题目:**给定一个无重复元素的数组 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]
]
代码:
/**
* time:O(2^n)
* space:O(n)
*/
public class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
if(candidates.length==0 || candidates==null) return res;
//先对candidates进行排序,便于剪枝
Arrays.sort(candidates);
ArrayList<Integer> list = new ArrayList<>();
dfs(res,candidates,list,target,0);
return res;
}
/**
* @param res 要返回的外层的大的数组
* @param candidates 要遍历的数组
* @param list 大的数组中的每个小的数组
* @param target 目标值
* @param start 初始值,用来去除已经遍历过的重复元素
*/
private void dfs(List<List<Integer>> res, int[] candidates, ArrayList<Integer> list,int target, int start) {
//递归的终止条件,剪枝
if(target<0) return;
if(target==0){
//说明满足条件,将小的数组加入到大的数组中
res.add(new ArrayList<>(list));
//剪枝,结束当前循环
return;
}
//如果后面的数比target要大,就不需要遍历了
for(int i=start;i<candidates.length && target>=candidates[i];i++){
//将当前数加入到数组中
list.add(candidates[i]);
//继续进行深度优先遍历
dfs(res,candidates,list,target-candidates[i],i);
//递归回溯到上一层之后,要消除上一层新加入的数的影响,加入该层新遍历的数,继续深度优先搜索
list.remove(list.size()-1);
}
}
}
40、组合总数II(递归回溯)
**题目:**给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
[1,2,2],
[5]
]
**思路:**本题和上一题基本相同,唯一不同的地方在于candidates 中的每个数字在每个组合中只能使用一次,通知需要去除重复组合
代码:
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
if(candidates.length==0||candidates==null) return res;
List<Integer> list = new ArrayList<>();
Arrays.sort(candidates);
dfs(res,list,candidates,target,0);
return res;
}
private void dfs(List<List<Integer>> res, List<Integer> list, int[] candidates, int target, int start) {
if(target<0) return;
if(target==0){
res.add(new ArrayList<>(list));
return;
}
for(int i=start;i<candidates.length && target>=candidates[i];i++){
//去重
if(i!=start && candidates[i]==candidates[i-1]) continue;
list.add(candidates[i]);
//candidates 中的每个数字在每个组合中只能使用一次。
dfs(res,list,candidates,target-candidates[i],i+1);
list.remove(list.size()-1);
}
}
}
42、接雨水(双指针!)
**题目:**给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例:
输入: [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6
思路:
-
首先我们需要搞清楚,下标为
i
的雨水量是由什么决定的.
是由i
左右两边最大值中较小的那个减去height[i]
决定的.例[0,1,0,2,1,0,1,3,2,1,2,1]
中,下标为2
的位置 值为0,而它的用水量是由左边的最大值1
,右边最大值3
中较小的那个 也就是1减去0得到的。 -
本题解的双指针先找到当前维护的左、右最大值中较小的那个,例当前
i
处左边的最大值如果比右边的小,那么就可以不用考虑i
处右边最大值的影响了,因为i
处 右边真正的最大值绝对比左边的最大值要大,在不断遍历时,更新left_max
和right_max
以及返回值即可。例[0,1,0,2,1,0,1,3,2,1,2,1]
中i=2
时,值为0,此时left_max
一定为1,当前right_max
如果为2,即便right_max
不是真正的i右边的最大值,也可忽略右边最大值的影响,因为右边真正的最大值一定比左边真正的最大值大。
代码:
public class Soluiton {
public int trap(int[] height) {
//使用双指针解法
int left = 0;
int right = height.length-1;
//这两个指针用来维护当前列左边序列和当前列右边序列中值较小的那个位置
int left_max = 0;
int right_max = 0;
int res = 0;
while(left<=right){
//如果当前序列左边的最大值比右边最大值小
if(left_max<right_max){
//以左边最大值为准
if(height[left]<left_max) {
res += left_max-height[left];
} else {
left_max = height[left];
}
left++;
}else{
//以右边最大值为准
if(height[right]<right_max) {
res += right_max-height[right];
} else {
right_max = height[right];
}
right--;
}
}
return res;
}
}
43、字符串相乘(双指针)
**题目:**给定两个以字符串形式表示的非负整数 num1
和 num2
,返回 num1
和 num2
的乘积,它们的乘积也表示为字符串形式。
示例 1:
输入: num1 = "2", num2 = "3"
输出: "6"
示例 2:
输入: num1 = "123", num2 = "456"
输出: "56088"
思路:
该算法是通过两数相乘时,乘数某位与被乘数某位相乘,与产生结果的位置的规律来完成。具体规律如下:
乘数 num1 位数为 M,被乘数 num2 位数为 N, num1 x num2 结果 res 最大总位数为 M+N
num1[i] x num2[j] 的结果为 tmp(位数为两位,"0x","xy"的形式),其第一位位于 res[i+j],第二位位于 res[i+j+1]。
当index1=1,index0=0
时,就是2*4=08
,因此08位于indices[i+j,i+j+8]=indices[1+0,1+0+1]=indices[1,2]
位置。
i=2;j=1
res[i+j] = res[3]=0
res[i+j+1] = res[4]=0
sum = res[i+j+1]+num1[i]*num2[j]=15
res[i+j+1] = res[4]=15%10=5
res[i+j] = res[i+j]+res[3]=0+15/10=1
i=1;j=1
res[i+j] = res[2]=0
res[i+j+1] = res[3]=1
sum = res[i+j+1]+num1[i]*num2[j]=1+10=11
res[i+j+1] = res[3]=11%10=1
res[i+j] = res[2]+res[2]=0+11/10=1
i=0;j=1
res[i+j] = res[1]=0
res[i+j+1] = res[2]=1
sum = res[i+j+1]+num1[i]*num2[j]=1+5=6
res[i+j+1] = res[2]=6%10=6
res[i+j] =res[i+j]+ res[1]=0+6/10=0
经过第一轮计算结果为0610,后面的依次类推。
代码:
public class Solution {
public String multiply(String num1, String num2) {
//排除345*0和0*345的情况
if(num1.equals("0") || num2.equals("0")) return "0";
//最后结果的总位数就是两个字符串长度相加的结果
int[] res = new int[num1.length()+num2.length()];
//倒叙遍历这两个字符串,进行相乘
for(int i=num1.length()-1; i>=0; i--){
//将这个字符转成实际的数
int n1 = num1.charAt(i)-'0';
for(int j=num2.length()-1; j>=0; j--){
int n2 = num2.charAt(j)-'0';
int sum = res[i+j+1]+n1*n2;
res[i+j+1] = sum%10;
res[i+j] += sum/10;
}
}
//最后将计算的结果使用StringBuilder拼接起来
StringBuilder stringBuilder = new StringBuilder();
for(int i=0;i<res.length;i++){
if(i==0 && res[i]==0) continue;
stringBuilder.append(res[i]);
}
return stringBuilder.toString();
}
}
44、 通配符匹配(双指针!)
**题目:**给定一个字符串 (s) 和一个字符模式 § ,实现一个支持 ‘?’ 和 ‘*’ 的通配符匹配。
‘?’ 可以匹配任何单个字符。
‘*’ 可以匹配任意字符串(包括空字符串)。
两个字符串完全匹配才算匹配成功。
说明:
s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 *。
示例 1:
输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。
示例 2:
输入:
s = "aa"
p = "*"
输出: true
解释: '*' 可以匹配任意字符串。
示例 3:
输入:
s = "cb"
p = "?a"
输出: false
解释: '?' 可以匹配 'c', 但第二个 'a' 无法匹配 'b'。
示例 4:
输入:
s = "adceb"
p = "*a*b"
输出: true
解释: 第一个 '*' 可以匹配空字符串, 第二个 '*' 可以匹配字符串 "dce".
示例 5:
输入:
s = "acdcb"
p = "a*c?b"
输出: false
思路:
本题和正则表达式匹配很相似,这题可以使用动态规划忽或者双指针,上题正则表达式匹配值使用的是动态规划,使用双指针比较好记和理解。
time:O(m*n)
space:O(1)
代码:
class Solution {
public boolean isMatch(String s, String p) {
//定义两个指针来遍历s,p
int sPositon = 0;
int pPositon = 0;
//定义指针记录p中*的初始位置pPosition
int star = -1;
//定义指针记录当p中为*时,对应的sPosition的位置
int match = 0;
while (sPositon<s.length()){
//情况1:sPosition对应的字符和pPosition对应的字符正好相同,或者pPosition对应的字符为?
if(pPositon<p.length() && (p.charAt(pPositon)==s.charAt(sPositon) ||p.charAt(pPositon)=='?')){
sPositon++;
pPositon++;
//情况2:pPosition对应的字符为*,需要更新star和match
}else if(pPositon<p.length() && p.charAt(pPositon)=='*'){
//更新*对应的初始位置
star = pPositon;
//更新*字符对应的s中的字符
match = sPositon;
pPositon++;
}else if(star!=-1){
//让pPosition处于star后面的一个位置,让sPotion不断的向后移动,比较sPotion和pPositon会不会相等
pPositon=star+1;
match++;
sPositon=match;
}else{
return false;
}
}
//情况3:p很可能还没走到最后的位置
while (pPositon<p.length() ){
//如果没走完的位置不是*,就无法匹配
if((p.charAt(pPositon)!='*')) return false;
pPositon++;
}
return true;
}
}
55、跳跃游戏I(贪心算法)
题目:给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个位置。
示例 1:
输入: [2,3,1,1,4]
输出: true
解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。
示例 2:
输入: [3,2,1,0,4]
输出: false
解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。
思路:贪心
我们可以用贪心的方法解决这个问题。
设想一下,对于数组中的任意一个位置y
,我们如何判断它是否可以到达?根据题目的描述,只要存在一个位置 x
,它本身可以到达,并且它跳跃的最大长度为 x+nums[x]
,这个值大于等于 y
,即 x+nums[x]≥y
,那么位置 y
也可以到达。
换句话说,对于每一个可以到达的位置x
,它使得x+1,x+2,⋯ ,x+nums[x]
这些连续的位置都可以到达。
这样以来,我们依次遍历数组中的每一个位置,并实时维护 最远可以到达的位置。对于当前遍历到的位置 x
,如果它在 最远可以到达的位置 的范围内,那么我们就可以从起点通过若干次跳跃到达该位置,因此我们可以用 x+nums[x]
更新最远可以到达的位置。
在遍历的过程中,如果 最远可以到达的位置 大于等于数组中的最后一个位置,那就说明最后一个位置可达,我们就可以直接返回 True 作为答案。反之,如果在遍历结束后,最后一个位置仍然不可达,我们就返回 False 作为答案。
以题目中的示例一
[2, 3, 1, 1, 4]
为例:
我们一开始在位置 0,可以跳跃的最大长度为 2,因此最远可以到达的位置被更新为 2;
我们遍历到位置 1,由于 1≤2,因此位置 1 可达。我们用 1 加上它可以跳跃的最大长度 3 ,将最远可以到达的位置更新为 4 。由于 4 大于等于最后一个位置 4,因此我们直接返回 True。
我们再来看看题目中的示例二
[3, 2, 1, 0, 5]
我们一开始在位置 000,可以跳跃的最大长度为 333,因此最远可以到达的位置被更新为 333;
我们遍历到位置 111,由于 1≤3 ,因此位置 1 可达,加上它可以跳跃的最大长度 2 得到 3 ,没有超过最远可以到达的位置;
位置 2 、位置 3 同理,最远可以到达的位置不会被更新;
我们遍历到位置 4 ,由于 4>3 ,因此位置 4 不可达,我们也就不考虑它可以跳跃的最大长度了。
在遍历完成之后,位置 4 仍然不可达,因此我们返回 False。
代码:
/**
time:O(n)
space:O(1)
*/
public class Solution {
public boolean canJump(int[] nums) {
if(nums==null) return false;
//最大可以跳的步数
int max = 0;
for(int i=0;i<nums.length;i++){
if(i>max) return false;
max = Math.max(nums[i]+i,max);
}
return true;
}
}
45、跳跃游戏II(贪心算法)
**题目:**给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
示例:
输入: [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
代码:
class Solution {
public int jump(int[] nums) {
int step = 0;
//每一步可以跳跃的数的边界
int end = 0;
//最大可以跳跃的位置
int maxPosition = 0;
for(int i=0;i<nums.length-1;i++){
//确定最大可以跳跃的位置
maxPosition = Math.max(maxPosition,nums[i]+i);
if(i==end){
end = maxPosition;
step++;
}
}
return step;
}
}
46、全排列I(递归回溯算法)
**题目:**给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
思路:
https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-xiang-jie-by-labuladong-2/
1、路径:也就是已经做出的选择。
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件。
代码方面,回溯算法的框架:
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」
for 选择 in 选择列表:
# 做选择
将该选择从选择列表移除
路径.add(选择)
backtrack(路径, 选择列表)
# 撤销选择
路径.remove(选择)
将该选择再加入选择列表
代码:
/**
* time:O(n!)
* space:O(n)
*/
class Solution {
//定义一个list存放最终的结果
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
if(nums.length==0 || nums==null){
return null;
}
//定义一个list存放选择路径
List<Integer> pathList = new ArrayList<>();
//深度优先遍历
dfs(nums,pathList);
return res;
}
private void dfs(int[] nums, List<Integer> pathList) {
//如果pathList的长度和nums长度相同,就说找到了一个结果
if(nums.length==pathList.size()){
res.add(new ArrayList<>(pathList));
return;
}
for(int i=0;i<nums.length;i++){
//排除不合法的选择
if(pathList.contains(nums[i])){
continue;
}
//选择
pathList.add(nums[i]);
//进行下一层树的结果的搜索
dfs(nums,pathList);
//撤销上一次的选择
pathList.remove(pathList.size()-1);
}
}
}
47、全排列II(递归回溯算法)
**题目:**给定一个可包含重复数字的序列,返回所有不重复的全排列。
示例:
输入: [1,1,2]
输出:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
思路:
路径:也就是已经做出的选择。
选择列表:也就是你当前可以做的选择。
结束条件:也就是到达决策树底层,无法再做选择的条件。
我们所使用的框架基本就是:
LinkedList result = new LinkedList();
public void backtrack(路径,选择列表){
if(满足结束条件){
result.add(结果);
}
for(选择:选择列表){
做出选择;
backtrack(路径,选择列表);
撤销选择;
}
}
其中最关键的点就是:在递归之前做选择,在递归之后撤销选择。
由于本题需要返回所有不重复的全排列,有限制条件,所以需要进行剪枝。这里第一步先要给数组进行排序。
首先,先要给nums进行排序,这样的做目的是方便剪枝
其次,我们已经选择过的不需要再放进去了
接下来,如果当前节点与他的前一个节点一样,并其他的前一个节点已经被遍历过了,那我们也就不需要了。
代码:
public class Solution {
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
if(nums.length==0 || nums==null) return null;
//对数组进行排序
Arrays.sort(nums);
List<Integer> pathList = new ArrayList<>();
dfs(pathList,nums,new boolean[nums.length]);
return res;
}
private void dfs(List<Integer> pathList, int[] nums, boolean[] used) {
//说明找到符合条件的结果
if(pathList.size()==nums.length){
res.add(new ArrayList<>(pathList));
return;
}
for(int i=0;i<nums.length;i++){
//已经放进路径中的数就不要再放进路径中了
if(used[i]){
continue;
}
//如果当前节点与他的前一个节点一样,并其他的前一个节点已经被遍历过了,那我们也就不需要了。
if(i>0 && nums[i]==nums[i-1] && used[i-1]){
break;
}
//选择
pathList.add(nums[i]);
used[i] = true;
//继续下一层的遍历
dfs(pathList, nums, used);
//撤销选择
pathList.remove(pathList.size()-1);
used[i] = false;
}
}
}
48、旋转图像(二维数组)
**题目:**给定一个 n × n 的二维矩阵表示一个图像。将图像顺时针旋转 90 度。
说明:你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。
示例 1:
给定 matrix =
[
[1,2,3],
[4,5,6],
[7,8,9]
],
原地旋转输入矩阵,使其变为:
[
[7,4,1],
[8,5,2],
[9,6,3]
]
示例 2:
给定 matrix =
[
[ 5, 1, 9,11],
[ 2, 4, 8,10],
[13, 3, 6, 7],
[15,14,12,16]
],
原地旋转输入矩阵,使其变为:
[
[15,13, 2, 5],
[14, 3, 4, 1],
[12, 6, 8, 9],
[16, 7,10,11]
]
思路:
给定 matrix =
[
[1,2,3],
[4,5,6],
[7,8,9]
],
先按照对角线交换位置:
[
[1,4,7],
[2,5,8],
[3,6,9]
]
再按照中间的数轴交换位置:
[
[7,4,1],
[8,5,2],
[9,6,3]
]
代码:
public class Solution {
public void rotate(int[][] matrix) {
//经过两次对折即可
//第一次:沿着右对角线对折
for(int i=0;i<matrix.length;i++){
for(int j=i;j<matrix[0].length;j++){
int tmp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = tmp;
}
}
//第二次,沿着矩阵的垂直线对折
for(int i=0;i<matrix.length;i++){
for(int j=0;j<matrix[0].length/2;j++){
int temp = matrix[i][j];
matrix[i][j] = matrix[i][matrix.length-1-j];
matrix[i][matrix.length-1-j] = temp;
}
}
}
}
49、字母异位词分组(hash)
**题目:**给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串。
示例:
输入: ["eat", "tea", "tan", "ate", "nat", "bat"]
输出:
[
["ate","eat","tea"],
["nat","tan"],
["bat"]
]
说明:
所有输入均为小写字母。
不考虑答案输出的顺序。
代码:
public class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
//用于存放结果
List<List<String>> res = new ArrayList<>();
if(strs.length==0 || strs==null) return null;
//键用于存放排序后的字符串,值用于存放该字符串在res的位置
HashMap<String,Integer> map = new HashMap<>();
for(int i=0;i<strs.length;i++){
//将字符串转换为数组,使用Array工具类对字符串按照字典顺序进行排序
char[] chars = strs[i].toCharArray();
Arrays.sort(chars);
//重新转换为字符串
String s = String.valueOf(chars);
//如果map中没有包含s
if(!map.containsKey(s)){
ArrayList<String> list = new ArrayList<>();
list.add(strs[i]);
map.put(s,res.size());
res.add(list);
}else {
//map.get(s)代表获取res中存放s的数组索引
//res.get(0)通过索引获取res中存放的list
List<String> list = res.get(map.get(s));
//将strs[i]存放到list中
list.add(strs[i]);
}
}
return res;
}
}
50、Pow(x, n)(分治算法)
**题目:**实现 pow(x, n) ,即计算 x 的 n 次幂函数。
示例 1:
输入: 2.00000, 10
输出: 1024.00000
示例 2:
输入: 2.10000, 3
输出: 9.26100
示例 3:
输入: 2.00000, -2
输出: 0.25000
解释: 2-2 = 1/22 = 1/4 = 0.25
说明:
-100.0 < x < 100.0
n 是 32 位有符号整数,其数值范围是 [−231, 231 − 1] 。
思路:
代码:
class Solution {
public double myPow(double x, int n) {
if(n>0){
return pow(x,n);
}else{
return 1/pow(x,n);
}
}
private double pow(double x, int n) {
if(n==0) return 1;
double y = pow(x,n/2);
//如果n为偶数:2^2 = 2^1 * 2^1=(2^0 *2^0 *2) * (2^0 *2^0 *2)=4
if(n%2==0){
return y*y;
//如果n为奇数:2^3 = 2^1 * 2^1 * 2=(2^0 * 2^0 *2) * (2^0 *2^0 *2)*2=8
}else {
return y*y*x;
}
}
}
53、最长子序和(动态规划!)
**题目:**给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
**思路:**动态规划解法
此处的状态转移方程也可以切换为 dp[i] = Math.max(nums[i], nums[i] + dp[i - 1]);
因为是需要最大和的连续子数组,因此可以保存每个数组元素所在位置能够得到的最大连续元素和,这个最大值可以通过判断前一个最大值是否为负数来判断(因为负数无论加任何数都比当前数小),如果为负,当前元素的最大和就是其本身。
//写法1:
public class Solution {
public int maxSubArray(int[] nums) {
if(nums.length==0||nums==null) return 0;
//创建dp表
int[] dp = new int[nums.length];
dp[0] = nums[0];
int res = dp[0];
//遍历数组
for(int i=1;i<nums.length;i++){
dp[i] = Math.max(dp[i-1]+nums[i],nums[i]);
res = Math.max(dp[i],res);
}
return res;
}
}
//写法2:
public class Solution {
public int maxSubArray(int[] nums) {
if(nums.length==0||nums==null) return 0;
//创建dp表
int[] dp = new int[nums.length];
dp[0] = nums[0];
int res = dp[0];
//遍历数组
for(int i=1;i<nums.length;i++){
dp[i] = dp[i-1]>0 ? dp[i-1]+nums[i]:nums[i];
res = Math.max(dp[i],res);
}
return res;
}
}
54、螺旋矩阵(二维数组)
**题目:**给定一个包含 m x n 个元素的矩阵(m 行, n 列),请按照顺时针螺旋顺序,返回矩阵中的所有元素。
示例 1:
输入:
[
[ 1, 2, 3 ],
[ 4, 5, 6 ],
[ 7, 8, 9 ]
]
输出: [1,2,3,6,9,8,7,4,5]
示例 2:
输入:
[
[1, 2, 3, 4],
[5, 6, 7, 8],
[9,10,11,12]
]
输出: [1,2,3,4,8,12,11,10,9,5,6,7]
思路:
//time:O(m*n)
//space:O(m*n)
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> list = new ArrayList<>();
if(matrix==null || matrix.length==0 || matrix[0].length==0 || matrix[0]==null){
return list;
}
int rowBegin = 0;
int rowEnd = matrix[0].length-1;
int colBegin = 0;
int colEnd = matrix.length-1;
while (true){
for(int i=rowBegin;i<=rowEnd;i++) {
//将最上面一行加入list
list.add(matrix[colBegin][i]);
}
//加入后将该行去掉
colBegin++;
if(colBegin>colEnd) break;
for(int i=colBegin;i<=colEnd;i++){
//将最右面一行加入list
list.add(matrix[i][rowEnd]);
}
//加入后将该列去掉
rowEnd--;
if(rowEnd<rowBegin) break;
for(int i=rowEnd;i>=rowBegin;i--){
//将最下面一行加入list
list.add(matrix[colEnd][i]);
}
//加入后将该行去掉
colEnd--;
if(colEnd<colBegin) break;
for(int i=colEnd;i>=colBegin;i--){
//将最左面一行加入list
list.add(matrix[i][rowBegin]);
}
//加入后将该列去掉
rowBegin++;
if(rowBegin>rowEnd) break;
}
return list;
}
}
55、跳跃游戏(贪心算法)
题目:给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个位置。
示例 1:
输入: [2,3,1,1,4]
输出: true
解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。
示例 2:
输入: [3,2,1,0,4]
输出: false
解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。
代码:
public class Solution {
public boolean canJump(int[] nums) {
if(nums==null) return false;
//最大可以跳的步数
int max = 0;
for(int i=0;i<nums.length;i++){
if(i>max) return false;
max = Math.max(nums[i]+i,max);
}
return true;
}
}
56、合并区间(重要!!)
**题目:**给出一个区间的集合,请合并所有重叠的区间。
示例 1:
输入: [[1,3],[2,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]
解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入: [[1,4],[4,5]]
输出: [[1,5]]
解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。
思路:
需要对所有的区间按照左端点升序排序,然后遍历。
如果当前遍历到的区间的左端点 > 结果集中最后一个区间的右端点,说明它们没有交集,把区间添加到结果集;
|----| |-----|
1 4 1 3
|-----| |-----|
3 6 5 7
如果当前遍历到的区间的左端点 <= 结果集中最后一个区间的右端点,说明它们有交集,此时产生合并操作,即:对结果集中最后一个区间的右端点更新(取两个区间的最大值)。
在具体的算法描述中:
前提:区间按照左端点排序;
贪心策略:在右端点的选择中,如果产生交集,总是将右端点的数值更新成为最大的,这样就可以合并更多的区间,这种做法是符合题意的。
代码:
class Solution {
public int[][] merge(int[][] intervals) {
if(intervals==null || intervals.length==0) return intervals;
//对数组的头进行升序排序
Arrays.sort(intervals, new Comparator<int[]>() {
/**
* 当前对象>比较对象,则返回1;当这样是升序排序的。
* 当前对象>比较对象,则返回-1;这样是降序排序的
*/
@Override
public int compare(int[] o1, int[] o2) {
return o1[0]-o2[0];
}
});
//使用list存放合并区间后的结果
List<int[]> res = new ArrayList<>();
res.add(intervals[0]);
//遍历二维数组
for(int i=0;i<intervals.length;i++){
int[] currentInterval = intervals[i];
int[] peek = res.get(res.size()-1);
//如果当前遍历到的区间的左端点 > 结果集中最后一个区间的右端点,说明它们没有交集,把区间添加到结果集;
if(currentInterval[0]>peek[1]){
res.add(currentInterval);
}else {
//如果当前遍历到的区间的左端点 <= 结果集中最后一个区间的右端点,说明它们有交集,此时产生合并操作
//即:对结果集中最后一个区间的右端点更新(取两个区间的最大值)。
peek[1] = Math.max(currentInterval[1],peek[1]);
}
}
//将链表转化为二维数组的方式
return res.toArray(new int[res.size()][]);
}
}
57、插入区间(重要!!)
给出一个*无重叠的 ,*按照区间起始端点排序的区间列表。
在列表中插入一个新的区间,你需要确保列表中的区间仍然有序且不重叠(如果有必要的话,可以合并区间)。
示例 1:
输入: intervals = [[1,3],[6,9]], newInterval = [2,5]
输出: [[1,5],[6,9]]
示例 2:
输入: intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]
输出: [[1,2],[3,10],[12,16]]
解释: 这是因为新的区间 [4,8] 与 [3,5],[6,7],[8,10] 重叠。
|--| |---| |---| |---| |---|
1 2 3 5 6 7 8 10 12 16
|----------|
4 8
代码:
public class Solution {
public int[][] insert(int[][] intervals, int[] newInterval) {
List<int[]> res = new ArrayList<>();
/**
* |--| |---| |---| |---| |---|
* 1 2 3 5 6 7 8 10 12 16
* |----------|
* 4 8
*/
int i = 0;
//对于左边,如果当前区间的右端点<插入区间的左端点,直接插入结果集中
while (i < intervals.length && intervals[i][1] < newInterval[0]) {
res.add(intervals[i]);
i++;
}
int[] temp = new int[]{newInterval[0],newInterval[1]};
//对于中间,如果当前区间的左端点小于插入区间的右端点,就更新插入区间
while (i < intervals.length && intervals[i][0] <= newInterval[1]) {
temp[0] = Math.min(intervals[i][0], temp[0]);
temp[1] = Math.max(intervals[i][1], temp[1]);
i++;
}
res.add(temp);
//对于右边(当前区间的左端点>插入区间的右端点)
while (i < intervals.length) {
res.add(intervals[i]);
i++;
}
//将链表转换为二维数组
return res.toArray(new int[res.size()][]);
}
}
58、最后一个单词的长度
给定一个仅包含大小写字母和空格 ' '
的字符串 s
,返回其最后一个单词的长度。如果字符串从左向右滚动显示,那么最后一个单词就是最后出现的单词。如果不存在最后一个单词,请返回 0 。
**说明:**一个单词是指仅由字母组成、不包含任何空格字符的 最大子字符串。
示例:
输入: "Hello World"
输出: 5
代码:
class Solution {
public int lengthOfLastWord(String s) {
/***
* abc def
* 0123456
*
* 7-3-1=3
*
* lastIndexOf()返回指定字符在此字符串中最后一次出现处的索引。
*/
return s.trim().length()-s.trim().lastIndexOf(" ")-1;
}
}
59、螺旋矩阵II(二维数组)
**题目:**给定一个正整数 n,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。
示例:
输入: 3
输出:
[
[ 1, 2, 3 ],
[ 8, 9, 4 ],
[ 7, 6, 5 ]
]
代码:
public class Solution {
public int[][] generateMatrix(int n) {
//和原来的螺旋矩阵解法相同,不同的是那题是遍历二维数组,这题是给二维数组赋值
int[][] martix = new int[n][n];
int rowBegin = 0;
int rowEnd = n-1;
int colBegin = 0;
int colEnd = n-1;
int num = 1;
while (true){
for(int i=colBegin;i<=colEnd;i++){
martix[rowBegin][i] = num++;
}
rowBegin++;
if(rowBegin>rowEnd) break;
for(int i=rowBegin;i<=rowEnd;i++){
martix[i][colEnd] = num++;
}
colEnd--;
if(colEnd<colBegin) break;
for(int i=colEnd;i>=colBegin;i--){
martix[rowEnd][i] = num++;
}
rowEnd--;
if(rowEnd<rowBegin) break;
for(int i=rowEnd;i>=rowBegin;i--){
martix[i][colBegin] = num++;
}
colBegin++;
if(colBegin>colEnd) break;
}
return martix;
}
}