数组&字符串
9 回文数
26 从排序数组中删除重复项
27 移除元素
#80 从排序数组中删除重复项II
#旋转数组
存在重复元素
136 只出现一次的数字 位运算
137 只出现一次的数字II
260 只出现一次的数字III 多个出现一次
两个数组的交集 II
加一
移动零
有效的数独
#旋转图像
反转字符串
#反转整数
#字符串中的第一个唯一字符
有效的字母异位词
验证回文字符串
字符串转整数 (atoi)
实现strStr()
数数并说
#最长公共前缀
#1 两数之和
#15 三数之和 双指针
#16 最接近的三数之和 双指针
#18 四数之和 双指针+双循环
#11盛最多水的容器 双指针
#31 下一个排列 双指针
88 合并两个有序数组 逆向 双指针
#四数相加
#矩阵置零
#字谜分组
滑动窗口
643 子数组最大平均数 滑动窗口
#3 无重复字符的最长子串 滑动窗口
#209 长度最小的子数组 滑动窗口
#1695 删除子数组的最大得分 滑动窗口
#438 找到字符串中所有字母异位词
#567 字符串的排列
#1004 最大连续1的个数III
#1208 尽可能使字符串相等
#1423 可获得的最大点数
459 重复的子字符串
#5 最长回文子串 中心扩散
贪心
409 最长回文串 贪心,构建
605 种花问题 贪心
455 分发饼干 贪心
#179 最大数 贪心
lambda表达式
#134 加油站 贪心
#406 根据身高重建队列 贪心 排序
#1996 游戏中的弱角色 贪心 排序
#字符串转换整数
#334 递增的三元子序列
##寻找两个有序数组的中位数
##42 接雨水 双指针
链表
#237 删除链表中的节点 复制值+跳过
203 移除链表元素
83 删除链表重复数字的节点 双指针
#82 删除排序链表中的重复元素II 双指针
#19 删除链表的倒数第N个节点 双指针
#206 反转链表
#92 反转链表II 部分反转+拼接
21 合并两个有序链表
#147 链表插入排序
#148 排序链表 快慢指针 + 归并排序
#234 回文链表
#141 环形链表 双指针追及
#142 环形链表II
两数相加
#328 奇偶链表
#160 相交链表
#重排链表
#分隔链表
#对链表进行插入排序
树&图
递归法
三种遍历二叉树——递归
三种遍历二叉树——非递归
144 二叉树的前序遍历-根左右
#94 二叉树的中序遍历-左根右
#145 二叉树的后序遍历
104 二叉树的最大深度
100 相同的树
55 平衡二叉树
#98 验证二叉搜索树
52 展平二叉搜索树 剑指II
101 对称二叉树
226 二叉树的镜像 剑指27 lc
#102 二叉树的层次遍历 BFS——自顶向下
BFS:
107 二叉树的层序遍历2 ——自下向上
108 将有序数组转换为二叉搜索树
#先序遍历构造二叉树
重建二叉树 剑指7 根据前序和中序构建二叉树
根据中序和后序构建二叉树 106
#103 二叉树的锯齿形层次遍历 BFS
#199 二叉树的右视图 BFS
#515 在每个树行中找最大值 BFS
637 二叉树的层平均值 BFS
814 二叉树剪枝
每个节点的右向指针
二叉搜索树中第K小的元素
岛屿的个数
课程表
排序和搜索
快速排序
#33 搜索旋转排序数组 二分
#34 在排序数组中查找元素的第一个和最后一个位置
35 搜索插入位置
#第一个错误的版本
#找出一个无序数组的中位数
颜色分类
#前K个高频元素
#数组中的第K个最大元素
#寻找峰值
合并区间
搜索二维矩阵 II
跳跃游戏
动态规划
70 爬楼梯
121 买卖股票的最佳时机
动态规划
优化dp空间
53 最大子序和
#55 跳跃游戏 倒推!!
动态规划
思路2:贪心 逆序倒推
贪心 正序推
#45 跳跃游戏II 贪心
#122 买卖股票的最佳时机II 二维dp
方法1:动态规划
方法2:贪心
#123 买卖股票的最佳时机III
方法1:三维dp
方法2:节省空间
188 最多交易K次
#309 最佳买卖股票时机含冷冻期 二维/三维
背包问题
01背包
完全背包
多重背包
#198 打家劫舍
#213 打家劫舍II
#337 打家劫舍III 树+dfs+dp
不同路径
#322 零钱兑换
常规动态规划-完全背包思路
#518 零钱兑换2
思路1:类似组合总和39题,dfs 超时
思路2:dp 类似70题爬楼梯
#983 最低票价 动态规划
最长上升子序列
设计问题
Shuffle an Array
#最小栈
#二叉树的序列化与反序列化
#常数时间插入、删除和获取随机元素
数学
Fizz Buzz
计数质数
#3的幂
#罗马数字转整数
快乐数
#阶乘后的零
Excel表列序号
其他
#位1的个数
汉明距离
颠倒二进制位
杨辉三角
20 有效的括号
缺失数字
#朋友圈
1447最简分数
回溯算法
题型一:排列、组合、子集相关问题46 47 39 40 77 78 90 60 93
#46 全排列 dfs 回溯+used数组
#47 全排列II dfs
#39 组合总和 回溯+剪枝+begin
#40 组合总和II 回溯+剪枝+begin
#77 组合 回溯+begin
#78 子集
#90 子集II
题型二:Flood Fill 200 130 79 733
题型三:字符串中的回溯问题 17 22 784
#17 电话号码
#784 字母大小写全排列
#22 括号生成 回溯+剪枝
题型四:游戏问题 51 37 488 529[困难]
DFS
排列组合问题见回溯
二叉树的DFS:
112 路径总和
#113路径总和II 回溯
二叉树的BFS:
网格的DFS
#200 岛屿数量
#695 岛屿最大面积
##827 最大人工岛
463 岛屿的周长 1.数学方法 2.dfs
BFS:
层序遍历树
最短路径
网格结构的层序遍历 BFS:
#1162 离开陆地的最远距离
#542 01矩阵
#994 腐烂的橘子
#310 最小高度树
#子集
单词搜索
极小化极大
1.nim游戏
数组&字符串
9 回文数
public boolean isPalindrome(int x) {
if(x < 0) return false;
String str = Integer.toString(x);
char[] charArr = str.toCharArray();
for(int i = 0; i < charArr.length/2; i++){
if(charArr[i] != charArr[charArr.length-1-i]){
return false;
}
}
return true;
}
方法2:
public boolean isPalindrome(int x) {
if(x<0) return false;
if(x % 10 == 0 && x != 0) return false; //末位是0,不可能是回文数
int halfReserve = 0;
while((x - halfReserve) > 0){
halfReserve = halfReserve*10 + x%10;
x = x/10;
}
System.out.println(halfReserve+" "+ x);
if(x == halfReserve) return true;
if(x == halfReserve/10) return true;//121
return false;
}
26 从排序数组中删除重复项
给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
很简单,直接上代码:
class Solution {
public int removeDuplicates(int[] nums) {
int index=0;
for(int i =1 ; i < nums.length ; i++){
if ( nums[i] != nums[index]){
index ++;
nums[index] = nums[i];
}
}
return ++index;
}
}
27 移除元素
原地移除所有=val的值,返回新数组的长度[3,2,2,3] 3 --> [2,2]
public int removeElement(int[] nums, int val) {
if(nums == null || nums.length == 0) return 0;
int index = 0;
for(int i = 0; i < nums.length; i++){
if(nums[i] != val){
nums[index] = nums[i];
index++;
}
}
return index;
}
#80 从排序数组中删除重复项II
/*
public class DeleteRepeatedItemsII {
public static void main(String[] args) {
DeleteRepeatedItemsII d = new DeleteRepeatedItemsII();
int[] nums = {0,0,1,1,1,1,2,3,3,4};
int length = d.removeDuplicates(nums);
for(int i = 0; i < length; i++) {
System.out.println(nums[i]);
}
}
public int removeDuplicates(int[] nums) {
if(nums.length == 0 || nums == null) return 0;
int i = 1;
for(int j = 2; j < nums.length; j++) {
if(nums[j] != nums[i-1]) {
i += 1;
nums[i] = nums[j];
}
}
return i+1;
}
}
#旋转数组
给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。
先将数组的前len-k个元素反转,后k个元素反转,然后再将整个数组反转。注意先让k对len取模。
class Solution {
public void rotate(int[] nums, int k) {
int len = nums.length;
k = k%len;
if(len<2||k==0)return;
swap(nums, 0, len-k-1);
swap(nums, len-k, len-1);
swap(nums, 0, len-1);
}
public void swap(int[] nums, int left, int right){
while(left
nums[left] = nums[right];
nums[right] = temp;
left++;
right–;
}
}
}
存在重复元素
给定一个整数数组,判断是否存在重复元素。
如果任何值在数组中出现至少两次,函数返回 true。如果数组中每个元素都不相同,则返回 false。
方案一:利用HashSet。
class Solution {
public boolean containsDuplicate(int[] nums) {
int len = nums.length;
if(len<2)return false;
HashSet hashSet = new HashSet<>();
for(int num:nums)
if(!hashSet.add(num))return true;
return false;
}
}
方案二:利用Arrays.sort()方法先对数组进行排序,然后判断。
class Solution {
public boolean containsDuplicate(int[] nums) {
int len = nums.length;
if(len<2)return false;
Arrays.sort(nums);
for(int i=0;i+1
}
return false;
}
}
136 只出现一次的数字 位运算
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
异或,出现偶数次的元素会被清零,于是留下来的就是那个只出现一次的数字。
public int singleNumber(int[] nums) {
int res = 0;
for(int i = 0; i < nums.length; i++){
res = res ^ nums[i];
}
return res;
}
137 只出现一次的数字II
给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。
方法1:map
执行用时:5 ms, 在所有 Java 提交中击败了30.98%的用户
内存消耗:41 MB, 在所有 Java 提交中击败了18.70%的用户
public int singleNumberII(int[] nums) {
Map
int res = 0;
for(int i = 0; i < nums.length; i++){
if(!map.containsKey(nums[i])){
map.put(nums[i],1);
}else{
Integer v = map.get(nums[i]);
map.put(nums[i],v+1);//对于已有的key,新的value会覆盖之前的
}
}
//用迭代器遍历map
Set entrySet = map.entrySet();
Iterator it = entrySet.iterator();
while(it.hasNext()){
Map.Entry entry = it.next();
if(entry.getValue().equals(1)){
res = (Integer)entry.getKey();
}
}
return res;
}
方法2:位运算
260 只出现一次的数字III 多个出现一次
给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。
public int singleNumberIII(int[] nums) {
Map
List reslist = new ArrayList();
for(int i = 0; i < nums.length; i++){
if(!map.containsKey(nums[i])){
map.put(nums[i],1);
}else{
Integer v = map.get(nums[i]);
map.put(nums[i],v+1);
}
}
Set entrySet = map.entrySet();
Iterator it = entrySet.iterator();
while(it.hasNext()){
Map.Entry entry = it.next();
if(entry.getValue().equals(1)){
reslist.add((Integer)entry.getKey());
}
}
int[] resarr = new int[reslist.size()];
for(int i = 0 ; i < reslist.size(); i++){
resarr[i] = reslist.get(i);
}
return resarr;
}
位运算:
两个数组的交集 II
给定两个数组,写一个方法来计算它们的交集。
先排序,再比较。
class Solution {
public int[] intersect(int[] nums1, int[] nums2) {
int len1 = nums1.length;
int len2 = nums2.length;
if(len10||len20)return new int[0];
Arrays.sort(nums1);
Arrays.sort(nums2);
List list = new ArrayList<>();
int i=0;
int j=0;
while(i
list.add(nums1[i]);
i++;
j++;
continue;
}
else if(nums1[i]>nums2[j])j++;
else i++;
}
int[] res = new int[list.size()];
for(int k=0;k
}
}
加一
给定一个非负整数组成的非空数组,在该数的基础上加一,返回一个新的数组。
最高位数字存放在数组的首位, 数组中每个元素只存储一个数字。
你可以假设除了整数 0 之外,这个整数不会以零开头。
注意进位的处理。
class Solution {
public int[] plusOne(int[] digits) {
int len = digits.length;
if(len==0)return new int[0];
int[] res = new int[len+1];
res[len] = (digits[len-1]+1)%10;
int carry = (digits[len-1]+1)/10;
for(int i=len-2;i>=0;i–){
res[i+1] = (digits[i]+carry)%10;
carry = (digits[i]+carry)/10;
}
res[0] = carry;
if(carry!=0)return res;
int[] res1 = new int[len];
System.arraycopy(res, 1, res1, 0, len);
return res1;
}
}
移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
class Solution {
public void moveZeroes(int[] nums) {
int len = nums.length;
if(len<2)return;
int index = 0;
for(int i=0;i
}
for(int i=index;i
}
有效的数独
判断一个 9x9 的数独是否有效。只需要根据以下规则,验证已经填入的数字是否有效即可。
使用一个二维数组seat保存所有数字的位置,遍历输入的数独,遇见数字则先判断该位置是否与seat中相同数字的位置冲突,若冲突则返回false,否则将当前数字的位置保存至seat中。
class Solution {
public boolean isValidSudoku(char[][] board) {
boolean[][] seat=new boolean[9][27];
for(int i=0;i<9;i++){
for(int j=0;j<9;j++){
if(board[i][j]==’.’)continue;
int num=board[i][j]-‘0’;
int grid=(i/3)*3+(j/3);
if(seat[num-1][i]||seat[num-1][j+9]||seat[num-1][grid+18])return false;
else{
seat[num-1][i]=true;
seat[num-1][j+9]=true;
seat[num-1][grid+18]=true;
}
}
}
return true;
}
}
#旋转图像
给定一个 n × n 的二维矩阵表示一个图像。
将图像顺时针旋转 90 度。
你必须在原地旋转图像。
先按主对角线翻转,再按垂直对称轴翻转。
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
for(int i=0;i
for(int i=0;i
public void swap(int[][] matrix, int i, int j){
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
public void swap(int [][] matrix, int i){
int left = 0;
int right = matrix[i].length-1;
while(left
matrix[i][left] = matrix[i][right];
matrix[i][right] = temp;
left++;
right–;
}
}
}
反转字符串
编写一个函数,其作用是将输入的字符串反转过来。
注意不要想得太复杂了就好。
class Solution {
public String reverseString(String s) {
int len = s.length();
if(len<2)return s;
char[] chars = s.toCharArray();
int left = 0;
int right = len-1;
while(left
chars[left] = chars[right];
chars[right] = temp;
left++;
right–;
}
return new String(chars);
}
}
#反转整数
给定一个 32 位有符号整数,将整数中的数字进行反转。
假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−231, 231 − 1]。根据这个假设,如果反转后的整数溢出,则返回 0。
不能使用long!!!
class Solution {
public int reverse(int x) {
int res = 0;
while (x != 0){
int temp = res;
res = res * 10 + x % 10;
if (res/10 != temp) return 0;//校验溢出
x /= 10;
}
return res;
}
}
#字符串中的第一个唯一字符
给定一个字符串,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回-1。
方案一:利用HashMap存下每个字符和其索引的键值对,若该字符重复出现则将HashMap中的key为该字符的value设为-1,最后遍历HashMap中所有的value,找到最小值。
class Solution {
public int firstUniqChar(String s) {
int len = s.length();
if(len0)return -1;
HashMap
for(int i=0;i
if(map.containsKey(curr))map.put(curr,-1);
else map.put(curr,i);
}
int index = Integer.MAX_VALUE;
Iterator it = map.values().iterator();
while(it.hasNext()){
int i = (int)it.next();
if(i!=-1)index = Math.min(index,i);
}
return index
}
}
方案二:使用一个256大小的数组来存放每个字符出现的次数,然后再遍历字符串中的每个字符,如果该字符出现的次数为1则返回。
class Solution {
public int firstUniqChar(String s) {
int len = s.length();
if(len==0)return -1;
int[] charTable = new int[256];
for(int i=0;i
charTable[index]++;
}
for(int i=0;i
if(charTable[index]==1)return i;
}
return -1;
}
}
有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的一个字母异位词。
如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?
class Solution {
public boolean isAnagram(String s, String t) {
int lens = s.length();
int lent = t.length();
if(lens!=lent)return false;
HashMap
for(int i=0;i
if(map.containsKey(curr))map.put(curr, map.get(curr)+1);
else map.put(curr, 1);
}
for(int i=0;i
if(!map.containsKey(curr)||map.get(curr)==0)return false;
map.put(curr, map.get(curr)-1);
}
Iterator it = map.values().iterator();
while(it.hasNext()){
if((int)it.next()!=0)return false;
}
return true;
}
}
验证回文字符串
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
说明:本题中,我们将空字符串定义为有效的回文串。
注意小写字符比大写字符大32!
class Solution {
public boolean isPalindrome(String s) {
int len = s.length();
if(len<2)return true;
int left = 0;
int right = len-1;
while(left
right–;
}
return true;
}
public Character letter(char c){
if(c>=‘a’&&c<=‘z’||c>=‘0’&&c<=‘9’)return c;
else if(c>=‘A’&&c<=‘Z’)return (char)(c+32);
else return null;
}
}
字符串转整数 (atoi)
实现 atoi,将字符串转为整数。
在找到第一个非空字符之前,需要移除掉字符串中的空格字符。如果第一个非空字符是正号或负号,选取该符号,并将其与后面尽可能多的连续的数字组合起来,这部分字符即为整数的值。如果第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。
字符串可以在形成整数的字符后面包括多余的字符,这些字符可以被忽略,它们对于函数没有影响。
当字符串中的第一个非空字符序列不是个有效的整数;或字符串为空;或字符串仅包含空白字符时,则不进行转换。
若函数不能执行有效的转换,返回 0。
假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−231, 231 − 1]。如果数值超过可表示的范围,则返回 INT_MAX (231 − 1) 或 INT_MIN (−231) 。
public class Solution {
public int myAtoi(String str) {
str = str.trim();
if (str.isEmpty()) return 0;
int sign = 1, base = 0, i = 0, n = str.length();
if (str.charAt(i) == ‘+’ || str.charAt(i) == ‘-’) {
sign = (str.charAt(i++) == ‘+’) ? 1 : -1;
}
while (i < n && str.charAt(i) >= ‘0’ && str.charAt(i) <= ‘9’) {
if (base > Integer.MAX_VALUE / 10 || (base == Integer.MAX_VALUE / 10 && str.charAt(i) - ‘0’ > 7)) {
return (sign == 1) ? Integer.MAX_VALUE : Integer.MIN_VALUE;
}
base = 10 * base + (str.charAt(i++) - ‘0’);
}
return base * sign;
}
}
实现strStr()
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回-1。如果needle是空字符串,则返回0 。
class Solution {
public int strStr(String haystack, String needle) {
int len1 = haystack.length();
int len2 = needle.length();
if(len2==0)return 0;
if(len1
int j = 0;
while(i
j++;
i++;
}else{
i = i-j+1;
j = 0;
}
}
if(jlen2)return i-j;
else return -1;
}
}
数数并说
报数序列是指一个整数序列,按照其中的整数的顺序进行报数,得到下一个数。其前五项如下:
给定一个正整数 n ,输出报数序列的第 n 项。
注意:整数顺序将表示为一个字符串
class Solution {
public String countAndSay(int n) {
String countSequence=“1”;
if(n==1)return countSequence;
for(int i=2;i<=n;i++){
StringBuffer sb=new StringBuffer();
for(int j=0;j
int num=0;
while(j
j++;
}
sb.append(String.valueOf(num));
sb.append(current);
}
countSequence=sb.toString();
}
return countSequence;
}
}
#最长公共前缀
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 “”。
使用startsWith()方法判断一个字符串是不是另一个字符串的前缀。
class Solution {
public String longestCommonPrefix(String[] strs) {
int count=strs.length;
String prefix="";
if(count!=0){
prefix=strs[0];
}
for(int i=0;i
prefix=prefix.substring(0,prefix.length()-1);
}
}
return prefix;
}
}
#1 两数之和
给定一个整数数组和一个目标值,找出数组中和为目标值的两个数,返回它们的坐标。
你可以假设每个输入只对应一种答案,且同样的元素不能被重复利用。
用HashMap!!!
class Solution {
public int[] twoSum(int[] nums, int target) {
int len = nums.length;
if(len<2)return null;
HashMap
for(int i=0;i
return new int[]{i, map.get(target-nums[i])};
}
map.put(nums[i], i);
}
return null;
}
}
#15 三数之和 双指针
给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
先对数组进行排序,然后遍历数组中小于等于0的部分,每次将当前元素设为三数中的其中一个,然后对数组剩余的部分运用两数之和的算法。
public List threeSum(int[] nums) {
List res = new ArrayList
();
if(nums == null || nums.length <= 2) return res;
Arrays.sort(nums);
for(int i = 0; i < nums.length; i++){
if(nums[i] > 0) break;
if(i > 0 && nums[i] == nums[i-1]) continue; //去重
int target = -nums[i];
int left = i+1;
int right = nums.length-1;
while(left < right){
if(nums[left] + nums[right] == target){
List sublist = new ArrayList();
sublist = Arrays.asList(nums[i],nums[left],nums[right]);
res.add(sublist);
left++;
right–;
while (nums[left] == nums[left-1] && left < right) left++;
while (nums[right] == nums[right+1] && left < right) right–;
}else if(nums[left] + nums[right] < target){
left++;
}else{
right–;
}
}
}
return res;
}
class Solution {
public List threeSum(int[] nums) {
List res = new ArrayList<>();
int len = nums.length;
if(len<3)return res;
Arrays.sort(nums);
int i = 0;
while(i
int left = i+1;
int right = len-1;
while(left
res.add(Arrays.asList(nums[i], nums[left], nums[right]));
int l = nums[left];
while(left
while(left
else left++;
}
int temp = nums[i];
while(i
return res;
}
}
#16 最接近的三数之和 双指针
三数之和最接近target,返回三数之和。假定存在一个解
public int threeSumClosest(int[] nums, int target) {
Arrays.sort(nums);
int res = nums[0]+nums[1]+nums[2];
for(int i = 0; i < nums.length-2; i++){
int left = i+1;
int right = nums.length-1;
while (left < right){
int sum = nums[i]+nums[left]+nums[right];
int dev = Math.abs(sum-target);
if(Math.abs(res-target) > dev){
res = sum;
}
if(sum < target){
left++;
}else{
right--;
}
}
}
return res;
}
#18 四数之和 双指针+双循环
/*
给定一个包含 n 个整数的数组 nums 和一个目标值 target,
判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?
找出所有满足条件且不重复的四元组。
给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。
满足要求的四元组集合为:
[
[-1, 0, 0, 1],
[-2, -1, 1, 2],
[-2, 0, 0, 2]
]
使用双循环固定两个数,用双指针找另外两个数,通过比较与target 的大小(3数之和),移动指针。
??如何跳过重复值
方法一:使用set:不能存重复值
方法二:当有重复值,改变索引(例如sum3)
*/
public class Sum4 {
public List fourSum(int[] nums, int target) {
List resList = new ArrayList
();
Arrays.sort(nums);
Set set = new HashSet
();
int l = nums.length;
for(int i = 0; i < l-3; i++) {
if(nums[i] > target && target > 0) break;
for(int j = i+1; j < l-2; j++) {
int start = j+1;
int end = l-1;
while(start < end) {
int sum = nums[i]+nums[j]+nums[start]+nums[end];
if(sum == target) {
set.add(Arrays.asList(nums[i],nums[j],nums[start],nums[end]));
start++;
end–;
}else if(sum < target) {
start++;
}else {
end–;
}
}
}
}
for(List list : set) {
resList.add(list);
}
return resList;
}
}
#11盛最多水的容器 双指针
public int maxArea(int[] height) {
if(height == null || height.length <= 1) return 0;
int left = 0;
int right = height.length-1;
int res = 0;
while(left < right){
res = Math.max(res, (right-left)*Math.min(height[right],height[left]));
if(height[left] < height[right]){
left++;
}else{
right--;
}
}
return res;
}
#31 下一个排列 双指针
必须 原地 修改,只允许使用额外常数空间。
思路:
从右往左遍历,找到num[i-1]
空间复杂度:O(1)O(1),只需要常数的空间存放若干变量
public void nextPermutation(int[] nums) {
int left = -1;
int right = 0;
for(int i = nums.length - 1; i > 0; i–) {
if(nums[i-1] < nums[i]) {
left = i-1;
break;
}
}
if(left >= 0) { //对于321,其下一个排列是第一个123,所以第二次循环和交换不需要做
for(int i = nums.length - 1; i > left; i–) {
if(nums[i] > nums[left]){
right = i;
break;
}
}
int tmp = nums[left];
nums[left] = nums[right];
nums[right] = tmp;
}
reserve(left + 1, nums);
}
public void reserve(int begin, int[] nums){
int end = nums.length - 1;
while(begin < end){
int tmp = nums[begin];
nums[begin] = nums[end];
nums[end] = tmp;
begin ++;
end --;
}
}
88 合并两个有序数组 逆向 双指针
逆向
//非递减数组,合并后仍保持非递减
//输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
//输出:[1,2,2,3,5,6]
public void merge(int[] nums1, int m, int[] nums2, int n) {
int last = m+n; //逆向
while(n > 0){
if(m > 0 && nums1[m-1] > nums2[n-1]){
nums1[–last] = nums1[–m];
}else{
nums1[–last] = nums2[–n];
}
}
}
#四数相加
/*
给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。
遍历 A 和 B 所有元素和的组合情况,并记录在 ab_map 中,ab_map 的 key 为两数和,value 为该两数和出现的次数
遍历 C 和 D 所有元素和的组合情况,取和的负值判断其是否在 ab_map 中,若存在则取出 ab_map 对应的 value 值,count = count + value
*/
public class Sum4_2 {
public int fourSumCount(int[] A, int[] B, int[] C, int[] D) {
int count = 0;
Map
int len = A.length;
for(int i = 0; i < len; i++) {
for(int j = 0; j < len; j++) {
int val = 0;
if(map.containsKey(A[i]+B[j])) {
val = (A[i]+B[j])+1;
}else {
val = 1;
}
map.put(A[i]+B[j], val);
}
}
for(int m = 0; m < len; m++) {
for(int n = 0; n < len; n++) {
int sum = -(C[m]+D[n]);
int val = 0;
if(map.containsKey(sum)) {
val = sum;
}
count += val;
}
}
return count;
}
}
#矩阵置零
给定一个 m x n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0。请使用原地算法。
你能想出一个常数空间的解决方案吗?
用第一行的元素表示每一列是否存在0,第一列的元素表示每一行是否存在0,再使用额外两个变量firstRowIsZero,firstColIsZero判断第一行以及第一列本身是否存在0。
class Solution {
public void setZeroes(int[][] matrix) {
int row = matrix.length;
if(row0)return;
int col = matrix[0].length;
if(col0)return;
boolean firstRowIsZero = false;
boolean firstColIsZero = false;
for(int i=0;i
matrix[i][0] = 0;
matrix[0][j] = 0;
}else if(matrix[i][j]0){
firstRowIsZero = i0 ? true:firstRowIsZero;
firstColIsZero = j0 ? true:firstColIsZero;
}
}
}
for(int i=1;i
for(int j=1;j
}
for(int j=1;j
for(int i=1;i
}
if(firstRowIsZero){
for(int j=0;j
if(firstColIsZero){
for(int i=0;i
}
}
#字谜分组
给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串
说明:
● 所有输入均为小写字母。
● 不考虑答案输出的顺序
创建一个HashMap
class Solution {
public List groupAnagrams(String[] strs) {
int len = strs.length;
if(len==0)return new ArrayList();
Map
for(String str:strs){
char[] chars = str.toCharArray();
Arrays.sort(chars);
String keyStr = String.valueOf(chars);
if(!map.containsKey(keyStr))map.put(keyStr, new ArrayList());
map.get(keyStr).add(str);
}
return new ArrayList(map.values());
}
}
滑动窗口
class Solution:
def problemName(self, s: str) -> int:
# Step 1: 定义需要维护的变量们 (对于滑动窗口类题目,这些变量通常是最小长度,最大长度,或者哈希表)
x, y = …, …
# Step 2: 定义窗口的首尾端 (start, end), 然后滑动窗口
start = 0
for end in range(len(s)):
# Step 3: 更新需要维护的变量, 有的变量需要一个if语句来维护 (比如最大最小长度)
x = new_x
if condition:
y = new_y
'''
------------- 下面是两种情况,读者请根据题意二选1 -------------
'''
# Step 4 - 情况1
# 如果题目的窗口长度固定:用一个if语句判断一下当前窗口长度是否达到了限定长度
# 如果达到了,窗口左指针前移一个单位,从而保证下一次右指针右移时,窗口长度保持不变,
# 左指针移动之前, 先更新Step 1定义的(部分或所有)维护变量
if 窗口长度达到了限定长度:
# 更新 (部分或所有) 维护变量
# 窗口左指针前移一个单位保证下一次右指针右移时窗口长度保持不变
# Step 4 - 情况2
# 如果题目的窗口长度可变: 这个时候一般涉及到窗口是否合法的问题
# 如果当前窗口不合法时, 用一个while去不断移动窗口左指针, 从而剔除非法元素直到窗口再次合法
# 在左指针移动之前更新Step 1定义的(部分或所有)维护变量
while 不合法:
# 更新 (部分或所有) 维护变量
# 不断移动窗口左指针直到窗口再次合法
# Step 5: 返回答案
return ...
643 子数组最大平均数 滑动窗口
public double findMaxAverage(int[] nums, int k) {
double sum = 0;
double max_avg = Integer.MIN_VALUE;
int start = 0;
for(int end = 0; end < nums.length; end++){
sum += nums[end];
if(end-start+1 == k){
max_avg = Math.max(max_avg,sum/(double)k);
}
if(end >= k-1){
sum -= nums[start];
start++;
}
}
return max_avg;
}
#3 无重复字符的最长子串 滑动窗口
https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/solution/yi-ge-mo-ban-miao-sha-10dao-zhong-deng-n-sb0x/
给定一个字符串,找出不含有重复字符的最长子串的长度。
每遇见一个字符,就判断该字符在起点到当前位置是否出现过,如果出现过,则更新子串的长度,然后将起点更新为该字符在起点之后出现的第一个位置。
class Solution {
public int lengthOfLongestSubstring(String s) {
int maxlen = 0;//滑动窗口的长度
int left = 0,right = 1;
if(s.length() <= 1) return s.length();
for(; right < s.length(); right++){
if(s.indexOf(s.charAt(right),left) < right){ // 表示在右窗口之前存在和右窗口相同的元素
maxlen = Math.max(maxlen, right-left); //对于完全没有重复元素的,maxlen不会更新
left = s.indexOf(s.charAt(right),left) + 1; //左窗口滑动到出现重复字符位置后一位
}
}
return Math.max(maxlen,right-left);
}
}
题解套路:
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
# Step 1: 定义需要维护的变量,
//本题求最大长度,所以需要定义max_len, 该题又涉及去重,因此还需要一个哈希表
max_len, hashmap = 0, {}
# Step 2: 定义窗口的首尾端 (start, end), 然后滑动窗口
start = 0
for end in range(len(s)):
# Step 3
# 更新需要维护的变量 (max_len, hashmap)
# i.e. 把窗口末端元素加入哈希表,使其频率加1,并且更新最大长度
hashmap[s[end]] = hashmap.get(s[end], 0) + 1
if len(hashmap) == end - start + 1:
max_len = max(max_len, end - start + 1)
# Step 4:
# 根据题意, 题目的窗口长度可变: 这个时候一般涉及到窗口是否合法的问题
# 这时要用一个while去不断移动窗口左指针, 从而剔除非法元素直到窗口再次合法
# 当窗口长度大于哈希表长度时候 (说明存在重复元素),窗口不合法
# 所以需要不断移动窗口左指针直到窗口再次合法, 同时提前更新需要维护的变量 (hashmap)
while end - start + 1 > len(hashmap):
head = s[start]
hashmap[head] -= 1
if hashmap[head] == 0:
del hashmap[head]
start += 1
# Step 5: 返回答案 (最大长度)
return max_len
#209 长度最小的子数组 滑动窗口
public int minSubArrayLen(int target, int[] nums) {
int minlen = Integer.MAX_VALUE;
int sum = 0;
int start = 0;
for(int end = 0; end < nums.length; end++){
sum += nums[end];
if(sum >= target){
minlen = Math.min(minlen,end-start+1);
}
while (sum >= target){
sum -= nums[start];
minlen = Math.min(minlen, end-start+1);
start++;
}
}
if(minlen == Integer.MAX_VALUE) minlen = 0;
return minlen;
}
#1695 删除子数组的最大得分 滑动窗口
// 给你一个正整数数组 nums ,请你从中删除一个含有 若干不同元素 的子数组(子数组是连续子序列)。删除子数组的 得分 就是子数组各元素之 和 。
// 返回 只删除一个 子数组可获得的 最大得分 。
public int maximumUniqueSubarray(int[] nums) {
int max_sum = 0;
int sum = 0;
int start = 0;
for(int end = 0; end < nums.length; end++){
sum += nums[end];
if(!contain(nums,nums[end],start,end)){
max_sum = Math.max(max_sum, sum);
}
while (contain(nums,nums[end],start,end)){
sum -= nums[start];
start++;
}
}
return max_sum;
}
public boolean contain(int[] arr, int num, int begin, int end){
for(int i = begin; i < end; i++){
if(arr[i] == num) return true;
}
return false;
}
#438 找到字符串中所有字母异位词
// 给定两个小写字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
// 异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
public List findAnagrams(String s, String p) {
List res = new ArrayList();
//用来统计每个字母出现的次数,如果出现次数相同,就是异位词
//对于小写或全大写可用26大小数组
int[] count1 = new int[26];
int[] count2 = new int[26];
for(int i = 0; i < p.length(); i++){
count1[p.charAt(i) - ‘a’] ++;
}
int start = 0;
for(int end = 0; end < s.length(); end++){
count2[s.charAt(end) -'a'] ++;
if(end > p.length()){
count2[s.charAt(start)-'a']--;
start++;
}
if(check(count1,count2)){
res.add(start);
}
}
return res;
}
public boolean check(int[] arr1, int[] arr2){
for(int j = 0; j < arr1.length; j++){
if(arr1[j] != arr2[j]) return false;
}
return true;
}
#567 字符串的排列
// 给你两个小写字符串 s1 和 s2 ,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true ;否则,返回 false 。
public boolean checkInclusion(String s1, String s2) {
int[] count1 = new int[26];
int[] count2 = new int[26];
int start = 0;
for(int i = 0; i < s1.length(); i++){
count1[s1.charAt(i)-‘a’]++;
}
for(int end = 0; end < s2.length(); end++){
count2[s2.charAt(end)-‘a’]++;
//先滑动窗口
if(end > s1.length()-1){
count2[s2.charAt(start)-‘a’]–;
start++;
}
//复用上一题的check
if(check(count1,count2)) return true;
}
return false;
}
#1004 最大连续1的个数III
输入:nums = [1,1,1,0,0,0,1,1,1,1,0], K = 2
输出:6
解释:[1,1,1,0,0,1,1,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 6。
class Solution {
public int longestOnes(int[] nums, int k) {
int maxlen = 0;
int count = 0;
int start = 0;
for(int end = 0; end < nums.length; end++){
if(nums[end] == 0) count++;
if(count <= k){
maxlen = Math.max(maxlen,end-start+1);
}
while(count > k){
if(nums[start] == 0) count–;
start++;
}
}
return maxlen;
}
}
#1208 尽可能使字符串相等
// 输入:s = “abcd”, t = “bcdf”, maxCost = 3
// 输出:3
// 解释:s 中的 “abc” 可以变为 “bcd”。开销为 3,满足≤maxcost,所以输出abc的长度为 3。
//建立开销数组 cost[i] = t[i]-s[i] --> {1,1,1,2} 连续子序列和≤maxcost
// “krrgw” “zjxss” 19
// cost是绝对值
//
public int equalSubstring(String s, String t, int maxCost) {
int[] cost = new int[s.length()];
for(int i = 0 ; i < s.length(); i++){
cost[i] = Math.abs(t.charAt(i) - s.charAt(i));
}
int sum = 0;
int maxlen = 0;
int start = 0;
for(int end = 0; end < s.length(); end++){
sum += cost[end];
if(sum <= maxCost){
maxlen = Math.max(maxlen, end-start+1);
}
while(sum > maxCost){
sum -= cost[start];
start++;
}
}
return maxlen;
}
#1423 可获得的最大点数
思路:转化为求最小子序列和,然后总和 - 最小子序列和
public int maxScore(int[] cardPoints, int k) {
int sum = 0;
for(int i = 0; i < cardPoints.length; i++){
sum += cardPoints[i];
}
int minsum = Integer.MAX_VALUE;
int subsum = 0;
int start = 0;
for(int end = 0; end < cardPoints.length; end++){
subsum += cardPoints[end];
if(end > cardPoints.length-k-1){
subsum -= cardPoints[start];
start ++;
}
if(end - start + 1 == cardPoints.length-k){
minsum = Math.min(minsum,subsum);
}
}
if(minsum == Integer.MAX_VALUE) minsum = 0;
return sum-minsum;
}
459 重复的子字符串
public boolean repeatedSubstringPattern(String s) {
String tmp = (s + s).substring(1, s.length() * 2 - 1);
return tmp.contains(s);
}
#5 最长回文子串 中心扩散
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为1000。
遍历每个元素,将每个元素作为回文中心并扩展,直到不满足回文条件,得到该元素为中心的回文最大长度。
这道题的关键是要从中间扩展回文串,还要注意回文串的长度为奇数和偶数两种情况,在更新start时要注意。
时间复杂度:O(n^2),其中 n 是字符串的长度。长度为 1 和 2 的回文中心分别有 n 和 n−1 个,每个回文中心最多会向外扩展 O(n) 次。
空间复杂度:O(1)。
public String longestPalindrome(String s) {
if(s.length() <= 1) return s;
int start = 0, end = 0;
for(int i = 0; i < s.length(); i++){
int len = expand(s,i,i); //对于奇数长度的aba
int len2 = expand(s,i,i+1); //对于偶数的abba
int lenmax = Math.max(len,len2);
if(end - start < lenmax){
start = i - (lenmax-1)/2;
//对于偶数的子串,start会定位到前一个,因为i在偏左位置
end = i + lenmax/2;
}
}
return s.substring(start,end+1); //右边界不包含
}
public int expand(String str, int left, int right){
while (left >= 0 && right < str.length()
&& str.charAt(left) == str.charAt(right)){
left–;
right++;
}
return right-left-1;
}
贪心
409 最长回文串 贪心,构建
给定一个包含大写字母和小写字母的字符串 s ,返回 通过这些字母构造成的 最长的回文串的长度。
思路:统计每个字母出现的次数,例如元素x出现的次数为count,则可以用count / 2 * 2个x去构建回文串;如果count是奇数,且当前构建出的回文串长度是偶数,则还可以选一个x作为回文中心。
1.用map
执行用时:10 ms, 在所有 Java 提交中击败了7.35%的用户
内存消耗:39.8 MB, 在所有 Java 提交中击败了20.96%的用户
public int longestPalindrome409(String s) {
Map
for(int i = 0; i < s.length(); i++){
if(map.containsKey(s.charAt(i))){
map.put(s.charAt(i), map.get(s.charAt(i))+1);
}else {
map.put(s.charAt(i), 1);
}
}
int len = 0;
Set entryset = map.entrySet();
Iterator
while (it.hasNext()){
Map.Entry entry = it.next();
int count = (Integer) entry.getValue();
len += count / 2 * 2;
//遇到出现奇数次的,如果此时回文串长度是偶数,这个奇数次的元素可以取一个做回文中心
if(count % 2 == 1 && len % 2 == 0){
len ++;
}
}
return len;
}
2.只有字母,数组统计
执行用时:2 ms, 在所有 Java 提交中击败了61.64%的用户
内存消耗:39.6 MB, 在所有 Java 提交中击败了42.37%的用户
public int longestPalindrome409(String s) {
int[] charCount = new int[58];//ASCII码表里 A-Z与a-z中间还有6个其他字符
for(int i = 0; i < s.length(); i++){
charCount[s.charAt(i) - ‘A’] ++;
}
int len = 0;
for(int j = 0; j < charCount.length; j++){
len += charCount[j] / 2 * 2;
if(charCount[j] % 2 == 1 && len % 2 == 0){ //遇到出现奇数次的,如果此时回文串长度是偶数,这个奇数次的元素可以取一个做回文中心
len ++;
}
}
return len;
}
605 种花问题 贪心
数组0为没种,1为已种,不可以连续种,求能不能种入n朵花。
输入:flowerbed = [1,0,0,0,1], n = 1
输出:true
能种就种
执行用时:1 ms, 在所有 Java 提交中击败了85.05%的用户
内存消耗:41.5 MB, 在所有 Java 提交中击败了52.09%的用户
public boolean canPlaceFlowers(int[] flowerbed, int n) {
for(int i = 0; i < flowerbed.length; i++){
if(flowerbed[i] == 0){
if((i == 0 || flowerbed[i-1] == 0) && (i == flowerbed.length-1 || flowerbed[i+1] == 0)){
n–;
flowerbed[i] = 1;
if(n <= 0) return true;
}
}
}
return n <= 0;//对于n=0的情况
}
455 分发饼干 贪心
数组g为小孩的胃口,s为饼干的尺寸,目标:尽可能满足越多数量的孩子,输出满足的数量
思路:
为了满足更多的孩子,大饼干给大胃口的孩子,尽可能不造成饼干浪费。
执行用时:7 ms, 在所有 Java 提交中击败了99.95%的用户
内存消耗:42.4 MB, 在所有 Java 提交中击败了37.08%的用户
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g); // 小孩
Arrays.sort(s); // 饼干
int sindex = s.length - 1;
int maxnum = 0;
for(int i = g.length-1; i >= 0; i–){
if(sindex >= 0 && s[sindex] >= g[i]){
maxnum ++;
sindex --;
}
}
return maxnum;
}
时间复杂度和空间复杂度:
时间复杂度:O(m \log m + n \log n)O(mlogm+nlogn),其中 mm 和 nn 分别是数组 gg 和 ss 的长度。对两个数组排序的时间复杂度是 O(m \log m + n \log n)O(mlogm+nlogn),遍历数组的时间复杂度是 O(m+n)O(m+n),因此总时间复杂度是 O(m \log m + n \log n)O(mlogm+nlogn)。
空间复杂度:O(\log m + \log n)O(logm+logn),其中 mm 和 nn 分别是数组 gg 和 ss 的长度。空间复杂度主要是排序的额外空间开销。
#179 最大数 贪心
非负整数数组,输出组成的最大数,返回字符串。
思路:利用字符串排序,字符串排序是从第一个字符开始对比,如果ab的数字比ba大,则a排在前。降序!
执行用时:4 ms, 在所有 Java 提交中击败了87.29%的用户
内存消耗:41 MB, 在所有 Java 提交中击败了40.44%的用户
public String largestNumber(int[] nums) {
if(nums == null || nums.length == 0) return “”;
String[] strarr = new String[nums.length];
for(int i = 0; i < nums.length; i++){
strarr[i] = String.valueOf(nums[i]);
}
Arrays.sort(strarr, new Comparator() {
@Override
public int compare(String o1, String o2) {
return (o2 + o1).compareTo(o1 + o2);
}
});
if(strarr[0].equals(“0”)){
// 全是0的情况
return “0”;
}
StringBuffer buffer = new StringBuffer();
for(int j = 0; j < strarr.length; j++){
buffer.append(strarr[j]);
}
return buffer.toString();
}
lambda表达式
执行用时:4 ms, 在所有 Java 提交中击败了87.29%的用户
内存消耗:41.1 MB, 在所有 Java 提交中击败了25.65%的用户
Arrays.sort(strarr, (o1, o2) -> (o2 + o1).compareTo(o1 + o2));
#134 加油站 贪心
执行用时:2 ms, 在所有 Java 提交中击败了77.36%的用户
内存消耗:61.4 MB, 在所有 Java 提交中击败了5.15%的用户
public int canCompleteCircuit(int[] gas, int[] cost) {
//无法绕行一周的情况:总油量 < 总耗油量
if(getSum(gas) < getSum(cost)) return -1;
int total = 0;
int start = 0;
for(int i = 0; i < gas.length; i++){
total += gas[i] - cost[i];
if(total < 0){
start = i + 1;
total = 0;
}
}
return start;
}
public int getSum(int[] nums){
int sum = 0;
for(int i = 0; i < nums.length; i++){
sum += nums[i];
}
return sum;
}
#406 根据身高重建队列 贪心 排序
思路:
一般数组对还涉及到排序的,可以第一个元素降序,第二个元素顺序,或第一个顺序,第二个降序排序,来简化。
先排序,后插入
● 排序:第一个元素降序、第二个元素升序,重写compare()
排序后数组:
[7,0][7,1][6,1][5,0][5,2][4,4]
● 插入:遍历排序后的数组,当第二个元素小于当前位置时,就插入到第二个元素对应的位置
https://blog.csdn.net/ly0724ok/article/details/119927924?spm=1001.2014.3001.5502
执行用时:6 ms, 在所有 Java 提交中击败了81.43%的用户
内存消耗:41.8 MB, 在所有 Java 提交中击败了81.29%的用户
public int[][] reconstructQueue(int[][] people) {
if(people == null || people.length == 0) return null;
//第一个元素降序,第二个元素升序
Arrays.sort(people, new Comparator
@Override
public int compare(int[] o1, int[] o2) {
if(o1[0] != o2[0]){//第一个元素不同时按第一个元素降序
return o2[0] - o1[0];
}
return o1[1] - o2[1];//第一个元素相同时按第二个元素升序
}
});
List
for(int i = 0; i < people.length; i++){
if(people[i][1] >= i){
list.add(people[i]);
}else{
list.add(people[i][1], people[i]);
}
}
return list.toArray(new int[list.size()][]);
}
#1996 游戏中的弱角色 贪心 排序
思路:攻击值降序,防守值升序,所以如果防守值 > 下一个攻击值区间最大的那个值,就存在至少一个弱角色。
执行用时:95 ms, 在所有 Java 提交中击败了44.00%的用户
内存消耗:87.2 MB, 在所有 Java 提交中击败了67.78%的用户
时间复杂度:排序快排是O(n logn),遍历数组O(n),所以时间复杂度O(n logn+n) = O(n logn)
空间复杂度:排序时使用的栈空间为O(logn)
public int numberOfWeakCharacters(int[][] properties) {
if(properties == null || properties.length == 0) return 0;
//第一个降序,第二个升序
Arrays.sort(properties, new Comparator
@Override
public int compare(int[] o1, int[] o2) {
if(o1[0] != o2[0]){
return o2[0] - o1[0];
}
return o1[1] - o2[1];
}
});
int maxDefens = properties[0][1];
int count = 0;
for(int i = 1; i < properties.length; i++){
if(maxDefens > properties[i][1]){
count ++;
}else{
maxDefens = properties[i][1];
}
}
return count;
}
#字符串转换整数
/*
将字符串转换成整数。 相似题目:整数反转(简单) IntegerReverse
第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来;第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数
注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。返回0
java.lang.ArrayIndexOutOfBoundsException: 2
/
public class StringToInt {
public static void main(String[] args) {
StringToInt s = new StringToInt();
System.out.println(s.myAtoi("-2147483647"));
}
public int myAtoi(String str) {
if(str.length() == 0 || str == null) return 0;
char[] strarr = str.toCharArray();
//找到第一个非空字符
int i = 0;
while(i < str.length() && strarr[i] == ’ ') {i++;}
if(i == str.length()) return 0;
//找数字正负
int zf = 1;
if(strarr[i] == ‘-’) {
zf = -1;
i++;
}else if(strarr[i] == ‘+’){
i++;
}
long res = 0;
while(i < str.length() && strarr[i] >= ‘0’ && strarr[i] <= ‘9’) {
res = res10 + strarr[i++] - ‘0’;
if(res > Integer.MAX_VALUE) {
if(zf == 1) {
return Integer.MAX_VALUE;
}else {
return Integer.MIN_VALUE;
}
}
}
return (int)res*zf;
}
}
#334 递增的三元子序列
给定一个未排序的数组,判断这个数组中是否存在长度为 3 的递增子序列。
数学表达式如下:
如果存在这样的 i, j, k, 且满足 0 ≤ i < j < k ≤ n-1,
使得 arr[i] < arr[j] < arr[k] ,返回 true ; 否则返回 false 。
说明: 要求算法的时间复杂度为 O(n),空间复杂度为 O(1) 。
class Solution {
public boolean increasingTriplet(int[] nums) {
int len = nums.length;
if(len<3)return false;
int numI = Integer.MAX_VALUE;
int numJ = Integer.MAX_VALUE;
for(int i=0;i
else if(nums[i]<=numJ)numJ = nums[i];
else return true;
}
return false;
}
}
##寻找两个有序数组的中位数
/*
求两个有序数组的中位数 O(logN)
思路:
A - m B - n
中位数前面有c = (m+n-1)/2个数比它小,要么出现在A中要么出现在B中,先从A中找:
若A[p]恰好位于B[c-p-1]和B[c-p]之间,则A[p]是中位数
若A[p]
若A[p]>B[c-p-1],说明A[p]太大了,要去A[0]-A[p-1]中找
若A中没找到去B中找
*/
public class ByteDance_MidNumInTwoArr {
public double findMedianSortedArrays(int[] A, int[] B) {
int m = A.length;
int n = B.length;
if (m > n) { // to ensure m<=n
int[] temp = A; A = B; B = temp;
int tmp = m; m = n; n = tmp;
}
int iMin = 0, iMax = m, halfLen = (m + n + 1) / 2;
while (iMin <= iMax) {
int i = (iMin + iMax) / 2;
int j = halfLen - i;
if (i < iMax && B[j-1] > A[i]){
iMin = i + 1; // i is too small
}
else if (i > iMin && A[i-1] > B[j]) {
iMax = i - 1; // i is too big
}
else { // i is perfect
int maxLeft = 0;
if (i == 0) { maxLeft = B[j-1]; }
else if (j == 0) { maxLeft = A[i-1]; }
else { maxLeft = Math.max(A[i-1], B[j-1]); }
if ( (m + n) % 2 == 1 ) { return maxLeft; }
int minRight = 0;
if (i == m) { minRight = B[j]; }
else if (j == n) { minRight = A[i]; }
else { minRight = Math.min(B[j], A[i]); }
return (maxLeft + minRight) / 2.0;
}
}
return 0.0;
}
}
##42 接雨水 双指针
public int trap(int[] height) {
if(height.length == 0 || height ==null){
return 0;
}
int sum = 0;
int[] leftMax = new int[height.length];
int[] rightMax = new int[height.length];
leftMax[0] = height[0];
for(int i = 1; i < height.length; i++){
leftMax[i] = Math.max(leftMax[i-1],height[i]);
}
rightMax[height.length-1] = height[height.length-1];
for(int j = height.length-2; j >= 0; j--){
rightMax[j] = Math.max(rightMax[j+1],height[j]);
}
for(int i = 0; i < height.length; i++){
sum += Math.min(leftMax[i],rightMax[i])-height[i];
}
return sum;
}
链表
获取倒数第k个元素,获取中间位置的元素,判断链表是否存在环,判断环的长度等和长度与位置有关的问题。这些问题都可以通过灵活运用双指针来解决。
#237 删除链表中的节点 复制值+跳过
请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点,你将只被给定要求被删除的节点。
链表至少包含两个节点。
链表中所有节点的值都是唯一的。
给定的节点为非末尾节点并且一定是链表中的一个有效节点。
不要从你的函数中返回任何结果
class Solution {
public void deleteNode(ListNode node) {
node.val = node.next.val;
node.next = node.next.next;
}
}
203 移除链表元素
public ListNode removeElements(ListNode head, int val) {
if(head == null) return null;
ListNode res = new ListNode(-1);
res.next = head;
ListNode cur = head;
ListNode pre = res;
while (cur != null && cur.next != null){
if(cur.val == val){
cur.val = cur.next.val;
cur.next = cur.next.next;
}else {
cur = cur.next;
pre = pre.next;
}
}
if(cur.val == val){ //移除最后一个节点
pre.next = null;
}
return res.next;
}
方法2:
public ListNode removeElements(ListNode head, int val) {
if(head == null) return null;
ListNode pre = new ListNode(0);
pre.next = head;
ListNode curr = head;
ListNode slow = pre;
while(curr != null) {
if(curr.val == val) {
slow.next = curr.next;
curr = curr.next;
}else {
slow = slow.next;
curr = curr.next;
}
}
return pre.next;
}
83 删除链表重复数字的节点 双指针
// 输入:head = [1,1,2,3,3]
// 输出:[1,2,3]
public ListNode deleteDuplicates(ListNode head) {
if(head == null) return null;
ListNode res = new ListNode(-1);
res.next = head;
ListNode fast = res.next;
ListNode slow = res;
while (fast != null){
while (fast.next != null && fast.val == fast.next.val){
fast = fast.next;
}
slow.next = fast;
slow = slow.next;
fast = fast.next;
}
return res.next;
}
public ListNode deleteDuplicates(ListNode head) {
if(head == null || head.next == null) return head;
ListNode cur = head;
while(cur.next != null && cur != null) {
if(cur.val == cur.next.val) {
cur.next = cur.next.next;
}else {
cur = cur.next;
}
}
return head;
}
#82 删除排序链表中的重复元素II 双指针
有重复的数字全删除
// 输入:head = [1,2,3,3,4,4,5] 输出:[1,2,5]
// 输入:head = [1,1,1,2,3] 输出:[2,3]
public ListNode deleteDuplicatesII(ListNode head) {
if(head == null || head.next == null) return head;
ListNode res = new ListNode(-1);
res.next = head;
ListNode slow = res;
ListNode fast = head;
while(fast != null){
while (fast.next!=null && fast.val == fast.next.val){
fast = fast.next;
}
if(slow.next != fast){
slow.next = fast.next;
}else{
slow = slow.next;
}
fast = fast.next;
}
return res.next;
}
#19 删除链表的倒数第N个节点 双指针
给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。
// 输入:head = [1,2,3,4,5], n = 2
// 输出:[1,2,3,5]
public ListNode removeNthFromEnd(ListNode head, int n) {
if(head == null) return null;
ListNode res = new ListNode(-1);
res.next = head;
ListNode slow = head;
ListNode fast = head;
for(int i = 0; i < n; i++){
fast = fast.next;
}
//删除的就是头节点的情况
if(fast == null){
return head.next;
}
while(fast.next != null){
slow = slow.next;
fast = fast.next;
}
slow.next = slow.next.next; //对于[1,2] 1,如果是slow.next = fast —> [1,2]
return res.next;
}
#206 反转链表
反转一个单链表。
迭代:
public ListNode again_ReverseList(ListNode head) {
if(head == null || head.next == null) return head;
ListNode pre = null;
ListNode cur = head;
while(cur != null) {
ListNode nex = cur.next;//为了方便current后移先进行记录
cur.next = pre;
pre = cur;
cur = nex;
}
return pre;
}
递归:
class Solution {
public ListNode reverseList(ListNode head) {
return reverse(head);
}
public ListNode reverse(ListNode head){
if(headnull||head.nextnull)return head;;
ListNode pre = reverse(head.next);
head.next.next = head;
head.next = null;
return pre;
}
}
#92 反转链表II 部分反转+拼接
反转从m到n位置的链表
public ListNode reverseBetween(ListNode head, int m, int n) {
if(head == null) return null;
ListNode flag = new ListNode(-1);
flag.next = head;
ListNode pre = flag;
for(int i = 0; i < m-1; i++) {
pre = pre.next;
}
ListNode curr = pre.next;
ListNode next = null;
for(int i = 0; i < n-m+1; i++) {
ListNode temp = curr.next;
curr.next = next;
next = curr;
curr = temp;
}
//结束后的链表状态为:
//1->2->null 2<-3<-4 5
//p n c
//连接首尾
pre.next.next = curr; //即让2->5
pre.next = next;//让1->4
//得到1->4->3->2->5
return flag.next;
}
//方法二:
public ListNode reverseBetween2(ListNode head, int m, int n) {
ListNode bef = new ListNode(0);
ListNode prev = bef;
bef.next = head;
for(int i = 1; i < m; i++) {
prev = prev.next;
}
ListNode curr = prev.next;
for(int i = m; i < n; i++) {
ListNode next = curr.next;
curr.next = next.next;
next.next = prev.next;
prev.next = next;
}
return bef.next;
}
方法3:对m-n的链表进行反转再拼接
方法3
public ListNode reverseBetween(ListNode head, int m, int n) {
if(m == n) return head;
ListNode hep = new ListNode(0);
hep.next = head;
ListNode start = null;
ListNode end = head;
ListNode bef = hep;
ListNode aft = null;
for(int i = 0; i < m-1; i++) {
bef = bef.next;
}
start = bef.next;
for(int i = 0; i < n-1; i++) {
end = end.next;
}
aft = end.next;
end.next = null;
ListNode newStart = reverse(start);
bef.next = newStart;
start.next = aft;
return hep.next;
}
private ListNode reverse(ListNode start) {
ListNode hep = new ListNode(0);
hep.next = start;
ListNode pre = start;
ListNode cur = pre.next;
ListNode tmp;
while(cur != null) {
tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
}
hep.next = null;
return pre;
}
21 合并两个有序链表
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
迭代:
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode pre = new ListNode(-1);
ListNode curr = pre;
while(l1!=null&&l2!=null){
if(l1.val<=l2.val){
curr.next = l1;
l1 = l1.next;
}else{
curr.next = l2;
l2 = l2.next;
}
curr = curr.next;
}
if(l1==null)curr.next = l2;
else curr.next = l1;
return pre.next;
}
}
递归:
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1null)return l2;
if(l2null)return l1;
if(l1.val<=l2.val){
l1.next = mergeTwoLists(l1.next, l2);
return l1;
}else{
l2.next = mergeTwoLists(l2.next, l1);
return l2;
}
}
}
#147 链表插入排序
#148 排序链表 快慢指针 + 归并排序
时间复杂度是 O(nlogn) 的排序算法包括归并排序、堆排序和快速排序(快速排序的最差时间复杂度是 O(n2))。插入排序时间复杂度是n2,其中最适合链表的排序算法是归并排序
要求:时间空间复杂度分别为O(nlogn)和O(1)
迭代分割-合并
递归调用会带来logN的空间复杂度,如果要达到O(1)的空间复杂度,不可以递归
方法一:自顶向下 ——O(nlogn)和O(n)
public ListNode sortList(ListNode head) {
if(head == null || head.next == null) return head;
//分割:使用快慢指针找到中间节点,快指针速度2
ListNode slow = head, fast = head.next;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
//得到slow为中间节点,偶数时,slow为中间左边节点
ListNode tmp = slow.next;
slow.next = null;
ListNode left = sortList(head);
ListNode right = sortList(tmp);
//合并,左右节点排序
ListNode res = new ListNode(-1);
ListNode cur = res;
while(left != null && right != null){
if(left.val < right.val){
cur.next = left;
left = left.next;
}else{
cur.next = right;
right = right.next;
}
cur = cur.next;
}
if(left == null){
cur.next = right;
}else{
cur.next = left;
}
return res.next;
}
方法二:自底向上 ——O(nlogn)和O(1)
class Solution {
// 自底向上归并排序
public ListNode sortList(ListNode head) {
if(head == null){
return head;
}
// 1. 首先从头向后遍历,统计链表长度
int length = 0; // 用于统计链表长度
ListNode node = head;
while(node != null){
length++;
node = node.next;
}
// 2. 初始化 引入dummynode
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
// 3. 每次将链表拆分成若干个长度为subLen的子链表 , 并按照每两个子链表一组进行合并
for(int subLen = 1;subLen < length;subLen <<= 1){ // subLen每次左移一位(即sublen = sublen*2) PS:位运算对CPU来说效率更高
ListNode prev = dummyHead;
ListNode curr = dummyHead.next; // curr用于记录拆分链表的位置
while(curr != null){ // 如果链表没有被拆完
// 3.1 拆分subLen长度的链表1
ListNode head_1 = curr; // 第一个链表的头 即 curr初始的位置
for(int i = 1; i < subLen && curr != null && curr.next != null; i++){ // 拆分出长度为subLen的链表1
curr = curr.next;
}
// 3.2 拆分subLen长度的链表2
ListNode head_2 = curr.next; // 第二个链表的头 即 链表1尾部的下一个位置
curr.next = null; // 断开第一个链表和第二个链表的链接
curr = head_2; // 第二个链表头 重新赋值给curr
for(int i = 1;i < subLen && curr != null && curr.next != null;i++){ // 再拆分出长度为subLen的链表2
curr = curr.next;
}
// 3.3 再次断开 第二个链表最后的next的链接
ListNode next = null;
if(curr != null){
next = curr.next; // next用于记录 拆分完两个链表的结束位置
curr.next = null; // 断开连接
}
// 3.4 合并两个subLen长度的有序链表
ListNode merged = mergeTwoLists(head_1,head_2);
prev.next = merged; // prev.next 指向排好序链表的头
while(prev.next != null){ // while循环 将prev移动到 subLen*2 的位置后去
prev = prev.next;
}
curr = next; // next用于记录 拆分完两个链表的结束位置
}
}
// 返回新排好序的链表
return dummyHead.next;
}
// 此处是Leetcode21 --> 合并两个有序链表
public ListNode mergeTwoLists(ListNode l1,ListNode l2){
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
while(l1 != null && l2!= null){ // 退出循环的条件是走完了其中一个链表
// 判断l1 和 l2大小
if (l1.val < l2.val){
// l1 小 , curr指向l1
curr.next = l1;
l1 = l1.next; // l1 向后走一位
}else{
// l2 小 , curr指向l2
curr.next = l2;
l2 = l2.next; // l2向后走一位
}
curr = curr.next; // curr后移一位
}
// 退出while循环之后,比较哪个链表剩下长度更长,直接拼接在排序链表末尾
if(l1 == null) curr.next = l2;
if(l2 == null) curr.next = l1;
// 最后返回合并后有序的链表
return dummy.next;
}
}
#234 回文链表
请判断一个链表是否为回文链表。
O(n) 时间复杂度和 O(1) 空间复杂度。
//找到中间节点,翻转后半部分
public boolean isPalindrome(ListNode head) {
ListNode pre = new ListNode(-1);
pre.next = head;
ListNode slow = head;
ListNode fast = head;
if(head == null || head.next == null) return true;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
pre = pre.next;
}
//slow 偶数个节点时,在中间右边节点
pre.next = null; // 左边链表
ListNode right = revs(slow);
//如果是奇数个节点,左链表比较短
while(head != null){
if(head.val != right.val) return false;
head = head.next;
right = right.next;
}
return true;
}
public ListNode revs(ListNode head){
ListNode pre = null;
ListNode newHead = null;
ListNode cur = head;
while(cur != null){
ListNode next = cur.next;
if(next == null) newHead = cur;
cur.next = pre;
pre = cur;
cur = next;
}
return newHead;
}
#141 环形链表 双指针追及
给定一个链表,判断链表中是否有环。
不要使用额外的空间。
当一个链表有环时,快慢指针都会陷入环中进行无限次移动,然后变成了追及问题。想象一下在操场跑步的场景,只要一直跑下去,快的总会追上慢的。当两个指针都进入环后,每轮移动使得慢指针到快指针的距离增加一,同时快指针到慢指针的距离也减少一,只要一直移动下去,快指针总会追上慢指针。
如果存在环,如何判断环的长度呢?方法是,快慢指针相遇后继续移动,直到第二次相遇。两次相遇间的移动次数即为环的长度。
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while(fast!=null&&fast.next!=null){
slow = slow.next;
fast = fast.next.next;
if(slow==fast)return true;
}
return false;
}
}
#142 环形链表II
返回入环的第一个节点
方法1:set
public ListNode detectCycle(ListNode head) {
Set set = new HashSet();
while (head != null){
if(set.contains(head)) return head;
set.add(head);
head = head.next;
}
return head;
}
方法2:
根据环的特性,相遇节点到入环节点个数 = 头节点到入环节点的个数:
假设总结点数有m+n-1个,入环节点为第m个,环长度为n,假设相遇节点为第q个,则slow=m+q-1,fast = m+n+q-1,fast = 2*slow,得到m=n-q+1,即相遇节点到入环节点个数 = 头节点到入环节点的个数。
public ListNode detectCycle(ListNode head) {
if(head == null) return null;
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
if(slow == fast) break;
}
//没有环时
if(fast == null || fast.next == null) return null;
fast = head;
while (fast != slow){
fast = fast.next;
slow = slow.next;
}
return fast;
}
两数相加
给定两个非空链表来表示两个非负整数。位数按照逆序方式存储,它们的每个节点只存储单个数字。将两数相加返回一个新的链表。
你可以假设除了数字 0 之外,这两个数字都不会以零开头。
比较简单,注意进位的处理就好了。
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode l3 = new ListNode(-1);
ListNode pre = l3;
int carry = 0;
while(l1!=null&&l2!=null){
l3.next = new ListNode((l1.val+l2.val+carry)%10);
carry = (l1.val+l2.val+carry)/10;
l1 = l1.next;
l2 = l2.next;
l3 = l3.next;
}
while(l1!=null){
l3.next = new ListNode((l1.val+carry)%10);
carry = (l1.val+carry)/10;
l1 = l1.next;
l3 = l3.next;
}
while(l2!=null){
l3.next = new ListNode((l2.val+carry)%10);
carry = (l2.val+carry)/10;
l2 = l2.next;
l3 = l3.next;
}
if(carry!=0)l3.next = new ListNode(carry);
return pre.next;
}
}
#328 奇偶链表
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。
说明:
● 应当保持奇数节点和偶数节点的相对顺序。
● 链表的第一个节点视为奇数节点,第二个节点视为偶数节点,以此类推。
分别将奇数节点和偶数节点连接成链,注意保存奇数链的头部和偶数链的头部,然后将奇数链的尾部连接至偶数链的头部,偶数链的尾部指向NULL。
public ListNode oddEvenList(ListNode head) {
ListNode oddpre = new ListNode(-1);
ListNode evenpre = new ListNode(-1);
ListNode odd = oddpre;
ListNode even = evenpre;
ListNode cur = head;
int index = 1;
while(cur != null){
if(index % 2 == 1){
odd.next = cur;
odd = odd.next;
}else{
even.next = cur;
even = even.next;
}
cur = cur.next;
index ++;
}
even.next = null; // 避免形成环
odd.next = evenpre.next;
return oddpre.next;
}
#160 相交链表
编写一个程序,找到两个单链表相交的起始节点。
例如,下面的两个链表:
A: a1 → a2
↘
c1 → c2 → c3
↗
B: b1 → b2 → b3
在节点 c1 开始相交。
这道题出现次数真是太多了!
首先分别遍历两条链表获取其长度,如果A链表的尾部与B链表的尾部不相同则直接返回null,否则求出长度差,然后再从两条链表的头部开始,先将较长链表往前走与长度差相同的步数,然后开始从两条链表同时往前走,如果遇到一样的节点就是交点。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
int len1 = 0;
int len2 = 0;
ListNode cur1 = headA;
ListNode cur2 = headB;
while(cur1 != null){
cur1 = cur1.next;
len1++;
}
while (cur2 != null){
cur2 = cur2.next;
len2 ++;
}
if(cur1 != cur2) return null;//末尾节点不同则没有相交
cur1 = headA;
cur2 = headB;
if(len1 > len2){
for(int i = 0; i < len1-len2; i++){
cur1 = cur1.next;
}
}else {
for(int j = 0; j < len2-len1; j++){
cur2 = cur2.next;
}
}
while(cur1 != cur2){
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}
#重排链表
/*
*
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
例如:给定链表 1->2->3->4, 重新排列为 1->4->2->3
*/
public class ListNode_ResortList {
public void reorderList(ListNode head) {
if(head == null) return;
ListNode slow = head;
ListNode fast = head;
while(fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
//对后半段进行反转链表
ListNode newBehind = slow.next;
slow.next = null;
newBehind = ReverseList(newBehind);
//将两个链表合并
ListNode curr = head;
while(curr != null & newBehind != null) {
ListNode behind = curr.next;
ListNode curr2 = newBehind;
newBehind = newBehind.next;
curr2.next = curr.next;
curr.next = curr2;
curr = behind;
}
}
public ListNode ReverseList(ListNode head) {
//反转链表的表头
ListNode pReverseHead = null;
//当前结点
ListNode pCurrent = head;
//当前结点的前一个结点
ListNode pPre = null;
while(pCurrent != null) {
//当前结点的后一个结点
ListNode pNext = pCurrent.next;
//判断pNext是否为空,为空则链表只有一个结点
if(pNext == null) pReverseHead = pCurrent;
//反转 将原链表的前一结点赋给新链表的后一结点
pCurrent.next = pPre;
pPre = pCurrent;
pCurrent = pNext;
}
return pReverseHead;
}
}
#分隔链表
/*
给定一个链表和一个特定值 x,对链表进行分隔,使得所有小于 x 的节点都在大于或等于 x 的节点之前。
输入: head = 1->4->3->2->5->2, x = 3
输出: 1->2->2->4->3->5
思路:当node.val>=3时,把这个节点移到尾结点
*/
public class ListNode_Partition {
public static void main(String[] args) {
ListNode head = new ListNode(1);
ListNode p2 = new ListNode(2);
head.next = p2;
ListNode_Partition po = new ListNode_Partition();
System.out.println(po.partition(head, 0).val + " " +po.partition(head, 0).next.val);
}
public ListNode partition(ListNode head, int x) {
if(head == null) return null;
ListNode pre = new ListNode(0);
pre.next = head;
ListNode start = pre;
ListNode end = pre.next;
int count = 0;
while(end.next != null) {
end = end.next;
count++;
}
while(start != end && start.next != null && count>=0) {
if(start.next.val >= x) {
end.next = new ListNode(start.next.val);
end = end.next;
start.next = start.next.next;
}else {
start = start.next;
}
count--;
}
return pre.next;
}
}
#对链表进行插入排序
思路:通过pre和tmp移动到需要插入的位置,通过cur和lat移动到需要被插入元素的位置https://blog.csdn.net/qq_17550379/article/details/80708238
public ListNode insertionSortList(ListNode head) {
if(head == null || head.next == null) return head;
ListNode reff = new ListNode(0);
reff.next = head;
ListNode curr = head;
ListNode lat = curr.next;
while(lat != null) {
ListNode temp = reff.next;
ListNode pre = reff;
while(temp != lat && temp.val < lat.val) {
temp = temp.next;
pre = pre.next;
}
if(temp == lat) {
curr = lat;
}else {
curr.next = lat.next;
lat.next = temp;
pre.next = lat;
}
lat = curr.next;
}
return reff.next;
}
树&图
递归法
1.确定递归函数的参数和返回值
2.确定终止条件
3.确定单层递归的逻辑
三种遍历二叉树——递归
public List TreeTraversal(TreeNode root){
List list = new ArrayList();
pretraversal(root,list);//先序
traversalin(root,list);//中序
poTreaversal(root,list);//后序
return list;
}
//先序
public void pretraversal(TreeNode treeNode, List list){
if(treeNode == null) return;
list.add(treeNode.val);
pretraversal(treeNode.left,list);
pretraversal(treeNode.right,list);
}
//中序
public void traversalin(TreeNode treeNode, List list){
if(treeNode == null) return;
traversalin(treeNode.left,list);
list.add(treeNode.val);
traversalin(treeNode.right,list);
}
//后序
public void poTreaversal(TreeNode treeNode, List list){
if(treeNode == null) return;
poTreaversal(treeNode.left,list);
poTreaversal(treeNode.right,list);
list.add(treeNode.val);
}
三种遍历二叉树——非递归
144 二叉树的前序遍历-根左右
方法1:
先压入
步骤1 :压入1
步骤2: 弹出1, 访问1,判断左右的情况 压入 3,2
步骤3:弹出2, 访问2,判断左右儿子节点是否为空,压入 5,4 ,栈中元素变成3,5,4
步骤:弹出 4, 访问 4, 然后4的左右子节点为空,所以不加入
步骤:弹出5, 访问5, 然后5的左右节点为空, 所以不加入
步骤:弹出3,访问3 ,然后加入 7, 6
步骤 :弹出6, 访问 6 不加入
步骤: 弹出 7, 不加入 ,
栈为空, 程序停止
public List preorderTraversal3(TreeNode root) {
List res = new ArrayList();
if(root == null) return res;
Stack stack = new Stack();
TreeNode curr = root;
stack.push(curr);
while (! stack.isEmpty()){
curr = stack.pop();
res.add(curr.val);
if(curr.right != null) stack.push(curr.right);
if(curr.left != null) stack.push(curr.left);
}
return res;
}
方法2:
class Solution {
public List preorderTraversal(TreeNode root) {
List res = new ArrayList<>();
Stack stack = new Stack<>();
TreeNode curr = root;
while(true){
while(curr!=null){
res.add(curr.val);
stack.push(curr);
curr = curr.left;
}
if(stack.isEmpty())break;
curr = stack.pop();
curr = curr.right;
}
return res;
}
}
#94 二叉树的中序遍历-左根右
非递归:
把所有的左节点依次放入栈中,栈顶的节点就是最后一层的最左节点,再去遍历根节点左子树的右节点,左子树遍历完后pop出根节点,再去遍历右子树
方法1:
时间复杂度:O(n),其中 n 为二叉树节点的个数。二叉树的遍历中每个节点会被访问一次且只会被访问一次。
空间复杂度:O(n)。空间复杂度取决于栈深度,而栈深度在二叉树为一条链的情况下会达到 O(n)O(n) 的级别。
public List inorderTraversal3(TreeNode root) {
List res = new ArrayList();
Stack stack = new Stack();
TreeNode curr = root;
while (!stack.isEmpty() || curr != null){
if(curr != null){//把左节点放入栈
stack.push(curr);
curr = curr.left;
}else {//当遍历到最左叶子时弹出,并访问,然后去遍历左叶子的右子树
TreeNode tmp = stack.pop();
res.add(tmp.val);
curr = tmp.right;
}
}
return res;
}
方法2:
class Solution {
public List inorderTraversal(TreeNode root) {
List res = new ArrayList<>();
Stack stack = new Stack<>();
TreeNode curr = root;
while(true){
while(curr!=null){
stack.push(curr);
curr = curr.left;
}
if(stack.isEmpty())break;
curr = stack.pop();
res.add(curr.val);
curr = curr.right;
}
return res;
}
}
#145 二叉树的后序遍历
重点:因为压栈是先把左节点压入,然后去找右节点。从栈中弹出的节点有可能会弹出2次:
第一次弹出时,如果有右节点,需要把弹出的节点重新压入栈,然后遍历其右节点。
第二次弹出时即弹出并打印,此时前一个打印的肯定是其右节点,所以打印时需要判断prev
我们只能确定其左子树肯定访问完了,但是无法确定右子树是否访问过。因此,我们在后序遍历中,引入了一个prev来记录历史访问记录。
public List postorderTraversal3(TreeNode root) {
List res = new ArrayList();
if(root == null) return res;
Stack stack = new Stack();
TreeNode curr = root;
TreeNode prev = null;
while (curr != null||!stack.isEmpty()){
while (curr != null){
stack.push(curr);
curr = curr.left;
}
curr = stack.pop();
if(curr.right == null || curr.right == prev){
res.add(curr.val);
prev = curr;
curr = null;
}else {
stack.push(curr);
curr = curr.right;
}
}
return res;
}
class Solution {
public List postorderTraversal(TreeNode root) {
List res = new ArrayList<>();
Stack stack = new Stack<>();
TreeNode pre = null;
TreeNode curr = root;
while(true){
while(curr!=null){
stack.push(curr);
curr = curr.left;
}
if(stack.isEmpty())break;
curr = stack.peek();
if(curr.right!=null&&curr.right!=pre){
curr = curr.right;
}else{
curr = stack.pop();
res.add(curr.val);
pre = curr;
curr = null;
}
}
return res;
}
}
还有一种比较巧妙的解法。既然每个节点都会两次出现于栈顶,那么可以在每次压入栈时重复压入两次,每次出栈的节点可以与栈顶节点相比较,如果相等,说明刚刚出栈的节点是第一次出现于栈顶,如果不相等说明此节点是第二次出现于栈顶,可以直接加入遍历链表中。此解法需要注意入栈的顺序,我们希望的出栈顺序应该是左子树→右子树→根节点,因此入栈顺序应该与此相反。
class Solution {
public List postorderTraversal(TreeNode root) {
List res = new ArrayList<>();
if(rootnull)return res;
TreeNode curr = root;
Stack stack = new Stack<>();
stack.push(curr);
stack.push(curr);
while(!stack.isEmpty()){
curr = stack.pop();
if(!stack.isEmpty()&&currstack.peek()){
if(curr.right!=null){
stack.push(curr.right);
stack.push(curr.right);
}
if(curr.left!=null){
stack.push(curr.left);
stack.push(curr.left);
}
}else res.add(curr.val);
}
return res;
}
}
104 二叉树的最大深度
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
用递归,很简单
class Solution {
public int maxDepth(TreeNode root) {
if(root==null)return 0;
return 1+Math.max(maxDepth(root.left),maxDepth(root.right));
}
}
100 相同的树
public boolean isSameTree(TreeNode p, TreeNode q) {
if(p == null && q == null) return true;
if(p == null || q == null) return false;
if(p.val == q.val) {
return isSameTree(p.left,q.left) && isSameTree(p.right,q.right);
} else {
return false;
}
}
55 平衡二叉树
左右子树深度相差不超过1
public boolean isBalanced(TreeNode root) {
if(root == null) return true;
if(Math.abs(maxDepth(root.left)-maxDepth(root.right)) <= 1
&& isBalanced(root.left) && isBalanced(root.right)){
return true;
}
return false;
}
public int maxDepth(TreeNode root) {
if(root == null) return 0;
return 1 + Math.max(maxDepth(root.left),maxDepth(root.right));
}
#98 验证二叉搜索树
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
● 节点的左子树只包含小于当前节点的数。
● 节点的右子树只包含大于当前节点的数。
● 所有左子树和右子树自身必须也是二叉搜索树。
二叉搜索树的中序遍历是升序的,可以结合中序遍历来判断
public boolean isValidBST(TreeNode root) {
List list = new ArrayList();
midorder(root, list);
for(int i = 0; i < list.size()-1; i++){
if(list.get(i+1) <= list.get(i)){
return false;
}
}
return true;
}
public void midorder(TreeNode treeNode, List list){
if(treeNode == null) return;
midorder(treeNode.left,list);
list.add(treeNode.val);
midorder(treeNode.right,list);
}
方法二:递
class Solution {
TreeNode pre;
public boolean isValidBST(TreeNode root) {
if(rootnull)return true;
boolean f1 = isValidBST(root.left);
boolean f2 = prenull||pre.val
boolean f3 = isValidBST(root.right);
return f1&&f2&&f3;
}
}
52 展平二叉搜索树 剑指II
public void midorder(TreeNode treeNode, List list){
if(treeNode == null) return;
midorder(treeNode.left,list);
list.add(treeNode.val);
midorder(treeNode.right,list);
}
//展平二叉搜索树 剑指52
public TreeNode increasingBST(TreeNode root) {
if(root == null) return null;
List list = new ArrayList();
midorder(root,list);
TreeNode resnode = new TreeNode(list.get(0));
TreeNode curr = resnode;
for(int i = 1; i < list.size(); i++){
TreeNode right = new TreeNode(list.get(i));
curr.right = right;
curr = curr.right;
}
return resnode;
}
101 对称二叉树
给定一个二叉树,检查它是否是镜像对称的。
递归:
public boolean isSymmetric(TreeNode root) {
if(root == null) return true;
TreeNode curr = root;
return isSymmetric(curr.left,curr.right);
}
public boolean isSymmetric(TreeNode left, TreeNode right){
if(left == null && right == null) return true;
if(left == null || right == null) return false;
if(left.val != right.val) return false;
//对称左子树的左节点与右子树的右节点值相等
return isSymmetric(left.left,right.right) && isSymmetric(right.left,left.right);
}
迭代解法硬是没搞出来,郁闷。。
226 二叉树的镜像 剑指27 lc
public TreeNode mirrorTree(TreeNode root) {
if(root == null) return null; //终止条件
//递归1.递归到左、右子树叶子节点,交换
TreeNode left = mirrorTree(root.left);
TreeNode right = mirrorTree(root.right);
root.left = right;
root.right = left;
//递归2.递归左子树将其赋给右子树节点
TreeNode tmp = root.left;
root.left = mirrorTree(root.right);
root.right = mirrorTree(tmp);
return root;
}
非递归
//按层级遍历放入队列,再重构
public TreeNode mirrorTreeII(TreeNode root) {
if(root == null) return null;
LinkedList list = new LinkedList();
list.add(root);
while (!list.isEmpty()){
//交换队列中节点的左右子树
TreeNode tmp = list.poll();
TreeNode left = tmp.left;
tmp.left = tmp.right;
tmp.right = left;
if(tmp.left != null){
list.add(tmp.left);
}
if(tmp.right != null){
list.add(tmp.right);
}
}
return root;
}
#102 二叉树的层次遍历 BFS——自顶向下
BFS:
https://leetcode-cn.com/problems/binary-tree-level-order-traversal/solution/bfs-de-shi-yong-chang-jing-zong-jie-ceng-xu-bian-l/
二叉树的BFS:
void bfs(TreeNode root) {
Queue queue = new ArrayDeque<>();
queue.add(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll(); // Java 的 pop 写作 poll()
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
}
}
给定一个二叉树,返回其按层次遍历的节点值。(即逐层地,从左到右访问所有节点)。
思路:需要三个数据结构:1.存放返回结果;2.存放每层节点值的list;3.存放每层的节点,使用queue
需要每次一次性遍历掉同一层的,所以有for循环去遍历,遍历次数是queue长度,即每层节点数量。且levelList不能写在while之外后面再clear,因为是引用,清空之后res也会清。
class Solution {
public List levelOrder(TreeNode root) {
List resList = new ArrayList<>();
if(root==null)return resList;
Queue queue = new LinkedList();
queue.offer(root);
while(!queue.isEmpty()){
List levelList = new ArrayList<>();
int len = queue.size();//每层的len
for(int i=0;i
levelList.add(curr.val);
if(curr.left!=null)queue.offer(curr.left);
if(curr.right!=null)queue.offer(curr.right);
}
resList.add(levelList);
}
return resList;
}
}
107 二叉树的层序遍历2 ——自下向上
1.对102的结果翻转
翻转list的方式:
Collections.reverse(list);
2.使用add(index,element)将遍历新得到的sublist添加在首位
resList.add(0, levelList);
108 将有序数组转换为二叉搜索树
将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。
本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
每次取数组中间元素作为根节点,然后递归地使用数组的左半部分生成当前根节点的左子树,数组的右半部分生成当前根节点的右子树。
public TreeNode sortedArrayToBST(int[] nums) {
if(nums == null || nums.length == 0) return null;
return bst(nums, 0, nums.length-1);
}
public TreeNode bst(int[] nums, int begin, int end){
if(begin > end) return null;
int mid = begin + (end - begin)/2;
TreeNode treeNode = new TreeNode(nums[mid]);
treeNode.left = bst(nums, begin,mid-1);
treeNode.right = bst(nums,mid+1,end);
return treeNode;
}
#先序遍历构造二叉树
/*
思路:找到数组中比第一个元素小的都是左子树,大的都是右子树 递归
*/
public class Tree_BuildTreeByPre {
public static void main(String[] args) {
Tree_BuildTreeByPre t = new Tree_BuildTreeByPre();
int[] preorder = {8,5,1,7,10,12};
TreeNode res = t.bstFromPreorder(preorder);
}
public TreeNode bstFromPreorder(int[] preorder) {
if(preorder == null || preorder.length == 0) return null;
return digui(preorder, 0, preorder.length);
}
public TreeNode digui(int[] prearr, int start, int end) {
if(start == end) return null;
int rootnum = prearr[start];
TreeNode root = new TreeNode(rootnum);
for(int i = start+1; i < end; i++) {
if(prearr[i] > rootnum) {
root.left = digui(prearr, start+1, i);
root.right = digui(prearr,i, end);
return root;
}
}
root.left = digui(prearr,start+1, end);
return root;
}
}
重建二叉树 剑指7 根据前序和中序构建二叉树
前序数组的首元素肯定是根节点,,此元素在中序数组中进行定位,左边的是左子树节点值,右边的是右子树节点值。使用递归
public TreeNode buildTree(int[] preorder, int[] inorder) {
if(preorder == null || inorder == null || preorder.length == 0 || inorder.length == 0) return null;
//需要能增删头元素的数据结构 list
List preList = new ArrayList();
for(int i = 0; i < preorder.length; i++){
preList.add(preorder[i]);
}
return build(preList, inorder);
}
//递归
public TreeNode build(List prelist, int[] inorder){
if(prelist.size() == 0 || inorder == null || inorder.length == 0) return null;
int rootval = prelist.get(0);
TreeNode treeNode = new TreeNode(rootval);
prelist.remove(0);
int[] left = null;
int[] right = null;
for(int i = 0; i < inorder.length; i++){
if(inorder[i] == rootval){
if(i == 0) left = null;//注意边界
left = Arrays.copyOfRange(inorder,0,i);//右边界不包含
//System.arraycopy(inorder,0, left, 0, i-1);//因为left和right是null,长度为0,arraycopy必须明确目标数据的大小,超过会抛out of index异常
if(i == inorder.length-1) right = null;
right = Arrays.copyOfRange(inorder,i+1,inorder.length);
//System.arraycopy(inorder,i+1, right, 0, inorder.length-i-1);
}
}
treeNode.left = build(prelist, left);
treeNode.right = build(prelist,right);
return treeNode;
}
根据中序和后序构建二叉树 106
public TreeNode buildTreeII(int[] inorder, int[] postorder) {
if(inorder == null || postorder == null ||
inorder.length 0 || postorder.length0) return null;
List postList = new ArrayList();
for(int i = 0; i < postorder.length; i++){
postList.add(postorder[i]);
}
return buildII(inorder,postList);
}
public TreeNode buildII(int[] inorder, List postList){
if(inorder == null || inorder.length == 0 || postList.isEmpty()) return null;
int rootval = postList.get(postList.size()-1);
System.out.println(rootval);
TreeNode treeNode = new TreeNode(rootval);
postList.remove(postList.size()-1);
int[] left = null;
int[] right = null;
for(int i = 0; i < inorder.length; i++){
if(inorder[i] == rootval){
if(i == 0) left = null;
left = Arrays.copyOfRange(inorder,0,i);
if(i == inorder.length-1) right = null;
right = Arrays.copyOfRange(inorder, i+1, inorder.length);
}
}
treeNode.right = buildII(right,postList);
treeNode.left = buildII(left,postList);
return treeNode;
}
#103 二叉树的锯齿形层次遍历 BFS
给定一个二叉树,返回其节点值的锯齿形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
维护一个层数变量,偶数层倒序的实现:根据层数判断是否偶数层,在list头部插入
//奇数层顺序,偶数层倒序,在层序遍历的基础上增加标志
public List zigzagLevelOrder(TreeNode root) {
List res = new ArrayList
();
if(root == null) return res;
Queue queue = new LinkedList();
queue.offer(root);
int level = 1;
while(!queue.isEmpty()){
List sublist = new ArrayList();
int len = queue.size();
for(int i = 0; i < len; i++) {
TreeNode curr = queue.poll();
if(level % 2 == 0) {
sublist.add(0, curr.val);//插入到首位
}else {
sublist.add(curr.val);
}
if(curr.left != null) queue.offer(curr.left);
if(curr.right != null) queue.offer(curr.right);
}
level++;
res.add(sublist);
}
return res;
}
创建一个双端队列,偶数层节点从左边插入左边取出,奇数层节点从右边插入右边取出。
class Solution {
public List zigzagLevelOrder(TreeNode root) {
List res = new ArrayList<>();
if(rootnull)return res;
Deque deque = new LinkedList<>();
int level = 0;
TreeNode curr = root;
deque.offerFirst(curr);
while(!deque.isEmpty()){
List list = new ArrayList<>();
int len = deque.size();
for(int i=0;i
curr = deque.pollFirst();
list.add(curr.val);
if(curr.left!=null)deque.offerLast(curr.left);
if(curr.right!=null)deque.offerLast(curr.right);
}else{
curr = deque.pollLast();
list.add(curr.val);
if(curr.right!=null)deque.offerFirst(curr.right);
if(curr.left!=null)deque.offerFirst(curr.left);
}
}
res.add(list);
level++;
}
return res;
}
}
#199 二叉树的右视图 BFS
即找到每一层的最右节点
执行用时:1 ms, 在所有 Java 提交中击败了80.98%的用户
内存消耗:40.2 MB, 在所有 Java 提交中击败了23.40%的用户
public List rightSideView(TreeNode root) {
List res = new ArrayList();
if(root == null) return res;
Queue queue = new LinkedList();
queue.offer(root);
while (!queue.isEmpty()){
int num = queue.size();
for (int i = 0; i < num; i++){
TreeNode cur = queue.poll();
if(i == num-1) res.add(cur.val);
if(cur.left != null) queue.offer(cur.left);
if(cur.right != null) queue.offer(cur.right);
}
}
return res;
}
#515 在每个树行中找最大值 BFS
执行用时:2 ms, 在所有 Java 提交中击败了89.27%的用户
内存消耗:41.7 MB, 在所有 Java 提交中击败了20.37%的用户
public List largestValues(TreeNode root) {
List res = new ArrayList();
if(root == null) return res;
Queue queue = new LinkedList();
queue.offer(root);
while (!queue.isEmpty()) {
int num = queue.size();
int max = Integer.MIN_VALUE;
for(int i = 0; i < num; i++){
TreeNode cur = queue.poll();
max = Math.max(max, cur.val);
if(cur.left != null) queue.offer(cur.left);
if(cur.right != null) queue.offer(cur.right);
}
res.add(max);
}
return res;
}
637 二叉树的层平均值 BFS
执行用时:2 ms, 在所有 Java 提交中击败了94.68%的用户
内存消耗:42.6 MB, 在所有 Java 提交中击败了68.35%的用户
public List averageOfLevels(TreeNode root) {
List res = new ArrayList();
if(root == null) return res;
Queue queue = new LinkedList();
queue.offer(root);
while (!queue.isEmpty()){
double sum = 0;
int len = queue.size();
for(int i = 0; i < len; i++){
TreeNode cur = queue.poll();
sum += cur.val;
if(cur.left != null) queue.offer(cur.left);
if(cur.right != null) queue.offer(cur.right);
}
res.add(sum / len);
}
return res;
}
814 二叉树剪枝
public TreeNode pruneTree(TreeNode root) {
if(root == null) return null;
root.left = pruneTree(root.left);
root.right = pruneTree(root.right);
//左右节点都为空且当前节点值为0就可以剪枝
if(root.left == null && root.right == null && root.val == 0) return null;
return root;
}
每个节点的右向指针
给定一个二叉树
struct TreeLinkNode {
TreeLinkNode *left;
TreeLinkNode *right;
TreeLinkNode *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。
说明:
● 你只能使用额外常数空间。
● 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。
● 你可以假设它是一个完美二叉树(即所有叶子节点都在同一层,每个父节点都有两个子节点)。
示例:
给定完美二叉树,
1
/
2 3
/ \ /
4 5 6 7
调用你的函数后,该完美二叉树变为:
1 -> NULL
/
2 -> 3 -> NULL
/ \ /
4->5->6->7 -> NULL
这道题只要画出示意图就简单,直接用递归搞定。
public class Solution {
public void connect(TreeLinkNode root) {
if(rootnull)return;
if(root.left!=null)root.left.next = root.right;
if(root.right!=null)root.right.next = root.nextnull ? null:root.next.left;
connect(root.left);
connect(root.right);
}
}
二叉搜索树中第K小的元素
给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素。
说明:
你可以假设 k 总是有效的,1 ≤ k ≤ 二叉搜索树元素个数。
就是考察二叉搜索树的中序遍历。
class Solution {
public int kthSmallest(TreeNode root, int k) {
Stack stack = new Stack<>();
TreeNode curr = root;
while(true){
while(curr!=null){
stack.push(curr);
curr = curr.left;
}
if(stack.isEmpty())break;
curr = stack.pop();
if(–k==0)return curr.val;
curr = curr.right;
}
return -1;
}
}
岛屿的个数
给定一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。
这题做过无数遍了。。
class Solution {
public int numIslands(char[][] grid) {
int count = 0;
for(int i=0;i
}
}
return count;
}
public int dfs(char[][] grid, int i, int j){
if(i<0||i>=grid.length||j<0||j>=grid[0].length||grid[i][j]==‘0’)return 0;
grid[i][j] = ‘0’;
int up = dfs(grid, i-1, j);
int down = dfs(grid, i+1, j);
int left = dfs(grid, i, j-1);
int right = dfs(grid, i, j+1);
return 1+up+down+left+right;
}
}
课程表
/*207
现在你总共有 n 门课需要选,记为 0 到 n-1。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
给定课程总量以及它们的先决条件,判断是否可能完成所有课程的学习?
注意:
输入由边缘列表表示的图,不是邻接矩阵
例:
2 [1,0] 表示先完成0再完成1 所以1的入度为1
拓扑排序:
先将入度为0的顶点放在栈或者队列中。
当队列不空时,删除一个顶点v,然后更新与顶点v邻接的顶点的入度。
只要有一个顶点的入度降为0,则将之入队列。此时,拓扑排序就是顶点出队的顺序。
如果结果集中的数量不等于结点的数量,就不能完成课程任务,这一点是拓扑排序的结论
该算法的时间复杂度为O(V+E)
*/
public class TuoPuSort_Timetable {
public boolean canFinish(int numCourses, int[][] prerequisites) {
if(numCourses <= 0) return false;
if(prerequisites.length == 0) return true;
int[] indegree = new int[numCourses];
for(int[] p : prerequisites) {
indegree[p[0]]++;
}
//用队列存入度为0的点
LinkedList queue = new LinkedList();
for(int i = 0; i < numCourses; i++) {
if(indegree[i] == 0) {
queue.add(i);// 将元素添加队尾:boolean add() void addLast()
}
}
//拓扑排序
//按出队顺序存顶点
List res = new ArrayList();
while(!queue.isEmpty()) {
int pointnum = queue.removeFirst();
res.add(pointnum);
//把与该顶点相关的边删除,该点之后的点的入度-1,并把新的入度为0的点加入到队尾
for(int[] p : prerequisites) {
if(p[1] == pointnum) {
indegree[p[0]]--;
if(indegree[p[0]] == 0) {
queue.add(p[0]);
}
}
}
}
System.out.println(res);
return res.size() == numCourses;
}
}
排序和搜索
插入排序
希尔排序
选择排序
冒泡排序
堆排序
快速排序
将首位作为基准数,每一轮就是将基准数归位
https://blog.csdn.net/qq_40941722/article/details/94396010?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522164706686416780265497538%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=164706686416780265497538&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_positive~default-1-94396010.first_rank_v2_pc_rank_v29_v2&utm_term=%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F&spm=1018.2226.3001.4187
//快排
public void quickSort(int[] nums, int begin, int end){
if(begin > end) return;
int i = begin, j = end;
int temp = nums[begin];
while(j!=i){
while(nums[j] >= temp && j > i){
j–;
}
while(nums[i] <= temp && j > i){
i++;
}
if(j > i){
int swap = nums[i];
nums[i] = nums[j];
nums[j] = swap;
}
}
nums[begin] = nums[i];
nums[i] = temp;
quickSort(nums,begin,i-1);
quickSort(nums,i+1,end);
}
#33 搜索旋转排序数组 二分
//升序无重复数组,向左移动k后得到nums,返回target在旋转数组的下标
//先找到分界值,然后再在有序数组中找target
//[3,1] 1
public int search(int[] nums, int target) {
int low = 0;
int high = nums.length-1;
while (low <= high){
int mid = low + (high-low)/2;
if(nums[mid] == target){
return mid;
}
//保证target和mid在同一边
//target在左半段
if(nums[0] <= target){
//如果mid在右边
if(nums[mid] < nums[0]){
nums[mid] = Integer.MAX_VALUE;
}
}else{
if(nums[mid] >= nums[0]){
nums[mid] = Integer.MIN_VALUE;
}
}
//在有序数组中搜索,在这之前要保证mid和target在同一边
if(nums[mid] < target){
low = mid+1;
}else{
high = mid-1;
}
}
return -1;
}
#34 在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
你的算法时间复杂度必须是 O(log n) 级别。
如果数组中不存在目标值,返回 [-1, -1]。
public int[] searchRange(int[] nums, int target) {
int[] res = {-1,-1};
if(nums == null || nums.length == 0) return res;
int left = 0;
int right = nums.length-1;
while(left <= right){
// System.out.println(left+" "+right);
if(nums[left] > target) break;
if(nums[left] == target && nums[right] > target){
res[0] = left;
right–;
}else if(nums[left] < target && nums[right] == target){
res[1] = right;
left++;
}else if(nums[left] == target && nums[right] == target){
res[0] = left;
res[1] = right;
break;
}else{
left++;
right–;
}
}
// printArr(res);
return res;
}
这题简单,二分查找法搞定:
class Solution {
public int[] searchRange(int[] nums, int target) {
int left = 0;
int right = nums.length-1;
int mid = 0;
while(left<=right){
mid = left+(right-left)/2;
if(nums[mid]==target)break;
else if(nums[mid]>target)right = mid-1;
else left = mid+1;
}
if(left>right)return new int[]{-1,-1};
int start = mid;
int end = mid;
while(start>=0&&nums[start]==target)start–;
while(end
}
}
35 搜索插入位置
在有序数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
public int searchInsert(int[] nums, int target) {
if(nums == null || nums.length == 0) return 0;
int flag = 0;
for(int i = 0; i < nums.length; i++){
if(nums[i] == target){
return i;
}else if(nums[i] < target){
flag++;
}
}
return flag;
}
#第一个错误的版本
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
就是经典的二分查找,需要注意的是,当当前版本是正确版本时,二分查找的左边界毫无疑问为当前版本号加一,但是若当前版本是错误版本,则二分查找的右边界不应该是常规的当前版本号减一,而应该就是当前版本号。
public class Solution extends VersionControl {
public int firstBadVersion(int n) {
return binarySearch(1, n);
}
public int binarySearch(int left, int right){
if(left>=right)return left;
int mid = left+(right-left)/2;
if(!isBadVersion(mid))return binarySearch(mid+1, right);
else return binarySearch(left, mid);
}
}
#找出一个无序数组的中位数
方案一:先排序再返回中位数
public static double median(int[] array) {
Arrays.sort(array);
int len = array.length;
if(len%21)return array[len/2];
else return (double)(array[len/2-1]+array[len/2])/2;
}
方案二:利用快排思想,每次选取一个基准进行一趟快排,若此时该基准的索引小于len/2则说明中位数在左边,否则在右边,递归调用直到基准索引恰好等于len/2。
public static int median(int[] nums, int start, int end){
int left = start;
int right = end;
int key = nums[left];
while(left
nums[left] = nums[right];
while(left
}
nums[left] = key;
int len = nums.length;
if(left
else if(left>len/2)return median(nums, start, left-1);
else return median(nums, left+1, end);
}
方案三:利用小顶堆,使用数组的前len/2+1个元素建立一个小顶堆,然后遍历后面的元素,如果当前元素大于堆顶元素,则将堆顶元素替换为当前元素,然后调整堆。最后,堆顶的元素即为数组的中位数。
public static double median(int[] nums){
PriorityQueue heap = new PriorityQueue<>();
int len = nums.length;
for(int i=0;i
heap.poll();
heap.add(nums[i]);
}
}
if(len%21)return heap.peek();
else return (double)(heap.poll()+heap.peek())/2;
}
方案四:创建一个大顶堆和一个小顶堆,大顶堆中存放较小数,小顶堆中存放较大数。当前元素下标为奇数时放入小顶堆,为偶数时放入大顶堆。此方案适合实时获取当前数组的中位数。
public static double median(int[] nums){
//小顶堆中存放较大的元素
PriorityQueue minHeap = new PriorityQueue<>();
//大顶堆中存放较小的元素
PriorityQueue maxHeap = new PriorityQueue(new MyComparator());
int len = nums.length;
for(int i=0;i
//放入小顶堆之前要先判断该元素是否小于大顶堆的堆顶元素
if(!maxHeap.isEmpty()&&nums[i]
maxHeap.offer(nums[i]);
}else minHeap.offer(nums[i]);
}else{
//放入大顶堆之前要先判断该元素是否大于小顶堆的堆顶元素
if(!minHeap.isEmpty()&&nums[i]>minHeap.peek()){
maxHeap.offer(minHeap.poll());
minHeap.offer(nums[i]);
}else maxHeap.offer(nums[i]);
}
}
if(len%2==1)return maxHeap.peek();
else return (double)(minHeap.peek()+maxHeap.peek())/2;
}
颜色分类
给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
一共需要三个指针!!一个左边界,一个右边界,一个当前元素。
class Solution {
public void sortColors(int[] nums) {
int left = 0;
int right = nums.length-1;
int i=0;
while(i<=right){
if(nums[i]==0){
swap(nums, i, left++);
i++;
}else if(nums[i]==1){
i++;
}else{
swap(nums, i, right–);
}
}
}
public void swap(int[] nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
#前K个高频元素
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
首先还是用HashMap统计出每个元素出现的次数,然后建立一个数组,数组的下表对应着不同的频次。有可能会出现两个不同元素,但它们出现的次数相同,因此数组中每个元素是一个链表。最后按索引从高到低遍历数组,找出k个元素。
class Solution {
public List topKFrequent(int[] nums, int k) {
List list=new ArrayList<>();
HashMap
int len=nums.length;
for(int i=0;i
if(count==null)frequent.put(nums[i],1);
else frequent.put(nums[i],count+1);
}
List[] bucket=new List[len+1];
for(Integer key:frequent.keySet()){
int count=frequent.get(key);
if(bucket[count]==null){
ArrayList temp=new ArrayList<>();
temp.add(key);
bucket[count]=temp;
}else bucket[count].add(key);
}
for(int i=len;i>=0;i–){
List temp=bucket[i];
if(temp!=null){
for(int j=0;j
}
}
}
return list;
}
}
#数组中的第K个最大元素
在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
用快排!
class Solution {
public int findKthLargest(int[] nums, int k) {
return quickSort(nums, k-1, 0, nums.length-1);
}
public int quickSort(int[] nums, int k, int start, int end){
int left = start;
int right = end;
int key = nums[left];
while(left
while(left
nums[right] = nums[left];
}
nums[left] = key;
if(left==k)return nums[left];
else if(left
}
}
#寻找峰值
峰值元素是指其值大于左右相邻值的元素。
给定一个输入数组 nums,其中 nums[i] ≠ nums[i+1],找到峰值元素并返回其索引。
数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞。
你的解法应该是 O(logN) 时间复杂度的。
采用二分搜索,每次迭代计算出中间元素的索引,如果中间元素大于其相邻的后续元素,说明中间元素左侧(包括中间元素)必然包含一个局部最大值,因此将右边界设为中间元素的索引;如果中间元素小于其相邻的后续元素,说明中间元素右侧(不包括中间元素)必然包含一个局部最大值,因此将左边界设为mid+1。
class Solution {
public int findPeakElement(int[] nums) {
int left = 0;
int right = nums.length-1;
while(left
if(nums[mid]>nums[mid+1])right = mid;
else left = mid+1;
}
return left;
}
}
合并区间
给出一个区间的集合,请合并所有重叠的区间。
class Solution {
public List merge(List intervals) {
List res = new ArrayList<>();
if(intervals.isEmpty())return res;
Collections.sort(intervals, new startComparator());
Iterator it = intervals.iterator();
Interval first = (Interval)it.next();
int start = first.start;
int end = first.end;
while(it.hasNext()){
Interval curr = (Interval)it.next();
if(curr.start>end){
res.add(new Interval(start, end));
start = curr.start;
end = curr.end;
}else end = Math.max(end, curr.end);
}
res.add(new Interval(start, end));
return res;
}
}
class startComparator implements Comparator{
public int compare(Interval i1, Interval i2){
if(i1.start>i2.start)return 1;
else if(i1.start
}
}
搜索二维矩阵 II
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target。该矩阵具有以下特性:
● 每行的元素从左到右升序排列。
● 每列的元素从上到下升序排列。
从数组的右上角开始搜索,当前元素小于目标值则下移一行,当前元素大于目标值则左移一列。
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int row = matrix.length;
if(row0)return false;
int col = matrix[0].length;
if(col0)return false;
int i = 0;
int j = col-1;
while(i
if(matrix[i][j]==target)return true;
else if(matrix[i][j]>target)j–;
else i++;
}
return false;
}
}
跳跃游戏
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个位置。
class Solution {
public boolean canJump(int[] nums) {
int len = nums.length;
int farest = 0;
for(int i=0;i<=farest;i++){
if(farest>=len-1)return true;
farest = Math.max(farest, i+nums[i]);
}
return false;
}
}
动态规划
https://blog.csdn.net/hollis_chuang/article/details/103045322?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522164741093916780366519238%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=164741093916780366519238&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_positive~default-1-103045322.142v2pc_search_insert_es_download,143v4control&utm_term=%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92&spm=1018.2226.3001.4187
70 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
public int climbStairs(int n) {
if(n == 1) return 1;
if(n == 2) return 2;
int[] dp = new int[n+1];
dp[1] = 1;
dp[2] = 2;
for(int i = 3; i < n+1; i++){
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
121 买卖股票的最佳时机
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
注意你不能在买入股票前卖出股票。
输入:[7,1,5,3,6,4]
输出:5
在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
动态规划
1.一维数组,时间复杂度O(N),空间复杂度O(N)
public int maxProfit(int[] prices) {
if(prices.length <= 1) return 0;
int[] dp = new int[prices.length]; //第i天的利润
dp[0] = 0;
int max = 0;
for(int i = 1; i < prices.length; i++){
dp[i] = Math.max(dp[i-1] + prices[i]-prices[i-1], 0);
max = Math.max(max, dp[i]);
}
return max;
}
优化dp空间
因为第i天的利润只和前一天相关,时间复杂度O(N),空间复杂度O(1)
public int maxProfit(int[] prices) {
if(prices.length <= 1) return 0;
int pre = 0, cur = 0;
int max = 0;
for(int i = 1; i < prices.length; i++){
cur = Math.max(pre + prices[i]-prices[i-1], 0);
max = Math.max(max,cur);
pre = cur;
}
return max;
}
53 最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
class Solution {
public int maxSubArray(int[] nums) {
int max = nums[0];
int sum = nums[0];
for(int i=1;i
max = Math.max(max, sum);
}
return max;
}
}
动态规划:
示例:输入:[-2,1,-3,4,-1,2,1,-5,4] 输出: 6
public int maxSubArraydp(int[] nums) {
int[] dp = new int[nums.length];//dp[i]表示到第i个元素的最大和
dp[0] = nums[0];//初始值
int max = nums[0];
for(int i = 1; i < nums.length; i++){
dp[i] = Math.max(nums[i],nums[i]+dp[i-1]);//关系式
max = Math.max(max,dp[i]);
}
return max;
}
#55 跳跃游戏 倒推!!
非负整数数组,位于第一个下标,每个下标代表在该位置可以跳跃的最大长度,判断能否到最后一个下标
思路1:
动态规划
dp表示第i个位置是否可达。可达的条件是i-1可达且nums[i-1]大于1,但对于nums[i]
为0的情况,要找max值跳过来的余量作为值。
问题:0值时只考虑到从最大值跳过来,忽略了中间的值
// nums = [2,3,1,1,4] true
// nums = [3,2,1,0,4] false
// nums = [2,0,0] true
// nums = [3,0,8,2,0,0,1] true
// [5,9,3,2,1,0,2,3,3,1,0,0] true 不通过
通过率164/169
public boolean canJump(int[] nums) {
int[] dp = new int[nums.length]; //表示i位置能否可达
dp[0] = 1;
int max = nums[0];
int maxindex = 0;
for(int i = 1; i < nums.length; i++) {
max = Math.max(max,nums[i]);
if(max == nums[i]){
maxindex = i;
}
if(dp[i-1] == 1 && nums[i-1] >= 1){
if(nums[i] == 0){
nums[i] = nums[maxindex] - i + maxindex;
}
dp[i] = 1;
}else if(dp[i-1] == 0){
return false;
}
}
return dp[nums.length-1] == 1;
}
思路2:贪心 逆序倒推
倒推看能否推到第一个位置
index:从后往前推能到达的下标。能从i到index的条件是i与index的距离<=位置i的值
public boolean canJumpFunc2(int[] nums) {
int index = nums.length-1;
for(int i = nums.length-2; i >= 0; i–){
//因为倒推的话最后一个下标肯定是可达的
if(index-i <= nums[i]){//能从i到index的条件
index = i;
}
}
return index==0;
}
贪心 正序推
对位置x能达到的位置最远是x+nums[x],从x至x+nums[x]之间所有位置均可达,所以判断最后一个位置,就遍历每一个位置,并判断其能到达的最远位置是否已经覆盖了最后一个位置。遍历结束后最后一个仍不可达即false。
执行用时:2 ms, 在所有 Java 提交中击败了94.56%的用户
内存消耗:41.8 MB, 在所有 Java 提交中击败了44.90%的用户
// 3 2 1 0 4
public boolean canJumpFunc2(int[] nums) {
int maxTouch = 0;
for(int i = 0; i < nums.length; i++){
if(i <= maxTouch){ //关键 3 2 1 0 4 当游标已经大于上一轮的可达位置时,表示此处不可达
maxTouch = Math.max(maxTouch, i + nums[i]);
if(maxTouch >= nums.length-1) return true;
}
}
return false;
}
#45 跳跃游戏II 贪心
数组元素表示能跳跃的最大步数,求到达最后位置最小的跳跃次数。
执行用时:1 ms, 在所有 Java 提交中击败了99.19%的用户
内存消耗:41.6 MB, 在所有 Java 提交中击败了62.38%的用户
重点:
1.最后一个位置不访问
2.需要i <= maxTouch
//使用最小的跳跃次数达到最后一个位置 2 3 1 1 4
public int jump(int[] nums) {
//当前位置能达到的最远位置
int maxTouch = 0;
//上一次跳的最远边界,遍历位置达到时可以更新为maxTouch,然后进行下一次跳跃
int end = 0;
int step = 0;
for(int i = 0; i < nums.length - 1; i++){//最后一个位置不访问,否则如果是刚好可达最后一个会多加一次
if(i <= maxTouch){ // 保证最后位置可达,如果游标已经大于上一轮的最大可达,表示此处不可达
maxTouch = Math.max(maxTouch, i + nums[i]);
if(i == end){
end = maxTouch;
step++;
}
}
}
return step;
}
#122 买卖股票的最佳时机II 二维dp
多笔交易,但同一时刻只能持一股
方法1:动态规划
执行用时:3 ms, 在所有 Java 提交中击败了23.93%的用户
内存消耗:41.4 MB, 在所有 Java 提交中击败了21.71%的用户
public int maxProfitII(int[] prices) {
if(prices.length <= 1) return 0;
int[][] dp = new int[prices.length][2];
//i表示第i天,j为0表示没有持有股票,1表示持有股票,最后一天的最大利润即dp[n-1][0]
dp[0][0] = 0;
dp[0][1] = -prices[0];
for(int i = 1; i < prices.length; i++){
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
}
return dp[prices.length-1][0];
}
方法2:贪心
执行用时:1 ms, 在所有 Java 提交中击败了85.51%的用户
内存消耗:41.4 MB, 在所有 Java 提交中击败了29.69%的用户
public int maxProfit(int[] prices) {
if(prices.length <= 1) return 0;
int max = 0;
for(int i = 1; i < prices.length; i++){
max += Math.max(prices[i] - prices[i - 1], 0);
}
return max;
}
#123 买卖股票的最佳时机III
最多交易2次
有以下五种状态:
● 未进行过任何操作;
● 只进行过一次买操作;
● 进行了一次买操作和一次卖操作,即完成了一笔交易;
● 在完成了一笔交易的前提下,进行了第二次买操作;
● 完成了全部两笔交易。
方法1:三维dp
注意:
1.不存在之前持两股今日卖出交易一次,因为不能同时参与多笔交易
2.注意初始值的设计
public int maxProfitIII(int[] prices) {
if(prices.length <= 1) return 0;
int[][][] dp = new int[prices.length][2][3];
//初始值
dp[0][0][0] = 0;
dp[0][1][0] = -prices[0];
dp[0][0][1] = Integer.MIN_VALUE/2;//因为min-1=max 影响最大利润比较
dp[0][0][2] = Integer.MIN_VALUE/2;
dp[0][1][1] = Integer.MIN_VALUE/2;
dp[0][1][2] = Integer.MIN_VALUE/2;
for(int i = 1; i < prices.length; i++){
//未持股,未卖出
dp[i][0][0] = 0;
//未持股,卖出一次:今日卖出或之前卖出,之前卖出,今日利润=i-1日利润
dp[i][0][1] = Math.max(dp[i-1][0][1],dp[i-1][1][0]+prices[i]);
dp[i][0][2] = Math.max(dp[i-1][0][2],dp[i-1][1][1]+prices[i]);
dp[i][1][0] = Math.max(dp[i-1][1][0],dp[i-1][0][0]-prices[i]);
//持股,卖出1次,因为只能买卖买卖,所以不存在之前买了2次,当天卖出的情况
// dp[i-1][1][0]+prices[i] 的状态变化为–> dp[i][0][1]
dp[i][1][1] = Math.max(dp[i-1][1][1],dp[i-1][0][1]-prices[i]);
dp[i][1][2] = Integer.MIN_VALUE/2;
}
int maxdp = Math.max(dp[prices.length-1][0][1],dp[prices.length-1][0][2]);
return Math.max(0,maxdp);//未交易为0
}
方法2:节省空间
状态:
买1[i] —— 不买的利润:买1[i-1],买入的利润: - price,即:
买1[i] = max(买1[i-1], - prices[i])
卖1[i] = max(卖1[i-1], 买1[i-1] + prices[i])
买2[i] = max(买2[i-1], 卖1[i-1] - prices[i])
卖2[i] = max(卖2[i-1], 买2[i-1] + prices[i])
因为是否在同一天买入且卖出不会影响最后的收益,所以状态方程可以把i-1忽略掉
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int buy1 = -prices[0], sell1 = 0;
int buy2 = -prices[0], sell2 = 0;
for (int i = 1; i < n; ++i) {
buy1 = Math.max(buy1, -prices[i]);
sell1 = Math.max(sell1, buy1 + prices[i]);
buy2 = Math.max(buy2, sell1 - prices[i]);
sell2 = Math.max(sell2, buy2 + prices[i]);
}
return sell2;
}
}
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/solution/mai-mai-gu-piao-de-zui-jia-shi-ji-iii-by-wrnt/
188 最多交易K次
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/solution/javayi-ge-si-lu-da-bao-suo-you-gu-piao-t-pd1p/
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/solution/188-by-lyyprogrammer-2mo4/
#309 最佳买卖股票时机含冷冻期 二维/三维
三维:dp[第i天][是否持股][是否冷冻期]
由于不能同时参与多笔交易,所以不存在又持股又在冷冻期,即dp[i][1][1] = Integer.MIN_VALUE/2;
状态关系:
不持股不冷冻 <— 前一天也不持股不冷冻,或前一天不持股冷冻
不持股冷冻 <— 前一天持股且卖出(仅此情况,不能按冷冻状态使其dp=dp[i-1][0][0],因为dp[i-1][0][0]有多种情况)
持股不冷冻 <— 前一天持股不冷冻,或今日买入
持股且冷冻 <— 不存在
执行用时:1 ms, 在所有 Java 提交中击败了78.09%的用户
内存消耗:41.2 MB, 在所有 Java 提交中击败了5.13%的用户
public int maxProfit309(int[] prices) {
if(prices.length == 0 || prices == null) return 0;
int[][][] dp = new int[prices.length][2][2];
dp[0][0][0] = 0;
dp[0][0][1] = Integer.MIN_VALUE/2;
dp[0][1][0] = -prices[0];
dp[0][1][1] = Integer.MIN_VALUE/2;
for(int i = 1; i < prices.length; i++){
dp[i][0][0] = Math.max(dp[i-1][0][0],dp[i-1][0][1]);
dp[i][0][1] = dp[i-1][1][0] + prices[i];
dp[i][1][0] = Math.max(dp[i-1][1][0],dp[i-1][0][0]-prices[i]);
dp[i][1][1] = Integer.MIN_VALUE/2;
}
return Math.max(0,Math.max(dp[prices.length-1][0][0],dp[prices.length-1][0][1]));
}
二维:dp[第i天][j] j:0 持股; 1 不持股不冻结; 2 不持股冻结
执行用时:1 ms, 在所有 Java 提交中击败了78.09%的用户
内存消耗:39.5 MB, 在所有 Java 提交中击败了46.76%的用户
public int maxProfit309II(int[] prices) {
int[][] dp = new int[prices.length][3];
// 0 持股; 1 不持股不冻结; 2 不持股冻结
dp[0][0] = -prices[0];
for(int i = 1; i < prices.length; i++){
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]-prices[i]);
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][2]);
dp[i][2] = dp[i-1][0]+prices[i];
}
return Math.max(dp[prices.length-1][1],dp[prices.length-1][2]);
}
背包问题
一套框架解决「背包问题」 - 零钱兑换 II - 力扣(LeetCode) (leetcode-cn.com)
01背包
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8
二维:
//容量为V的背包,每件物品只能使用一次,输出包里能装的最大价值
public void package01(int V, int[] v, int[] w){
//V:背包容量 v:体积 w:价值
int[][] dp = new int[v.length][V+1];
//dp[i][j]: i 第i件物品 j 当前背包剩余空间 dp:最大价值
dp[0][0] = 0;
for(int i = 1; i < v.length; i++){
for(int j = 0; j <= V; j++){
if(j >= v[i]){
dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
}else {
dp[i][j] = dp[i-1][j];
}
}
}
System.out.println(dp[v.length-1][V]);
}
一维:
dp[j]:背包空间为j时的最大价值。注意逆序
public void package01(int V, int[] v, int[] w){
//V:背包容量 v:体积 w:价值
int[] dp = new int[V+1];
//dp[i][j]: i 第i件物品 j 当前背包剩余空间 dp:最大价值
dp[0] = 0;
for(int i = 1; i < v.length; i++){
for(int j = V; j >= v[i]; j–){//逆序
dp[j] = Math.max(dp[j],dp[j-v[i]]+w[i]);
}
}
System.out.println(dp[V]);
}
如果要求装满背包,需要将dp数组初始化为负无穷,dp[0] = 0。
完全背包
一件物品可以无限次选择
F[0][] ← {0}
F[][0] ← {0}
for i←1 to N
do for j←1 to V
do for k←0 to j/C[i]
if(j >= kC[i])
then F[i][k] ← max(F[i][k],F[i-1][j-kC[i]]+k*W[i])
return F[N][V]
一维:
public void packageall(int V, int[] v, int[] w){
int[] dp = new int[V+1];
//dp[i][j]: i 第i件物品 j 当前背包剩余空间 dp:最大价值
dp[0] = 0;
for(int i = 0; i < v.length; i++){
for(int j = v[i]; j <= V; j++){//顺序
dp[j] = Math.max(dp[j],dp[j-v[i]]+w[i]);
}
}
System.out.println(dp[V]);
}
多重背包
给定每件物品的数量,即限制选择的次数
public void packagemul(int V, int[] v, int[] w, int[] num){
//V:背包容量 v:体积 w:价值 num:数量
int[] dp = new int[V+1];
//dp[i][j]: i 第i件物品 j 当前背包剩余空间 dp:最大价值
dp[0] = 0;
for(int i = 1; i < v.length; i++){
for(int j = V; j >= v[i]; j–){//逆序
for(int k = 0; k <= num[i] && j >= kv[i]; k++){
dp[j] = Math.max(dp[j],dp[j-kv[i]]+k*w[i]);
}
}
}
System.out.println(“multi:”+dp[V]);
}
#198 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
public int rob(int[] nums) {
if(nums.length == 0) return 0;
if(nums.length == 1) return nums[0];
int[] dp = new int[nums.length];
dp[0] = nums[0];
dp[1] = Math.max(nums[0],nums[1]);
if(nums.length == 2) return dp[1];
for(int i = 1; i < nums.length; i++){
dp[i] = Math.max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[nums.length-1];
}
内存优化:
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:38.8 MB, 在所有 Java 提交中击败了40.54%的用户
public int rob(int[] nums) {
if(nums.length == 0) return 0;
if(nums.length == 1) return nums[0];
int first = nums[0];
int second = Math.max(nums[0],nums[1]);
int res = second;
if(nums.length == 2) return second;
for(int i = 2; i < nums.length; i++){
res = Math.max(second,first+nums[i]);
first = second;
second = res;
}
return res;
}
#213 打家劫舍II
第一间和最后一间是连接的
思路:求第一间-第n-1间的最大金额与第二间-第n间的最大金额,然后取最大
public int robII(int[] nums) {
if(nums.length == 0) return 0;
if(nums.length == 1) return nums[0];
if(nums.length == 2) return Math.max(nums[0],nums[1]);
return Math.max(robRange(nums,0,nums.length-1),
robRange(nums,1,nums.length));
}
public int robRange(int[] nums, int begin, int end){
int first = nums[begin];
int second = Math.max(nums[begin],nums[begin+1]);
int res = second;
for(int i = begin+2; i < end; i++){
res = Math.max(second,first+nums[i]);
first = second;
second = res;
}
return res;
}
#337 打家劫舍III 树+dfs+dp
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。
除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:40.8 MB, 在所有 Java 提交中击败了46.17%的用户
public int robIII(TreeNode root) {
if(root == null) return 0;
//根据当前节点选||不选建立状态转移方程 —>
// 选:左子不选 + 右子不选 + 当前选
// 不选:左子选or不选 + 右子选or不选
//如何求每个节点选或不选能得到的最大sum —>
// 建立数组,0为不选,1为选
// 求左/右子的选或不选的sum —> dfs
int[] res = dfs337(root);
return Math.max(res[0],res[1]);
}
public int[] dfs337(TreeNode root){
int[] sum = new int[2];
if(root == null) return sum;
int[] left = dfs337(root.left);
int[] right = dfs337(root.right);
sum[0] = Math.max(left[0],left[1]) + Math.max(right[0],right[1]);
sum[1] = root.val + left[0] + right[0];
return sum;
}
不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
class Solution {
public int uniquePaths(int m, int n) {
int[][] paths = new int[n][m];
for(int i=0;i
else paths[i][j] = paths[i-1][j]+paths[i][j-1];
}
}
return paths[n-1][m-1];
}
}
#322 零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
常规动态规划-完全背包思路
执行用时:14 ms, 在所有 Java 提交中击败了41.96%的用户
内存消耗:41 MB, 在所有 Java 提交中击败了38.50%的用户
dp[i]定义为容量为i时的最少硬币数。
public int coinChange(int[] coins, int amount) {
//当容量为i时最少硬币个数
int[] dp = new int[amount + 1];
for(int i = 0; i < dp.length; i++){
dp[i] = Integer.MAX_VALUE;
}
dp[0] = 0;
for(int i = 0; i < coins.length; i++){
for(int j = coins[i]; j <= amount; j++){
if(dp[j - coins[i]] != Integer.MAX_VALUE){
dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
}
}
}
return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
}
#518 零钱兑换2
给出能组成amount的组合数量
思路1:类似组合总和39题,dfs 超时
注意,count要写成类成员变量,不然在dfs回溯时count会被清到上一个值。
int sum = 0;
int count = 0;
public int change(int amount, int[] coins) {
//dfs
dfs518(amount, coins, 0);
return count;
}
private void dfs518(int amount, int[] coins, int begin) {
if(sum == amount){
count++;
return;
}
if(sum > amount) return;
for(int i = begin; i < coins.length; i++){
sum += coins[i];
dfs518(amount, coins, i);
sum -= coins[i];
}
}
思路2:dp 类似70题爬楼梯
public int change(int amount, int[] coins) {
//动态规划 dp[i]表示金额为i时的硬币组合数
int[] dp = new int[amount + 1];
dp[0] = 1;
for(int i = 0; i < coins.length; i++){
for(int j = coins[i]; j < dp.length; j++){
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
#983 最低票价 动态规划
https://leetcode-cn.com/problems/minimum-cost-for-tickets/solution/java-dong-tai-gui-hua-si-lu-bu-zou-cong-hou-xiang-/
思路:从后往前。
public int mincostTickets(int[] days, int[] costs) {
int daylen = days.length;
int maxday = days[daylen - 1];
int minday = days[0];
int[] dp = new int[maxday + 31];
int i = daylen - 1;
for(int d = maxday; d >= minday; d–){
if(d == days[i]){
dp[d] = Math.min(Math.min(costs[0] + dp[d + 1], costs[1] + dp[d + 7]), costs[2] + dp[d + 30]);
i–;
}else {
dp[d] = dp[d+1];
}
}
return dp[minday];
}
最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
● 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
● 你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
额,暂时还没有想到O(n log n)的解法,脑壳疼
class Solution {
public int lengthOfLIS(int[] nums) {
int len = nums.length;
int[] maxLen = new int[len];
for(int i=0;i
for(int j=0;j if(nums[i]>nums[j])maxLen[i] = Math.max(maxLen[i], maxLen[j]+1);
}
}
int res = 0;
for(int i=0;i
}
}
设计问题
Shuffle an Array
打乱一个没有重复元素的数组。
遍历原始的数组,每次循环中随机生成一个数组大小内的随机数,并将当前元素与索引为该随机数的元素交换。
class Solution {
private int len;
private int[] nums;
public Solution(int[] nums) {
len = nums.length;
this.nums = new int[len];
System.arraycopy(nums, 0, this.nums, 0, len);
}
/** Resets the array to its original configuration and return it. */
public int[] reset() {
return this.nums;
}
/** Returns a random shuffling of the array. */
public int[] shuffle() {
int[] shuffleNums = new int[len];
System.arraycopy(nums, 0, shuffleNums, 0, len);
Random rand = new Random();
for(int i=0;i
}
#最小栈
设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。
● push(x) – 将元素 x 推入栈中。
● pop() – 删除栈顶的元素。
● top() – 获取栈顶元素。
● getMin() – 检索栈中的最小元素。
使用两个栈,一个栈用来进行常规的压栈、弹栈和返回栈顶元素等操作,另一个栈用来保存当前最小的栈内元素。
class MinStack {
private Stack dataStack;
private Stack minStack;
/** initialize your data structure here. */
public MinStack() {
dataStack = new Stack<>();
minStack = new Stack<>();
}
public void push(int x) {
dataStack.push(x);
if(minStack.isEmpty()||x<=minStack.peek())minStack.push(x);
}
public void pop() {
int x = dataStack.pop();
if(x==minStack.peek())minStack.pop();
}
public int top() {
return dataStack.peek();
}
public int getMin() {
return minStack.peek();
}
}
#二叉树的序列化与反序列化
序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。
请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
if(root==null)return "null";
String res = "";
Queue queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()){
TreeNode curr = queue.poll();
if(curr==null)res += "null ";
else{
res += curr.val+" ";
queue.offer(curr.left);
queue.offer(curr.right);
}
}
return res.trim();
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
if(data.equals("null"))return null;
String[] strs = data.split(" ");
int index = 0;
TreeNode root = createTreeNode(strs[index++]);
Queue queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()){
TreeNode curr = queue.poll();
curr.left = createTreeNode(strs[index++]);
curr.right = createTreeNode(strs[index++]);
if(curr.left!=null)queue.offer(curr.left);
if(curr.right!=null)queue.offer(curr.right);
}
return root;
}
public TreeNode createTreeNode(String str){
if(str.equals("null"))return null;
else return new TreeNode(Integer.parseInt(str));
}
}
#常数时间插入、删除和获取随机元素
设计一个支持在平均 时间复杂度 O(1) 下,执行以下操作的数据结构。
使用一个ArrayList来存储数据,使用HashMap来存储数据及其对应的索引。插入和随机返回元素都比较简单,关键是删除。
删除的时候先利用HashMap找到目标元素的索引值,然后使用ArrayList中的最后一个元素覆盖掉目标索引处的元素,最后删掉ArrayList中的最后一个元素。
class RandomizedSet {
ArrayList nums;
Map
public RandomizedSet() {
nums = new ArrayList();
map = new HashMap();
}
public boolean insert(int val) {
if(map.containsKey(val))return false;
map.put(val, nums.size());
nums.add(val);
return true;
}
public boolean remove(int val) {
if(!map.containsKey(val))return false;
int index = map.remove(val);
if(index!=nums.size()-1){
nums.set(index, nums.get(nums.size()-1));
map.put(nums.get(index), index);
}
nums.remove(nums.size()-1);
return true;
}
public int getRandom() {
Random rand = new Random();
return nums.get(rand.nextInt(nums.size()));
}
}
数学
Fizz Buzz
写一个程序,输出从 1 到 n 数字的字符串表示。
我不明白这道题想考什么??
class Solution {
public List fizzBuzz(int n) {
List result=new ArrayList<>();
for(int i=1;i<=n;i++){
if(i%150)result.add(“FizzBuzz”);
else if(i%30)result.add(“Fizz”);
else if(i%5==0)result.add(“Buzz”);
else result.add(String.valueOf(i));
}
return result;
}
}
计数质数
统计所有小于非负整数 n 的质数的数量。
class Solution {
public int countPrimes(int n) {
if(n<=2)return 0;
List primes = new ArrayList<>();
primes.add(2);
int sqrtRoot = 1;
for(int i=3;i
if(sqrtRoot*sqrtRoot for(int j=0;j
isPrime = false;
break;
}
}
if(isPrime)primes.add(i);
}
return primes.size();
}
}
#3的幂
给定一个整数,写一个函数来判断它是否是 3 的幂次方。
不使用递归或循环。
这解法太神奇了!
class Solution {
public boolean isPowerOfThree(int n) {
double temp = Math.log10(n)/Math.log10(3);
return temp==(int)temp? true:false;
}
}
#罗马数字转整数
罗马数字包含以下七种字符:I, V, X, L,C,D 和 M。
字符 数值
I 1
V 5
X 10
L 50
C 100
D 500
M 1000
例如, 罗马数字 2 写做 II ,即为两个并列的 1。12 写做 XII ,即为 X + II 。 27 写做 XXVII, 即为 XX + V + II 。
通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:
I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。
C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。
给定一个罗马数字,将其转换成整数。输入确保在 1 到 3999 的范围内。
使用HashMap存储字符和对应数值间的键值对。
当且仅当左边的字符对应的数值小于右边的字符对应的数值时才会是六种特殊情况之一。
class Solution {
public int romanToInt(String s) {
Map
map.put(‘I’, 1);
map.put(‘V’, 5);
map.put(‘X’, 10);
map.put(‘L’, 50);
map.put(‘C’, 100);
map.put(‘D’, 500);
map.put(‘M’, 1000);
int res = 0;
int i = 0;
while(i
i += 2;
}else{
res += map.get(s.charAt(i));
i++;
}
}
return res;
}
}
快乐数
编写一个算法来判断一个数是不是“快乐数”。
一个“快乐数”定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是无限循环但始终变不到 1。如果可以变为 1,那么这个数就是快乐数。
使用一个HashSet来存储出现过的数,如果出现1,直接返回true;如果重复出现了,就代表不是快乐数,返回false。
class Solution {
public boolean isHappy(int n) {
Set set = new HashSet<>();
while(n!=1){
if(set.contains(n))return false;
set.add(n);
int next = 0;
while(n!=0){
next += Math.pow(n%10, 2);
n /= 10;
}
n = next;
}
return true;
}
}
#阶乘后的零
给定一个整数 n,返回 n! 结果尾数中零的数量。
这种题目,还是死记住吧。
class Solution {
public int trailingZeroes(int n) {
int zero = 0;
while(n!=0){
zero += n/5;
n /= 5;
}
return zero;
}
}
Excel表列序号
给定一个Excel表格中的列名称,返回其相应的列序号。
例如,
A -> 1
B -> 2
C -> 3
…
Z -> 26
AA -> 27
AB -> 28
…
其实就是26进制转10进制。
class Solution {
public int titleToNumber(String s) {
int res = 0;
for(int i=0;i
res = res*26+curr;
}
return res;
}
}
其他
#位1的个数
编写一个函数,输入是一个无符号整数,返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为汉明重量)。
每次判断最低位是否为1,然后无符号右移一位,直到变为0。
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int count = 0;
while(n!=0){
if((n&1)==1)count++;
n >>>= 1;
}
return count;
}
}
汉明距离
两个整数之间的汉明距离指的是这两个数字对应二进制位不同的位置的数目。
给出两个整数 x 和 y,计算它们之间的汉明距离。
上一题的变种。
class Solution {
public int hammingDistance(int x, int y) {
int distance = 0;
int n = x^y;
while(n!=0){
if((n&1)==1)distance++;
n >>>= 1;
}
return distance;
}
}
颠倒二进制位
颠倒给定的 32 位无符号整数的二进制位。
public class Solution {
// you need treat n as an unsigned value
public int reverseBits(int n) {
int m = 0;
for(int i=0;i<32;i++){
m <<= 1;
m |= n&1;
n >>= 1;
}
return m;
}
}
杨辉三角
给定一个非负整数 numRows,生成杨辉三角的前 numRows 行。
在杨辉三角中,每个数是它左上方和右上方的数的和。
class Solution {
public List generate(int numRows) {
List res = new ArrayList<>();
for(int i=0;i
list.add(1);
for(int j=1;j if(i>0)list.add(1);
res.add(list);
}
return res;
}
}
20 有效的括号
给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串,判断字符串是否有效。
有效字符串需满足:
● 左括号必须用相同类型的右括号闭合。
● 左括号必须以正确的顺序闭合。
● 注意空字符串可被认为是有效字符串。
class Solution {
public boolean isValid(String s) {
int len = s.length();
if(len0)return true;
Stack stack = new Stack<>();
for(int i=0;i
if(curr
else if(curr==’[’)stack.push(’]’);
else if(curr==’{’)stack.push(’}’);
else if(stack.isEmpty()||stack.pop()!=curr)return false;
}
return stack.isEmpty();
}
}
缺失数字
给定一个包含 0, 1, 2, …, n 中 n 个数的序列,找出 0 … n 中没有出现在序列中的那个数。
class Solution {
public int missingNumber(int[] nums) {
int len = nums.length;
int sum = 0;
for(int i=1;i<=len;i++)sum += i;
for(int i=0;i
}
}
#朋友圈
/*
班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。
给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。
*/
public class UnionFind_Friends {
public static void main(String[] args) {
int[][] M = {{1,1,0},
{1,1,0},
{0,1,1}};
UnionFind_Friends u = new UnionFind_Friends();
System.out.println(u.findCircleNum(M));
}
//连通分量的个数
private int count;
private int[] parent;
//以索引为i的元素为根节点的树的深度
private int[] rank;
public int findCircleNum(int[][] M) {
int n = M.length;
this.count = n;
this.parent = new int[n];
this.rank = new int[n];
for(int i = 0; i < n; i++) {
this.parent[i] = i;
this.rank[i] = 1;//初始化时所有元素只包含自己,所以为1
}
for(int i = 0; i < n; i++) {
for(int j = 0; j < i; j++) {
if(M[i][j] == 1) {
union(i,j);
}
}
}
return this.count;
}
public int find(int p) {
//路径压缩
while(p != this.parent[p]) {
this.parent[p] = this.parent[this.parent[p]];
p = this.parent[p];
}
return p;
}
public void union(int p, int q) {
int proot = find§;
int qroot = find(q);
if(proot == qroot) {
return;
}
if(rank[proot] > rank[qroot]) {
parent[qroot] = proot;
}else if(rank[proot] < rank[qroot]) {
parent[proot] = qroot;
}else {
parent[qroot] = proot;
rank[proot]++;
}
count–;//每次union后连通分量-1
}
public int getCount() {
return this.count;
}
public boolean isConnected(int p, int q) {
return find§==find(q);
}
}
1447最简分数
互质判断:a与b相除余数为r,互质的两个数辗转相除后,当余数为0时,只有被1除
回溯算法
● java是值传递,对象类型变量在传参的过程中复制的是变量的地址,被添加到res,但实际上指向的是同一块内存,stack指向的栈在dfs中只有一份,遍历完成后回到根节点,就成了空列表,所以res.add()的时候要做一次拷贝。
if (depth == len) {
res.add(new ArrayList<>(path)); //res.add(path) ×
return;
}
● dfs遍历结束后要回到上一层结点,需要撤销上一次的操作,重置状态和路径stack尾部的值
path.add(nums[i]);
used[i] = true;
dfs(nums, len, depth + 1, path, used, res);
// 注意:下面这两行代码发生 「回溯」,回溯发生在从 深层结点 回到 浅层结点 的过程,代码在形式上和递归之前是对称的
used[i] = false;
path.remove(path.size() - 1);
● 状态变量:begin used[]——排列问题讲究顺序,需要记录哪些数字已经使用,需要used数组;组合问题不讲究顺序,2 2 3与2 3 2视为同一个结果,需要按照某种顺序搜索,需要begin
● 用剪枝提高效率,剪枝需要先排序,对于组合总和问题,总和<当前数组元素时剪枝。画树形图,思考剪枝的条件
题型一:排列、组合、子集相关问题46 47 39 40 77 78 90 60 93
提示:这部分练习可以帮助我们熟悉「回溯算法」的一些概念和通用的解题思路。解题的步骤是:先画图,再编码。去思考可以剪枝的条件, 为什么有的时候用 used 数组,有的时候设置搜索起点 begin 变量,理解状态变量设计的想法。
#46 全排列 dfs 回溯+used数组
不重复数组,返回所有排列
用used数组标记已使用过的数字,避免重复
复杂度分析:
时间复杂度:N×N!组合的数量为ANN,即N!,然后需要遍历一遍N
空间复杂度:N×N!递归树深度logN;全排列个数N!,每个全排列占空间N,取较大者
public List permute(int[] nums) {
List res = new ArrayList
();
if(nums.length == 0) return res;
Stack path = new Stack();
boolean used[] = new boolean[nums.length];
dfs46(nums,used,path,res);
printList(res);
return res;
}
public void dfs46(int[] nums, boolean[] used, Stack stack, List res){
//边界判断
if(stack.size() == nums.length){
//做拷贝
res.add(new ArrayList(stack));
return;
}
for(int i = 0; i < nums.length; i++){
//使用过的数字
if(used[i]) continue;
//加入路径,并更新状态
stack.push(nums[i]);
used[i] = true;
dfs46(nums, used, stack, res);
//回溯发生在从深层结点回到浅层结点的过程,和在递归之前的代码对称
used[i] = false;
stack.pop();
}
}
#47 全排列II dfs
数组中元素有重复,需要剪枝
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
46题的解法,输入[1,1,3]
输出[1,1,3,][1,3,1,][1,1,3,][1,3,1,][3,1,1,][3,1,1,]
方法1:用set去重
执行用时:74 ms, 在所有 Java 提交中击败了6.96%的用户
内存消耗:42.1 MB, 在所有 Java 提交中击败了22.44%的用户
public List permuteUnique(int[] nums) {
List res = new ArrayList
();
if(nums.length == 0) return res;
Stack path = new Stack();
boolean used[] = new boolean[nums.length];
Set set = new HashSet
();
dfs46(nums,used,path,set);
for(List list : set){
res.add(list);
}
return res;
}
public void dfs46(int[] nums, boolean[] used, Stack stack, Set res){
if(stack.size() == nums.length){
res.add(new ArrayList(stack));
return;
}
for(int i = 0; i < nums.length; i++){
if(used[i]) continue;
stack.push(nums[i]);
used[i] = true;
dfs46(nums, used, stack, res);
used[i] = false;
stack.pop();
}
}
方法2:在搜索的时候避免重复——剪枝
剪枝思路:
剪枝首先要排序,然后如果当前元素和前一个元素相同,有可能重复,但有一种1 1 2,第二个1是在第一个1的下一层,是没有问题的,前一个元素被选择时的相同元素是没有问题的,即used[i-1]为true表示当前i是下一层;used[i-1]为false表示和i是同一层,值重复就要剪枝。
if(i > 0 && nums[i] == nums[i-1] && !used[i-1]) continue;
public List permuteUnique(int[] nums) {
List res = new ArrayList
();
if(nums.length == 0) return res;
Stack path = new Stack();
Arrays.sort(nums);
boolean used[] = new boolean[nums.length];
dfs47(nums,used,path,res);
return res;
}
public void dfs47(int[] nums,boolean[] used, Stack stack, List res){
if(stack.size() == nums.length){
res.add(new ArrayList(stack));
return;
}
for(int i = 0; i < nums.length; i++){
if(used[i]) continue;
if(i > 0 && nums[i] == nums[i-1] && !used[i-1]) continue;
stack.push(nums[i]);
used[i] = true;
dfs47(nums, used, stack, res);
used[i] = false;
stack.pop();
}
}
#39 组合总和 回溯+剪枝+begin
题解中有对回溯的理解
● 无重复数组candidates,数字和为target的所有不同组合
● 同一个数字可以无限制重复被选,返回不重复的组合(组合内的数字不计顺序)!!![2,2,3] [2,3,2]视为同一个组合
● 去重的关键操作:搜索起点从当前元素开始及以后,不可以再搜索之前搜索过的元素
● 搜搜路径就是结果,栈
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
public List combinationSum(int[] candidates, int target) {
List res = new ArrayList
();
if(candidates.length == 0) return res;
Stack stack = new Stack();
dfs39(candidates,0,target,stack,res);
return res;
}
public void dfs39(int[] candidates, int begin, int target, Stack stack, List res){
if(target < 0) return;//剪枝
if(target == 0) {
res.add(new ArrayList(stack));
return;
}
//搜索
for(int j = begin; j < candidates.length; j++){
//把路径加入
stack.push(candidates[j]);
dfs39(candidates, j,target-candidates[j], stack, res);//搜索起点begin还是j,因为同一个数字可以无限制重复被选
//因为每次搜索要从当前层开始,所以上次搜索添加的路径要清除
stack.pop();
}
}
#40 组合总和II 回溯+剪枝+begin
有重复数组,且每个下标的数字在每个组合只能使用一次。
输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]
重点:同一层相同数字剪枝,i > begin && can[i]==can[i-1] i>0时,i-1是i的下一层
public List combinationSum2(int[] candidates, int target) {
List res = new ArrayList
();
if(candidates.length == 0) return res;
Stack path = new Stack();
Arrays.sort(candidates); //[1,1,2,5,6,7,10]
dfs40(candidates,target,0, path,res);
// printList(res);
return null;
}
public void dfs40(int[] candidates, int target, int begin, Stack path, List> res){
if(target == 0){
res.add(new ArrayList(path));
// System.out.println();
return;
}
for(int i = begin; i < candidates.length; i++){
// System.out.println(“i:”+i+",begin:"+begin);
if(target - candidates[i] < 0) return;
if(i > begin && candidates[i] == candidates[i-1]){
//为什么i>begin ? 同一层当前值=之前的值就要剪枝 i>0会剪掉不同层级的相同元素,
continue;
}
path.push(candidates[i]);
// System.out.println(“before:”+path);
dfs40(candidates,target-candidates[i],i+1, path,res);
path.pop();
// System.out.println(“after:”+path);
}
}
#77 组合 回溯+begin
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
//给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
public List combine(int n, int k) {
List res = new ArrayList
();
if(k <= 0 || n < k) return res;
Stack path = new Stack();
dfs77(n,k,1,path,res);
printList(res);
return res;
}
public void dfs77(int n, int k, int begin, Stack path, List res){
if(path.size() == k){
res.add(new ArrayList(path));
return;
}
//搜索
for(int i = begin; i <= n; i++){
path.push(i);
dfs77(n,k,i+1,path,res);
path.pop();
}
}
#78 子集
给你一个无重复整数数组 nums ,返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
思路:每次遍历都会写入path,终止的条件是begin超过了数组的长度
public List subsets(int[] nums) {
List res = new ArrayList
();
Stack path = new Stack();
dfs78(nums, 0, path, res);
return res;
}
public void dfs78(int[] nums, int begin, Stack path, List res){
if(begin > nums.length) return;
if(begin <= nums.length){
res.add(new ArrayList(path));
}
for(int i = begin; i < nums.length; i++){
path.push(nums[i]);
dfs78(nums,i+1,path,res);
path.pop();
}
}
#90 子集II
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
public List subsetsWithDup(int[] nums) {
List res = new ArrayList
();
Stack path = new Stack();
Arrays.sort(nums);
dfs90(nums,0,path,res);
return res;
}
public void dfs90(int[] nums, int begin, Stack path, List res){
if(begin > nums.length) return;
if(begin <= nums.length){
res.add(new ArrayList(path));
}
for(int i = begin; i < nums.length; i++){
if(i > begin && nums[i] == nums[i-1]) continue;
path.push(nums[i]);
System.out.println(path);
dfs90(nums,i+1,path,res);
path.pop();
}
}
题型二:Flood Fill 200 130 79 733
提示:Flood 是「洪水」的意思,Flood Fill 直译是「泛洪填充」的意思,体现了洪水能够从一点开始,迅速填满当前位置附近的地势低的区域。类似的应用还有:PS 软件中的「点一下把这一片区域的颜色都替换掉」,扫雷游戏「点一下打开一大片没有雷的区域」。
下面这几个问题,思想不难,但是初学的时候代码很不容易写对,并且也很难调试。我们的建议是多写几遍,忘记了就再写一次,参考规范的编写实现(设置 visited 数组,设置方向数组,抽取私有方法),把代码写对。
题型三:字符串中的回溯问题 17 22 784
提示:字符串的问题的特殊之处在于,字符串的拼接生成新对象,因此在这一类问题上没有显示「回溯」的过程,但是如果使用 StringBuilder 拼接字符串就另当别论。
在这里把它们单独作为一个题型,是希望朋友们能够注意到这个非常细节的地方。
#17 电话号码
public List letterCombinations(String digits) {
List res = new ArrayList();
if(digits == null||digits.length() == 0) return res;
StringBuffer sb = new StringBuffer();
String[] strarr = {“abc”,“def”,“ghi”,“jkl”,“mno”,“pqrs”,“tuv”,“wxyz”};
dfs17(digits,strarr,0,sb,res);
return res;
}
public void dfs17(String digits, String[] strarr, int index, StringBuffer sb, List res){
if(sb.length() == digits.length()){
res.add(new String(sb.toString()));
return;
}
String substr = strarr[Integer.parseInt(digits.substring(index,index+1))-2];
for(int j = 0; j < substr.length(); j++){
sb.append(substr.substring(j,j+1));
dfs17(digits,strarr,index+1,sb,res);
sb.deleteCharAt(sb.length()-1);
}
}
#784 字母大小写全排列
给定一个字符串 s ,通过将字符串 s 中的每个字母转变大小写,我们可以获得一个新的字符串。
输入:s = “a1b2”
输出:[“a1b2”, “a1B2”, “A1b2”, “A1B2”]
只需要转换大小写,不需要打乱顺序全排列。
public List letterCasePermutation(String s) {
List res = new ArrayList();
char[] chararr = s.toCharArray();
dfs784(s,chararr, 0,res);
return res;
}
public void dfs784(String s, char[] chararr, int depth,List res){
if(depth == s.length()){
res.add(new String(chararr));
return;
}
dfs784(s,chararr, depth+1, res);
if(!Character.isDigit(chararr[depth])) {
chararr[depth] ^= 1 << 5;
dfs784(s,chararr, depth+1, res);
}
}
#22 括号生成 回溯+剪枝
以空字符串为根节点
public List generateParenthesis(int n) {
List res = new ArrayList();
if(n == 0) return res;
dfs("",n,n,res);
return res;
}
public void dfs(String str, int left, int right, List res){
if(left == 0 && right == 0) {
res.add(str);
return; //结算,递归终止
}
if(left > right) return; //剪枝
if(left > 0){
dfs(str+"(",left-1,right,res);
}
if(right > 0){
dfs(str+")",left,right-1,res);
}
}
题型四:游戏问题 51 37 488 529[困难]
回溯算法是早期简单的人工智能,有些教程把回溯叫做暴力搜索,但回溯没有那么暴力,回溯是有方向地搜索。「力扣」上有一些简单的游戏类问题,解决它们有一定的难度,大家可以尝试一下。
作者:liweiwei1419
链接:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
DFS
排列组合问题见回溯
二叉树的DFS:
void traverse(TreeNode root) {
// 判断 base case
if (root == null) {
return;
}
// 访问两个相邻结点:左子结点、右子结点
traverse(root.left);
traverse(root.right);
}
112 路径总和
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。
1.DFS
public boolean hasPathSum(TreeNode root, int targetSum) {
if(root == null) return false;
if(root.left == null && root.right == null){
return root.val == targetSum;
}
return hasPathSum(root.left, targetSum - root.val) ||
hasPathSum(root.right, targetSum - root.val);
}
#113路径总和II 回溯
时间复杂度:O(N^2),其中 N 是树的节点数。在最坏情况下,树的上半部分为链状,下半部分为完全二叉树,并且从根节点到每一个叶子节点的路径都符合题目要求。此时,路径的数目为 O(N),并且每一条路径的节点个数也为 O(N),因此要将这些路径全部添加进答案中,时间复杂度为 O(N^2)。
空间复杂度:O(N),其中 N 是树的节点数。空间复杂度主要取决于栈空间的开销,栈中的元素个数不会超过树的节点数。
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/path-sum-ii/solution/lu-jing-zong-he-ii-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
执行用时:2 ms, 在所有 Java 提交中击败了12.65%的用户
内存消耗:41.8 MB, 在所有 Java 提交中击败了41.37%的用户
public List pathSum(TreeNode root, int targetSum) {
List res = new ArrayList
();
if(root == null) return res;
Stack path = new Stack();
dfs113(res, path, root, targetSum);
return res;
}
private void dfs113(List res, Stack path, TreeNode root, int targetSum) {
if(root == null) return;
path.push(root.val);
targetSum -= root.val;
if(targetSum == 0 && root.left == null && root.right == null) {
res.add(new ArrayList(path));
}
dfs113(res, path, root.left, targetSum);
dfs113(res, path, root.right, targetSum);
path.pop();
}
二叉树的BFS:
void bfs(TreeNode root) {
Queue queue = new ArrayDeque<>();
queue.add(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll(); // Java 的 pop 写作 poll()
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
}
}
作者:nettee
链接:https://leetcode-cn.com/problems/binary-tree-level-order-traversal/solution/bfs-de-shi-yong-chang-jing-zong-jie-ceng-xu-bian-l/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
网格的DFS
https://leetcode-cn.com/problems/number-of-islands/solution/dao-yu-lei-wen-ti-de-tong-yong-jie-fa-dfs-bian-li-/
void dfs(int[][] grid, int r, int c) {
// 判断 base case
// 如果坐标 (r, c) 超出了网格范围,直接返回
if (!inArea(grid, r, c)) {
return;
}
// 访问上、下、左、右四个相邻结点
dfs(grid, r - 1, c);
dfs(grid, r + 1, c);
dfs(grid, r, c - 1);
dfs(grid, r, c + 1);
}
// 判断坐标 (r, c) 是否在网格中
boolean inArea(int[][] grid, int r, int c) {
return 0 <= r && r < grid.length
&& 0 <= c && c < grid[0].length;
}
会重复遍历,为了避免需要标记已经遍历过的格子——将遍历过的格子值改为2,避免重复的dfs:
● 0 —— 海洋格子
● 1 —— 陆地格子(未遍历过)
● 2 —— 陆地格子(已遍历过)
void dfs(int[][] grid, int r, int c) {
// 判断 base case
if (!inArea(grid, r, c)) {
return;
}
// 如果这个格子不是岛屿,直接返回
if (grid[r][c] != 1) {
return;
}
grid[r][c] = 2; // 将格子标记为「已遍历过」
// 访问上、下、左、右四个相邻结点
dfs(grid, r - 1, c);
dfs(grid, r + 1, c);
dfs(grid, r, c - 1);
dfs(grid, r, c + 1);
}
// 判断坐标 (r, c) 是否在网格中
boolean inArea(int[][] grid, int r, int c) {
return 0 <= r && r < grid.length
&& 0 <= c && c < grid[0].length;
}
#200 岛屿数量
public int numIslands(char[][] grid) {
int count = 0;
for(int i = 0; i < grid.length; i++){
for(int j = 0; j < grid[0].length; j++){
if(grid[i][j] == ‘1’){//找到第一个岛屿边界,开始搜索
dfs200(grid,i,j);
count++;
}
}
}
return count;
}
public void dfs200(char[][] grid, int r, int c){
if(!inArea(grid,r,c)) return; // 边界:不在区域内
if(grid[r][c] != ‘1’){ //非岛屿
return;
}
grid[r][c] = ‘2’;//标记
dfs200(grid,r-1,c);
dfs200(grid,r+1,c);
dfs200(grid,r,c-1);
dfs200(grid,r,c+1);
}
public boolean inArea(char[][] grid, int r, int c){
return r >= 0 && r < grid.length && c >= 0 && c < grid[0].length;
}
#695 岛屿最大面积
对每个岛屿做DFS遍历,并求每个岛屿的面积——每遍历到一个格子就面积+1,最后取max
public int maxAreaOfIsland(int[][] grid) {
int area = 0;
for(int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if(grid[i][j] == 1){
int subarea = dfs695(grid,i,j);
area = Math.max(area,subarea);
}
}
}
return area;
}
public int dfs695(int[][] grid, int r, int c){
if(!inArea(grid,r,c)) return 0;
if(grid[r][c] != 1){ //非岛屿
return 0;
}
grid[r][c] = 2;
return 1+dfs695(grid,r-1,c)
+dfs695(grid,r+1,c)
+dfs695(grid,r,c-1)
+dfs695(grid,r,c+1);
}
public boolean inArea(int[][] grid, int r, int c){
return r >= 0 && r < grid.length && c >= 0 && c < grid[0].length;
}
##827 最大人工岛
给你一个大小为 n x n 二进制矩阵 grid 。最多 只能将一格 0 变成 1 。
返回执行此操作后,grid 中最大的岛屿面积是多少?
岛屿 由一组上、下、左、右四个方向相连的 1 形成。
思路:对网格做两边DFS:
● 第一遍:遍历陆地,并计算每个岛屿的面积,并对岛屿编号,将编号和面积映射到map中
● 第二遍:遍历海洋,观察海洋格子相邻的岛屿,计算当前海洋格子连通陆地的面积,最后求max
public int largestIsland(int[][] grid) {
if (grid == null || grid.length == 0) return 1;
int res = 0;
int index = 2;
HashMap
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[0].length; c++) {
//遍历陆地
if (grid[r][c] == 1) {
int area = area(grid, r, c, index);
indexAndAreas.put(index, area);
index++;
res = Math.max(res, area);
}
}
}
System.out.println(res);
if (res == 0) return 1;
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[0].length; c++) {
if (grid[r][c] == 0) {//遍历海洋格子
//用set去重,避免海洋格子相邻的陆地属于同一个岛
HashSet hashSet = findNeighbour(grid, r, c);
if (hashSet.size() < 1) continue;
int twoIsland = 1;
for (Integer i : hashSet) twoIsland += indexAndAreas.get(i);
res = Math.max(res, twoIsland);
}
}
}
return res;
}
private HashSet findNeighbour(int[][] grid, int r, int c) {
HashSet hashSet = new HashSet<>();
if (inArea(grid, r - 1, c) && grid[r - 1][c] != 0)
hashSet.add(grid[r - 1][c]);
if (inArea(grid, r + 1, c) && grid[r + 1][c] != 0)
hashSet.add(grid[r + 1][c]);
if (inArea(grid, r, c - 1) && grid[r][c - 1] != 0)
hashSet.add(grid[r][c - 1]);
if (inArea(grid, r, c + 1) && grid[r][c + 1] != 0)
hashSet.add(grid[r][c + 1]);
return hashSet;
}
private int area(int[][] grid, int r, int c, int index) {
if (!inArea(grid, r, c)) return 0;
if (grid[r][c] != 1) return 0;
grid[r][c] = index;
return 1 + area(grid, r - 1, c, index) + area(grid, r + 1, c, index) + area(grid, r, c - 1, index) + area(grid, r, c + 1, index);
}
private boolean inArea(int[][] grid, int r, int c) {
return r >= 0 && r < grid.length && c >= 0 && c < grid[0].length;
}
463 岛屿的周长 1.数学方法 2.dfs
数学方法:
一块土地原则上会带来 4 个周长,但岛上的土地存在接壤,每一条接壤,会减掉 2 个边长。
所以,总周长 = 4 * 土地个数 - 2 * 接壤边的条数。
遍历矩阵,遍历到土地,就 land++,如果它的右/下边也是土地,则 border++,遍历结束后代入公式。
public int islandPerimeter(int[][] grid) {
int land = 0, with = 0;
for(int i = 0; i < grid.length; i++){
for(int j = 0; j < grid[0].length; j++){
if(grid[i][j] == 1){
land++;
if(i < grid.length-1 && grid[i+1][j] == grid[i][j]){
with++;
}
if(j < grid[0].length-1 && grid[i][j+1] == grid[i][j]){
with++;
}
}
}
}
return 4*land-2*with;
}
dfs:
https://leetcode-cn.com/problems/number-of-islands/solution/dao-yu-lei-wen-ti-de-tong-yong-jie-fa-dfs-bian-li-/
BFS:
https://leetcode-cn.com/problems/binary-tree-level-order-traversal/solution/bfs-de-shi-yong-chang-jing-zong-jie-ceng-xu-bian-l/
层序遍历树
● LeetCode 102. Binary Tree Level Order Traversal 二叉树的层序遍历
● LeetCode 103. Binary Tree Zigzag Level Order Traversal 之字形层序遍历
● LeetCode 199. Binary Tree Right Side View 找每一层的最右结点
● LeetCode 515. Find Largest Value in Each Tree Row 计算每一层的最大值
● LeetCode 637. Average of Levels in Binary Tree 计算每一层的平均值
最短路径
● 1162离开陆地的最远距离
网格结构的层序遍历 BFS:
// 网格结构的层序遍历
// 从格子 (i, j) 开始遍历
void bfs(int[][] grid, int i, int j) {
Queue
queue.add(new int[]{r, c});
while (!queue.isEmpty()) {
int n = queue.size();
for (int i = 0; i < n; i++) {
int[] node = queue.poll();
int r = node[0];
int c = node[1];
if (r-1 >= 0 && grid[r-1][c] == 0) {
grid[r-1][c] = 2;
queue.add(new int[]{r-1, c});
}
if (r+1 < N && grid[r+1][c] == 0) {
grid[r+1][c] = 2;
queue.add(new int[]{r+1, c});
}
if (c-1 >= 0 && grid[r][c-1] == 0) {
grid[r][c-1] = 2;
queue.add(new int[]{r, c-1});
}
if (c+1 < N && grid[r][c+1] == 0) {
grid[r][c+1] = 2;
queue.add(new int[]{r, c+1});
}
}
}
}
#1162 离开陆地的最远距离
「BFS 遍历」、「层序遍历」、「最短路径」实际上是递进的关系。在 BFS 遍历的基础上区分遍历的每一层,就得到了层序遍历。在层序遍历的基础上记录层数,就得到了最短路径。
图可以与多个源点,将其都先放入队列
class Solution {
public int maxDistance(int[][] grid) {
int N = grid.length;
//把陆地的坐标放入队列中
Queue
for(int i = 0; i < N; i++) {
for(int j = 0; j < N; j++) {
if(grid[i][j] == 1){
queue.offer(new int[]{i,j});
}
}
}
//如果只有陆地或只有海洋,返回-1
if(queue.size() == N*N || queue.isEmpty()) {
return -1;
}
//
int[][] moves = {{0,1},{0,-1},{1,0},{-1,0}};
//
int distance = 0;
while (!queue.isEmpty()){
distance++;
//以队列的点开始向四个方向扩展判断,是海洋就标记并放入队列,便于下一次扩展
int n = queue.size();
for(int i = 0; i < n; i++) {
int[] point = queue.poll();
for(int[] move : moves) {
int row = point[0] + move[0];
int col = point[1] + move[1];
if(inArea(grid, row, col) && grid[row][col] == 0) {
grid[row][col] = 2;
queue.offer(new int[]{row, col});
}
}
}
}
return distance-1;
}
public boolean inArea(int[][] grid, int r, int c){
return r >= 0 && r < grid.length && c >= 0 && c < grid[0].length;
}
}
#542 01矩阵
思路:首先把每个源点 00入队,然后从各个 0 同时开始一圈一圈的向 11扩散(每个 1都是被离它最近的 0 扩散到的 ),扩散的时候可以设置 int[][] dist 来记录距离(即扩散的层次)并同时标志是否访问过。对于本题是可以直接修改原数组 int[][] matrix 来记录距离和标志是否访问的,这里要注意先把 matrix 数组中 1 的位置设置成 -1 (设成Integer.MAX_VALUE啦,m * n啦,10000啦都行,只要是个无效的距离值来标志这个位置的 1 没有被访问过就行辣~)
复杂度分析:
每个点入队出队一次,所以时间复杂度:O(n * m)O(n∗m)
虽然我们是直接原地修改的原输入数组来存储结果,但最差的情况下即全都是 00 时,需要把 m * nm∗n 个 00 都入队,因此空间复杂度是 O(n * m)O(n∗m)
class Solution {
public int[][] updateMatrix(int[][] mat) {
int N = Math.max(mat.length, mat[0].length);
Queue
for(int i = 0; i < mat.length; i++) {
for(int j = 0; j < mat[0].length; j++) {
if(mat[i][j] == 0) {
queue.offer(new int[]{i, j});
}else {
mat[i][j] = N + 1;
}
}
}
int[][] moves = {{0,1},{0,-1},{1,0},{-1,0}};
while (!queue.isEmpty()){
int n = queue.size();
for(int i = 0; i < n; i++){
int[] point = queue.poll();
for(int[] move : moves){
int row = point[0] + move[0];
int col = point[1] + move[1];
if(inArea(mat,row,col) && mat[row][col] == N+1) {
mat[row][col] = mat[point[0]][point[1]] + 1;
queue.offer(new int[]{row,col});
}
}
}
}
return mat;
}
public boolean inArea(int[][] grid, int r, int c){
return r >= 0 && r < grid.length && c >= 0 && c < grid[0].length;
}
}
#994 腐烂的橘子
注意边界!如果没有腐烂的橘子:对于全0,输出0;对于存在0或1,返回-1
执行用时:1 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:41.2 MB, 在所有 Java 提交中击败了6.69%的用户
public int orangesRotting(int[][] grid) {
Queue
int m = grid.length;
int n = grid[0].length;
int count_1 = 0;//统计1的个数,如果不能把所有好橘子腐坏要返回-1
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
if(grid[i][j] == 2) {
queue.offer(new int[]{i, j});
}
if(grid[i][j] == 1) {
count_1 ++;
}
}
}
//对于全0,输出0,对于存在0或1,返回-1
if(queue.isEmpty()){
if(count_1 == 0) return 0;
return -1;
}
int time = 0;
int[][] moves = {{0,1},{0,-1},{1,0},{-1,0}};
while (!queue.isEmpty()) {
time++;
int num = queue.size();
for(int i = 0; i < num; i++) {
int[] point = queue.poll();
for(int[] move : moves) {
int row = point[0] + move[0];
int col = point[1] + move[1];
if(inArea(grid, row, col) && grid[row][col] == 1){
grid[row][col] = 2;
count_1--;
queue.offer(new int[]{row, col});
}
}
}
}
return count_1 == 0 ? time - 1 : -1;
}
#310 最小高度树
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class FindMinHeightTrees {
public List findMinHeightTrees(int n, int[][] edges) {
List res = new ArrayList<>();
/如果只有一个节点,那么他就是最小高度树/
if (n == 1) {
res.add(0);
return res;
}
/建立各个节点的出度表/
int[] degree = new int[n];
/建立图关系,在每个节点的list中存储相连节点/
List map = new ArrayList<>();
for (int i = 0; i < n; i++) {
map.add(new ArrayList<>());
}
for (int[] edge : edges) {
degree[edge[0]]++;
degree[edge[1]]++;/出度++/
map.get(edge[0]).add(edge[1]);/添加相邻节点/
map.get(edge[1]).add(edge[0]);
}
/建立队列/
Queue queue = new LinkedList<>();
/把所有出度为1的节点,也就是叶子节点入队/
for (int i = 0; i < n; i++) {
if (degree[i] == 1) queue.offer(i);
}
/循环条件当然是经典的不空判断/
while (!queue.isEmpty()) {
res = new ArrayList<>();/这个地方注意,我们每层循环都要new一个新的结果集合,
这样最后保存的就是最终的最小高度树了/
int size = queue.size();/这是每一层的节点的数量/
for (int i = 0; i < size; i++) {
int cur = queue.poll();
res.add(cur);/把当前节点加入结果集,不要有疑问,为什么当前只是叶子节点为什么要加入结果集呢?
因为我们每次循环都会新建一个list,所以最后保存的就是最后一个状态下的叶子节点,
这也是很多题解里面所说的剪掉叶子节点的部分,你可以想象一下图,每层遍历完,
都会把该层(也就是叶子节点层)这一层从队列中移除掉,
不就相当于把原来的图给剪掉一圈叶子节点,形成一个缩小的新的图吗/
List neighbors = map.get(cur);
/这里就是经典的bfs了,把当前节点的相邻接点都拿出来,
* 把它们的出度都减1,因为当前节点已经不存在了,所以,
* 它的相邻节点们就有可能变成叶子节点/
for (int neighbor : neighbors) {
degree[neighbor]–;
if (degree[neighbor] == 1) {
/如果是叶子节点我们就入队/
queue.offer(neighbor);
}
}
}
}
return res;/返回最后一次保存的list/
}
}
作者:xiao-xin-28
链接:https://leetcode-cn.com/problems/minimum-height-trees/solution/zui-rong-yi-li-jie-de-bfsfen-xi-jian-dan-zhu-shi-x/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
#子集
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
class Solution {
List res = new ArrayList<>();
public List subsets(int[] nums) {
helper(nums, 0, new ArrayList());
return res;
}
public void helper(int[] nums, int index, List list){
res.add(list);
for(int i=index;i
newList.add(nums[i]);
helper(nums, i+1, newList);
}
}
}
方法2:位运算
public List subsets(int[] nums) {
List res = new ArrayList
();
int len = nums.length;
for(int i = 0; i < (1 << len); i++) {
List sub = new ArrayList();
for(int j = 0; j < len; j++) {
//将i值二进制位上为1的存入sub
if((i & (1<
}
}
res.add(sub);
}
return res;
}
单词搜索
给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
记得在递归调用之后恢复原状!!!
class Solution {
public boolean exist(char[][] board, String word) {
int row = board.length;
int col = board[0].length;
boolean[][] visited = new boolean[row][col];
for(int i=0;i
}
}
return false;
}
public boolean found(char[][] board, boolean[][] visited, int i, int j, String word, int index){
int row = board.length;
int col = board[0].length;
if(index==word.length())return true;
if(i<0||i>=row||j<0||j>=col||board[i][j]!=word.charAt(index)||visited[i][j])return false;
visited[i][j] = true;
boolean flag = found(board, visited, i-1, j, word, index+1)
||found(board, visited, i+1, j, word, index+1)
||found(board, visited, i, j-1, word, index+1)
||found(board, visited, i, j+1, word, index+1);
visited[i][j] = false;
return flag;
}
}
极小化极大
1.nim游戏
极小化极大算法,是一种找出失败的最大可能性中的最小值的算法。Minimax算法常用于棋类等由两方较量的游戏和程序,这类程序由两个游戏者轮流,每次执行一个步骤。我们众所周知的五子棋、象棋等都属于这类程序,所以说Minimax算法是基于搜索的博弈算法的基础。该算法是一种零总和算法,即一方要在可选的选项中选择将其优势最大化的选择,而另一方则选择令对手优势最小化的方法。