2. 两数相加
解题思路:模拟,利用一个变量记录当前节点相加是否产生进位,利用一个虚拟节点作为头节点,循环判断该节点的节点值是否大于10,若大于10进行修改,否则记录下来,只有当两链表全部遍历结束,结束记录,最后判断最后一位是否产生进位,若产生进位,再生成一个节点值为1的节点,否则直接返回。
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
int carry = 0;//用于进位
int sum = 0;//用于记录当前节点和
ListNode res = new ListNode(-1);
ListNode cur = res;
while (l1 != null || l2 != null){
int val1 = l1 == null ? 0 :l1.val;
int val2 = l2 == null ? 0 :l2.val;
sum = val1 + val2 + carry;
carry = sum >= 10 ? 1 : 0;
sum = sum >= 10 ? sum - 10 : sum;
ListNode node = new ListNode(sum);
cur.next = node;
l1 = l1 == null ? null :l1.next;
l2 = l2 == null ? null :l2.next;
cur = cur.next;
}
if(carry == 1){
cur.next = new ListNode(carry);
}
return res.next;
}
3. 无重复字符的最长子串
解题思路:哈希表,利用一个哈希表更新当前字符的位置,每当哈希表已存在该字符(说明当前位置到前一个起始位置已经出现了重复字符),更新一个起始位置,通过比较当前位置到起始位置的大小,得到最大无重复字符的最长子串。
public int lengthOfLongestSubstring(String s) {
if(s.length()==0){
return 0;
}
Map<Character,Integer> map = new HashMap<>();
int res = 1;
int start = -1;
for (int i = 0; i < s.length(); i++) {
if(map.containsKey(s.charAt(i))){
//更新起始位置
start = Math.max(start,map.get(s.charAt(i)));
}
map.put(s.charAt(i),i);
//每次更新返回大小
res = Math.max(res,i - start);
}
return res;
}
4. 寻找两个正序数组的中位数
解题思路:模拟,判断总数组长度为奇数还是偶数 ->计算中位数位置->分别比较nums1 和nums2大小,用num循环记录,记录的过程中,避免数组越界,当到达中位数位置时,更新返回值即可
当总数组长度为偶数时,分别记录pos和pos+1位置的值,返回这两个数之和的平均值
int len1 = nums1.length;
int len2 = nums2.length;
boolean flag = (len1 + len2) % 2 == 1 ?true :false;
int pos = (len1 + len2 -1) /2;
int index = 0;
int index1 =0;
int index2 =0;
double res = 0.0;
int num = 0;
while (index < len1 + len2){
if(index1 >= len1){
num = nums2[index2];
index2++;
}else if(index2 >= len2){
num = nums1[index1];
index1++;
}else {
if(nums1[index1] > nums2[index2]){
num = nums2[index2];
index2++;
}else {
num = nums1[index1];
index1++;
}
}
if(index == pos){
if(flag){
res += num;
return (double) res ;
}else {
res += num;
}
}else if(index == pos + 1){
res += num;
res /=2;
break;
}
index ++;
}
return res;
5. 最长回文子串
解题思路:动态规划 dp i j 表示字符串从i到j是否为回文串
if(right - left <= 2) dp(i)(j)]= true;
else dp(i)(j) = dp(i+1)(j-1);
注意:循环的时候外循环时right,内循环left,这样可以保证判断外部是否为回文串的时候,内部已经判断好结果了
public String longestPalindrome(String s) {
int start = 0;
int maxLen = 1;
//表示从第i到第j个为回文字串是否为回文串
boolean dp[][] = new boolean[s.length()][s.length()];
for (int right = 1; right < s.length(); right++) {
for (int left = 0; left < right; left++) {
if(s.charAt(left) != s.charAt(right)){
dp[left][right] = false;
}else {
if(left == right){
dp[left][right] = true;
}else if(right - left <= 2){
dp[left][right] = true;
}else {
dp[left][right] = dp[left + 1][right - 1];
}
if(dp[left][right] && right-left+1>maxLen){
maxLen = right - left + 1;
start = left;
}
}
}
}
return s.substring(start,start+maxLen);
}
6. Z 字形变换
解题思路:矩阵模拟的方法,通过边界改变数组注入的方向,如果是循环可以通过取余操作
注意:Sring在java中为对象,建立数组对象时,需要初始化对象,否则会报空指针异常
public String convert(String s, int numRows) {
if(numRows == 1){
return s;
}
String[] sb = new String[numRows];
for (int i = 0; i < numRows; i++) {
sb[i] = "";
}
int index = 0;
boolean flag =false;
for (int i = 0; i < s.length(); i++) {
if(index == 0){
flag = false;
}else if(index == numRows -1){
flag = true;
}
char charAt = s.charAt(i);
if(!flag){
sb[index++]+= charAt;
}else {
sb[index--]+= charAt;
}
}
StringBuilder res = new StringBuilder();
for(String str : sb){
res.append(str);
}
return res.toString();
}
7. 整数反转
解题思路:模拟,由于本题假设不能存储64位数,则通过
if(res > Integer.MAX_VALUE /10 || res < Integer.MIN_VALUE/10)
语句提前返回
int res = 0;
while(x != 0){
if(res > Integer.MAX_VALUE /10 || res < Integer.MIN_VALUE/10){
return 0;
}
int temp = x % 10;
res = res * 10 + temp;
x =x /10;
}
return res ;
10. 正则表达式匹配
解题思路:动态规划 dp(s.length + 1)(p.length + 1) ,表明前i个s与前j个p字符是否匹配
初始化 : dp(0)(0) = true 无字符一定匹配
dp(0)(j) = dp(0)(j-2)且 p(i -1) = ‘*’: 首行 s 为空字符串,因此当 p 的偶数位为 * 时才能够匹配(即让 p 的奇数位出现 0 次,保持 p 是空字符串)。因此,循环遍历字符串 p ,步长为 2(即只看偶数位)。
状态转移:
当p(i -1) = ‘*’ 时, dp[i][j] 在当以下任一情况为 truetrue 时等于 true :
-
dp[i][j - 2]: 即将字符组合 p[j - 2] * 看作出现 0 次时,能否匹配;
-
dp(i -1)(j)且 s[i - 1] = p[j - 2]: 即让字符 p[j - 2] 多出现 1 次时,能否匹配;
-
dp(i-1)(j)且 p[j - 2] = ‘.’ : 即让字符 ‘.’ 多出现 1 次时,能否匹配;
当 p(i -1)!= ‘*’ 时, dp[i][j] 在当以下任一情况为 truetrue 时等于 true:
-
dp(i-1)(j-1)且s[i - 1] = p[j - 1]: 即让字符 p[j - 1] 多出现一次时,能否匹配;
-
dp(i-1)(j-1)且 p[j - 1] = ‘.’: 即将字符 . 看作字符 s[i - 1] 时,能否匹配;
public boolean isMatch(String s, String p) {
boolean[][] dp = new boolean[s.length() + 1][p.length() + 1];
dp[0][0] = true;
for (int i = 2; i <= p.length(); i+=2) {
if(p.charAt(i-1)=='*' && dp[0][i-2] ){
dp[0][i] = true;
}
}
for (int i = 1; i <= s.length(); i++) {
for (int j = 1; j <= p.length(); j++) {
char charAt = p.charAt(j - 1);
if(charAt == '*'){
if(dp[i][j-2]) {
dp[i][j] = true;
}else if(dp[i - 1][j] && s.charAt(i-1) == p.charAt(j-2)){
dp[i][j] = true;
}else if(dp[i - 1][j] && p.charAt(j-2) == '.'){
dp[i][j] = true;
}
}else {
if(dp[i-1][j-1] && p.charAt(j-1) == s.charAt(i-1) ){
dp[i][j] = true;
}else if(dp[i-1][j-1]&&p.charAt(j-1) == '.' ){
dp[i][j] = true;
}
}
}
}
return dp[s.length()][p.length()];
}
11. 盛最多水的容器
解题思路:采用双指针+贪心,
贪心规则:若当前left > right ,left+1;若right > left right-1;形成局部最优
public int maxArea(int[] height) {
int left = 0;
int right = height.length -1;
int res = 0;
while (right > left){
int area = Math.min(height[left],height[right])*(right - left);
res = Math.max(res,area);
if(height[left] > height[right]){
right--;
}else {
left++;
}
}
return res;
}
15. 三数之和
解题思路:排序+双指针
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>>res = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
//当前数已经大于0,不可能存在和为0的三个数,直接退出
if(nums[i] > 0){
break;
}
//去重
if(i > 0 && nums[i] == nums[i - 1]){
continue;
}
int left = i + 1;
int right =nums.length -1;
while (right > left){
int sum = nums[i] + nums[left]+nums[right];
if(sum == 0){
res.add(Arrays.asList(nums[i], nums[left], nums[right]));
//去重
while (left < right &&nums[right] == nums[right-1]){
right--;
}
//去重
while (left < right &&nums[left] == nums[left+1]){
left++;
}
left ++;
right --;
}else if(sum > 0){
right--;
}else {
left++;
}
}
}
return res;
}
16. 最接近的三数之和
解题思路:双指针
public int threeSumClosest(int[] nums, int target) {
int res = Integer.MIN_VALUE;
int div = Integer.MAX_VALUE;;
Arrays.sort(nums);
for (int i = 0; i < nums.length; i++) {
if(i > 0 && nums[i] == nums[i-1]){
continue;
}
int left = i + 1;
int right = nums.length - 1;
while (right > left){
int sum = nums[left] + nums[right] + nums[i];
if(sum == target){
return sum;
}else {
if(sum > target){
while (left <right && nums[right] == nums[right-1]){
right--;
}
right--;
if(div > Math.abs(sum -target)){
div = Math.abs(sum -target);
res = sum;
}
}else {
while (left < right && nums[left] == nums[left+1]){
left ++;
}
left++;
if(div > Math.abs(sum -target)){
div = Math.abs(sum -target);
res = sum;
}
}
}
}
}
return res;
}
17. 电话号码的字母组合
解题思路:构建电话号码hash + 回溯
public List<String> letterCombinations(String digits) {
Map<Integer,String> map = new HashMap(){{
put(2,"abc");
put(3,"def");
put(4,"ghi");
put(5,"jkl");
put(6,"mno");
put(7,"pqrs");
put(8,"tuv");
put(9,"wxyz");
}};
List<String> res = new ArrayList<>();
if( digits.length() == 0){
return new ArrayList<>();
}
backTracking(digits,map,res,new StringBuilder(),0);
return res;
}
private void backTracking(String digits, Map<Integer, String> map, List<String> res, StringBuilder stringBuilder, int start){
if(stringBuilder.length() ==digits.length()){
res.add(stringBuilder.toString());
}
for (int i = start; i < digits.length() ; i++) {
String s = map.get(Integer.parseInt(digits.charAt(i)+""));
for (int j = 0; j < s.length(); j++) {
char charAt = s.charAt(j);
stringBuilder.append(charAt);
backTracking(digits,map,res,stringBuilder,i+1);
stringBuilder.delete(stringBuilder.length()-1,stringBuilder.length());
}
}
}
19. 删除链表的倒数第 N 个结点
解题思路:双指针(快慢指针) 快指针记录到达第k个节点,慢指针到达倒数第K个节点,并记录倒数第k个节点的前节点,用于删除节点
public ListNode removeNthFromEnd(ListNode head, int n) {
if(head == null){
return head;
}
ListNode res = new ListNode(-1);
ListNode cur = res;
res.next = head;
while (n-- > 0){
head = head.next;
}
while (head != null){
cur = cur.next;
head = head.next;
}
cur.next = cur.next.next;
return res.next;
}
20. 有效的括号
解题思路:堆的使用,利用堆的特性,当遇到(,{,】时,)} 】入堆,当遇到)} 】判断堆顶是否匹配,不匹配则返回false,如果匹配则弹出堆顶,
public boolean isValid(String s) {
if(s.length() % 2 == 1){
return false;
}
Stack<Character> stack = new Stack<>();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if(ch == '('){
stack.add(')');
}else if(ch == '{'){
stack.push('}');
}else if(ch == '[') {
stack.push(']');
}else if(stack.isEmpty() || ch != stack.peek()){
return false;
}
else {
stack.pop();
}
}
return stack.isEmpty();
}
21. 合并两个有序链表
解题思路:直接遍历链表,判断两链表相对大小,较小的节点接入当前链表的next指针,若到达其中一个链表结尾,直接将当前节点的next指针指向令一链表节点.
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode cur = new ListNode(-1);
ListNode res = cur;
while (list1 != null && list2!=null){
if(list1.val > list2.val){
cur.next = list2;
list2 = list2.next;
}else {
cur.next = list1;
list1 = list1.next;
}
cur = cur.next;
}
cur.next = list1 == null? list2:list1;
return res.next;
}
22. 括号生成
解题思路:DFS + 回溯
用left和right分别记录左括号和右括号的个数,为保证有序括号生成,必须保证左括号left大于右括号数right时,才能添加右括号,回溯时,删除字符串最后一个字符
public List<String> generateParenthesis(int n) {
List<String> list = new ArrayList<>();
backTracking(n,list,0,0,new StringBuilder());
return list;
}
private void backTracking(int n, List<String> list, int left, int right, StringBuilder s) {
if(s.length() == n * 2){
list.add(s.toString());
return;
}
if(left < n){
s.append("(");
backTracking(n,list,left + 1,right,s);
s.delete(s.length()-1,s.length());
}
if(right < n && right < left){
s.append(")");
backTracking(n,list,left,right + 1 ,s);
s.delete(s.length()-1,s.length());
}
}
23. 合并K个升序链表
解题思路:依次合并两个升序链表,即可完成堆k个链表的合并
public ListNode mergeKLists(ListNode[] lists) {
if(lists.length == 0){
return null;
}
ListNode cur = null;
for (int i = 1; i < lists.length; i++) {
cur = merge(cur,lists[i]);
}
return cur;
}
private ListNode merge(ListNode l1, ListNode l2) {
ListNode res = new ListNode(-1);
ListNode cur = res;
while (l1!= null && l2 != null){
if(l1.val > l2.val){
cur.next = l2;
l2 = l2.next;
}else {
cur.next = l1;
l1 = l1.next;
}
cur = cur.next;
}
cur.next = l1 == null ? l2 : l1;
return res.next;
}
解题思路2:采用优先队列形成小堆顶,通过定义一个类,用于优先队列排序,依次将lists的头节点放入队列中,每把当前节点放入队列中时,需要判断当前节点是否存在下一个节点,若存在,再将下一个节点放入队列。
注意:不能一次性将所有节点放入队列,那样会造成val值相同的节点会重构成同一个对象,导致出现错误
class CompareListNode implements Comparable<CompareListNode>{
int val;
ListNode node;
public CompareListNode(int val, ListNode node) {
this.val = val;
this.node = node;
}
@Override
public int compareTo(CompareListNode o) {
return this.val - o.val;
}
}
PriorityQueue<CompareListNode> queue = new PriorityQueue<>();
public ListNode mergeKLists(ListNode[] lists) {
if (lists.length == 0) {
return null;
}
for (int i = 0; i < lists.length; i++) {
ListNode node = lists[i];
if(node!=null){
queue.add(new CompareListNode(node.val, node));
}
}
ListNode cur = new ListNode(-1);
ListNode res = cur;
while (!queue.isEmpty()){
CompareListNode poll = queue.poll();
cur.next = poll.node;
cur = cur.next;
if (poll.node.next != null) {
queue.offer(new CompareListNode(poll.node.next.val, poll.node.next));
}
}
return res.next;
31. 下一个排列
题目分析:我们需要将一个左边的「较小数」与一个右边的「较大数」交换,以能够让当前排列变大,从而得到下一个排列。同时我们要让这个「较小数」尽量靠右,而「较大数」尽可能小。
解题思路:
- 找到第1组nums[i] > nums [i-1]的位置index(从后向前遍历,找到后一个比前一个数大的情况)
- 如果找到第1组nums[i] > nums [i-1]的位置,
- 找到index ~ nums.length之间最小值,使得最小值大于nums[index - 1],交换nums[i - 1]和最小值,后对index到nums.length排序
- 没有找到,则排序,如 3 2 1;
public void nextPermutation(int[] nums) {
int index = -1;
//找到第1组nums[i] > nums [i-1]的位置
for (int i = nums.length - 1; i > 0 ; i--) {
if(nums[i] > nums[i - 1]){
index = i;
break;
}
}
if(index != -1){
//如果找到第1组nums[i] > nums [i-1]的位置
//如1 3 2 i = 1 下一组排列是 2 1 3
//找到index ~ nums.length之间最小值,使得最小值大于nums[index - 1],交换nums[i - 1]和最小值
int minIndex = index;
for (int i = index; i < nums.length; i++) {
if(nums[i] > nums[index -1] && nums[i] <nums[minIndex]){
minIndex = i;
}
}
//交换minIndex和i-1之间的值
int temp = nums[minIndex];
nums[minIndex] = nums[index -1];
nums[index-1] = temp;
Arrays.sort(nums,index,nums.length);
}else {
//没有找到 如3 2 1
//数组逆转
Arrays.sort(nums);
}
}
32. 最长有效括号
解题思路:暴力
由于最长有效括号一定为偶数,那么找到小于等于s.length的最大整数,依次找到以该位长度的子串,进行判断,若判断成功,则直接返回,否则继续判断。O(n^3)
public int longestValidParentheses(String s) {
int len = s.length();
if(len % 2 == 1){
len = len -1;
}
String sub = "";
for (int i = len; i > 0 ; i = i - 2) {
for (int j = 0; j < s.length(); j++) {
if(i + j <= s.length()){
sub = s.substring(j,i + j);
}
if(isVaild(sub)){
return sub.length();
}
}
}
return 0;
}
private boolean isVaild(String sub) {
Stack<Character> stack = new Stack<>();
for (int i = 0; i < sub.length(); i++) {
char charAt = sub.charAt(i);
if(charAt == '('){
stack.add(')');
}else if(!stack.isEmpty()&&charAt == stack.peek()){
stack.pop();
}else {
return false;
}
}
return stack.isEmpty();
}
解题思路2:动态规划
dp()()以i为结尾的最长子串个数,由于有效子串是以)结尾,只有当前字符为)才有可能是组成有效子串
当前字符为)时,判断下标为i - dp[i - 1] - 1是否为(,且i - dp[i - 1] > 0时:
状态转移:当前有效子串 = 2 + 前一个有效子串 + 外部有效子串
其中需要判断,外部有效字串的下标是否溢出。
eg:没有溢出 ()(());溢出((()));
public int longestValidParentheses(String s) {
int[] dp = new int[s.length()]; //以i为结尾的最长子串个数
int res = 0;
for (int i = 1; i < s.length(); i++) {
char ch = s.charAt(i);
if(ch == ')'){
if(i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '('){
dp[i] = 2 + dp[i - 1] + (i - dp[i-1] -2 > -1? dp[i - dp[i - 1] - 2]:0);
}
res = Math.max(res,dp[i]);
}
}
return res;
}
解题思路:栈,用于存储下标,
- 若当前字符为(,下标入栈,
- 否则弹出栈顶元素,此时判断栈是否为空,
- 若为空,则说明此时的字符分割了左右字符子串,并把当前下标入栈
- 否则,说明配对成功,配对有效子串。
public int longestValidParentheses(String s) {
int len = 0;
int max_len = 0;
Stack<Integer> stack = new Stack<>();
stack.add(-1);
for (int i = 0; i < s.length(); i++) {
char charAt = s.charAt(i);
if(charAt == '('){
stack.push(i);
}else {
stack.pop();
if(stack.isEmpty()){
stack.push(i);
}else {
len = i - stack.peek();
max_len = Math.max(len,max_len);
}
}
}
return max_len;
}
33. 搜索旋转排序数组
解题思路:二分法 + 分类讨论
根据当前子数组的最左侧和最右侧数值大小关系,判断出当前子数组是否为升序数组,
- 若为升序数组,则直接进行普通二分法
- 否则,需要重新瓜分数组,通过target在不是升序数组中的位置,继续瓜分数组
- 若mid > left,则left -mid 都是升序 //判断哪一侧为升序
- 如果target > nums[mid] 说明 target在右侧 left = mid +1;
- 否则,target在左侧
- 如果left > target,tareget在右侧
- 否则在左侧
- 否则mid - right 都是升序
- 如果 target>mid
- 如果 target >right right = mid -1;
- 否则left = mid +1
- 否则 right= mid -1
- 如果 target>mid
- 若mid > left,则left -mid 都是升序 //判断哪一侧为升序
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length -1;
while (right >= left){
if(nums[left] == target) return left;
if(nums[right] == target) return right;
int mid = (right + left) >> 1;
if (nums[mid] == target){
return mid;
}
boolean flag = nums[left] < nums[right];
if(flag){
//左右为正常递增序列
//正常二分法
if(target > nums[mid]){
left = mid + 1;
}else {
right = mid - 1;
}
}else {
//分区间讨论
boolean flag1 = nums[mid] > nums[left];
if(flag1) {
//mid 在第一个递增区间
if(target > nums[mid]){
left = mid + 1;
}else {
if(nums[left] > target){
//target 在第二的递增区间
left = mid +1;
}else {
//target 在第二的递增区间
right = mid -1;
}
}
}else {
if(target > nums[mid]){
if(target > nums[right]){
//target 在第一个递增区间
right = mid -1;
}else {
//target 在第二个递增区间
left = mid +1;
}
}else {
right = mid - 1;
}
}
}
}
return -1;
}
34. 在排序数组中查找元素的第一个和最后一个位置
解题思路 : 二分法
通过二分法,找到target,然后通过向左向右遍历 依次得到左右终点 返回即可
public int[] searchRange(int[] nums, int target) {
int[] res = new int[2];
Arrays.fill(res,-1);
int left = 0;
int right = nums.length-1;
while (right >= left){
int mid = (right + left) >> 1;
if(nums[mid] == target){
for (int i = mid; i <= right; i++) {
if(nums[i] == target){
res[1] = i;
}
}
for (int j = mid; j >=0 ; j--) {
if(nums[j] == target){
res[0] = j;
}
}
break;
}else if(nums[mid] > target){
right = mid -1;
}else {
left = mid+1;
}
}
return res;
}
39. 组合总和
解题思路:回溯
注意考虑到一个数字可以多次使用,每次返回的时候,start点保持不变
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backTracking(candidates,target,0,new ArrayList<Integer>());
return res;
}
private void backTracking(int[] candidates, int sum, int start, ArrayList<Integer> list) {
if(sum == 0){
res.add(new ArrayList<>(list));
return;
}
if(sum < 0){
return ;
}
for (int i = start; i < candidates.length; i++) {
sum -= candidates[i];
list.add(candidates[i]);
backTracking(candidates,sum,i,list);
list.remove(list.size()-1);
sum += candidates[i];
}
}
42. 接雨水
解题思路:单调栈 单调递增栈
public int trap(int[] height) {
int res = 0;
Deque<Integer> deque = new LinkedList<>();
for (int i = 0; i < height.length; i++) {
while (!deque.isEmpty() &&height[deque.peekLast()] < height[i]){
int top = height[deque.pollLast()] ;
if(deque.isEmpty()){ //如果为空,则说明没有左墙壁,不能装水
break;
}
int left = deque.peekLast();
int high = Math.min(height[left],height[i]) - top;
int wid = i - left - 1;
res += wid * high;
}
deque.addLast(i);
}
return res;
}
解题思路:的双指针
46. 全排列
解题思路 :回溯 无重复数字
注意:每个元素只能使用一次,则可以利用used数组来判断当前该数是否已经访问
public List<List<Integer>> permute(int[] nums) {
if(nums.length == 0){
return new ArrayList<>();
}
List<List<Integer>> res = new ArrayList<>();
boolean[] used = new boolean[nums.length];
backTracking(nums,0,new ArrayList<Integer>(),res,used);
return res;
}
private void backTracking(int[] nums, int start, ArrayList<Integer> list, List<List<Integer>> res, boolean[] used) {
if(list.size() == nums.length){
res.add(new ArrayList<>(list));
return ;
}
if(list.size() >nums.length ){ //剪枝
return;
}
for (int i = start; i <nums.length ; i++) {
if(used[i] == false){
list.add(nums[i]);
used[i] = true;
backTracking(nums,start,list,res,used);
list.remove(list.size()-1);
used[i] = false;
}else {
continue;
}
}
}
47. 全排列 II
题解:回溯 存在重复数字,可以通过提前排序,使相同数字在相邻,则在回溯的时候方便判断
public List<List<Integer>> permuteUnique(int[] nums) {
if(nums.length == 0){
return new ArrayList<>();
}
Arrays.sort(nums);
boolean[] used = new boolean[nums.length];
List<List<Integer>> res = new ArrayList<>();
backTracking(nums,new ArrayList<Integer>(),res,used);
return res;
}
private void backTracking(int[] nums, ArrayList<Integer> list, List<List<Integer>> res, boolean[] used) {
if(list.size() == nums.length){
res.add(new ArrayList<>(list));
return;
}
for (int i = 0 ; i < nums.length; i++) {
if(i>0 && nums[i] == nums[i - 1]&&used[i-1]==false){
continue;
}
if(used[i] == false){
used[i] = true;
list.add(nums[i]);
backTracking(nums,list,res,used);
list.remove(list.size()-1);
used[i]=false;
}
}
}
48. 旋转图像
解题思路 : 先水平翻转 -> 主对角线反转 On^2 所有方法都是On^2
public void rotate(int[][] matrix) {
int len = matrix.length >> 1;
//水平反转 最后一行提升至第一行 最后第二行提升至第二行 依次类推
for (int i = 0; i < len; i++) {
for (int j = 0; j < matrix.length; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[matrix.length - i-1][j];
matrix[matrix.length - i-1][j] = temp;
}
}
//主对角线反转
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < i; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
}
49. 字母异位词分组
解题分析:当两个单词为异位词时,字母排序相同,用排序值作为map的Key,当前字母作位值,即可记录异位词
解题思路:字符串排序+哈希表
注意:当map中没有list时,需要重新建立一个list,
map.values()该函数返回map的value;
public List<List<String>> groupAnagrams(String[] strs) {
if(strs.length == 0){
return new ArrayList<>();
}
Map<String,List<String>> map = new HashMap<>();
for (int i = 0; i < strs.length; i++) {
char[] chars = strs[i].toCharArray();
Arrays.sort(chars);
String str = new String(chars);
List<String> list = map.getOrDefault(str, new ArrayList<>());
list.add(strs[i]);
map.put(str,list);
}
return new ArrayList(map.values());
}
解题思路2:可以记录当前单词字母的个数,然后通过记录当前字母的个数,恢复字符串,作为哈希表的key,放入map中
public List<List<String>> groupAnagrams(String[] strs) {
if(strs.length == 0){
return new ArrayList<>();
}
Map<String,List<String>> map = new HashMap<>();
for (int i = 0; i < strs.length; i++) {
char[] chars = strs[i].toCharArray();
Arrays.sort(chars);
String str = new String(chars);
List<String> list = map.getOrDefault(str, new ArrayList<>());
list.add(strs[i]);
map.put(str,list);
}
return new ArrayList(map.values());
}
50. Pow(x, n)
解题思路:将指数转换为2进制数表示,如果当前为1,
eg: 2^10 = 2^2 * 2^8 10的二进制1010
public double myPow(double x, int n) {
long N = n;
return N >= 0 ? quickMul(x, N) : 1.0 / quickMul(x, -N);
}
public double quickMul(double x, long N) {
double ans = 1.0;
// 贡献的初始值为 x
double x_contribute = x;
// 在对 N 进行二进制拆分的同时计算答案
while (N > 0) {
if (N % 2 == 1) {
// 如果 N 二进制表示的最低位为 1,那么需要计入贡献
ans *= x_contribute;
}
// 将贡献不断地平方
x_contribute *= x_contribute;
// 舍弃 N 二进制表示的最低位,这样我们每次只要判断最低位即可
N /= 2;
}
return ans;
}
55. 跳跃游戏
解题思路:动态规划+贪心
计算当前位置的最大跳跃步数,作为循环的阈值,只要当前跳跃步数>数组长度,则可以到达终点
public boolean canJump(int[] nums) {
if(nums.length == 1)return true;
int maxStep = 0;
for (int i = 0; i <= maxStep;i++ ) {
maxStep = Math.max(maxStep,i+nums[i]);
if(maxStep>= nums.length-1){
return true;
}
}
return false;
}
56. 合并区间
解题思路:根据左区间大小对数组进行排序,然后依次遍历数组判断当前左区间是否大于右区间
- 若大于,则得到一个独立区间,并更新当前的左右区间
- 否则,更新右区间
注意,最后一个区间需要单独加入
public int[][] merge(int[][] intervals) {
Arrays.sort(intervals,(o1,o2)->{
if(o1[0]!=o2[0]){
return o1[0]-o2[0];
}else {
return o1[1]-o2[1];
}
});
List<int[]> res = new ArrayList<>();
int preLeft = intervals[0][0];
int preRight = intervals[0][1];
for (int i = 1; i < intervals.length; i++) {
if(intervals[i][0] > preRight){
res.add(new int[]{preLeft,preRight});
preRight = intervals[i][1];
preLeft = intervals[i][0];
}else {
preRight = Math.max(preRight,intervals[i][1]);
}
}
res.add(new int[]{preLeft,preRight});
return res.toArray(new int[res.size()][]);
}
62. 不同路径
解题思路:动态规划 dp[][]记录到达当前位置的的走法总数‘
初始化 :当i = 0 j= 0 时,dp = 1
转移方程 : dp i j = dp i-1 j + dp i j-2;
public int uniquePaths(int m, int n) {
int n = obstacleGrid.length, m = obstacleGrid[0].length;
int[][] dp = new int[n][m];
for (int i = 0; i < m; i++) {
if (obstacleGrid[0][i] == 1) break; //一旦遇到障碍,后续都到不了
dp[0][i] = 1;
}
for (int i = 0; i < n; i++) {
if (obstacleGrid[i][0] == 1) break; 一旦遇到障碍,后续都到不了
dp[i][0] = 1;
}
for (int i = 1; i < n; i++) {
for (int j = 1; j < m; j++) {
if (obstacleGrid[i][j] == 1) continue;
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[n - 1][m - 1];
}
64. 最小路径和
解题思路 :动态规划 dp[][]表示到达当前位置的最小路径和
初始化 :当i = 0 时,dp i j = dp i-1j +grid i j ; j= 0dp i j =dp i j-1+grid i j;
状态转移 dp i j = Math.min(dp i-1 j, dp i j-1) ;
public int minPathSum(int[][] grid) {
int[][] dp = new int[grid.length][grid[0].length];
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if(i == 0 && j == 0){
dp[i][j] = grid[0][0];
}else if(i == 0 ){
dp[i][j] = grid[i][j] + dp[i][j-1];
}else if(j == 0 ){
dp[i][j] = grid[i][j] + dp[i-1][j];
}else {
dp[i][j] = grid[i][j] + Math.min(dp[i - 1][j],dp[i][j - 1]);
}
}
}
return dp[grid.length - 1][grid[0].length -1];
}
72. 编辑距离
解题思路 :动态规划 dp i j 表示 当前位置单词转换的最少步骤
状态转移: if(ch1 == ch2) dp[i][j] = dp[i-1][j-1];
else dp[i][j] = Math.min(dp[i-1][j],Math.min(dp[i-1][j-1],dp[i][j-1]))+ 1;
h | o | r | s | e | ||
---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | |
r | 1 | 1 | 2 | 2 | 3 | 3 |
s | 2 | 2 | 2 | 3 | 2 | 3 |
e | 3 | 3 | 3 | 3 | 4 | 3 |
public int minDistance(String word1, String word2) {
int[][] dp = new int[word1.length() + 1][word2.length() + 1];
for (int i = 0; i < word2.length() + 1; i++) {
dp[0][i] = i;
}
for (int i = 0; i < word1.length() + 1; i++) {
dp[i][0] = i;
}
for (int i = 1; i <word1.length() + 1 ; i++) {
char ch1 = word1.charAt(i - 1);
for (int j = 1; j < word2.length() + 1 ; j++) {
char ch2 = word2.charAt(j - 1);
if(ch1 == ch2){
dp[i][j] = dp[i-1][j-1];
}else {
dp[i][j] = Math.min(dp[i-1][j],Math.min(dp[i-1][j-1],dp[i][j-1]))+ 1;
}
}
}
return dp[word1.length()][word2.length()];
}
583. 两个字符串的删除操作
解题思路:动态规划 dp i j表示使前i个word1和前j个word2相同的最少删除步数
s | e | a | ||
---|---|---|---|---|
0 | 1 | 2 | 3 | |
e | 1 | 2 | 1 | 2 |
a | 2 | 3 | 2 | 1 |
t | 3 | 4 | 3 | 2 |
状态转移:
if(ch1 == ch2){dp i j = dp i -1j -1
else dp i j = Math.min(dp i-1 j ,dp i j-1)+1
public int minDistance(String word1, String word2) {
int[][] dp = new int[word1.length() + 1][word2.length() +1];
for (int i = 0; i < word2.length() + 1; i++) {
dp[0][i] = i;
}
for (int i = 0; i < word1.length() + 1; i++) {
dp[i][0] = i;
}
for (int i = 1; i < word1.length() + 1; i++) {
char ch1 = word1.charAt(i -1);
for (int j = 1; j < word2.length() + 1; j++) {
char ch2 = word2.charAt(j - 1);
if(ch1 == ch2){
dp[i][j] = dp[i -1][j -1];
}else {
dp[i][j] = Math.min(dp[i-1][j],dp[i][j-1]) + 1;
}
}
}
return dp[word1.length()][word2.length()];
}
75. 颜色分类
分析:由于只有三个数,直接使用排序(归并和快速可以完成题目,但是浪费了数据的规律)
解题思路:双指针,题中只存在三个数,那么可以只使用两个个指针分别指向当前 0 ,1的位置,遇到零,与当前index0指向位置交换数据,遇到1与当前index1位置交换数据,为保证数字1在数字0后面,当index1 > index0时,要将index1的位置向后移。
public void sortColors(int[] nums) {
int index0 = 0;
int index1 = 0;
for (int i = 0; i < nums.length; i++) {
if(nums[i] == 1){
int temp = nums[i];
nums[i] = nums[index1];
nums[index1] = temp;
index1++;
}else if(nums[i] == 0){
int temp = nums[i];
nums[i] = nums[index0];
nums[index0] = temp;
if(index0 < index1){
temp = nums[i];
nums[i] = nums[index1];
nums[index1] = temp;
}
index0++;
index1++;
}
}
}
76. 最小覆盖子串
解题思路:哈希表+滑动窗口
利用哈希表记录 字符个数,用于判断当前子串是否满足要求
- 若包含所有字符,左移滑动窗口,更新最小子串,直至不满足要求
- 否则,右移滑动串口
public static String minWindow(String s, String t) {
if(t.length() > s.length()){
return "";
}
Map<Character,Integer> sMap = new HashMap<>();
Map<Character,Integer> tMap = new HashMap<>();
for (int i = 0; i < t.length(); i++) {
tMap.put(t.charAt(i), tMap.getOrDefault(t.charAt(i),0)+1);
}
int left = 0;
String res = new String();
for (int right = 0; right < s.length(); right++) {
char charAt = s.charAt(right);
sMap.put(charAt, sMap.getOrDefault(charAt,0) + 1);
while (solve(sMap,tMap)){
String temp = s.substring(left,right + 1);
if(res.length() == 0){
res = temp;
}else {
if(res.length() > temp.length()){
res = temp;
}
}
char ch = s.charAt(left);
sMap.put(ch, sMap.getOrDefault(ch,0) - 1);
left ++;
}
}
return res;
}
private static boolean solve(Map<Character, Integer> sMap, Map<Character, Integer> tMap) {
for(Character ch : tMap.keySet()){
if(!sMap.containsKey(ch)||sMap.get(ch) < tMap.get(ch)){
return false;
}
}
return true;
}
78. 子集
解题思路: 回溯
注意:由于空数组也是子集,终止条件不要加入return
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
backTracking(nums,0,new ArrayList<Integer>());
return res;
}
private void backTracking(int[] nums, int start, ArrayList<Integer> list) {
if(list.size() <= nums.length){
res.add(new ArrayList<>(list));
}
for (int i = start; i < nums.length; i++) {
list.add(nums[i]);
backTracking(nums,i+1,list);
list.remove(list.size() -1);
}
return;
}
public boolean exist(char[][] board, String word) {
boolean[][] visited = new boolean[board.length][board[0].length];
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if(backTracking(i,j,0,visited,word,board)){
return true;
}
}
}
return false;
}
private boolean backTracking(int i, int j, int index, boolean[][] visited, String word, char[][] board) {
if(index >= word.length()){
return true;
}
if(i < 0 || j < 0 ||i >= visited.length|| j >= visited[0].length || word.charAt(index)!=board[i][j]
||visited[i][j]){
return false;
}
visited[i][j] = true;
boolean res = backTracking(i + 1,j,index + 1,visited,word, board)||
backTracking(i ,j+1,index + 1,visited,word, board)||
backTracking(i - 1,j,index + 1,visited,word, board)||
backTracking(i ,j -1,index + 1,visited,word, board);
visited[i][j] = false;
return res;
}
79. 单词搜索
解题思路:回溯 依次以数字为起点,利用回溯依次遍历;
public boolean exist(char[][] board, String word) {
boolean[][] visited = new boolean[board.length][board[0].length];
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if(backTracking(i,j,0,visited,word,board)){
return true;
}
}
}
return false;
}
private boolean backTracking(int i, int j, int index, boolean[][] visited, String word, char[][] board) {
if(index >= word.length()){
return true;
}
if(i < 0 || j < 0 ||i >= visited.length|| j >= visited[0].length || word.charAt(index)!=board[i][j]
||visited[i][j]){
return false;
}
visited[i][j] = true;
boolean res = backTracking(i + 1,j,index + 1,visited,word, board)||
backTracking(i ,j+1,index + 1,visited,word, board)||
backTracking(i - 1,j,index + 1,visited,word, board)||
backTracking(i ,j -1,index + 1,visited,word, board);
visited[i][j] = false;
return res;
}
84. 柱状图中最大的矩形
解题思路:双指针(超时):假设当前的高度为最高的高度(有点贪心的意思),以当前位置为中心,向左,向右依次遍历,直至左右的位置比当前位置低,则退出遍历,更新面积。
public int largestRectangleArea(int[] heights) {
int res = 0;
for (int i = 0; i < heights.length; i++) {
int l = i ;
int r = i ;
for (int j = i-1; j >= 0 ; j--) {
if(heights[j] < heights[i]){
break;
}
l--;
}
for (int j = i + 1; j < heights.length; j++) {
if(heights[j]<heights[i]){
break;
}
r++;
}
res = Math.max(res,heights[i] * (r - l + 1));
}
return res;
}
解题思路:单调栈(维护一个单调递减栈)
0 1 2 入栈,当遇到heights[i] >heights[ stack.peak()],右边界为i,高度为 stack.pop(),左边界为stack.peak(),然后,更新面积,直到heights[i] <= heights[ stack.peak()],
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
height | 0 | 2 | 1 | 5 | 6 | 2 | 3 | 0 |
area | 0 | 2 | 0 | 6 | 10 | 8 | 3 | 6 |
public int largestRectangleArea(int[] heights) {
Stack<Integer> st = new Stack();
int [] newHeights = new int[heights.length + 2];
newHeights[0] = 0;
newHeights[newHeights.length - 1] = 0;
for (int index = 0; index < heights.length; index++){
newHeights[index + 1] = heights[index];
}
int res = 0;
st.add(0);
for (int i = 1; i < newHeights.length; i++) {
if(newHeights[i] > newHeights[st.peek()]){
st.push(i);
}else if(newHeights[i] == newHeights[st.peek()]){
st.pop();
st.push(i);
}else {
while (!st.isEmpty() && newHeights[st.peek()] > newHeights[i]){
int mid = st.peek();
st.pop();
int left = st.peek();
int right = i;
int w = right - left - 1;
int h = newHeights[mid];
res = Math.max(res,w * h);
}
st.push(i);
}
}
return res;
}
85. 最大矩形
题意分析:初看该题,可能为一道新题,但仔细一想,则是题84从一维空间发展到二维空间,只要计算每一行的柱状高度,然后带入柱状图中最大矩形的求法,即可完成本题求解
解题思路:双指针
public int maximalRectangle(char[][] matrix) {
if (matrix.length == 0) {
return 0;
}
int res = 0;
int[] height = new int[matrix[0].length];
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
if (matrix[i][j] == '0') {
height[j] = 0;
} else {
height[j] += 1;
}
}
res = Math.max(res,largestRectangleArea(height));
}
return res;
}
private int largestRectangleArea(int[] heights) {
int res = 0;
for (int i = 0; i < heights.length; i++) {
int l = i ;
int r = i ;
for (int j = i -1; j >= 0 ; j--) {
if(heights[j] < heights[i]){
break;
}
l--;
}
for (int j = i + 1; j < heights.length; j++) {
if(heights[j]<heights[i]){
break;
}
r++;
}
res = Math.max(res,heights[i] * (r - l + 1));
}
return res;
}
解题思路:单调栈
public int maximalRectangle(char[][] matrix) {
if (matrix.length == 0) {
return 0;
}
int res = 0;
int[] height = new int[matrix[0].length];
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
if (matrix[i][j] == '0') {
height[j] = 0;
} else {
height[j] += 1;
}
}
res = Math.max(res,largestRectangleArea(height));
}
return res;
}
private int largestRectangleArea(int[] heights) {
Stack<Integer> st = new Stack<Integer>();
int[] newHeights = new int[heights.length + 2];
newHeights[0] = 0;
newHeights[newHeights.length - 1] = 0;
for (int index = 0; index < heights.length; index++) {
newHeights[index + 1] = heights[index];
}
int res = 0;
st.add(0);
for (int i = 1; i < newHeights.length; i++) {
if (newHeights[i] > newHeights[st.peek()]) {
st.push(i);
} else if (newHeights[i] == newHeights[st.peek()]) {
st.pop();
st.push(i);
} else {
while (!st.isEmpty() && newHeights[st.peek()] > newHeights[i]) {
int mid = st.peek();
st.pop();
int left = st.peek();
int right = i;
int w = right - left - 1;
int h = newHeights[mid];
res = Math.max(res, w * h);
}
st.push(i);
}
}
return res;
}
96. 不同的二叉搜索树
解题思路:动态规划 定义dp[] 1到i节点组成二叉搜索树的个数位dp[i];
解析见:
https://programmercarl.com/0096.%E4%B8%8D%E5%90%8C%E7%9A%84%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91.html#%E6%80%9D%E8%B7%AF
public int numTrees(int n) {
int[] dp = new int[n + 1]; // 1到i节点组成二叉搜索树的个数位dp[i];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n ; i++) {
for (int j = 1; j <=i; j++) {
dp[i] += dp[i-j] *dp[j - 1];
}
}
return dp[n];
}
98. 验证二叉搜索树
解题思路:中序遍历
根据搜索树的特点,中序遍历二叉树后,遍历结果集是否满足从小到大排列
public boolean isValidBST(TreeNode root) {
if(root == null){
return true;
}
List<Integer> list = new ArrayList<>();
dfs(root,list);
for (int i = 0; i < list.size() - 1; i++) {
if(list.get(i+1)<= list.get(i))
return false;
}
return true;
}
private void dfs(TreeNode root, List<Integer> list) {
if(root == null){
return;
}
dfs(root.left,list);
list.add(root.val);
dfs(root.right,list);
return;
}
解题思路:递归 + 中序遍历
注意:
- 需要使用一个临时变量记录上一次迭代遇到的节点,根据搜索树的定义,通过中序遍历可知,当前节点的值一定大于前一节点的值,若小于,则直接返回false;
- 遇到空节点也认为是二叉搜索树
TreeNode node ;
public boolean isValidBST(TreeNode root) {
if(root == null){
return true;
}
return dfs(root);
}
private boolean dfs(TreeNode root) {
if(root == null){
return true;
}
boolean left = dfs(root.left);
if(!left){
return false;
}
if(max!=null && root.val <= max.val){
return false;
}
max = root;
boolean right = dfs(root.right);
return right;
}
101. 对称二叉树
解题思路:递归+中序遍历 将树分为内外两侧,依次判断内外侧是否对称
public boolean isSymmetric(TreeNode root) {
if(root == null){
return true;
}
return dfs(root.left,root.right);
}
private boolean dfs(TreeNode left, TreeNode right) {
if(left == null && right != null){
return false;
}else if(left != null && right==null){
return false;
}else if(left == null && right == null){
return true;
}else if(left.val != right.val){
return false;
}
boolean flag1 = dfs(left.left, right.right //内侧
boolean flag2 = dfs(left.right, right.left); // 外侧
return flag1 == false ? false : flag2;
}
105. 从前序与中序遍历序列构造二叉树
解题思路 :递归 前序遍历的顺序是 中 左 右 中序遍历的顺序是 左 中 右 ,可以从pre数组中获得根节点,然后在in数组中找到该节点的位置,那么该节点的左边就是根节点的左子树,右边是根节点的右子树,递归遍历,直到左右区间重合 返回null
pre 3 9 20 15 7
in 9 3 15 20 7 -> 左子树 3 右子树
public TreeNode buildTree(int[] preorder, int[] inorder) {
if(preorder.length == 0){
return null;
}
return solve(preorder,0,preorder.length - 1,inorder,0,inorder.length - 1);
}
private TreeNode solve(int[] preorder, int preLeft, int preRight, int[] inorder, int inLeft, int inRight) {
if(preLeft > preRight || inLeft > inRight){
return null;
}
//找到根节点坐标
int rootValue = preorder[preLeft];
int index = inLeft;
for (int i = inLeft; i <= inRight; i++) {
if(inorder[i] == rootValue){
index = i;
break;
}
}
TreeNode node = new TreeNode(rootValue);
node.left = solve(preorder,preLeft + 1,preLeft + (index - inLeft),inorder,inLeft,index -1);
node.right = solve(preorder,preLeft +(index - inLeft) + 1,preRight,inorder,index+1,inRight);
return node;
}
106. 从中序与后序遍历序列构造二叉树
解题思路:递归
public TreeNode buildTree(int[] inorder, int[] postorder) {
if(inorder.length == 0){
return null;
}
return solve(inorder,0,inorder.length,postorder,0,postorder.length);
}
private TreeNode solve(int[] inorder, int inLeft, int inRight, int[] postorder, int postLeft, int postRight) {
// 没有元素了
if (inRight - inLeft < 1) {
return null;
}
// 只有一个元素了
if (inRight - inLeft == 1) {
return new TreeNode(inorder[inLeft]);
}
int rootValue = postorder[postRight - 1];
int index = 0;
for (int i = inLeft; i < inRight ; i++) {
if(inorder[i] == rootValue){
index = i;
break;
}
}
TreeNode node = new TreeNode(rootValue);
node.left = solve(inorder,inLeft,index,postorder,postLeft,postLeft + (index - inLeft));
node.right = solve(inorder,index + 1,inRight,postorder,postLeft + (index - inLeft),postRight-1);
return node;
}
114. 二叉树展开为链表
解题思路:先前序遍历 + 递归构造
public void flatten(TreeNode root) {
List<TreeNode> list = new ArrayList<TreeNode>();
preorderTraversal(root, list);
int size = list.size();
for (int i = 1; i < size; i++) {
TreeNode prev = list.get(i - 1), curr = list.get(i);
prev.left = null;
prev.right = curr;
}
}
public void preorderTraversal(TreeNode root, List<TreeNode> list) {
if (root != null) {
list.add(root);
preorderTraversal(root.left, list);
preorderTraversal(root.right, list);
}
}
解题思路:模拟
public void flatten(TreeNode root) {
while (root != null){
if(root.left == null){
root = root.right;
}else {
//找到左子树的最右边节点
TreeNode pre = root.left;
while (pre.right != null){
pre = pre.right;
}
//将右子树接到左子树的右边的节点
pre.right = root.right;
//将左子树查到root.right中
root.right = root.left;
root.left = null;
//考虑下一个节点
root = root.right;
}
}
}
124. 二叉树中的最大路径和
解题思路:哈希表 用于记录当前数字相邻的最大长度,递归寻找当前数字+1出现的最长相邻数字
public int longestConsecutive(int[] nums) {
if(nums.length <= 1){
return nums.length;
}
Map<Integer,Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
map.put(nums[i],1);
}
int res = 0;
for(int i : nums){
solve(map ,i);
}
for(Integer i :map.keySet()){
res = Math.max(res,map.get(i));
}
return res;
}
private int solve(Map<Integer, Integer> map, int num) {
if(!map.containsKey(num)){
return 0;
}
int count = map.get(num);
if(count > 1){
return count;
}
count = solve(map,num + 1)+ 1;
map.put(num,count);
return count;
}
139. 单词拆分
解题思路:动态规划 dp[i] 表示前i个字符是否存在字典集
状态转移:dp[i]=dp[j] && check(s[j…i−1])
注意:字符从后往前插入
public boolean wordBreak(String s, List<String> wordDict) {
//前i个字符是否存在wordDict中
boolean[] dp = new boolean[s.length() + 1];
dp[0] = true;
for (int i = 1; i <= s.length(); i++) {
for (int j = i - 1; j >= 0; j--) {
if(wordDict.contains(s.substring(j,i)) &&dp[j]){
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}
142. 环形链表 II
解题思路 :双指针(快慢指针)
public ListNode detectCycle(ListNode head) {
if(head == null){
return head;
}
ListNode slow = head;
ListNode fast = head;
while (fast != null){
if(fast.next == null){
break;
}
slow = slow.next;
fast = fast.next.next;
if(slow == fast){
fast = head;
while (slow != fast){
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
return null ;
}
146. LRU 缓存
解题思路:双端链表 + 哈希表模拟
构造器:建立哈希表和哈希表容量
get:map不存在直接返回-1;否则更新node.val,并更新双端链表,使当前节点放到head的下一个
put:如果存在当前key,更新值,并把当前node放至head.next;否则,判断是否大于capacity,若大于,删除tail前一个节点,并将该节点插入至head.next
class LRUCache {
class LinkNode{
int val;
int key;
LinkNode pre;
LinkNode next;
public LinkNode(int key, int val) {
this.val = val;
this.key = key;
}
}
int capacity ;
Map<Integer,LinkNode> map =new HashMap<>();
LinkNode head = new LinkNode(0,0);
LinkNode tail = new LinkNode(0,0);
public LRUCache(int capacity) {
this.capacity = capacity;
head.next = tail;
tail.pre = head;
}
public int get(int key) {
if(!map.containsKey(key)){
return -1;
}else {
LinkNode linkNode = map.get(key);
//因为用到了,要把当前节点放到最前面
moveTotop(linkNode);
return linkNode.val;
}
}
private void moveTotop(LinkNode node) {
//连接前后节点
node.pre.next = node.next;
node.next.pre = node.pre;
//将当前节点放到最前面
LinkNode temp = head.next;
head.next = node;
node.pre = head;
temp.pre = node;
node.next = temp;
}
public void put(int key, int value) {
if(map.containsKey(key)){
//因为用到数据需要将该节点放到最前面
LinkNode node = map.get(key);
node.val = value;
moveTotop(node);
}else {
//判断当前容量是否超过设定容量
if(map.size() == capacity){//头尾节点不入map
//需要删除最后一个节点
deleteLastNode();
}
LinkNode node = new LinkNode(key,value);
LinkNode temp = head.next;
node.next = temp;
node.pre = head;
temp.pre = node;
head.next = node;
map.put(key,node);
map.put(key,node);
}
}
private void deleteLastNode() {
LinkNode last = tail.pre;
last.pre.next = tail;
tail.pre = last.pre;
map.remove(last.key);
}
148. 排序链表
解题思路:内置sort
public ListNode sortList(ListNode head) {
List<ListNode> list = new ArrayList<>();
while (head!=null){
list.add(head);
head = head.next;
}
Collections.sort(list,(o1,o2)->{
return Integer.compare(o1.val,o2.val);
});
ListNode dummy = new ListNode(-1);
ListNode res = dummy;
for (int i = 0; i < list.size(); i++) {
dummy.next = list.get(i);
dummy = dummy.next;
}
dummy.next = null;
return res.next;
}
解题思路:归并排序(递归)
- 先通过快慢指针找到当前链表的中间节点,反复递归,直到把每个节点看出一个单一节点
- 合并节点,通过一个虚节点,将两节点根据val大小合并;
public ListNode sortList(ListNode head) {
if(head == null || head.next == null){
return head;
}
//可以用长度为2的链表举例
ListNode fast = head.next;
ListNode slow = head;
//分成左右两个链表
while (fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
ListNode temp = slow.next;
slow.next = null;
ListNode left = sortList(head);
ListNode right = sortList(temp);
ListNode cur = new ListNode(0);
ListNode res = cur;
// 合并
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;
}
cur.next = left == null ?right : left;
return res.next;
}
152. 乘积最大子数组
解题思路:动态规划
需要用两个dp数组记录的原因:引文如果前面的结果为负数,当前的数为负数,负负得正,也可能使最大值,所以需要用两数组记录到当前位置得最大值和最小值
public int maxProduct(int[] nums) {
int[] min = new int[nums.length]; //第i个结尾乘积最小子数组
int[] max = new int[nums.length]; //第i个结尾乘积最大子数组
min[0] = nums[0];
max[0] = nums[0];
int res = max[0];
for (int i = 1; i < nums.length; i++) {
min[i] = Math.min(min[i - 1] * nums[i],Math.min(nums[i],max[i-1] * nums[i]));
max[i] = Math.max(max[i - 1] * nums[i],Math.max(nums[i],min[i-1] * nums[i]));
res = Math.max(res,max[i]);
}
return res;
}
155. 最小栈
解题思路:利用两个栈维护
Stack<Integer> stack;
Stack<Integer> minStack;
public MinStack() {
stack = new Stack<>();
minStack = new Stack<>();
}
public void push(int val) {
if(minStack.isEmpty()){
minStack.push(val);
stack.push(val);
}else {
Integer peek = minStack.peek();
if(val > peek){
minStack.push(peek);
}else {
minStack.push(val);
}
stack.push(val);
}
}
public void pop() {
minStack.pop();
stack.pop();
}
public int top() {
return stack.peek();
}
public int getMin() {
return minStack.peek();
}
200. 岛屿数量
解题思路:DFS
依次遍历数组,遇到nums[i] j = 1,res ++;
递归:从该点出发,依次向 上下左右四个方向递归,如果该位置为1,将该位置得值改为0;
public int numIslands(char[][] grid) {
int res =0;
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if(grid[i][j] == '1'){
res ++;
dfs(grid,i,j);
}
}
}
}
private void dfs(char[][] grid, int i, int j) {
if(i < 0 || j <0 ||i > grid.length -1 || j > grid[0].length -1||grid[i][j] == '0'){
return ;
}
grid[i][j] = '0';
//四个方向
dfs(grid,i+1,j);
dfs(grid,i-1,j);
dfs(grid,i,j+1);
dfs(grid,i,j-1);
return;
}
private void dfs2(char[][] grid, int i, int j) {
if(i < 0 || j <0 ||i > grid.length -1 || j > grid[0].length -1||grid[i][j] == '0'){
return ;
}
grid[i][j] = '0';
//八个方向
dfs(grid,i+1,j);
dfs(grid,i-1,j);
dfs(grid,i,j+1);
dfs(grid,i,j-1);
dfs(grid,i+1,j+1);
dfs(grid,i+1,j-1);
dfs(grid,i-1,j-1);
dfs(grid,i-1,j+1);
return;
}
207. 课程表
解题思路:DFS+ 拓扑排序
初始化:根据课程学习关系,建立邻接表,建立visited (0为搜索 1搜索中 2搜索完成)用于深度搜索
boolean res = true;
ArrayList<ArrayList<Integer>> list = new ArrayList<>();
int[] visited;
public boolean canFinish(int numCourses, int[][] prerequisites) {
for (int i = 0; i < numCourses; i++) {
list.add(new ArrayList<>());
}
//(0 :为搜索 1:搜索中 2:搜索完成)
visited = new int[numCourses];
//建立邻接表
for (int i = 0; i < prerequisites.length; i++) {
list.get(prerequisites[i][0]).add(prerequisites[i][1]);
}
//利用DFS实现拓扑排序
for (int i = 0; i < numCourses && res; i++) {
if(visited[i] == 0){
dfs(i);
}
}
return res;
}
private void dfs(int i) {
visited[i] = 1;
for(int v : list.get(i)){
if(visited[v] == 0){
dfs(v);
}
if(!res){
return;
}else if(visited[i] == 1){
res =false;
return;
}
}
visited[i] = 2;
}
210. 课程表 II
解题思路:DFS+ 拓扑排序
初始化:根据课程学习关系,建立邻接表,建立visited (0为搜索 1搜索中 2搜索完成)用于深度搜索
boolean flag = true;
ArrayList<ArrayList<Integer>> list;
int[] visited;
int index;
int[] res;
public int[] findOrder(int numCourses, int[][] prerequisites) {
list = new ArrayList<>();
for (int i = 0; i < numCourses; i++) {
list.add(new ArrayList<>());
}
res = new int[numCourses];
//(0 :为搜索 1:搜索中 2:搜索完成)
visited = new int[numCourses];
index = numCourses -1;
//建立邻接表
for (int i = 0; i < prerequisites.length; i++) {
list.get(prerequisites[i][0]).add(prerequisites[i][1]);
}
//利用DFS实现拓扑排序
for (int i = 0; i < numCourses && flag; i++) {
if(visited[i] == 0){
dfs(i);
}
}
if(!flag){
return new int[0];
}
return res;
}
private void dfs(int i) {
visited[i] = 1;
for(int v : list.get(i)){
if(visited[v] == 0){
dfs(v);
if(!flag){
return;
}
}else if(visited[i] == 1){
flag = false;
return;
}
visited[i] = 2;
res[index--]= i;
}
}
297. 二叉树的序列化与反序列化
解题思路:层序遍历+栈 ,只有层序遍历才能保证树的唯一性
public String serialize(TreeNode root) {
if(root == null){
return null;
}
StringBuilder sb = new StringBuilder();
sb.append("[");
int sum = 1;
Queue<TreeNode> deque = new LinkedList<>();
deque.offer(root);
boolean flag = false;
while (!deque.isEmpty() && !flag){
int size = deque.size();
while (size > 0){
TreeNode node = deque.poll();
if(node == null && sum !=0){
sb.append("null,");
}else {
sb.append(node.val);
sum--;
if(node.left != null){
sum++;
}
if(node.right!= null){
sum++;
}
deque.offer(node.left);
deque.offer(node.right);
if(sum != 0){
sb.append(",");//未到达最后一个节点前都加入一个逗号
}else {
flag = true;
break;
}
size--;
}
}
}
sb.append("]");
return sb.toString();
}
public TreeNode deserialize(String data) {
if(data == null){
return null;
}
String substring = data.substring(1, data.length() - 1);
String[] strings = substring.split(",");
int index = 0;
TreeNode root = new TreeNode(getNode(strings[index++]));
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty() && index < strings.length){
TreeNode node = queue.poll();
if(!"null".equals(strings[index])){
node.left = new TreeNode(getNode(strings[index++]));
queue.offer(node.left);
}else {
node.left =null;
index++;
}
//保证存在右节点
if(index < strings.length && !"null".equals(strings[index])){
node.right = new TreeNode(getNode(strings[index++]));
queue.offer(node.right);
}else {
node.right =null;
index++;
}
}
return root;
}
private int getNode(String str) {
int flag = 1;
int res = 0;
int i = 0;
//如果是负数
if(str.charAt(0) == '-'){
flag = -1;
i++;
}
for ( ;i <str.length() ; i++) {
res = res * 10 + str.charAt(i) - '0';
}
return res * flag;
}
208. 实现 Trie (前缀树)
解题思路:哈希(set)逻辑简单
Set<String> set ;
public Trie() {
set = new HashSet<>();
}
public void insert(String word) {
set.add(word);
}
public boolean search(String word) {
return set.contains(word);
}
public boolean startsWith(String prefix) {
for(String s : set){
if(s.startsWith(prefix)){
return true;
}
}
return false;
}
解题思路:前缀树(字典树)(leetcode720 和692)
视频讲解:https://www.bilibili.com/video/BV1Az4y1S7c7?spm_id_from=333.337.search-card.all.click
private Map<Character,Trie> map;
private boolean isEnd;
public Trie() {
map = new HashMap<>();
isEnd = false; //用于标记当前node结尾是否组成了完整单词
}
public void insert(String word) {
Trie node = this;
for (int i = 0; i < word.length(); i++) {
char charAt = word.charAt(i);
if(!node.map.containsKey(charAt)){
node.map.put(charAt,new Trie());
}
node = node.map.get(charAt);
}
node.isEnd = true;
}
public boolean search(String word) {
Trie trie = searchPrefix(word);
return trie != null && trie.isEnd;
}
public boolean startsWith(String prefix) {
return searchPrefix(prefix)!=null;
}
private Trie searchPrefix(String prefix) {
Trie node = this;
for (int i = 0; i < prefix.length(); i++) {
char charAt = prefix.charAt(i);
if(!node.map.containsKey(charAt)){
return null;
}
node = node.map.get(charAt);
}
return node;
}
215. 数组中的第K个最大元素
解题思路:归并排序or快速排序
public int findKthLargest(int[] nums, int k) {
int[] temp = new int[nums.length];
sort(nums,0,nums.length -1,temp);
return nums[nums.length -k];
}
private void sort(int[] nums, int left, int right,int[] temp) {
if(left == right){
return ;
}
int mid = left + (right - left)/2;
sort(nums,left,mid,temp);
sort(nums,mid+1,right,temp);
merge(nums,left,mid,right,temp);
}
private void merge(int[] nums, int left, int mid, int right,int[] temp) {
for (int i = left; i <= right; i++) {
temp[i] = nums[i];
}
int i = left;
int j = mid + 1;
for (int k = left; k <= right ; k++) {
if(i == mid + 1){
nums[k] = temp[j++];
}else if(j == right + 1){
nums[k] = temp[i++];
}else if(temp[i] <= temp[j]){
nums[k] = temp[i++];
}else {
nums[k] = temp[j++];
}
}
}
221. 最大正方形
解题思路:动态规划
定义 :dp ij 以 i j 为右下角组成的正方形个数
状态转移:若字符为0,dp[i]j = 0;
否则 取Math.min(dp[i][j-1],Math.min(dp[i-1][j-1],dp[i-1][j])) + 1;
public int maximalSquare(char[][] matrix) {
int res = 0;
int[][] dp = new int[matrix.length][matrix[0].length];
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
if(matrix[i][j] == '0'){
dp[i][j] = 0;
}else {
if(i == 0 || j == 0){
dp[i][j] =1;
}else {
dp[i][j] = Math.min(dp[i][j-1],Math.min(dp[i-1][j-1],dp[i-1][j])) + 1;
}
res = Math.max(res,dp[i][j]);
}
}
}
return res *res;
}
236. 二叉树的最近公共祖先
解题思路:递归
解析见:https://programmercarl.com/0236.%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88.html
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null || root == p || root == q){
return root;
}
TreeNode left = lowestCommonAncestor(root.left,p,q);
TreeNode right = lowestCommonAncestor(root.right,p,q);
if(left ==null && right == null){
return null;
}else if(left == null && right != null){
return right;
}else if(left != null && right == null){
return left;
}else {
return root;
}
}
238. 除自身以外数组的乘积
解题思路:构造左右乘积列表
建立两个数组,分别计算当前数字左右两边的乘积
public int[] productExceptSelf(int[] nums) {
int[] res = new int[nums.length];
int[] l = new int[nums.length];// 第i的元素的左边的乘积
int[] r = new int[nums.length];// 第i个元素右边的乘积
l[0] = 1;
l[1] = nums[0];
r[nums.length - 1] = 1;
r[nums.length - 2] = nums[nums.length - 1];
for (int i = 2; i < nums.length; i++) {
l[i] = nums[i-1] * l[i - 1];
}
for (int i = nums.length - 3; i >= 0; i--) {
r[i] = nums[i+1] * r[i + 1];
}
for (int i = 0; i < nums.length; i++) {
res[i] = l[i] *r[i];
}
return res;
}
239. 滑动窗口最大值
解题思路:栈
public int[] maxSlidingWindow(int[] nums, int k) {
int[] res = new int[nums.length- k + 1];
int index = 0;
Deque<Integer> deque = new ArrayDeque<>();
for (int i = 0; i < nums.length; i++) {
// i ~ i + k -1
while (!deque.isEmpty() && deque.peekFirst() < i - k + 1 ){
deque.poll();
}
//维护一个单调递减栈
while (!deque.isEmpty() && nums[deque.peekLast()] <nums[i]){
deque.pollLast();
}
deque.addLast(i);
if(i >= k-1){
res[index++] = nums[deque.peekFirst()];
}
}
return res;
}
240. 搜索二维矩阵 II
解题思路:对每一行进行二分查找
public boolean searchMatrix(int[][] matrix, int target) {
for (int[] row : matrix) {
int index = search(row, target);
if (index >= 0) {
return true;
}
}
return false;
}
public int search(int[] nums, int target) {
int low = 0, high = nums.length - 1;
while (low <= high) {
int mid = (high - low) / 2 + low;
int num = nums[mid];
if (num == target) {
return mid;
} else if (num > target) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return -1;
}
解题思路:从(0,n-1)开始搜索,如果当前的数大于target,则说明该列全部大于target,则col–,否则row++;
public boolean searchMatrix(int[][] matrix, int target) {
int col = matrix[0].length - 1;
int row = 0;
while (row < matrix.length && col >=0){
if(matrix[row][col] == target){
return true;
}else {
if(matrix[row][col] > target){
col--;
}else {
row++;
}
}
}
return false;
}
279. 完全平方数
解题思路:动态规划 完全平方和为n的最少完全平方数为dp[i]
状态转移: dp[i] = Math.min(dp[i],dp[i-j*j] + 1);
public int numSquares(int n) {
int[] dp = new int[n + 1];
Arrays.fill(dp,Integer.MAX_VALUE);
dp[0] =0;
dp[1] =1;
for (int i = 1; i <= n ; i++) {
for (int j = 1; j*j <= i; j++) {
dp[i] = Math.min(dp[i],dp[i-j*j] + 1);
}
}
return dp[n];
}
283. 移动零
解题思路: 模拟
public void moveZeroes(int[] nums) {
int left = 0;
for (int right = 0; right < nums.length; right++) {
if(nums[right] != 0){
nums[left++] = nums[right];
}
while (right == nums.length-1 &&left<nums.length){
nums[left++] = 0;
}
}
}
301. 删除无效的括号
解题思路:回溯
确定需要删除的左右括号数
根据确定的删除左右括号数,进行递归回溯,判断有效括号
List<String> res = new ArrayList<>();
public List<String> removeInvalidParentheses(String s) {
//找到需要删除 左右括号的个数
int lDelete = 0;
int RDelete = 0;
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if(ch == '('){
lDelete ++;
}else if( ch == ')'){
if(lDelete > 0){
lDelete --;
}else {
RDelete ++;
}
}
}
//回溯 删除左右括号
backTracking(lDelete,RDelete,s,0);
return res;
}
private void backTracking(int lDelete, int rDelete, String s, int start) {
if(lDelete == 0 && rDelete == 0 && isValid(s)){
res.add(s);
}
for (int i = start; i < s.length(); i++) {
if(i > start && s.charAt(i) == s.charAt(i - 1)){
continue;
}
if(lDelete > 0 && s.charAt(i)=='('){
backTracking(lDelete - 1,rDelete,s.substring(0,i) + s.substring(i+1),i);
}
if(rDelete > 0 && s.charAt(i)==')'){
backTracking(lDelete ,rDelete - 1,s.substring(0,i) + s.substring(i+1),i);
}
}
}
//有效括号判断
private boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if(ch != '(' && ch != ')'){
continue;
}else {
if(ch == '('){
stack.push(')');
}else{
if(!stack.isEmpty() && stack.peek() == ch){
stack.pop();
}else {
return false;
}
}
}
}
return stack.isEmpty();
}
309. 最佳买卖股票时机含冷冻期
解题思路 动态规划
dp i 0 表示 第i天为买入状态的最大现金 ;dp i 1表示第i天为卖出度过冷冻期的最大现金
dp i 2 表示第i天卖出的最大现金;dp i 2表示第i天为冷冻期的最大现金
状态转移:
dp[i][0] =Math.max(Math.max(dp[i-1][3],dp[i-1][1])-prices[i],dp[i-1][0]);
dp[i][1] =Math.max(dp[i-1][1],dp[i-1][3]);
dp[i][2] =dp[i-1][0] +prices[i];
dp[i][3] =dp[i-1][2];
public int maxProfit(int[] prices) {
int[][] dp = new int[prices.length][4];
dp[0][0] = -prices[0];//买入状态
dp[0][1] = 0;//卖出过冷冻期
dp[0][2] = 0;//今天卖出
dp[0][3] = 0;//今天是冷冻期
for (int i = 1; i < prices.length; i++) {
dp[i][0] =Math.max(Math.max(dp[i-1][3],dp[i-1][1])-prices[i],dp[i-1][0]);
dp[i][1] =Math.max(dp[i-1][1],dp[i-1][3]);
dp[i][2] =dp[i-1][0] +prices[i];
dp[i][3] =dp[i-1][2];
}
return Math.max(dp[prices.length-1][1],Math.max(dp[prices.length - 1][2],dp[prices.length-1][3]));
}
322. 零钱兑换
解题思路:动态规划:01背包问题 先遍历物品,在遍历背包
解析见:https://programmercarl.com/0322.%E9%9B%B6%E9%92%B1%E5%85%91%E6%8D%A2.html#%E6%80%9D%E8%B7%AF
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp,amount+1);
dp[0] = 0;
for (int i = 0; i < coins.length; i++) {
for (int j = coins[i]; j <= amount; j++) { // 遍历背包
if (dp[j - coins[i]] != amount + 1) { // 如果dp[j - coins[i]]是初始值则跳过
dp[j] = Math.min(dp[j - coins[i]] + 1, dp[j]);
}
}
}
return dp[amount] == amount+1?-1:dp[amount];
}
解题思路:动态规划:01背包问题 先遍历背包 在遍历物品
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp,amount+1);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.length; j++) {
if(i >= coins[j]&& dp[i - coins[j]] != amount + 1 ){
dp[i] = Math.min(dp[i],dp[i - coins[j]] + 1);
}
}
}
return dp[amount] == amount + 1 ? -1:dp[amount];
}
337. 打家劫舍 III
解题思路:递归+动态规划
dp[0] 不偷根节点的最大值 = max(left[0],left[1]) + max(dp[0],dp[1])
dp[1] 偷根节点的最大值 = root + left[0] + right[0]
public int rob(TreeNode root) {
if(root == null){
return 0;
}
int[] res = dfs(root);
return Math.max(res[0],res[1]);
}
private int[] dfs(TreeNode root) {
int[] res = new int[2];
if(root == null){
return res;
}
int[] left = dfs(root.left);
int[] right = dfs(root.right);
res[0] = Math.max(left[0],left[1]) + Math.max(right[0] ,right[1]);
res[1] = root.val + left[0] + right[0];
return res;
}
解题思路:记忆化递归
可以使用一个map把计算过的结果保存一下,这样如果计算过孙子了,那么计算孩子的时候可以复用孙子节点的结果。
public int rob(TreeNode root) {
if(root == null){
return 0;
}
Map<TreeNode,Integer> map = new HashMap<>();
return dfs(root,map);
}
private int dfs(TreeNode root, Map<TreeNode, Integer> map) {
if(root == null){
return 0;
}
if(map.containsKey(root)){
return map.get(root);
}
int money = root.val;
if(root.left !=null){
money += dfs(root.left.left,map)+dfs(root.left.right,map);
}
if(root.right !=null){
money += dfs(root.right.left,map)+dfs(root.right.right,map);
}
int res = Math.max(money,dfs(root.left,map)+dfs(root.right,map));
map.put(root, res);
return res;
}
解题思路:暴力递归
public int rob(TreeNode root) {
if (root == null)
return 0;
int money = root.val;
if (root.left != null) {
money += rob(root.left.left) + rob(root.left.right);
}
if (root.right != null) {
money += rob(root.right.left) + rob(root.right.right);
}
return Math.max(money, rob(root.left) + rob(root.right));
}
338. 比特位计数
动态规划 :当前数1的个数等于该数除以2后数1的个数加上该数对2取余之和
状态转移: dp[i] = dp[i >> 1] + (i & 1);
public int[] countBits(int n) {
int[] dp = new int[n + 1];
dp[0] = 0;
for (int i = 1; i <= n; i++) {
// (i & 1) 为除以2 的余数
dp[i] = dp[i >> 1] + (i & 1);
}
return dp;
}
347. 前 K 个高频元素
解题思路:优先队列+哈希表
用哈希表记录数出现的次数,利用优先队列,根据<k,v>中的value进行排序,然后依次遍历数组放入queue中,取出前K个元素即可
public int[] topKFrequent(int[] nums, int k) {
int[] res = new int[k];
Map<Integer,Integer> map = new HashMap<>();
for (int i : nums){
map.put(i,map.getOrDefault(i,0) + 1);
}
Set<Map.Entry<Integer, Integer>> entries = map.entrySet();
//降序排列 P
PriorityQueue<Map.Entry<Integer, Integer>> queue = new PriorityQueue<>((o1, o2) -> o1.getValue() - o2.getValue());
for (Map.Entry<Integer, Integer> entry : entries) {
queue.offer(entry);
if (queue.size() > k) {
queue.poll();
}
}
for (int i = k - 1; i >= 0; i--) {
res[i] = queue.poll().getKey();
}
return res;
}
394. 字符串解码
解题思路:栈 + 字符串
定义两个栈,分别记录数字和字符,根据左右括号分隔和数字,判断进栈
- 遇到 [,就将数字进栈,并从cnt=0,将之前字符压进栈
- 遇到],弹出栈顶数字和栈顶字符,根据次数,接到结尾。
public String decodeString(String s) {
Deque<Integer> timesQue = new LinkedList<>();
Deque<String> stringsQue = new LinkedList<>();
StringBuilder sb = new StringBuilder();
int times = 0;
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if(ch >= '0' && ch <= '9'){
times = times * 10 + ch - '0';
}else if(ch =='['){
timesQue.push(times);
times = 0;
stringsQue.push(sb.toString());
sb.delete(0,sb.length());
}else if(ch == ']'){
int cnt = timesQue.pop();
String str = stringsQue.pop();
while (cnt-->0){
str += sb.toString();
}
sb = new StringBuilder(str);
}else {
sb.append(ch);
}
}
return sb.toString();
}
399. 除法求值
解题思路:并查集的底层为数组 一个权重数组和一个父节点数组
class Union{
int[] parent ; //每一个节点所对应的父节点的id
double[] weight; //节点指向父节点的权值
public Union(int capacity) {
parent = new int[capacity];
weight = new double[capacity];
for (int i = 0; i < capacity; i++) {
parent[i] = i;
weight[i] = 1.0;
}
}
public void union(Integer x, Integer y, double value) {
int rootX = find(x);
int rootY = find(y);
if(rootX == rootY){
return ;
}
parent[rootX] = rootY;
weight[rootX] = weight[y] * value / weight[x];
}
/**
* @author:
* @description: 路径压缩
* @param: [x]
* @date: 2022/4/28
* @return: int
*/
private int find(Integer x) {
if(x != parent[x]){
int pre = parent[x];
parent[x] = find(parent[x]);
weight[x] *= weight[pre];
}
return parent[x];
}
public double getValue(Integer x, Integer y) {
int rootX = find(x);
int rootY = find(y);
if(rootX == rootY){
return weight[x] / weight[y];
}else {
return -1.0d;
}
}
}
public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
int equationsLen = equations.size();
Map<String,Integer> map = new HashMap<>(2 * equationsLen);
int id = 0;
Union union = new Union(2 * equationsLen);
//预处理 构建并查集
for (int i = 0; i < equationsLen; i++) {
List<String> equation = equations.get(i);
String var1 = equation.get(0);
String var2 = equation.get(1);
if(!map.containsKey(var1)){
map.put(var1,id++);
}
if(!map.containsKey(var2)){
map.put(var2,id++);
}
union.union(map.get(var1),map.get(var2),values[i]);
}
//做查询
int queriesLen = queries.size();
double[] res = new double[queriesLen];
for (int i = 0; i < queriesLen; i++) {
List<String> list = queries.get(i);
String var1 = list.get(0);
String var2 = list.get(1);
if(!map.containsKey(var1) || !map.containsKey(var2)){
res[i] = -1.0d;
}else {
Integer x = map.get(var1);
Integer y = map.get(var2);
res[i] = union.getValue(x,y);
}
}
return res;
}
并查集相关题目
「力扣」第 547 题:省份数量(中等);
「力扣」第 684 题:冗余连接(中等);
「力扣」第 1319 题:连通网络的操作次数(中等);
「力扣」第 1631 题:最小体力消耗路径(中等);
「力扣」第 959 题:由斜杠划分区域(中等);
「力扣」第 1202 题:交换字符串中的元素(中等);
「力扣」第 947 题:移除最多的同行或同列石头(中等);
「力扣」第 721 题:账户合并(中等);
「力扣」第 803 题:打砖块(困难);
「力扣」第 1579 题:保证图可完全遍历(困难);
「力扣」第 778 题:水位上升的泳池中游泳(困难)
406. 根据身高重建队列
解题思路:数组排序 先按身高排序,身高相同按在前面的人数多少排序
public int[][] reconstructQueue(int[][] people) {
// 身高从大到小排(身高相同k小的站前面)
Arrays.sort(people,(p1,p2)->{
if(p1[0] == p2[0]){
return p1[1] - p2[1];
}
return p2[0] - p1[0];
});
List<int[]> que = new ArrayList<>();
//前面几个就在第几个插入
for (int[] p : people) {
que.add(p[1],p);
}
return que.toArray(new int[people.length][]);
}
416. 分割等和子集
解题思路:动态规划 转换成01背包问题
物品就是数组,容量是数组和/2。
public boolean canPartition(int[] nums) {
int sum = Arrays.stream(nums).sum();
if(sum % 2 == 1){
return false;
}
int ave = sum / 2;
int[] dp = new int[ave + 1];//和为i的最大子集和
dp[0] = 0;
for (int i = 0; i < nums.length; i++) {
for (int j = ave; j >=nums[i] ; j--) {
dp[j] = Math.max(dp[j],dp[j - nums[i]]+nums[i]);
}
}
if(dp[ave] == ave) {
return true;
}else {
return false;
}
}
437. 路径总和 III
解题思路:递归
由于不要求叶子节点技术和根节点开始,所以要从每个节点开始,开始递归路径和。
public int pathSum(TreeNode root, int targetSum) {
if(root == null){
return 0;
}
int res = 0;
res = dfs(root,targetSum);
res += pathSum(root.left,targetSum);
res += pathSum(root.right,targetSum);
return res;
}
private int dfs(TreeNode root, int targetSum) {
if(root ==null){
return 0;
}
int res =0;
if(root.val == targetSum){
res++;
}
res+=dfs(root.left,targetSum -root.val);
res+=dfs(root.right,targetSum-root.val);
return res;
}
112. 路径总和
解题思路:递归
boolean res = false;
public boolean hasPathSum(TreeNode root, int targetSum) {
if(root == null){
return false;
}
dfs(root,targetSum);
return res;
}
private void dfs(TreeNode root,int sum){
if(root == null){
return;
}
sum -= root.val;
if(root.left ==null && root.right == null && sum ==0){
res= true;
}
dfs(root.left,sum);
dfs(root.right,sum);
}
113. 路径总和 II
解题思路:回溯
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
if(root == null){
return new ArrayList<>();
}
dfs(root,targetSum,new ArrayList<>());
return res;
}
private void dfs(TreeNode root,int sum,ArrayList<Integer> list){
if(root == null){
return ;
}
list.add(root.val);
sum -= root.val;
if(root.left == null && root.right == null && sum == 0){
res.add(new ArrayList<>(list));
}
dfs(root.left,sum,list);
dfs(root.right,sum,list);
list.remove(list.size() -1);
}
438. 找到字符串中所有字母异位词
解题思路:滑动串口+哈希比较
由于都是小写字母,用两个数组记录滑动串口内的字符与比较的字符是否相等
public List<Integer> findAnagrams(String s, String p) {
if(p.length() > s.length()){
return new ArrayList<>();
}
List<Integer> res = new ArrayList<>();
int[] A = new int[26];
for (int i = 0; i < p.length(); i++) {
A[p.charAt(i) - 'a'] ++;
}
int[] B = new int[26];
for (int right = 0,left = 0; right< s.length(); right++) {
B[s.charAt(right) - 'a']++;
if(right - left + 1 >p.length() ){
B[s.charAt(left++) - 'a']--;
}
if(right - left + 1 == p.length()){
if(helper(A,B)){
res.add(left);
}
}
}
return res;
}
private boolean helper(int[] a, int[] b) {
for (int i = 0; i < a.length; i++) {
if(a[i] != b[i]){
return false;
}
}
return true;
}
解题思路:滑动窗口+排序 (大量实践浪费在排序上,性能差)
public List<Integer> findAnagrams(String s, String p) {
if(p.length() > s.length()){
return new ArrayList<>();
}
List<Integer>res = new ArrayList<>();
char[] B = p.toCharArray();
Arrays.sort(B);
for (int r = p.length(),l = 0; r <= s.length(); r++,l++) {
String substring = s.substring(l, r);
char[] A = substring.toCharArray();
Arrays.sort(A);
if(helper(A,B)){
res.add(l);
}
}
return res;
}
private boolean helper(char[] a, char[] b) {
for (int i = 0; i < b.length; i++) {
if(a[i]!=b[i]){
return false;
}
}
return true;
}
448. 找到所有数组中消失的数字
解题思路:哈希表,利用set存入数据,前后遍历两次即可
public List<Integer> findDisappearedNumbers(int[] nums) {
List<Integer> res = new ArrayList<>();
Set<Integer> set = new HashSet<>();
for (int i = 0; i < nums.length; i++) {
set.add(nums[i]);
}
for (int i = 1; i < nums.length + 1; i++) {
if(!set.contains(i)){
res.add(i);
}
}
return res;
}
461. 汉明距离
解题思路:位运算 汉明距离==两数异或后1的个数
public int hammingDistance(int x, int y) {
int num = x ^ y;
return hammingWeight(num);
}
private int hammingWeight(int num) {
int ret = 0;
for (int i = 0; i < 32; i++) {
if ((num & (1 << i)) != 0) {
ret++;
}
}
return ret;
}
494. 目标和
解题思路:动态规划 转换成01 背包问题
数组的和记为 sum,目标和位 target ,记数组部分和为x,则剩下部分和为sum - x;
那么 x-(sum -x) = target ,则有x = (tartget + sum)/2,问题转化成在数组内,存在和为x的个数
public int findTargetSumWays(int[] nums, int target) {
int sum = Arrays.stream(nums).sum();
if(sum < target) return 0;
//加法的总和为x 减法的总和为sum -x
//target = x - (sum -x) -> x = (target + sum) / 2;
if((target+sum) % 2 !=0 ){
return 0;
}
int x = (target + sum) / 2;
if(x < 0) x = -x;
int[] dp = new int[x+1];//填满j容量的包,有几种方法
dp[0] = 1;
//转化为01背包为问题描述为:nums中找到和为x的组合有几个
//x = 5 (05 14,23,32 41)
for (int i = 0; i < nums.length; i++) {
for (int j = x; j >= nums[i] ; j--) {
dp[j] += dp[j - nums[i]];
}
}
return dp[x];
}
解题思路:回溯,不仅可以计算出个数,也可以求出组合,同理。
数组的和记为 sum,目标和位 target ,记数组部分和为x,则剩下部分和为sum - x;
那么 x-(sum -x) = target ,则有x = (tartget + sum)/2,问题转化成在数组内,存在和为x的个数
注意:由于有重复数字,可以对数组先进行排序或者用set存(复杂度会高)
List<List<Integer>> res= new ArrayList<>();
public int findTargetSumWays(int[] nums, int target) {
int sum = Arrays.stream(nums).sum();
int ave = (sum + target) / 2;
if((sum + target) %2 ==1){
return 0;
}
Arrays.sort(nums);
backTracking(nums,ave,0,0,new ArrayList<Integer>());
return res.size();
}
private void backTracking(int[] nums, int target,int sum, int start, ArrayList<Integer> list) {
if(target == sum){
res.add(new ArrayList<>(list));
}
for (int i = start; i < nums.length && sum + nums[i] <= target; i++) {
sum += nums[i];
list.add(nums[i]);
backTracking(nums,target,sum,i+1,list);
list.remove(list.size()-1);
sum -= nums[i];
}
}
538. 把二叉搜索树转换为累加树
解题思路:逆后序遍历,记录sum并赋给当前节点的val即可
在求二叉树的属性时,均有后序遍历
int sum = 0;
public TreeNode convertBST(TreeNode root) {
if(root == null){
return null;
}
convertBST(root.right);
sum += root.val;
root.val = sum;
convertBST(root.left);
return root;
}
543. 二叉树的直径
解题思路:后序遍历 当前节点的最大直接 = 左右节点的最大深度的较大值
int res = 0;
public int diameterOfBinaryTree(TreeNode root) {
if(root == null || (root.left == null && root.right == null)){
return 0;
}
dfs(root);
return res;
}
private int dfs(TreeNode root) {
if(root == null){
return 0;
}
int left = dfs(root.left);
int right= dfs(root.right);
res = Math.max(res,left + right);
return Math.max(left,right)+1;
}
560. 和为 K 的子数组
解题思路:前缀和 +哈希
我们定义 {pre}[i]pre[i] 为 [0…i][0…i] 里所有数的和,则 pre}[i]pre[i] 可以由{pre}[i-1]pre[i−1] 递推而来,即:pre[i]=pre[i−1]+nums[i]
public int subarraySum(int[] nums, int k) {
int res = 0;
Map<Integer,Integer> map = new HashMap<>();
map.put(0,1);
int Sum = 0;
for(int num : nums){
Sum += num;
if(map.containsKey(Sum - k)){
res += map.get(Sum - k);
}
if(map.containsKey(Sum)){
map.put(Sum,map.get(Sum) + 1);
}else {
map.put(Sum,1);
}
}
return res;
}
581. 最短无序连续子数组
解题思路:排序:创建一个相同的数组进行排序,然后用该数组与原数组比较边界值,返回边界差即可
复杂度:O(n) O(n)
public int findUnsortedSubarray(int[] nums) {
int[] temp = Arrays.copyOf(nums, nums.length);
Arrays.sort(temp);
int left = 0,right = nums.length - 1;
for (; left < nums.length ; left++) {
if(nums[left] != temp[left]){
break;
}
if(left == nums.length -1){
return 0;
}
}
for (; right >= 0 ; right--){
if(nums[right]!=temp[right]){
break;
}
}
return right-left + 1;
}
解题思路:利用数组特征 [ nums1 nums 2 nums3]
复杂度:O(n) O(1)
public int findUnsortedSubarray(int[] nums) {
int left = -1;
int right = -1;
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (int i = 0; i < nums.length; i++) {
if(max > nums[i]){
right = i;
}else {
max = nums[i];
}
if(min < nums[nums.length - i - 1]){
left = nums.length - i - 1;
}else {
min = nums[nums.length - i - 1];
}
}
return right == -1?0:right - left + 1;
}
617. 合并二叉树
解题思路:前序遍历
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if(root1 == null && root2 == null){
return null;
}else if(root1 != null && root2 == null){
return root1;
}else if(root1 == null && root2 != null){
return root2;
}
root1.val = root1.val + root2.val;
root1.left = mergeTrees(root1.left,root2.left);
root1.right = mergeTrees(root1.right,root2.right);
return root1;
}
621. 任务调度器
解题思路:贪心
找到最大的同一种任务数量max,那么至少需要(max - 1) * (n + 1);
若存在多个等于max的任务,那么这些任务放到最后即可 AABB
public int leastInterval(char[] tasks, int n) {
if(n == 0){
return tasks.length;
}
int res = 0;
int[] count = new int[26];
int max = 0;
for (int i = 0; i < tasks.length; i++) {
count[tasks[i] - 'A']++;
max = Math.max(count[tasks[i]- 'A'],max);
}
res = (max - 1) * (n + 1);
for (int i = 0; i < 26; i++) {
if(max == count[i]){
res ++;
}
}
return Math.max(res,tasks.length);
}
647. 回文子串
解题思路:动态规划
public int countSubstrings(String s) {
boolean[][] dp = new boolean[s.length()][s.length()];
int res = 0;
for (int right = 0; right < s.length() ; right++) {
for (int left = 0; left <= right ; left++) {
if(right - left < 1){
res ++;
dp[left][right] = true;
}else {
if(s.charAt(left) == s.charAt(right)){
if(right - left ==1){
dp[left][right] = true;
res++;
}else if(dp[left + 1][right - 1]){
dp[left][right] = true;
res ++;
}else {
dp[left][right] = false;
}
}
}
}
}
return res;
}
739. 每日温度
解题思路:单调栈
public int[] dailyTemperatures(int[] temperatures) {
int[] res = new int[temperatures.length];
Deque<Integer> deque = new ArrayDeque<>();
for (int i = 0; i < temperatures.length; i++) {
while (!deque.isEmpty() && temperatures[deque.peekLast()] < temperatures[i]){
Integer integer = deque.pollLast();
res[integer] = i - integer;
}
deque.offerLast(i);
}
return res;
}