刷题日志

刷题日志

一. 刷题数目汇总

专项 总计
数组 25
动态规划 9
贪心法 6
字符串 6
数学运算 8
50
链表 3
哈希表 11
2
二分法 4
搜索(回溯、BFS、DFS) 4
双指针 11
排序 2
4
9
分治法 2
Union-Find 1
滑动窗口 2
其他
总计(有重叠) 182

二. 面试前必看经典题目汇总

DP类

  1. 基础类题目

  2. 字符串变换、正则匹配、字符串匹配

  3. Target Sum、背包问题(二者还是有点区别的,target sum里面可以有负值)

LeetCode 322. Coin Change
LeetCode 416. Partition Equal Subset Sum
  1. Unique Path
  2. 二维转一维
LeetCode 1105. Filling Bookcase Shelves

数组类

  1. 区间相关
  2. 删除重复数字

二分搜索

  1. 基本模板及基本题目
    二分搜索基础
  2. Upper Bound(右边界)
  3. Lower Bound(左边界)

链表类

  1. 链表合并
LeetCode 21. Merge Two Sorted Lists
LeetCode 23. Merge k Sorted Lists
  1. 链表找环
LeetCode 141. Linked List Cycle
LeetCode 142. Linked List Cycle II
  1. 翻转链表
LeetCode 206. Reverse Linked List
LeetCode 92. Reverse Linked List II
LeetCode 24. Swap Nodes in Pairs
  1. 链表加和
“LeetCode 2. Add Two Numbers”
“LeetCode 445. Add Two Numbers II”
  1. 删除重复节点

回溯法

  1. 组合(Combination Sum)
LeetCode 39. Combination Sum
LeetCode 40. Combination Sum II
LeetCode 216. Combination Sum III
LeetCode 377. Combination Sum IV
  1. 子集合
LeetCode 78. Subsets
LeetCode 90. Subsets II
  1. 全排列
LeetCode 46. Permutations
LeetCode 47. Permutations II
LeetCode 31. Next Permutation
LeetCode 556. Next Greater Element III
  1. 数独
LeetCode 37. Sudoku Solver

字符串类

图(Graph)

  1. 二分图
LeetCode-785-Is-Graph-Bipartite
LeetCode 886. Possible Bipartition
LeetCode 1042. Flower Planting With No Adjacent
  1. 拷贝图
LeetCode 138. Copy List with Random Pointer
LeetCode 133. Clone Graph
  1. 拓扑排序
“LeetCode 207. Course Schedule”
LeetCode 210. Course Schedule II
  1. grid + 连通分量
  2. DFS + 连通分量

树(Tree)

  1. Unique二叉搜索树
    LeetCode 95
    LeetCode 96
  2. 二叉树遍历
    LeetCode 94
    LeetCode 144
    LeetCode 145
    LeetCode 105
  3. 最低公共祖先
    LeetCode 235
    LeetCode 236

Union-Find类

  1. LeetCode 399. Evaluate Division(待完成)

三.刷题详细记录

日期 题目
更早 LeetCode 94
LeetCode 95
LeetCode 96
LeetCode 105
LeetCode 144
LeetCode 145
LeetCode 173
LeetCode 235
LeetCode 236
LeetCode 270
LeetCode 272
LeetCode 297
LeetCode 328
LeetCode 402
LeetCode 438
LeetCode 532
LeetCode 567
LeetCode 637
LeetCode 918
2020.05.19
(DP)
(4)
LeetCode 55. Jump Game
LeetCode 45. Jump Game II
LeetCode 1306. Jump Game III
LeetCode 901. Online Stock Span(无解析)
2020.05.20
(Tree)
(6)
LeetCode 112. Path Sum
LeetCode 113. Path Sum II
LeetCode 437. Path Sum III
LeetCode 666. Path Sum IV
LeetCode 129. Sum Root to Leaf Numbers
LeetCode 230. Kth Smallest Element in a BST(无解析)
2020.05.21 LeetCode 1277. Count Square Submatrices with All Ones
2020.05.22
(Tree)
(6)
LeetCode 450. Delete Node in a BST
LeetCode 508. Most Frequent Subtree Sum
LeetCode 814. Binary Tree Pruning
LeetCode 968. Binary Tree Cameras
LeetCode 451. Sort Characters By Frequency
LeetCode 100. Same Tree(无解析)
2020.05.23
(Search)
(5)
LeetCode 39. Combination Sum
LeetCode 40. Combination Sum II
LeetCode 216. Combination Sum II
LeetCode 377. Combination Sum IV
LeetCode 986. Interval List Intersections
2020.05.24
(Weekly Contest)
(5)
LeetCode 1455. Check If a Word Occurs As a Prefix of Any Word in a Sentence(无解析)
LeetCode 1456. Maximum Number of Vowels in a Substring of Given Length
LeetCode 1457. Pseudo-Palindromic Paths in a Binary Tree
LeetCode 1458. Max Dot Product of Two Subsequences
LeetCode 1008. Construct Binary Search Tree from Preorder Traversal
2020.05.25
(DP)
(9)
动态规划一
LeetCode 53. Maximum Subarray
LeetCode 70. Climbing Stairs
LeetCode 121. Best Time to Buy and Sell Stock
LeetCode 198. House Robber
LeetCode 303. Range Sum Query - Immutable
LeetCode 746. Min Cost Climbing Stairs
LeetCode 1137. N-th Tribonacci Number
LeetCode 1035. Uncrossed Lines
LeetCode 1218. Longest Arithmetic Subsequence of Given Difference
2020.05.26
(二分搜索)
(6)
二分搜索
LeetCode 69. Sqrt(x)
LeetCode 367. Valid Perfect Square
LeetCode 35. Search Insert Position
LeetCode 34. Find First and Last Position of Element in Sorted Array
LeetCode 50. Pow(x, n)
LeetCode 525. Contiguous Array(每日一题)
2020.05.27
(被33题和81题折磨,一天了也没搞懂)
下次做二分查找时的题目:33、81、153、154、162、852、1011)
2020.05.28
(Graph)
(6)
图算法总结(二分图/BFS/DFS/深拷贝图)
1. LeetCode-785-Is-Graph-Bipartite
2. LeetCode 886. Possible Bipartition
3. LeetCode 1042. Flower Planting With No Adjacent
4. LeetCode 138. Copy List with Random Pointer
5. LeetCode 133. Clone Graph
6. LeetCode 338. Counting Bits
2020.05.29
(String)
(8)
字符串总结
1. “LeetCode 516 最长回文子序列”
2. “LeetCode 647 回文子字符串数”
3. 最长回文子字符串
4. LeetCode 151. Reverse Words in a String(无解析)
5. LeetCode 125. Valid Palindrome(无解析)
6. LeetCode 344. Reverse String(无解析) 7. LeetCode 14. Longest Common Prefix
8. LeetCode 28. Implement strStr()
2020.05.30
2020.05.31
2020.06.01
2020.06.02
(DP & 杂项)
(总计:156)
1. “LeetCode 72. Edit Distance”
2. “LeetCode 207. Course Schedule”
3. LeetCode 210. Course Schedule II
4. LeetCode 62. Unique Paths
5. LeetCode 63. Unique Paths II
6. LeetCode 64. Minimum Path Sum
7. LeetCode 237. Delete Node in a Linked List(无解析)
8. LeetCode 226. Invert Binary Tree
2020.06.03
(List)
(总计:166)
1. “LeetCode 2. Add Two Numbers”
2. “LeetCode 445. Add Two Numbers II”
3. LeetCode 21. Merge Two Sorted Lists
4. LeetCode 23. Merge k Sorted Lists
5. LeetCode 141. Linked List Cycle
6. LeetCode 142. Linked List Cycle II
7. LeetCode 206. Reverse Linked List
8. LeetCode 92. Reverse Linked List II
9. LeetCode 24. Swap Nodes in Pairs
10. LeetCode 1029. Two City Scheduling
2020.06.04
(回溯法)
(总计:170)
回溯法总结
1. LeetCode 78. Subsets
2. LeetCode 90. Subsets II
3. LeetCode 17. Letter Combinations of a Phone Number(无解析)
2020.06.05
2020.06.06
(回溯法)
(总计:182)
回溯法总结
1. LeetCode 46. Permutations
2. LeetCode 47. Permutations II
3. LeetCode 31. Next Permutation
4. LeetCode 88. Merge Sorted Array(无解析)
2020.06.07
(周赛 & Union-Find)
(总计:189)
Union-Find总结
1. LeetCode 518. Coin Change 2(待完成)
2. Post not found: LeetCode-1473-Paint-House-III LeetCode 1473. Paint House III(待完成)
3. Post not found: LeetCode-1471-The-k-Strongest-Values-in-an-Array LeetCode 1471. The k Strongest Values in an Array(待完成)
4. LeetCode 399. Evaluate Division(待完成)
5. LeetCode 841. Keys and Rooms(无解析)
6. LeetCode 547. Friend Circles(无解析)
7. LeetCode 695. Max Area of Island(无解析)
8. LeetCode 200. Number of Islands(无解析)
9. LeetCode 802. Find Eventual Safe States(无解析)
10. LeetCode 213. House Robber II(待完成)
2020.06.08
(DP)
(总计:193)
1. LeetCode 416. Partition Equal Subset Sum(待完成)
2. LeetCode 300. Longest Increasing Subsequence(待完成)
3. Post not found: LeetCode-887-Super-Egg-Drop LeetCode 887. Super Egg Drop(待完成)
2020.06.09
(二分查找)
(总计:200)
二分搜索
1. LeetCode 378. Kth Smallest Element in a Sorted Matrix
2. LeetCode 875. Koko Eating Bananas
3. LeetCode 719. Find K-th Smallest Pair Distance(待完成)
4. LeetCode 240. Search a 2D Matrix II
5. LeetCode 1011. Capacity To Ship Packages Within D Days(无解析)
6. LeetCode 704. Binary Search(无解析)
2020.06.10
(DP)
(总计:205)
二分搜索
1. LeetCode 279. Perfect Squares(待补全算法种类)
2. LeetCode 875. Koko Eating Bananas
3. LeetCode 719. Find K-th Smallest Pair Distance(待完成)
4. LeetCode 240. Search a 2D Matrix II
5. LeetCode 1011. Capacity To Ship Packages Within D Days(无解析)
6. LeetCode 704. Binary Search(无解析)
2020.06.11 -2020-06.17 摸鱼
2020.06.18
(Graph & 顺序刷题)
(总计:230)
2020.06.18无解析题目汇总
1. LeetCode 130. Surrounded Regions
2. LeetCode 496. Next Greater Element I
3. LeetCode 503. Next Greater Element II
4. LeetCode 556. Next Greater Element III
5. LeetCode 32. Longest Valid Parentheses
6. LeetCode 37. Sudoku Solver
7. LeetCode 37. Sudoku Solver(无解析)
8. LeetCode 274. H-Index(无解析)
9. LeetCode 275. H-Index II(无解析)
10. LeetCode 36. Valid Sudoku(无解析)
2020.06.19
(DP & 顺序刷题)
(总计 234)
2020.06.19无解析题目汇总
1. LeetCode 1105. Filling Bookcase Shelves
2. LeetCode 41. First Missing Positive
3. LeetCode 268. Missing Number(无解析)
4. LeetCode 1048. Longest String Chain(无解析)
2020.06.22
(顺序刷题41-50)
(总计 234)
1. LeetCode 10. Regular Expression Matching
2. LeetCode 42. Trapping Rain Water
3. LeetCode 43. Multiply Strings
4. LeetCode 48. Rotate Image
5. LeetCode 49. Group Anagrams
2020.06.26
(顺序刷题51-70)
(总计 255)
1. 2020.06.26无解析题目汇总
2. LeetCode 51. N-Queens
3. LeetCode 52. N-Queens-II
4. LeetCode 54. Spiral Matrix
4. LeetCode 48. Rotate Image
5. LeetCode 49. Group Anagrams
2020.07.03
(近期未完成Blog题目汇总)

常用数据结构总结

1. ArrayList

1.1 增删改查

方法 ArrayList
初始化 List<X> arrayList = new ArrayList();
List<X> arrayList = new ArrayList(initialSize);
List<X> arrayList = new ArrayList(Collection)
备注:Collection包括List、ArrayList、Vector、LinkedList、Set、HashSet、TreeSet;
add(E e):该方法是将指定的元素添加到列表的尾部。当容量不足时,会调用 grow 方法增长容量。
add(int index, E element):在 index 位置插入 element,注意这个不是覆盖,会把后面的元素都向后靠一个位置。
addAll(Collection<? extends E> c) 和 addAll(int index, Collection<? extends E> c):将特定 Collection 中的元素添加到 Arraylist 末尾。
boolean remove(int index): 该方法首先调用rangeCheck(index)来校验 index 变量是否超出数组范围,超出则抛出异常。删除第index个元素,整体左移,返回oldValue。
boolean remove(Object o): 删除list中首次出现的元素o(可以为null),返回boolean值,成功为true,失败为false。
clear(): 从列表中移除所有元素。
removeAll(Collection<? extends E> c): 从列表中移除指定 collection 中包含的所有元素。
set(int index, E element):该方法首先调用rangeCheck(index)来校验 index 变量是否超出数组范围,超出则抛出异常。而后,取出原 index 位置的值,并且将新的 element 放入 Index 位置,返回 oldValue。
get(int index):查找第index个元素并返回。
indexOf(Object o):返回列表中首次出现指定元素的索引,如果列表不包含此元素,则返回 -1。
lastIndexOf(Object o):返回列表中最后一次出现指定元素的索引,如果列表不包含此元素,则返回 -1。
List subList(int fromIndex, int toIndex): 返回列表中指定的fromIndex(包括)和toIndex(不包括)之间的部分。
toArray(): 返回以正确顺序包含列表中的所有元素的数组。

1.2 遍历

3种遍历方式:

1.2.1 for循环下标遍历

for(int i = 0; i < list.size(); i++){
    System.out.println(list.get(i));
}

1.2.2 for循环直接遍历

for(String tmp : list){
    System.out.println(tmp);
}

1.2.3 iterator遍历

for(Iterator it2 = list.iterator();it2.hasNext();){
    System.out.println(it2.next());
}

1.3 排序

这里只使用Collections.sort()方法,默认是升序,如需要降序,则需要重写Comparator。

public class SortArrayListAscendingDescending {
    private ArrayList arrayList;
    // 升序排列
    public ArrayList sortAscending() {
        Collections.sort(this.arrayList);
        return this.arrayList;
    }
    // 降序排列
    public ArrayList sortDescending() {
        Collections.sort(this.arrayList, Collections.reverseOrder());
        return this.arrayList;
    }
}

如何重写comparator,使用comparator排序呢?这里需要重写compare方法,比较o1和o2两个对象,返回int值,默认的升序排序规则是:如果o1 > o2返回1,o1 < o2返回-1,o1 == o2返回0。下面代码是降序排序器的代码

public class Mycomparator implements Comparator {

    public int compare(Integer o1, Integer o2) {
        if (o1 < o2) return 1;
        else if (o1 > o2) return -1;
        else return 0;
    }
}

public class ListSort {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList();
        list.add(1);
        list.add(2);
        list.add(-1);
        Comparator comp = new Mycomparator();
        Collections.sort(list, comp);
    }
}

2. HashMap & HashTable

3. HashSet

3.0 实现原理

HashSet的底层实现是由HashMap来实现的,对于其所添加的对象,可能用到以下几种方法。

  1. equals()方法
    用来实现Set中元素的不重复性,如果不覆盖(override)equals()方法,默认使用父类Object的equals方法,则只是比较对象的引用是否相同。
  2. hashCode()
    hashCode()方法时为了实现HashSet和LinkedHashSet而实现的。只有知道对象的hash值,才能根据这个hash值确定 存放在散列表的槽的index。同样,如果不覆盖(override)hashCode()方法,默认使用父类Object的hashCode()方法。
  3. toString()方法
    toString()方法在打印对象时会调用。如果不覆盖(override)toString()方法,默认使用父类Object的。
  4. compareTo()方法
    用户类要实现Comparable接口。这个方法主要用于将对象存放在TreeSet()时保证顺序的。由于是接口,所以用户类必须要实现这个方法。

继承关系:

|--HashSet 底层是由HashMap实现的,通过对象的hashCode方法与equals方法来保证插入元素的唯一性,无序(存储顺序和取出顺序不一致),。
    |--LinkedHashSet 底层数据结构由哈希表和链表组成。哈希表保证元素的唯一性,链表保证元素有序。(存储和取出是一致)

实现原理:

往Haset添加元素的时候,HashSet会先调用元素的hashCode方法得到元素的哈希值 ,然后通过元素 的哈希值经过移位等运算,就可以算出该元素在哈希表中的存储位置。见下面2种情况:

  1. 如果算出元素存储的位置目前没有任何元素存储,那么该元素可以直接存储到该位置上。
  2. 如果算出该元素的存储位置目前已经存在有其他的元素了,那么会调用该元素的equals方法与该位置的元素再比较一次,如果equals返回的是true,那么该元素与这个位置上的元素就视为重复元素,不允许添加,如果equals方法返回的是false,那么该元素运行添加。

3.1 增删改查

方法 HashSet
初始化 HashSet() 构造一个新的空 set,其底层 HashMap 实例的默认初始容量是 16,加载因子是 0.75。
HashSet(Collection<? extends E> c) 构造一个包含指定 collection 中的元素的新 set。
HashSet(int initialCapacity) 构造一个新的空 set,其底层 HashMap 实例具有指定的初始容量和默认的加载因子(0.75)。
HashSet(int initialCapacity, float loadFactor) 构造一个新的空 set,其底层 HashMap 实例具有指定的初始容量和指定的加载因子。
备注:Collection包括List、ArrayList、Vector、LinkedList、Set、HashSet、TreeSet;
boolean add(Object o): 该方法用于向集合里添加一个元素。
boolean addAll(Collection c): 该方法把集合c里的所有元素添加到指定集合里。
void clear(): 清除集合里的所有元素,将集合长度变为0。
boolean remove(Object o): 删除集合中的指定元素o,当集合中包含了一个或多个元素o时,这些元素将被删除,该方法将返回true。
boolean removeAll(Collection c): 将集合中删除集合c里包含的所有元素(相当于用调用该方法的集合减集合c),如果删除了一个或一个以上的元素,则该方法返回true。
boolean retainAll(Collection c): 将集合中删除集合c里不包含的元素(相当于把调用该方法的集合变成该集合的集合c的交集),如果该操作改变了调用该方法的集合,则该方法返回true。
Object[] toArray(): 该方法把集合转换成一个数组,所有的集合元素变成对应的数组元素。
boolean contains(Object o): 返回集合里是否包含指定元素。
boolean containsAll(Collection c): 返回集合里是否包含集合c里的所有元素。

3.2 遍历

  1. Iterator迭代方式遍历
Set<String> set = new HashSet<String>();  
set.add("aaa");  
set.add("bbb");  
set.add("ccc");    
Iterator iterator = set.iterator();  
while (iterator.hasNext()) {  
System.out.println(iterator.next());
}
  1. for循环方式遍历
Set<String> set = new HashSet<String>();  
set.add("aaa");  
set.add("bbb");  
set.add("ccc");  
for (String s:set) {  
System.out.println(s);  
}

3.3 排序

4. Queue

5. Stack

6. Heap

7. TreeMap

hexo搭建个人blog及使用指南

Hexo使用过程踩坑记录

hexo new title 时title含有特殊字符无法识别

举个例子,我们想写一篇新的blog叫做”C++指南”的文章,”hexo new C++指南”会出现什么呢?如下:

hexo example 1

那么如何解决这个问题呢?使用另一种new blog的方式:

hexo new post "C++指南" -p "C++指南.md"

完美解决。参考issue: issue传送门

记录markdown时插入HTML使文字变色

<font color=12DBF1>Note:
<br>1 <= piles.length <= 10^4
<br>piles.length <= H <= 10^9
<br>1 <= piles[i] <= 10^9
</font>

效果如下:

Note:

1 <= piles.length <= 10^4

piles.length <= H <= 10^9

1 <= piles[i] <= 10^9

相关HTML颜色代码链接:颜色代码链接

相关HTML语法链接:W3School HTML

记录markdown时候的一些小语法

插入另外一篇blog的link

语法:

{% post_link markdown的名称(不带.md后缀) 'link显示的名称'%}
i.e.: {% post_link LeetCode-46-Permutations '力扣46题'%}

效果:

力扣46题

插入图片

语法:

![Soduku Solution](/images/leetcode37-2.png  "Soduku Solution")

效果:

Soduku Solution

使用latex公式

在markdown的最前面加上以下代码:

<script type="text/javascript" src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=default"></script>

效果:$a_x + b = 3$

算法模板

链表

题目链接:826. 单链表

快速排序

quick sort的一道题目,关于字符串排序的,剑指offer上的。剑指 Offer 45. 把数组排成最小的数

public static void quickSort(int[] arr, int l, int r){
    if (l >= r) {
        return;
    }
    
    int pivot = arr[l], i = l - 1, j = r + 1;
    
    while (i < j) {
        do i++; while(arr[i] < pivot);
        do j--; while(arr[j] > pivot);
        // 注意:重点在这里,要i < j的时候才需要swap,否则不需要!!!
        if (i < j) swap(arr, i, j);
    }

    // 注意,这里我们要用j来写下面的递归,不能用i,具体原因y总在第二堂课开头里有讲。主要是边界问题。反之,如果我们前面的pivot用得是arr[r],那么这里就不能用j,只能用i.
    quickSort(arr, l, j);
    quickSort(arr, j + 1, r);
}

// 这种写法应该更加亲民一点,只是while循环多了一点内容。
public static void quickSort(int[] arr, int l, int r){
    if (l >= r) {
        return;
    }
    int i = l, j = r;
    while (i < j) {
        // 注意,这里一定得是j在前,i在后
        while (i < j && arr[j] >= arr[l]) j--;
        while (i < j && arr[i] <= arr[l]) i++;
        swap(arr, i, j);
    }
    swap(arr, i, l);
    quickSort(arr, l, i - 1);
    quickSort(arr, i + 1, r);
}

Quick Select

和quick sort基本一致吧,就是找出top k元素。其实对于top k元素,找到k或者k-1均可,这里方便起见,直接80行就不换乘 i < k - 1了。

private void quickSelect(int[] nums, int l, int r, int k){
    Map<Integer, Integer> temp = map;
    if (l >= r) {
        return;
    }
    
    int pivot = nums[l], i = l, j = r;
    while (i < j) {
        while (i < j && nums[j] <= pivot) j--;
        while (i < j && nums[i] >= pivot) i++;
        swap(nums, i, j);
    }
    swap(nums, i, l);
    
    if (i > k) {
        quickSelect(nums, l, i - 1, k);
    } else if (i < k) {
        quickSelect(nums, i + 1, r, k);
    }
}

归并排序

上面的题目一样可以用mergeSort来做,在submission里面可以看。


public static void mergeSort(int[] arr, int l, int r) {
    if (l >= r) {
        return;
    }

    int mid = l  + r >> 1;
    mergeSort(arr, l, mid);
    mergeSort(arr, mid + 1, r);

    int[] temp = new int[r - l + 1];
    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r) {
        if (arr[i] <= arr[j]) {
            temp[k++] = arr[i++];
        } else {
            temp[k++] = arr[j++];
        }
    }

    while (i <= mid) {
        temp[k++] = arr[i++];
    }
    while (j <= r) {
        temp[k++] = arr[j++];
    }

    for (int i = 0; i < temp.length; i++) {
        arr[l + i] = temp[i];
    }
} 

二分法

二分查找的边界问题一直很困扰人额,有两种二分查找,一种是求左边界,另一种是求右边界。最直接的题目就是Leetcode 34。既要求左边界,也要求右边界。

注意,这里左边界和右边界只是一种象征,实际的题目不会这么直白。例如LeetCode 875.

回到正题,网上有很多二分查找的模板,这里只写我自己的模板,以下所有的区间全都都是闭区间,思路参考acwing的yxc。

这里有两种情况

区间被分为[l, mid]和[mid + 1, r]

这种情况的意思就是,当我们做check(mid)的时候,mid是否在我们希望的区间内。换句话说,当mid满足条件时,我们是把它放到左区间还是右区间。比如{1,2,2,2,3,4,5,5},我们要找到2的左边界,那么当我们找到1个“2”的时候,这个2有可能就是我们要找的值,对吧?那么我们就应该让r = mid,把右边不要的部分去掉,再去遍历左边。反之亦然。

这里的终止条件都是l==r!!!!

// 找左边界
public static void binarySearch1(int l, int r){
    int mid = l + r >> 1;
    if (check(mid)){
        r = mid;
    } else {
        l = mid + 1;
    }
}

区间被分为[l, mid - 1]和[mid, r]

道理和上面类似,只是此时需要mid = (l + r + 1)/2。原因在于除法是向下取整,存在精读缺失,有可能无限死循环。

// 找右边界,注意l=mid的时候,要+1
public void binarySearch2(int l, int r){
    int mid = (l + r + 1)/2;
    if (check(mid)) {
        l = mid;
    } else {
        r = mid - 1;
    }
}

取模

这样为了防止负数!!!一定要 +mod 再 % mod

private int mod(int a) {
    return (a % mod + mod) % mod;
}

快速幂

快速幂就是求大数字a的大数字b次方对p取模。极其重要!!!

private static int process(int a, int b, int p) {
    int ans = 1 % p;
    while (b > 0) {
        if ((b & 1) == 1)  {
            ans = (int)((ans * 1L * a) % p); 
        }
        a = (int)((a * 1L * a) % p);
        b = b >> 1;
    }
    return ans;
}

最大公约数

int gcd(int a, int b) {
    if (b == 0) {
        return a;
    }
    return gcd(b, a % b);
}

二维前缀和

#include <iostream>

using namespace std;

const int N = 1010;

int a[N][N], s[N][N];

int main() {
    int n, m, q;
    cin >> n >> m >> q;

    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++) {
            scanf("%d", &a[i][j]);
            s[i][j] = s[i][j - 1] + s[i - 1][j] - s[i - 1][j - 1] + a[i][j]; // 求前缀和
        }

    while (q--) {
        int x1,y1,x2,y2;
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        // 算子矩阵的和
        printf("%d\n", s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1]); 
    }

    return 0;
}

单调栈

首先,从名字上来看,单调栈的意思就是,我们通过某些操作,使得栈里的元素都是单调的。

单调栈解决的问题是“给出一个无序数组,找到每个数字左(右)侧离他最近比它大(小)的数字”。如果一个问题能够抽象成这种类型,那么毫无疑问,马上用单调栈。或者说其他变种,类似于LeetCode 155. Min Stack,blog解析见LeetCode 456. 132 Pattern

举例:给出一个数组a[],若要找到 a[7]左侧比它大的数字,假设有a[3]和a[5],那么我们应该选择a[5]而不是a[3]。那么,换句话说,对于任意两个数a[i]和a[j],如果”a[i] <= a[j] && i < j,那么a[i]可以被抛弃掉,因为对于j后面的数字,a[j]一定是更优的解。”

这种做法的思想可以理解成保守式单调栈,要哪侧的“第一个最大/最小”,就从哪侧开始遍历,保证遍历到每个数的时候,其结果都能确定。

例题:LeetCode 496. Next Greater Element I,该题目的解析link传送门

这里我们写一个模板,是找到每个数字左侧第一个比它大的数字。

public void montonousStack(int[] nums) {
    Stack<Integer> stack = new Stack();

    for (int num : nums) {
        while (!stack.isEmpty() && stack.peek() <= num) {
            stack.pop();
        }

        if (stack.isEmpty()) {
            // 左侧没有数字比它更大
            System.out.print("-1 ");
        } else {
            System.out.printf("%d ", stack.peek());
        }

        stack.push(num);
    }
}
注意:单调栈并不一定是从左向右,或者从右向左!!!

如上面的左侧第一个更大数字,是从左向右遍历的。那么换种思想,我们从右向左遍历:如果当前数字比栈顶或者等于,则压入;若比栈顶大,则不断弹出,直到栈为空或者小于等于栈顶,然后压栈。最后栈底剩下的元素,是没有左侧更大元素的!

这种做法的思想可以理解成探索式单调栈,对于未知的情况,我们先保留,最后再一起处理。

public void montonousStack(int[] nums) {
    Stack<Integer> stack = new Stack();

    for (int i = nums.length; i >= 0; i--) {
        while (!stack.isEmpty() && stack.peek() < nums[i]) {
            System.out.printf("index %d left greater element is %d\n", stack.pop(), nums[i]);
        }
        stack.push(i);
    }
    while (!stack.isEmpty()) {
        System.out.printf("index %d left greater element is %d\n", stack.pop(), -1);
    }
}

单调队列

从单调队列这个名称来看,就是解决某些问题的时候,我们维护一个队列,使得队列里面的的数字呈现单调的情况。

单调队列解决的问题可以抽象为“求滑动窗口里的最大值和最小值”,最直接的题目就是LeetCode 239. Sliding Window Maximum

对于这种问题,我们举个例子,[1, 3, -1, -3, 5, 3, 6, 7],窗口大小为3,输出每个窗口里面的最小值(和239基本一致,只是有小区别)。 第一个窗口为[1, 3, -1],我们可以发现,当-1只要还在我们的窗口里的时候,在-1左侧的1和3就绝不可能为最小值。也就是说这两个数对于我们找窗口内最小值完全是冗余的!!!因此,我们需要一种数据结构,保证窗口的正常运转,要能够一边进,一边出,因此我们选择Java里的Deque,双端队列,逻辑如下。

    public int[] minSlidingWindow(int[] nums, int k) {
        Deque<Integer> deque = new LinkedList();
        int n = nums.length;
        // 这里用res数组来存储结果
        int[] res = new int[n - k + 1];
        
        for (int i = 0; i < n; i++) {
            // 如果当前队列里面存的数字过多,则将最前面的数字弹出,其实就是边界情况,当window里面数字全部都是升序的时候。注意判断是否为空!
            if (!deque.isEmpty() && i - k + 1 > deque.peekFirst()) {
                deque.pollFirst();
            }
            // 将队尾的所有比当前元素大的全部pop出去,因为它们不可能为解。注意,是队尾!!!不是队首!!!而且要注意有等于的情况!!!
            while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) {
                deque.pollLast();
            } 

            // 这里pollLast以后,队列里要么为空,要么是一列升序的数字!!!!!!至于这里为什么要存index,是因为前面需要判断长度的时候需要用index判断窗口大小。

            deque.offerLast(i);

            // 此时,队首的数字就是当前队列里的最小值!!!
            if (i >= k - 1) {
                res[i - k + 1] = nums[deque.peekFirst()];
            }
        }
        
        return res;
    }
}

我们还可以使用数组来模拟双端队列。注意写法。


// 这里用数组来模拟双端队列,q就是这个队列,里面存的也是index
int N = 100010, hh = 0, tt = -1;
int[] q = new int[N];

public int[] minSlidingWindow(int[] nums, int k) {
    int[] res = new int[nums.length - k + 1];
    
    for (int i = 0; i < nums.length; i++) {
        // 
        if (hh <= tt && i - k + 1 > q[hh]) hh++;
        while (hh <= tt && nums[q[tt]] >= nums[i]) tt--;
        
        q[++tt] = i;
        
        if (i >= k - 1) {
            res[i - k + 1] = nums[q[hh]];
        }
    }
    
    return res;
}

递归的写法没啥好说的,这里强调迭代的写法。

前序遍历

题目链接:前序遍历

这里提供两种遍历思路:

  1. 遍历时先压右子树,再压左子树。
class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        Stack<TreeNode> stack = new Stack<TreeNode>();
        List<Integer> list = new ArrayList<Integer>();
        TreeNode node = root;
        stack.push(node);

        while (!stack.isEmpty() && node != null) {
            node = stack.pop();
            list.add(node.val);
            if (node.right != null) {
                stack.push(node.right);
            }
            if (node.left != null) {
                stack.push(node.left);
            }
        }

        return list;
    }
}
  1. 压栈的目的只是为了回溯找右节点,不是压左右子树,而是访问过的节点。
class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        Stack<TreeNode> stack = new Stack<TreeNode>();
        List<Integer> list = new ArrayList<Integer>();
        TreeNode node = root;

        while (!stack.isEmpty() || node != null) {
            if (node != null) {
                list.add(node.val);
                stack.push(node);
                node = node.left;
            } else {
                node = stack.pop().right;
            }
        }

        return list;
    }
}

中序遍历

题目链接:中序遍历

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        Stack<TreeNode> stack = new Stack<TreeNode>();
        List<Integer> list = new ArrayList<Integer>();
        TreeNode node = root;

        while (node != null) { // 下面的循环里保证了node在遍历未结束时不为null
            if (node.left != null) {
                stack.push(node);
                node = node.left;
            } else {
                list.add(node.val);
                while (node.right == null && !stack.isEmpty()) {
                    node = stack.pop();
                    list.add(node.val);
                }
                node = node.right;
            }
        }

        return list;
    }
}
  1. 判断节点是否为空
class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        Stack<TreeNode> stack = new Stack<TreeNode>();
        List<Integer> list = new ArrayList<Integer>();
        TreeNode node = root;

        while (node != null || !stack.isEmpty()) {
            if (node != null) {
                stack.push(node);
                node = node.left;
            } else {
                node = stack.pop();
                list.add(node.val);
                node = node.right;
            }
        }

        return list;
    }
}

后序遍历

题目链接:后序遍历

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        Stack<TreeNode> stack = new Stack<TreeNode>();
        List<Integer> list = new ArrayList<Integer>();
        TreeNode node = root, lastNode = root;
        stack.push(node);

        while (!stack.isEmpty() && node != null) {
            node = stack.pop();
            if ((node.left == null && node.right == null) || node.left == lastNode || node.right == lastNode) {
                // node为叶子节点或者访问返回到当前节点
                list.add(node.val);
                lastNode = node;
            } else {
                // 栈还没压到叶子节点
                stack.push(node);
                if (node.right != null) {
                    stack.push(node.right);
                }
                if (node.left != null) {
                    stack.push(node.left);
                }
            }
        }

        return list;
    }
}

Morris中序遍历

其核心点在于,利用“左子树最右侧节点”来辅助判断是否已经遍历过左子树了。

public List<Integer> inorderTraversal(TreeNode root) {
    // Morris遍历
    TreeNode cur = root;
    List<Integer> list = new ArrayList<>();
    
    while (cur != null) {
        // 判断其有无左子树,如果有,找到它左子树的最右侧节点
        TreeNode mostRight = cur.left;
        
        if (mostRight != null) {
            while (mostRight.right != null && mostRight.right != cur) {
                mostRight = mostRight.right;
            }
            
            if (mostRight.right == null) {
                mostRight.right = cur;
                cur = cur.left;
                continue;
            } else {
                mostRight.right = null;
                list.add(cur.val);
            }
        } else {
            list.add(cur.val);
        }
        cur = cur.right;
    }
    
    return list;
}

并查集

并查集建议观看花花酱的并查集视频讲解

这里使用size来代替rank,也是可以的,复杂度上没有区别。LeetCode 547


class UF {
    int[] parents;
    int[] sizes;
    int count;

    public UF(int n) {
        parents = new int[n];
        sizes = new int[n];
        count = n;

        for (int i = 0; i < n; i++) {
            parents[i] = i;
            sizes[i] = 1;
        }
    }

    public int find(int u) {
        if (u == parents[u]) {
            return u;
        }
        parents[u] = find(parents[u]);
        return parents[u];
    }

    public void union(int u, int v) {
        int pu = find(u), pv = find(v);
        if (pu == pv) {
            return;
        } 

        if (sizes[pu] < sizes[pv]) {
            parents[pu] = pv;
            sizes[pv] += sizes[pu];
        } else {
            parents[pv] = pu;
            sizes[pu] += sizes[pv];
        }
    }
}

Trie

class TrieNode {
    TrieNode[] sons;
    boolean isWord;
    
    TrieNode() {
        sons = new TrieNode[26];
        isWord = false;
    }
    
    public TrieNode get(char c) {
        int idx = c - 'a';
        return sons[idx];
    }
    
    public boolean containsKey(char c) {
        int idx = c - 'a';
        return sons[idx] != null;
    }
    
    public void add(char c) {
        int idx = c - 'a';
        sons[idx] = new TrieNode();
    }
}

infra + cloud

front-side, visual team 25-30 people
Seattle, 20 engineers, backend engineer

3 teams:
bidding backend system,
SRE team

google cloud,Big Data,

  1. 确认输入输出,是否会有异常;确认输出输出。比如输入如果没有k个咋办。以及先说明定义的函数。
  2. 一边说一边说明每一段代码的逻辑,不用念代码;说代码是干啥的就行。
  3. 可以问面试官要一下hint,要一下example。写完以后主动.

Given the root of a binary tree and an integer targetSum, return all root-to-leaf paths where the sum of the node values in the path equals targetSum. Each path should be returned as a list of the node values, not node references.

A root-to-leaf path is a path starting from the root and ending at any leaf node. A leaf is a node with no children.

        5
    /      \
    4        8
  /        /    \
11       13       4

/ \ /
7 2 5 1

Input: root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
Output: [[5,4,11,2],[5,8,4,5]]

Given an integer array nums, return the length of the longest strictly increasing subsequence.

A subsequence is a sequence that can be derived from an array by deleting some or no elements without changing the order of the remaining elements. For example, [3,6,2,7] is a subsequence of the array [0,3,1,6,2,2,7].

反馈:

  1. 边界case没确认完全,比如root为null
  2. overflow没有确认

Java小tips记录

Java代码小知识点总结

List转Array

// 方法一:List.toArray(new Object[0])。注意,这种方法只对Object类型有效,换言之,int,char等无法使用。
String[] arr = list.toArray(new String[0]);

// 方法二:Stream。这种方式可以将Integer的list转为array
int[] array = list.stream().mapToInt(i->i).toArray();

// 方法三:新建array,一个个赋值,最稳健。
Integer[] arr = new Integer[list.size()];
for (int i = 0; i < list.size(); i++)) {
    arr[i] = list.get(i);
}

Array转List

// 方法一:Arrays.asList(arr)。注意:这种方式所返回的List不是我们常用的List,因此最好做一层转换。注意,这种方法只对Object类型有效,换言之,int,char等无法使用!!!

// 如果是int数组转Integer List,建议还是一个个加吧
List<Integer> list = new ArrayList<Integer>(Arrays.asList(arr));

// 方法二:Collections.addAll(list, arr)
List<Integer> list = new ArrayList<>();
Collections.addAll(list, arr);

// 方法三:Stream
List<Integer> list = Arrays.stream(arr).collect(Collectors.toList());

Collections.sort和Arrays.sort

参考:原文链接

Collections.sort()

其实主要是两种排序:Collections.sort(List,Comparator)List.sort(Comparator),其中Comparator可以用lamada表达式代替。

Attention:

  1. Comparator不能更改基本类型的比较方式,只能更改复合类型或者数组类型。数组类型的比较我们可以参考:LeetCode 973. K Closest Points to Origin
  2. 比较的时候,如果是stream的方式,不仅可以用两个object的属性比较,还可以使用额外的外面的变量,详情见:Post not found: LeetCode-1471-The-k-Strongest-Values-in-an-Array LeetCode 1471. The k Strongest Values in an Array
import com.google.common.collect.Lists;
import org.junit.Assert;
import org.junit.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class ComparatorTest {

    @Test
    public void test1(){
        /**
         * Collections.sort()使用
         */
        //被排序的集合
        List<User> userList = Lists.newArrayList(new User("Jack",11),new User("Jack",10));

        //1. Java8之前,使用匿名内部类的基本排序
        Collections.sort(userList, new Comparator<User>() {
            @Override
            public int compare(User user1, User user2) {
                return user1.getAge().compareTo(user2.getAge());
            }
        });

        //2. Java8,使用Lambda表达式的基本排序
        Collections.sort(userList,
                 (User user1, User user2) ->user1.getAge().compareTo(user2.getAge()));

        //userList.sort((User user1, User user2) -> user1.getAge().compareTo(user2.getAge()));

        //3. Java8,Lambda表达式可以简化,省略定义类型User
        userList.sort((user1, user2) -> user1.getAge().compareTo(user2.getAge()));

        //4. Java8,Lambda表达式,多条件排序
        userList.sort((user1, user2) -> {
            if (user1.getName().equals(user2.getName())) {
            return user1.getAge() - user2.getAge();
            } else {
            return user1.getName().compareTo(user2.getName());
            }
        });

        //5. Java8,多条件组合排序
        userList.sort(Comparator.comparing(User::getName).thenComparing(User::getAge));

        //6. Java8,提取Comparator进行排序
        Collections.sort(userList, Comparator.comparing(User::getName));

        //7. Java8,自定义静态的比较方法来排序(静态方法必须写在被比较的类(这里是User类)中)
        userList.sort(User::compareByAgeThenName);

        //8. Java8,反转排序
        Comparator<User> comparator = (user1, user2) -> user1.getName().compareTo(user2.getName());
        userList.sort(comparator);//先按name排序
        userList.sort(comparator.reversed());//反转排序
        Assert.assertEquals(userList.get(0),new User("Jack",10));
    }
}

Arrays.sort()

Arrays.sort(数组)默认是对数组进行升序排列,它有几种参数形式,这里以int数组为例:

  1. Arrays.sort(int[]):直接从小到大进行排序
  2. Arrays.sort(int[], int from, int to):在[from,to)区间进行排序,左闭右开
  3. Arrays.sort(Integer[], Comparator):自定义Comparator排序顺序。
import com.google.common.collect.Lists;
import org.junit.Assert;
import org.junit.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class ComparatorTest {

    @Test
    public void test1(){
        /**
         * Arrays.sort()使用
         */
        //被排序的字符串数组
        String[] months = {"January","February","March","April","May","June","July","August","September","October","December"};
        //按字符串长度排序
        //1.
        Arrays.sort(months, (a, b) -> Integer.signum(a.length() - b.length()));
        //2.
        Arrays.sort(months, Comparator.comparingInt(String::length));
        //3.
        Arrays.sort(months, (a, b) -> a.length() - b.length());
        //4.
        Arrays.sort(months,
                (String a, String b) -> { return Integer.signum(a.length() - b.length()); })
    }
}

构建List数组

Java不支持泛型数组的建立,那么如果需要ArrayList数组,需要怎么办?需要先建立指向List的n个引用,然后再依次新建对象。

注意,new后面一定不能带<Integer>之类的参数!!!

{
    List<Integer>[] graph = new ArrayList[n];
    for (int i = 0; i < n; i++) {
        graph[i] = new ArrayList<Integer>();
    }
}

构建大顶堆/小顶堆

Java中PriorityQueue是默认的小顶堆,对于一些复杂的例子,需要自定义Comparator。

// 构建大顶堆
PriorityQueue<Integer> pq = new PriorityQueue<Integer>(new Comparator<Integer>(){
    @Override
    public int compare(Integer o1, Integer o2){
        return o2 - o1;
    }
    });

位移符号

Java中一共有三种位移符号,关于无符号右移,参考题目191. 位1的个数.

  1. “>>”。表示有符号位移。负数填充1,正数填充0.
  2. “<<”。表示符号左移,全都填充0.
  3. “>>>”。表示无符号右移,全部填充0.

StringBuilder相关

StringBuilder去除最后一个字符

参考链接:传送门

一共有两种方法去除末尾的字符,文章里有写.我们使用后者,setLength函数,因为后者不需要copy。

1.使用 deleteCharAt(int index) 函数

public StringBuilder deleteCharAt(int index)

2.使用setLength(int length) 函数

public void setLength(int newLength)

LeetCode 145. Binary Tree Postorder Traversal

Problem

LeetCode 145. Binary Tree Postorder Traversal

1. 题目简述

给出一颗二叉树,返回其后序遍历的顺序(List形式),且尽量使用非递归的形式。例如:

Input: [1,null,2,3]
1
 \
  2
 /
3
Output: [3,2,1]  

2. 算法思路

Stack:

递归的形式很好写,普通的后序遍历,这里不再赘述。问题在于非递归(迭代)的形式怎么办。

后序遍历应该是三种遍历中最难的一种了,为什么呢?问题在于我们很难界定什么时候去访问节点,什么时候去压栈。我们访问的顺序应该是“左->右->中”,也就是说我们压栈的顺序是“中->右->左”。

以上面为例,如果当前节点node为2节点,我们如何判断接下来是将3节点push下去还是访问2节点然后跳出呢?这里用一个lastNode变量来判断上次访问是否是当前节点的孩子节点。

遍历过程:

  1. 将根节点压栈;
  2. 如果栈不为空,则循环:pop栈顶元素给node,如果当前节点为叶子节点者当前节点是lastNode的父节点,则访问该节点的值;如果都不满足,则说明当前节点的孩子还没被压栈,按照“中->右->左”的顺序压栈。
  3. 栈为空,循环终了

3. 解法

  1. 遍历时,注意lastNode的赋值

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        Stack<TreeNode> stack = new Stack<TreeNode>();
        List<Integer> list = new ArrayList<Integer>();
        TreeNode node = root, lastNode = root;
        stack.push(node);

        while (!stack.isEmpty() && node != null) {
            node = stack.pop();
            if ((node.left == null && node.right == null) || node.left == lastNode || node.right == lastNode) {
                // node为叶子节点或者访问返回到当前节点
                list.add(node.val);
                lastNode = node;
            } else {
                // 栈还没压到叶子节点
                stack.push(node);
                if (node.right != null) {
                    stack.push(node.right);
                }
                if (node.left != null) {
                    stack.push(node.left);
                }
            }
        }

        return list;
    }
}

C++算法模板

排序

快速排序

归并排序

个人不是很喜欢下面这种,写起来很难解释。


void quick_sort(int q[], int l, int r)
{
    if (l >= r) return;

    int i = l - 1, j = r + 1, x = q[l + r >> 1];
    while (i < j)
    {
        do i ++ ; while (q[i] < x);
        do j -- ; while (q[j] > x);
        if (i < j) swap(q[i], q[j]);
    }
    quick_sort(q, l, j), quick_sort(q, j + 1, r);
}

更倾向于下面这种:

void quickSort(int[] q, int l, int r){
    if (l >= r) {
        return;
    }
    int i = l, j = r;
    while (i < j) {
        // 注意,这里一定得是j在前,i在后
        while (i < j && q[j] >= q[l]) j--;
        while (i < j && q[i] <= q[l]) i++;
        swap(q, i, j);
    }
    swap(q, i, l);
    quickSort(q, l, i - 1);
    quickSort(q, i + 1, r);
}

70.74

2021秋招英文面经

  1. Hashcode() vs Equals()
​​Java equals() and hashCode() methods are present in Object class. So every java class gets the default implementation of equals() and hashCode().

equals() : This method checks if some other object passed to it as an argument is equal to the object in which this method is invoked. For the original Object.equals function, it returns true if o1 is the same object as o2, otherwise return false.

hashCode(): This method returns a hashCode() value as an Integer and is supported for the benefit of hashing based Collection classes like Hashtable, HashMap, HashSet etc. If a class overrides the equals() method, it must implement the hashCode() method as well.

In conclusion, if o1.equals(o2), o1.hashCode() == o2.hashCode(). But if o1.hashCode() == o2.hashCode(), o1.equals(o2) may return false.
  1. Override vs Overwrite (存疑)
In Java, there is no overwrite keyboard, or we can say override is the same conception as overrwrite. Therefore, this is a question for C++.

For override, it means making a method with virtual keyboard in the base class and the base class allows to the child classes  to make a body of same
method for itself.

Overwrite means override without virtual keyboard.

There is another keyboard which is overload. It means making multiple methods with different input parameters.
  1. How to make code scalable?
From the aspect of code, it should be humongous. 

First of all, it should be clear. To make a modification in the code, one has to first understand what is currently doing. So it's quite important that "the code should be easy to understand". There are some methods. For example, using single responsibility pinciple, breaking long methods into single function or simpler methods.

Secondly, design detailedly before programming. It means "one should not have to rewrite/refactor the old code to add a new feature". It increases the scope of testing because we need to test the functionality of the old code as we modified it.

Lastly, the code should be efficient. It means one should be able to make the changes without any effort or without writing a duplicate code anywhere. If a piece of code is written somewhere, one should be able to re-use it instead of copying.
  1. Testing tactics you often use in your daily work

Black-box testing and white-box testing.
Black-box testing is also known as

  1. Garbage collection vs tradition memory management
  2. Memory leak
  3. CPU keep rising but ram maintain the same?
  4. How to fix slow tests suite -
    • Identify flaky test
    • Break the tests suite into several categories
    • Mock response/testing data if needed. Reduce the external dependency when it comes to
      Testing
    • Create more unit tests instead of integration test. Reuse dataset for integration test
  5. Difference between inheritance vs composition
  6. How do you speed up release cycle - Continuous deploymen
  7. Dependency injection
  8. What needs to be paying attention to when adding a new class into collection framework
  9. What is race condition vs deadlock?
  10. Given a function, when you keep divide by 10 to find out how many digits it has, what’s the time complexity of it? - log
  11. When does O(nlogm) is faster than O(n+m) - it depends on the the value of m, if m is huge, then obviously the first one is better. Otherwise the second one.
  12. How to handle thread pool exhaustion - Thread exhaustion happens when you create too many threads to handle requests, which eventually cause taking up too many
    Resources.
  13. How to test web service QPS when it scale up?
  14. Convert text to bytes and find the sentences between two new lines? What would be the problem?
  15. Under what circumstance you should not use java garbage collection?
  16. What are some pros and cons for predefined testing?
  17. How do you select dataset for testing?
  18. How do you test a shortest path?
  19. How to scale up a service from 1 tps to 1000 tps? Broad questions -
    1. Codewise - Find bottleneck and fix it.
    2. Hardware wise
  20. There are 200 servers with distributed system. There are some random system that would be down during a specific time. Why?
  21. What is same origin policy? How to overcome?
  22. The graph is showing you fluctuate 500 errors - Why is that?
  23. The graph is showing you fluctuate latency time - Why is that
  24. What is the most important object oriented programming principle for complex system? - Separation of concerns - Make everything one module and start from there

Choose the most appropriate data structure for each problem type:

  • A hierarchical file system model = …………………………
  • An undo / redo history in an app = …………………………
  • Orders to be processed in-order and sequentially = …………………………
  • Boolean option flags in a memory constrained device = …………………………
  1. What are some potential reasons that the query is slow?
SELECT *
FROM Deployment
WHERE Deployment.ApplicationId =
    (SELECT ApplicationId
     FROM Applications
     WHERE Name = params.AppName)
  AND Deployment.Timestamp < params.SinceTime
  1. Your team ‍‍‍‍‌‍‍‍‍‌‍‌‍‍‌‍‍‍‍is building a RESTful API of the following form, that returns the car history given the VIN number (which is a unique identifier of all registered vehicles).
  2. How would you benchmark the performance and load capacity of this API
    System design:
  3. How to show a Facebook post to this person’s friend
  4. If there are 100 servers running google doc, how do you utilize load balancer - Please talk about load balance strategy.

LeetCode 664. Strange Printer

Problem

LeetCode 664. Strange Printer

1. 题目简述

有台奇怪的打印机有以下两个特殊要求:

打印机每次只能打印由 同一个字符 组成的序列。
每次可以在任意起始和结束位置打印新字符,并且会覆盖掉原来已有的字符。
给你一个字符串 s ,你的任务是计算这个打印机打印它需要的最少打印次数。

示例1:
Input: s = “aaabbb”
Output: 2
Explanation: Print “aaa” first and then print “bbb”.

要求:

1 <= s.length <= 100
s只有小写字母

2. 算法思路

Dynamic Programming

这道题目个人认为比较特殊,它是一个求最小值的问题,有点像dp,但是好像又和dp没什么关系,难以找出表达式。因此需要多复习,想想其状态转移方程的表示方法。

我们可以注意一下,对于任意一个子字符串s[i, j],它的最差的打印方式不外乎就是一个个字符去打印,那么,次数最多就是 j - i + 1 次。如何进行优化呢?

我们发现,如果s[i] == s[j],那么在最优解的时候,打印i的时候一定可以把j也附带着打印出来,反正中间的部分可以再被覆盖嘛,无所谓的。所以我们令dp[i][j]表示s[i, j]的打印最优解,此时dp[i][j] = dp[i][j - 1]。

如果s[i] ≠ s[j],那么也就是说s[i]和s[j]的打印一定是两次独立的打印,那么具体是什么时候,在哪打印呢?我们不清楚。所以对于s[i, j]这个字符串来说,遍历i到j之间所有的位置k作为“分割线”,通过dp[i][k] + dp[k + 1][j]的最小值,也就是整个s[i, j]的最小值(最优打印方法),至于具体怎么打印的,谁在前,谁在后,没有影响的。

注意:在我们找到了某个s[i, j]的最优打印次数后,s[i][j]的最优打印方法已经确定了,但对于任意一个包含s[i, j]的字符串来说,并不表示s[i, j]的最优打印解,也是它们最优打印解的子集!!!还是要去进行dp计算的!!!

3. 解法

Dynamic Programming bottom-up


class Solution {
    public int strangePrinter(String s) {
        
    }
}

LeetCode 174. Dungeon Game

Problem

LeetCode 174. Dungeon Game

力扣174. 地下城游戏

1. 题目简述

有一个m * n的矩阵,每个格子内有数字(有正有负)。勇士从矩阵的左上角出发,每次只能向右或者向下行动一步,目的地是矩阵右下角。如果要保证到达右下角时,勇士的血量大于0,那么勇士的初始血量最低为多少。

注:

  1. 骑士的健康点数没有上限。
  2. 任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。

例如,考虑到如下布局的地下城,如果骑士遵循最佳路径 右 -> 右 -> 下 -> 下,则骑士的初始健康点数至少为 7。

-2(起点) -3 3
-5 -10 1
-10 30 -5(终点)

2. 算法思路

DP

2021校招

国内实习

由于个人原因,导致暑期可能无法在国内实习,所以就找了少数几个日常实习,暑期实习只面了一个微软。由于我是有全职经验再回学校读书的,所以可能面试内容倾向于项目经验多一点。

微软(offer)

岗位:C+AI Summer Intern

一面

面试官小哥人很好,基本上都在交流项目,全程45分钟,聊了37分钟的项目和经历,算法题是比较常规的,而且只描述了思路,并没有实际coding。

聊项目经验的事情就先pass了,算法题目是力扣84. 柱状图中最大的矩形,这道题我还没有写题解,写完后会更新上来,这里先简要描述一下思路。

解法1: 以每个柱形为高,向左向右分别延伸,直到找到第一个比它矮的indexL和indexR,indexR-indexL即为“以当前柱形为高的最大矩形面积”,遍历所有柱形。时间复杂度为O(n^2)。

解法2:“左侧第一个更矮”和“右侧第一个更矮”,这个算法看起来很熟悉,这里我们可以使用单调栈来进行处理,从左到右过滤一次,从右到左过滤一次,找出每个位置左右第一个更矮的位置。这一步是O(n)的,然后从前至后再扫描一遍数组即可。整体复杂度O(n)。

二面

二面的面试官出了两道很有意思的题目,我都没有回答上来,这里记录一下。

  1. 从一个数组中找出最大和最小两个数字,要求尽可能少的比较次数(不考虑空间问题)。提示:$\frac{3}{2}n$次。(加减常数也不要考虑了)

解法:维持两个变量max 和min ,min 标记最小,max标记最大,每次比较相邻两个数,较大者与max比较,较小者与min比较,找出最大最小值,每2个元素比较了3次,总计比较次数为1.5N次。

  1. 从一个数组中找出最小的两个数字,要求尽可能少的比较次数(不考虑空间问题)。提示:$n+log_2^n$次。(加减常数就不要考虑了)

解法:将数字两两为一组进行比较,每一组筛选出较大的数字和较小的数字,然后将较小的那一组数字进行下一轮的比较,然后进行同样的操作,最终可以选出最小的那个数字,此时,我们总计比较了($1 + 2 + 4 + ….. + \frac{n}{2}$)次,也就是n次。然后第二小的数字,一定是和最小的那个数字比较过,从和最小的数字比较过的数字中找到最小的数字,即为第二小的数字。

算法题:传统的LCS。dp解法,这里就不展开讲了。

Lead面

目测是凉了,因为本人是Java选手,C++面经确实一点也没复习,痛了痛了。

  1. 基础题:

以下代码中二者有什么区别? a[10]是从栈上分配内存,new是从堆上分配内存。大致明白想考的依图,这样可以聊到C++的内存结构,还有一系列的内容。但是奈何没复习,就直接pass了,不给自己挖坑了。

int a[10];
int* a = new int[10];
  1. 算法题:

题目:给出一棵二叉树,给出其中的两个节点,计算两个节点之间的距离。

思路:先按照传统的LCA来找到p和q的Lowest Ancestor,再从LCA出发,找到p和q的depth,进行相加。special case就是p和q可能是LCA本身,这里做个判断即可。实现语言为Java。

public class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode() {};
    TreeNode(int val) {this.val = val;}
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.righr = right;
    }
}

public class Solution {
    TreeNode lowestAncestor = null;
    public int distance(TreeNode root, TreeNode p, TreeNode q) {
        // Find the LCA of p and q.
        LCA(root, p, q);
        
        // Return the distance of these two nodes.
        if (lowestAncestor == p) {
            return findDepth(p, q);
        } else if (lowestAncestor == q) {
            return findDepth(q, p);
        } else {
            return findDepth(lowestAncestor, p) + findDepth(lowestAncestor, q);
        }
    }
    
    private boolean LCA(TreeNode root, TreeNode p, TreeNode q) {
        if (lowestAncestor != null) {
            return false;
        }
        
        if (root == null) {
            return false;
        }
        
        int left = LCA(root.left, p, q) ? 1 : 0;
        int right = LCA(root.right, p, q) ? 1 : 0;
        int mid = root == p || root == q ? 1 : 0;
        
        if (left + mid + right > 1) {
            lowestAncestor = root;
        }
        
        return (left + mid + right) > 0;
    }
    
    private int findDepth(TreeNode root, TreeNode node) {
        if (root == null) {
            return -1;
        }
        
        if (root == node) {
            return 0;
        }
        
        int l = findDepth(root.left, node), r = findDepth(root.right, node);
        return Math.max(l, r) + 1;
    }
}

问:进一步,是否可以将TreeNode优化一下,变成支持子节点可以指向父节点的,可以有什么优化么?

答:可以用一个hashset记录p的所有ancestors,然后q依次向上去查找ancestor,直到找到第一个ancestor在p所记录的hashset中。

(算法题结束)

  1. 设计题:

这题是真没见过,记录一下给大家提个醒吧。

题目:给出a,b,c三个大小的水桶,给出一个正整数n,两个问题:(1)写程序判断是否能倒出n升水?(2)如果能倒出,怎么倒?

答:这是真没见过,见过一次就好了。这题应该用state machine来进行思考,每个水桶的初始状态是一个tuple(a,b,c),目的是倒出n升水,每一个state都会有一些合法的transition,把这些transition依次遍历,然后递归进行遍历。这里需要用一个set来记录当前路径上的所有state,如果出现了state“回溯”,则也放弃这条分支。直到找到n升水的解决办法。

注意,这里的n升水,n一定是小于(a,b,c)三者中的最大值的,因为多余的那部分可以用最大的那个桶给直接倒出来,倒$/frac{n}{c}$次。

字节(offer)

腾讯(offer)

计算机网络面试知识点

计算机网络面试知识点

备注:应该把CS438的手写笔记重新整理一遍的。。。拖延症啊拖延症。

概述

五层:应用层,传输层,网络层,链接层,物理层

问TCP/IP协议族就是问

应用层

HTTPS的CA证书认证过程

大致过程:

  1. 浏览器从服务器拿到证书。证书上有服务器的公钥和CA机构打上的数字签名。
  2. 拿到证书后验证其数字签名。具体就是,根据证书上写的CA签发机构,在浏览器内置的根证书里找到对应的公钥,用此公钥解开数字签名,得到摘要(digest,证书内容的hash值),据此验证证书的合法性。

多说两句https接下来的加密过程:

  1. 验证完合法性后,在证书里取出服务器的公钥。浏览器生成对称密钥。
  2. 使用服务器公钥对该对称密钥加密,发回给服务器。服务器使用私钥解密,得到对称密钥。
  3. 服务器使用该对称密钥加密后续http数据。使用对称密钥加密是因为比非对称加密高效。

HTTP和HTTPS的区别

  1. 端口 :HTTP默认使用端口80,而HTTPS默认使用端口443。

  2. 安全性和资源消耗: HTTP协议运行在TCP之上,所有传输的内容都是明文,客户端和服务器端都 无法验证对方的身份。HTTPS是运行在SSL/TLS之上的HTTP协议,SSL/TLS 运行在TCP之上。所有 传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称 加密。所以说,HTTP 安全性没有 HTTPS高,但是 HTTPS 比HTTP耗费更多服务器资源。

    对称加密:密钥只有一个,加密解密为同一个密码,且加解密速度快,典型的对称加密 算法有DES、AES等;

    非对称加密:密钥成对出现(且根据公钥无法推知私钥,根据私钥也无法推知公钥), 加密解密使用不同密钥(公钥加密需要私钥解密,私钥加密需要公钥解密),相对对称 加密速度􏰃慢,典型的非对称加密算法有RSA、DSA等。

在浏览器中输入url地址 ->> 显示主⻚的过程

  1. DNS解析
  2. TCP连接
  3. 发送HTTP请求
  4. 服务器处理请求并返回HTTP报文
  5. 浏览器解析渲染⻚面
  6. 连接结束

传输层

TCP和UDP的区别

  1. 基于连接和无连接;
  2. TCP是可靠,保证数据正确;UDP不可靠,不保证数据正确;
  3. TCP保证数据顺序到达;UDP不保证数据顺序到达;
  4. TCP速度慢,因为TCP必须创建连接;UDP速度较快,不需要建立连接;
  5. 因为上述开销,TCP是一个重量级协议;UDP是一个轻量级的协议;
  6. 一个TCP数据包报头的大小是20字节;一个UDP数据报报头是8个字节;
  7. TCP有流量控制和拥塞控制;UDP不能进行流量控制;
  8. TCP面向字节流;UDP面向报文;
  9. 应用场景不同,TCP适合对效率要求相对低,但对准确性要求相对高或者是有连接的场景,TCP一般用于文件传输(HTTP,HTTPS,FTP等协议),邮件(POP,SMTP等协议),远程登录等场景;UDP更适合对效率要求相对高,对准确性要求相对低的场景,UDP一般用于即时通信(QQ聊天),在线视频(rtsp流速度一定要快,偶尔丢包没关系),网络语音电话等场景;

TCP为什么不能有两次握手

因为需要同步好起始的seq number。注意这里关于序列号的问题,抽空自己总结一下。详见传送门

TCP为什么不能只有三次挥手

因TCP是全双工的,当FIN接收方收到时,还有可能继续发送信息,所以,中间的那两次不能合并。

网络层

网络层内容看这篇Blog就够了。传送门

网络层呢,干的事情主要是有两个,一个是Routing,一个是Forwarding。相关协议有NAT协议,ICMP协议,BGP协议,RIP协议,OSPF协议。

链接层(待完善,这里先写点大概)

链接层主要做的事情就是把network层规划好的内容,具体实际应该是如何传输的。我们如何把一个packet传到下一站?

IP层包的格式(datagram)

ARP协议(Address Resolution Protocol)

这个协议主要是把IP地址转化为MAC address,A维护一个ARP table,然后broadcast一个ARP query包,这个包包含了B的IP,如果把Dest MAC address设为FF-FF-FF-FF-FF-FF,那么所有的人都收到这个ARP包,如果B收到了ARP包以后,发现自己符合要求,就返回一个包含了自己MAC address的内容回去。(具体如何防止攻击这个没考虑过)

MAC Protocol

这个就分为很多种了。

Channel Partitioning

FDMA,TDMA。分时复用或者分频复用。以及其他。

Random Partitioning

比如ALOHA,slotted ALOHA,CSMA/CD算法,目的是找到碰撞,然后把包发出去。更加难的是wireless的情况下是如何发包的。CSMA/CD的那个状态转换图。

Taking Turns

大家用一个key去轮流发。

数据库面试知识点

数据库面试知识点(未完待续)

通用基础

事务与隔离级别

事务特性

ACID:原子性(要么做了,要么没做),一致性(多个事务对同一数据读出结果相同),隔离性(事务之间不会相互影响),持久性(事务被提交后,修改是永久的)。

并发事务导致问题

脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交 到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没 有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是 不正确的。

丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据, 那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修 改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取 A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。

不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束 时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修 改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不 一样的情况,因此称为不可重复读。

幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接 着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了 一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

注意不可重复读和幻读区别: 不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者 删除比如多次读取一条记录发现记录增多或减少了

SQL事务四大隔离级别

  1. READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导 致脏读、幻读或不可重复读。
  2. READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读 或不可重复读仍有可能发生。
  3. REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务 自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生
  4. SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个 执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及 幻读。

关于锁的内容,可以参见这篇Blog:传送门

锁的分类

表级锁,行级锁,页级锁。

页级锁: MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。页级进行了折衷,一次锁定相邻的一组记录。开销和加锁时间界于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。

MyISAM采用表级锁(table-level locking)。 InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁。

InnoDB实现行级锁的的三种方式:

  1. Record Lock: 对索引项加锁,锁定符合条件的行。其他事务不能修改和删除加锁项;
  2. Gap Lock: 对索引项之间的“间隙”加锁,锁定记录的范围(对第一条记录前的间隙或最后一条将记录后的间隙加锁),不包含索引项本身。其他事务不能在锁范围内插入数据,这样就防止了别的事务新增幻影行(但不能阻止减少)。
  3. Next-key Lock: 锁定索引项本身和索引范围。即Record Lock和Gap Lock的结合。可解决幻读问题。

备注:InnoDB的行级锁是基于索引实现的,如果查询语句为命中任何索引,那么InnoDB会使用表级锁. 此外,InnoDB的行级锁是针对索引加的锁,不针对数据记录,因此即使访问不同行的记录,如果使用了相同的索引键仍然会出现锁冲突,

MySQL

索引

其实关于索引的内容有很多,包括二级索引,辅助索引等,这篇文章洗的十分全面,有时间详细研读一下。关于索引的详解:索引详解

  1. 聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据
  2. 非聚簇索引:数据存储和索引分开放,索引结构的叶子节点指向了数据的对应行,myisam通过 key_buffer 把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在 key buffer 命中时,速度慢的原因(磁盘 IO)。

InnoDB的索引

InnoDB使用的是聚簇索引,将主键组织到一棵B+树中,而行数据就储存在叶子节点上,若使用”where id = 14”这样的条件查找主键,则按照B+树的检索算法即可查找到对应的叶节点,之后获得行数据。

若对Name列进行条件搜索,则需要两个步骤:第一步在辅助索引B+树中检索Name,到达其叶子节点获取对应的主键。第二步使用主键在主索引B+树种再执行一次B+树检索操作,最终到达叶子节点即可获取整行数据。(重点在于通过其他键需要建立辅助索引)

聚簇索引默认是主键,如果表中没有定义主键,InnoDB 会选择一个唯一的非空索引代替。如果没有这样的索引,InnoDB 会隐式定义一个主键来作为聚簇索引。InnoDB 只聚集在同一个页面中的记录。包含相邻健值的页面可能相距甚远。

MyISM的索引

MyISM使用的是非聚簇索引,非聚簇索引的两棵B+树看上去没什么不同,节点的结构完全一致只是存储的内容不同而已,主键索引B+树的节点存储了主键,辅助键索引B+树存储了辅助键。表数据存储在独立的地方,这两颗B+树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别。由于索引树是独立的,通过辅助键检索无需访问主键的索引树。

索引采用B+树的原因

B+树只有叶节点存放数据,其余节点用来索引,而B-树是每个索引节点都会有Data域。所以从Mysql(Inoodb)的角度来看,B+树是用来充当索引的,一般来说索引非常大,尤其是关系性数据库这种数据量大的索引能达到亿级别,所以为了减少内存的占用,索引也会被存储在磁盘上。

那么Mysql如何衡量查询效率呢?– 磁盘IO次数。 B-树/B+树 的特点就是每层节点数目非常多,层数很少,目的就是为了就少磁盘IO次数,但是B-树的每个节点都有data域(指针),这无疑增大了节点大小,说白了增加了磁盘IO次数(磁盘IO一次读出的数据量大小是固定的,单个数据变大,每次读出的就少,IO次数增多,一次IO多耗时),而B+树除了叶子节点其它节点并不存储数据,节点小,磁盘IO次数就少。这是优点之一。

另一个优点是: B+树所有的Data域在叶子节点,一般来说都会进行一个优化,就是将所有的叶子节点用指针串起来。这样遍历叶子节点就能获得全部数据,这样就能进行区间访问啦。在数据库中基于范围的查询是非常频繁的,而B树不支持这样的遍历操作。

AVL 树和红黑树基本都是存储在内存中才会使用的数据结构。在大规模数据存储的时候,红黑树往往出现由于树的深度过大而造成磁盘IO读写过于频繁,进而导致效率低下的情况。

补充面试题

  1. 怎么样确定使用到了索引?通过Explain语句查看是否使用到了索引。

Java面试知识点

Java面试知识点总结

参考:JavaGuide

JVM相关

JVM的内存分布

JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区域【JAVA 堆、方法区】、直接内存。

JVM GC相关

Java类加载机制

Java类加载过程

Java类加载模型——双亲委派模型

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。

关键字相关

关于 final 关键字的一些总结

final 关键字主要用在三个地方:变量、方法、类。

  1. 对于一个 final 变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改; 如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
  2. 当用 final 修饰一个类时,表明这个类不能被继承。final 类中的所有成员方法都会被隐式地 指定为 final 方法。
  3. 使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第 二个原因是效率。在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法 过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行这些优化了)。类中所有的 private 方法都隐式地指定为 final。

== 与 equals

==: 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型比较的是值,引用数据类型比较的是内存地址)。

equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

  1. 情况 1:类没有覆盖 equals() 方法。则通过 equals() 比􏰃该类的两个对象时,等价于通过 “WX”比􏰃这两个对象。
  2. 情况 2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比􏰃两个对象的内容是 否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

hashCode 与 equals

超强总结(附源码)

hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用 是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。

我们先以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode: 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与该位置其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如 果发现有相同 hashcode 值的对象,这时会调用 equals() 方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

synchronized和ReentrantLock 的区别

**相同点:**两者都是可重入锁,“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对 象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不 可锁重入的话,就会造成死锁。同一个线程每次获取可重入锁,锁的计数器都自增1,所以要等到锁的计数器 下降为0时才能释放锁。

不同点

  1. synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API。 准确来说,synchronized是一个关键字,是从JVM层面来支持的;但是ReentrantLock是一个类,是一个API,通过lock和unlock来实现的。
  2. ReentrantLock 比 synchronized 增加了一些高级功能
    ReentrantLock提供了一种能够中断等待锁的线程的机制。通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
    ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平 锁就是先等待的线程先获得锁。
    选择性通知。自己没写过,不再详细赘述。

CAS(Compare and Swap)和synchronize有什么区别?都用synchronize不行么?

CAS是乐观锁,不需要阻塞,硬件级 别实现的原子性;synchronize会阻塞,JVM级别实现的原子性。使用场景不同,线程冲突严重时 CAS会造成CPU压力过大,导致吞吐量下降,synchronize的原理是先自旋然后阻塞,线程冲突严 重仍然有􏰃高的吞吐量,因为线程都被阻塞了,不会占用CPU。

Java类相关

String StringBuffer 和 StringBuilder 的区别是什么?

线程安全性:
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、 append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

性能:
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象 引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升, 但却要冒多线程不安全的⻛险。

(重点)HashMap,HashTable和ConcurrentHashMap

HashMap

特性: 非线程安全,支持NULL为key,扩容每次都是double(和hash算法有关),负载因子是0.75.

  1. Java 7实现方式: 数组+链表
  2. Java 8实现方式: 最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。

HashTable

Hashtable 是遗留类,很多映射的常用功能与 HashMap 类似,不同的是它承自 Dictionary 类,并且是线程安全的,任一时间只有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap,因为 ConcurrentHashMap 引入了分段锁。Hashtable 不建议在新代码中使用,不需要线程安全的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。

ConcurrentHashMap

涉及知识点:可重入锁,自旋锁……(To be continued)

特性:线程安全,ConcurrentHashMap是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每 个 Segment 是线程安全的,也就实现了全局的线程安全。Segment默认为16.

  1. Java 7实现方式: 数组+链表
  2. Java 8实现方式: 与HashMap类似引入红黑树。且是使用synchronized+CAS,取消了ReentrantLock。

补充:

1. get需要加锁么,为什么? (不用,volatile关键字)

其他

BIO,NIO,AIO 有什么区别?

相关知识点:select, poll, epoll

  1. BIO (Blocking I/O): 同步阻塞 I/O 模式。最传统的一种 IO 模型,即在读写数据过程中会发生阻塞现象。当用户线程发出 IO 请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用 户线程交出 CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除 block 状态。典型的阻塞 IO 模型的例子为:data = socket.read();如果数据没有就绪,就会一直阻塞在 read 方法。
  2. NIO (Non-blocking/New I/O): NIO 是一种同步非阻塞的 I/O 模型。当用户线程发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦内核中的数据准备 好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。 所以事实上,在非阻塞 IO 模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞 IO 不会交出 CPU,而会一直占用 CPU。
  3. **多路复用 IO 模型:**是目前使用得比较多的模型。Java NIO 实际上就是多路复用 IO。在多路复用 IO 模型中,会有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真 正调用实际的 IO 读写操作。因为在多路复用 IO 模型中,只需要使用一个线程就可以管理多个 socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有 socket 读写事件进行时,才会使用 IO 资源,所以它大大减少了资源占用。在 Java NIO 中,是通 过 selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这 种方式会导致用户线程的阻塞。多路复用 IO 模式,通过一个线程就可以管理多个 socket,只有当 socket 真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用 IO 比较适合连接数比较多的情况。另外多路复用 IO 为何比非阻塞 IO 模型的效率高是因为在非阻塞 IO 中,不断地询问 socket 状态时通过用户线程去进行的,而在多路复用 IO 中,轮询每个 socket 状态是内核在进行的,这个效 率要比用户线程要高的多。
  4. AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型。异步 IO 模型才是最理想的 IO 模型,在异步 IO 模型中,当用户线程发起 read 操作之后,立刻就 可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个 asynchronous read 之后, 它会立刻返回,说明 read 请求已经成功发起了,因此不会对用户线程产生任何 block。然后,内 核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程 发送一个信号,告诉它 read 操作完成了。也就说用户线程完全不需要实际的整个 IO 操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示 IO 操作已经完成,可以直接去使用数据了

List<String> res = new ArrayList<>();
int[] dx = {-1, 1, 0, 0};
int[] dy = {0, 0, -1, 1};

List<String> findWords(char[][] m, String[] ws){
    for (String word : ws) {
        find(m, word);
    }
    return res;
}

void find(char[][] m, String word) {
    int l1 = m.length, l2 = m[0].length;
    boolean[][] isVisited = new boolean[l1][l2];
    
    for (int i = 0; i < l1; i++){
        for (int j = 0; j < l2; j++) {
            dfs(m, isVisited, i, j, word, 0);
            if (res.contains(word)) {
                return;
            }
        }
    }
    
    return;
}

// isVisited记录是否dfs到,i为当前横坐标,j为纵坐标,word为待查找单词,length为当前查找的word里的index,
// 终止条件为length和word.length()相等
void dfs(char[][] m, boolean[][] isVisited, int i, int j, String word, int length) {
    if (length == word.length()) {
        res.add(word);
        return;
    }
    
    // i和j的坐标越界
    if (i < 0 || i >= m.length || j < 0 || j >= m[0].length || isVisited[i][j]) {
        return;
    }
    
    // 当前位置符合下一个元素要求
    if (m[i][j] == word.charAt(length)) {
        isVisited[i][j] = true;
        for (int x = 0; x < 4; x++) {
            dfs(m, isVisited, i + dx[x], j + dy[x], word, length+1);
        }
        isVisited[i][j] = false;
    } else {
        // 当前位置不符合条件,直接return
        return;
    }
}

LeetCode 61. 旋转链表

Problem

LeetCode 61. 旋转链表

1. 题目简述

给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。

Example 1:

输入: 1->2->3->4->5->NULL, k = 2
输出: 4->5->1->2->3->NULL
解释:
向右旋转 1 步: 5->1->2->3->4->NULL
向右旋转 2 步: 4->5->1->2->3->NULL

Example 2:

输入: 0->1->2->NULL, k = 4
输出: 2->0->1->NULL
解释:
向右旋转 1 步: 2->0->1->NULL
向右旋转 2 步: 1->2->0->NULL
向右旋转 3 步: 0->1->2->NULL
向右旋转 4 步: 2->0->1->NULL

2. 算法思路

链表的经典题目之一。

LeetCode 343. Integer Break

Problem

剑指 Offer 14- I. 剪绳子 I
LeetCode 343. Integer Break

1. 题目简述

给出一个正整数数字n。将其分割为至少两个正整数,使其加和为n。要求分割后的数字乘积最大。返回乘积。

Example 1:

输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36

2 <= n <= 58

2. 算法思路

动态规划

dp[n]表示对于组正整数n,其切分后的最大乘积。我们可以得到递推式:

for j in range [1, n/2]:
    dp[n] = max(dp[n], max(j, dp[j]) * max(n - j, dp[n - j]));

其含义为我们依次取可能的最后一段切分j,那么最大值就等于前后两段分别的最大值乘积。

class Solution {
    public int cuttingRope(int n) {
        int[] dp = new int[n + 1];
        dp[1] = 1;

        for (int i = 2; i <= n; i++) {
            for (int j = 1; j <= i / 2; j++) {
                dp[i] = Math.max(dp[i], Math.max(j, dp[j]) * Math.max(i - j, dp[i - j]));
            }
        }

        return dp[n];
    }
}

数学方法(贪心法)

证明参考: Krahets的题解

通过数学证明,我们可以算出,如果想找最大值,当长度大于4时,每次至以3为大小进行切分,直到长度小于等于4,然后相乘。这道题的进阶版是需要对结果取模。这里要用到取余的性质,同见数学证明,以下的解法是包含了取余,对应题目: 剑指 Offer 14- II. 剪绳子 II

class Solution {
    public int cuttingRope(int n) {
        if (n < 4) {
            return n - 1;
        }

        long res = 1;
        while (n > 4) {
            n -= 3;
            res = 3 * res;
            res = res  % 1000000007;
        } 

        return (int)(res * n % 1000000007);
    }
}

剑指 Offer 20. 表示数值的字符串

Problem

剑指 Offer 20. 表示数值的字符串

1. 题目简述

请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串”+100”、”5e2”、”-123”、”3.1416”、”-1E-16”、”0123”都表示数值,但”12e”、”1a3.14”、”1.2.3”、”+-5”及”12e+5.4”都不是。

2. 算法思路

这道题是最经典的那种大烂题,基本上可以归类于if else类型,根据没通过的test case编程。不过这种逻辑也和工程中的业务代码很相似,比较繁琐。从算法题的角度来说,这道题是比较辣鸡的。此题的逻辑卸载注释中,不再赘述。

class Solution {
    public boolean isNumber(String s) {
        if(s == null || s.length() == 0){
            return false;
        }
        //标记是否遇到相应情况
        boolean numSeen = false;
        boolean dotSeen = false;
        boolean eSeen = false;
        char[] str = s.trim().toCharArray();
        for(int i = 0;i < str.length; i++){
            if(str[i] >= '0' && str[i] <= '9'){
                numSeen = true;
            }else if(str[i] == '.'){
                //.之前不能出现.或者e
                if(dotSeen || eSeen){
                    return false;
                }
                dotSeen = true;
            }else if(str[i] == 'e' || str[i] == 'E'){
                //e之前不能出现e,必须出现数
                if(eSeen || !numSeen){
                    return false;
                }
                eSeen = true;
                numSeen = false;//重置numSeen,排除123e或者123e+的情况,确保e之后也出现数
            }else if(str[i] == '-' || str[i] == '+'){
                //+-出现在0位置或者e/E的后面第一个位置才是合法的
                if(i != 0 && str[i-1] != 'e' && str[i-1] != 'E'){
                    return false;
                }
            }else{//其他不合法字符
                return false;
            }
        }
        return numSeen;
    }
}

LeetCode 295. Find Median from Data Stream

Problem

两道题是基本一致的,只不过一个是nums[i] > nums[j],另一个是nums[i] > 2 * nums[j],这里以剑指offer为例。

剑指 Offer 41. 数据流中的中位数

1. 题目简述

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。

例如,

[2,3,4] 的中位数是 3

[2,3] 的中位数是 (2 + 3) / 2 = 2.5

设计一个支持以下两种操作的数据结构:

void addNum(int num) - 从数据流中添加一个整数到数据结构中。
double findMedian() - 返回目前所有元素的中位数。

Example 1:
输入:
["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]

2. 算法思路

这道题也算是经典题目之一了,其设计思路基本上背过就行了。核心思想是维护两个栈,一个大顶堆,一个小顶堆。这里要注意的是小顶堆存的是比较大的那一半的数字,大顶堆存的是比较小的那一半数字。另外,每次当我们插入的时候,我们要保证小顶堆的大小是大于等于大顶堆的大小的(反之也OK),且相差不能超过1。如果两个堆的大小相同,则各取堆顶求均值,否则返回小顶堆的堆顶(我们前面保证了小顶堆大小更大)。

class MedianFinder {

    // 使用两个heap,一个大顶堆一个小顶堆。
    PriorityQueue<Integer> minHeap, maxHeap;

    /** initialize your data structure here. */
    public MedianFinder() {
        minHeap = new PriorityQueue<>();
        maxHeap = new PriorityQueue<Integer>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
    }
    
    public void addNum(int num) {
        if (minHeap.size() == 0 || num >= minHeap.peek()) {
            minHeap.add(num);
        } else {
            maxHeap.add(num);
        }

        if (minHeap.size() - maxHeap.size() > 1) {
            maxHeap.add(minHeap.poll());
        }
        if (maxHeap.size() - minHeap.size() > 0) {
            minHeap.add(maxHeap.poll());
        }
    }
    
    public double findMedian() {
        if (minHeap.size() == maxHeap.size()) {
            return (minHeap.peek() + maxHeap.peek()) / 2.0f;
        }
        return minHeap.peek();
    }
}

/**
 * Your MedianFinder object will be instantiated and called as such:
 * MedianFinder obj = new MedianFinder();
 * obj.addNum(num);
 * double param_2 = obj.findMedian();
 */

LeetCode 493. Reverse Pairs

Problem

剑指 Offer 51. 数组中的逆序对

1. 题目简述

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

Example 1: 
输入: [7,5,6,4]
输出: 5

限制:
0 <= 数组长度 <= 50000

2. 算法思路

这道题有两种做法,一种是分治法,或者说归并排序;另一种是树状数组。这里只写第一种做法,第二种工作法待更新。

分治法(归并排序)

对于一个数组,我们先将其分为两段,也就是两个数组,l和r,然后分别进行排序。排好序后,我们进行遍历,两个指针和j,一开始指向l[0]和r[0],然后r指针不断向后移动,直到r[j] >= r[i],此时,从mid+1到j之间的所有数字和l[0]构成逆序对!然后i也向后移,直到mid。这样一层层分下去,和归并算法的思路一致。

class Solution {
    public int reversePairs(int[] nums) {
        return mergeSort(nums, 0, nums.length - 1);
    }

    public int mergeSort(int[] nums, int l, int r) {
        if (l >= r) {
            return 0;
        }

        int mid = l + r >> 1, res = 0;
        res += mergeSort(nums, l, mid) + mergeSort(nums, mid+1, r);

        // 此时,左右两侧数组已经有序
        for (int i = l, j = mid + 1; i <= mid; i++) {
            while (j <= r && nums[i] > nums[j]) {
                j++;
            }
            res += j - mid - 1;
        }

        // 开始归并排序
        int[] temp = new int[r - l + 1];
        int k = 0, i = l, j  = mid + 1;
        while (i <= mid && j <= r) {
            if (nums[i] <= nums[j]) {
                temp[k++] = nums[i++];
            } else {
                temp[k++] = nums[j++];
            }
        }

        while (i <= mid) {
            temp[k++] = nums[i++];
        }
        while (j <= r) {
            temp[k++] = nums[j++];
        }

        for (int m = 0, n = l; m < temp.length; m++, n++) {
            nums[n] = temp[m];
        }

        return res;
    }
}

Java各种类型转换

String相关

各种类型转String

  1. valueOf(boolean b): 返回 boolean 参数的字符串表示形式。.

  2. valueOf(char c): 返回 char 参数的字符串表示形式。

  3. valueOf(char[] data): 返回 char 数组参数的字符串表示形式。

  4. valueOf(char[] data, int offset, int count): 返回 char 数组参数的特定子数组的字符串表示形式。

  5. valueOf(double d): 返回 double 参数的字符串表示形式。

  6. valueOf(float f): 返回 float 参数的字符串表示形式。

  7. valueOf(int i): 返回 int 参数的字符串表示形式。

  8. valueOf(long l): 返回 long 参数的字符串表示形式。

  9. valueOf(Object obj): 返回 Object 参数的字符串表示形式。

List相关

List转array

注意,Integer的List只能一个个添加转为int数组

String[] array = list.toArray(new String[list.size()])

其次,对于list.toArray方法来说,如果传入的数组的大小足够,则使用该数组,否则另外创建一个数组。这里举一个特殊的例子,题目见剑指 Offer 57 - II. 和为s的连续正数序列。这里的list存的是int[],然后将其转为二维数组,这里是这么写的。

List<int[]> list = new ArrayList<int[]>();
... 
return list.toArray(new int[0][0]);

array转list

注意,这里对于int类型,除非使用Java8的stream方式,否则也没有shortcut,只能一个个加到List<Integer>里。

List<String> list = new ArrayList<String>(Arrays.asList(array));

LeetCode 22. Generate Parentheses

Problem

LeetCode 22. Generate Parentheses

1. 题目简述

给出一个数字n,找出所有可能的括号排列形式。

Example 1:

Input: n = 3
Output: ["((()))","(()())","(())()","()(())","()()()"]
Example 2:

Input: n = 1
Output: ["()"]

2. 算法思路

相关问题:

  1. LeetCode 52. N-Queens II

极其经典的”八皇后问题“,用回溯法。跟数独问题基本上是一样的,但是要稍微简单些。

回溯法

典型的回溯法,终结条件为左括号少于右括号或者括号数量多余给定数字n。代码如下,这里需要注意的是删除最后一个字符的写法。

class Solution {
    public List<String> generateParenthesis(int n) {
        StringBuilder sb = new StringBuilder("");
        List<String> res = new ArrayList<>();
        
        backtrack(sb, res, 0, 0, n);
        
        return res;
    }
    
    private void backtrack(StringBuilder sb, List<String> res, int left, int right, int n) {
        if (right > left || left > n || right > n) {
            return;
        }
        
        if (right == n && left == n) {
            res.add(sb.toString());
        }
        
        // add a left parenthesis
        sb.append("(");
        backtrack(sb, res, left + 1, right, n);
        sb.setLength(sb.length() - 1);
        
        // add a right parenthesis
        sb.append(")");
        backtrack(sb, res, left, right + 1, n);
        sb.setLength(sb.length() - 1);
    }
}

LeetCode 475. Heaters

Problem

LeetCode 475. Heaters

1. 题目简述

给出两个数组houses和heaters,houses是房子摆放的位置,heaters是加热器摆放的位置,注意,heaters的值不一定是houses里的值!!!求一个最小的加热半径radius,使得所有的房子都可以得到供暖。

Example 1:
Input: houses = [1,2,3], heaters = [2]
Output: 1
Explanation: The only heater was placed in the position 2, and if we use the radius 1 standard, then all the houses can be warmed.

Example 2:
Input: houses = [1,2,3,4], heaters = [1,4]
Output: 1
Explanation: The two heater was placed in the position 1 and 4. We need to use radius 1 standard, then all the houses can be warmed.

2. 算法思路

这道题属于二分搜索的普通应用,确定好,我们的目的是找到一个满足要求的最小的radius,因此,是求左边界。

这里需要注意的是check函数里面,判断是否能够heat的逻辑比较麻烦一点点,这里是一个个house进行判断的,由于两个数组的单调性,我们可以保证二者的关系是单调的,后面house能够被heat的heater绝不会小于前一个house的。

代码如下:


class Solution {
    public int findRadius(int[] houses, int[] heaters) {
        Arrays.sort(houses);
        Arrays.sort(heaters);
        
        // 这里使用二分法,l = 0, r = 取heater和house的最大值
        int l = 0, r = Math.max(houses[houses.length - 1], heaters[heaters.length - 1]);
        
        while (l < r) {
            int mid = (l + r) / 2;
            if (canHeatAll(houses, heaters, mid)) {
                // 这里是找左边界
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        
        return l;
    }
    
    public boolean canHeatAll (int[] houses, int[] heaters, int radius) {
        // 这里的逻辑是保证了heaters和houses都是单调的,从前向后遍历的时候可以保证heater的下标永远<=house的下标
        for (int i = 0, j = 0; i < houses.length; i++) {
            while (j < heaters.length && Math.abs(houses[i] - heaters[j]) > radius) {
                // 说明当前的heater无法覆盖当前的house,这个写法思路很巧妙!!!
                j++;
            }
            
            if (j == heaters.length) {
                return false;
            }
        }
        
        return true;
    }
}

LeetCode 653. Two Sum IV - Input is a BST

Problem

LeetCode 653. Two Sum IV - Input is a BST

1. 题目简述

给出一棵BST树,以及一个目标数k,判断bst树里是否存在两个node使得其value和为k。

Example

Example: 

Input: root = [5,3,6,2,4,null,7], k = 9
Output: true

2. 算法思路

这题和two sum其实是基本一致的,而且可以利用bst树的性质,中序遍历是有序的。但是其实没必要用,直接DFS或者BFS都行,依然利用hashset来查找。

/**
* Definition for a binary tree node.
* public class TreeNode {
*     int val;
*     TreeNode left;
*     TreeNode right;
*     TreeNode() {}
*     TreeNode(int val) { this.val = val; }
*     TreeNode(int val, TreeNode left, TreeNode right) {
*         this.val = val;
*         this.left = left;
*         this.right = right;
*     }
* }
*/
class Solution {
    // 其实这题的核心点在于“two numbers”,也就是说,无论是BFS还是DFS遍历,假设存在a和b两个数字符合要求,那么无论先遍历到a还是b都无所谓,通过hashset来完成查找。
    public boolean findTarget(TreeNode root, int k) {
        Set < Integer > set = new HashSet();
        return find(root, k, set);
    }
    public boolean find(TreeNode root, int k, Set < Integer > set) {
        if (root == null)
            return false;
        if (set.contains(k - root.val))
            return true;
        set.add(root.val);
        return find(root.left, k, set) || find(root.right, k, set);
    }
}

LeetCode 102. Binary Tree Level Order Traversal

Problem

LeetCode 102. Binary Tree Level Order Traversal

1. 题目简述

给出一棵树,返回BFS(广度优先遍历)的结果。

Example :
Given binary tree [3,9,20,null,null,15,7],
    3
   / \
  9  20
    /  \
   15   7

return its level order traversal as:
[
[3],
[9,20],
[15,7]
]

2. 算法思路

没啥好说的,最经典的BFS遍历,5分钟以内应该能默写。

代码如下:

/**
* Definition for a binary tree node.
* public class TreeNode {
*     int val;
*     TreeNode left;
*     TreeNode right;
*     TreeNode() {}
*     TreeNode(int val) { this.val = val; }
*     TreeNode(int val, TreeNode left, TreeNode right) {
*         this.val = val;
*         this.left = left;
*         this.right = right;
*     }
* }
*/
class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        // 二叉树的BFS
        List<List<Integer>> res = new ArrayList<>();
        
        if (root == null) {
            return res;
        }
        
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        
        while (!queue.isEmpty()) {
            List<Integer> temp = new ArrayList<>();
            int levelSize = queue.size();
            
            while (levelSize-- > 0) {
                TreeNode node = queue.poll();
                temp.add(node.val);
                if (node.left != null) {
                    queue.offer(node.left);
                }
                if (node.right != null) {
                    queue.offer(node.right);
                }
            }
            
            res.add(temp);
        }
        
        return res;
    }
}

LeetCode 331. Verify Preorder Serialization of a Binary Tree

Problem

LeetCode 331. Verify Preorder Serialization of a Binary Tree

1. 题目简述

序列化二叉树的一种方法是使用前序遍历。当我们遇到一个非空节点时,我们可以记录下这个节点的值。如果它是一个空节点,我们可以使用一个标记值记录,例如 #。

     _9_
    /   \
   3     2
  / \   / \
 4   1  #  6
/ \ / \   / \
# # # #   # #

例如,上面的二叉树可以被序列化为字符串 "9,3,4,#,#,1,#,#,2,#,6,#,#",其中 # 代表一个空节点。

给定一串以逗号分隔的序列,验证它是否是正确的二叉树的前序序列化。编写一个在不重构树的条件下的可行算法。

每个以逗号分隔的字符或为一个整数或为一个表示 null 指针的 '#' 。

你可以认为输入格式总是有效的,例如它永远不会包含两个连续的逗号,比如 "1,,3" 。

示例 1:

输入: "9,3,4,#,#,1,#,#,2,#,6,#,#"
输出: true
示例 2:

输入: "1,#"
输出: false
示例 3:

输入: "9,#,#,1"
输出: false

2. 算法思路

参考:大神的解法

这道题个人感觉是很奇怪的一道题,有点摸不着头脑,直接看的solution。有一种解法真的十分巧妙。它是将整棵树看成一个图,且null也是一个节点。对于任意一个非root且非null的节点,它都有1个入度和两个出度。如果是一个合法的tree,那么它最终的入度和出度是相等的,如果遍历的时候发现degree数小于0,那么说明这棵树是不合法的。而且,感觉也只能用preorder来这么考,如果是其他的遍历方式,后序遍历或者中序遍历,则不行。想象一下后序遍历,“左右中”的顺序,我们无法知道什么时候到了“中”,它只是一列字符串,很难以判断。因此其实从直觉上讲,只能选择preorder来考。

class Solution {
    public boolean isValidSerialization(String preorder) {
        // 主要是要找到前序遍历的特性“中左右”,这题不会,看solution。
        int degree = 1;
        String[] nodes = preorder.split(",");
        
        for (String node : nodes) {
            if (--degree < 0) return false;
            if (!node.equals("#")) degree += 2;
        }
        
        return degree == 0;
    }
}

LeetCode 153. Find Minimum in Rotated Sorted Array

Problem

LeetCode 153. Find Minimum in Rotated Sorted Array

1. 题目简述

有一个单调递增的数组,它从中间某个地方被rotate了,找到数组中的最小数字,保证所有数字不重复。例如:

  • [4,5,6,7,0,1,2] if it was rotated 4 times.

  • [0,1,2,4,5,6,7] if it was rotated 7 times.

    Example:
    Input: nums = [3,4,5,1,2]
    Output: 1
    Explanation: The original array was [1,2,3,4,5] rotated 3 times.

2. 算法思路

Binary Search

这道题目也属于二分搜索的经典题目,与之一起的还有LeetCode 154. Find Minimum in Rotated Sorted Array II

其实这里就是普通的二分法查找,但是难点是在于check的条件,究竟是大于还是大于等于!!!

3. 解法

class Solution {
    public int findMin(int[] nums) {
        if (nums[nums.length - 1] >= nums[0]) {
            return nums[0];
        }
        
        int split = nums[0], l = 0, r = nums.length - 1;
        
        while(l < r) {
            int mid = l + r >> 1;
            // 这里需要注意的点在于是大于还是大于等于,乍一看数字是unique的,但是实际上,有等于的可能性。比如[2,1]这个反例,nums[mid] == split。这里要注意。
            if (nums[mid] >= split) {
                l = mid + 1;
            } else {
                r = mid;
            }
        }
        
        return nums[l];
    }
}

LeetCode 162. Find Peak Element

Problem

LeetCode 162. Find Peak Element

1. 题目简述

给出一个数组,找到其中一个peak element的index。peak element指的是严格大于其相邻两个数的元素。保证相邻的两个数字不相同,且假设 nums[-1] = nums[n] = -∞。

Example: 
Input: nums = [1,2,3,1]
Output: 2
Explanation: 3 is a peak element and your function should return the index number 2.

Constraints:
1 <= nums.length <= 1000
-231 <= nums[i] <= 231 - 1
nums[i] != nums[i + 1] for all valid i.

2. 算法思路

Binary Search

y总解析传送门,想法真的很巧妙:链接

当我们比较a[i]和a[i + 1]时,如果a[i] < a[i + 1],那么假设a[i…n]是单调递增的,那么a[n]就是一个peak;如果a[i…n]不是单调递增的,那么一定存在一个拐点下降,拐点下降的地方就是peak。其中一定存在peak,因为nums[-1] = nums[n] = -∞。如果a[i] > a[i + 1],同理,a[1…i + 1]一定存在一个拐点。那么对于等于的情况呢?题中保证了nums[i] != nums[i + 1]。

3. 解法

class Solution {
    public int findPeakElement(int[] nums) {
        if (nums.length <= 1) {
            return 0;
        }
        
        int l = 0, r = nums.length - 1;
        
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] < nums[mid + 1]) {
                // 这里仔细想想,mid可能是峰值么?不可能的,因为nums[mid] < nums[mid + 1],因此l = mid + 1。
                l = mid + 1;
            } else {
                r = mid;
            }
        }
        
        return r;
    }
}

LeetCode 456. 132 Pattern

Problem

LeetCode 456. 132 Pattern

1. 题目简述

Given an array of n integers nums, a 132 pattern is a subsequence of three integers nums[i], nums[j] and nums[k] such that i < j < k and nums[i] < nums[k] < nums[j]. Return true if there is a 132 pattern in nums, otherwise, return false.

Example 1:
Input: nums = [1,2,3,4]
Output: false
Explanation: There is no 132 pattern in the sequence.

Example2:
Input: nums = [-1,3,2,0]
Output: true
Explanation: There are three 132 patterns in the sequence: [-1, 3, 2], [-1, 3, 0] and [-1, 2, 0].

2. 算法思路

这道题属于单调栈的高阶玩法。注意,单调栈的模板归模板,实际应用会大于“下一个更大/更小元素”。会有变种,这道题就是一个例子。

首先,我们需要遍历所有数字,假设所有数字都是”3”,然后找到它左侧的最小值,然后找到它右侧比它小的最大值。但是“找到右侧比它小的最大值”这个问题不能用单调栈模板直接解,我们需要将其做一个转化。假设有a[i],a[i]右侧有一个数a[j],a[j]是第一个大于a[i]的数,那么我们需要找的就是[i+1, j-1]之间小于a[i]的最大值。为什么?因为我们知道,a[i]左侧的最小值a1一定是<=a[j]左侧的最小值的,且a[i] < a[j],如果a[j]的右侧有一个数字a[k],使得a1,a[i],a[k]满足“132”条件,那么a1,a[j],a[k]也一定满足“132”条件,所以当我们遍历到a[j]的时候就会包含这种情况。所以我们只需要考虑a[i]与a[j]之间的比a[i]小的数字。

根据以上分析,我们知道a[j]就是a[i]的“下一个更大的数字”。因此可以转化为单调栈问题。那么问题来了,我们是要从左到右遍历,还是从右到左遍历呢?记得next greater element是从左到右,这题也是么?不是的,如果我们从左到右遍历,我们怎么知道它右侧数字的情况呢?因此我们需要从右向左遍历。

代码如下:


class Solution {
    public boolean find132pattern(int[] nums) {
        if (nums.length < 3) {
            return false;
        }
        
        // 找到每个数字右侧的比它小的最大数字(在右侧第一个比它大的数字之前)。如果没有的话则为Integer.MIN_VALUE。
        Stack<Integer> stack = new Stack<>();
        int[] right_min = new int[nums.length];
        Arrays.fill(right_min, Integer.MIN_VALUE);
        for (int i = nums.length - 1; i >= 0; i--) {
            while (!stack.isEmpty() && nums[i] > stack.peek()) {
                right_min[i] = stack.pop();
            }
            stack.push(nums[i]);
        }
        
        // 找到每个位置左侧的最小值
        int temp_min = nums[0];
        for (int i = 0; i < nums.length; i++) {
            if (temp_min != nums[i] && temp_min < right_min[i]) {
                return true;
            }
            temp_min = Math.min(temp_min, nums[i]);
        }
        
        return false;
    }
}

LeetCode 239. Sliding Window Maximum

Problem

LeetCode 239. Sliding Window Maximum

1. 题目简述

给出一个数组arr和数字k,找出以k为大小的sliding window。例如:

Input: nums = [1,3,-1,-3,5,3,6,7], k = 3
Output: [3,3,5,5,6,7]
Explanation: 
Window position                Max
---------------               -----
[1  3  -1] -3  5  3  6  7       3
1 [3  -1  -3] 5  3  6  7       3
1  3 [-1  -3  5] 3  6  7       5
1  3  -1 [-3  5  3] 6  7       5
1  3  -1  -3 [5  3  6] 7       6
1  3  -1  -3  5 [3  6  7]      7

2. 算法思路

monotonous queue(单调队列)

这是单调队列,经典的数据结构之一,算法模板及思路参考:算法模板

// 解法一:使用Deque
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        // 其实就是单调队列的思想,要用到deque。其核心思想在于,当一个较大的数存在于当前的窗口的时候,在它前面的数字如果比它小,那么都不可能是结果。详情见yxc大佬的课程及笔记。
        Deque<Integer> deque = new LinkedList();
        int n = nums.length;
        int[] res = new int[n - k + 1];
        
        for (int i = 0; i < n; i++) {
            // 如果当前队列里面存的数字过多,则将最前面的数字弹出,其实就是边界情况,当window里面数字全部都是降序的时候。
            if (!deque.isEmpty() && i - k + 1 > deque.peekFirst()) {
                deque.pollFirst();
            }
            // 将队尾的所有比当前元素小的全部pop出去,因为它们不可能为解。注意,是队尾!!!不是队首!!!而且要注意有等于的情况!!!
            while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) {
                deque.pollLast();
            } 
            deque.offerLast(i);
            if (i >= k - 1) {
                res[i - k + 1] = nums[deque.peekFirst()];
            }
        }
        
        return res;
    }
}

class Solution {
    // 这里用数组来模拟双端队列,q就是这个队列,里面存的也是index
    int N = 100010, hh = 0, tt = -1;
    int[] q = new int[N];
    
    public int[] maxSlidingWindow(int[] nums, int k) {
        int[] res = new int[nums.length - k + 1];
        
        for (int i = 0; i < nums.length; i++) {
            // 
            if (hh <= tt && i - k + 1 > q[hh]) hh++;
            while (hh <= tt && nums[q[tt]] <= nums[i]) tt--;
            
            q[++tt] = i;
            
            if (i >= k - 1) {
                res[i - k + 1] = nums[q[hh]];
            }
        }
        
        return res;
    }
}

LeetCode 1539. Kth Missing Positive Number

Problem

LeetCode 1539. Kth Missing Positive Number

1. 题目简述

给出一个有序数组和一个正整数k,找到第k个缺失的正整数。例如:

Example:

Input: arr = [2,3,4,7,11], k = 5
Output: 9
Explanation: The missing positive integers are [1,5,6,8,9,10,12,13,...]. The 5th missing positive integer is 9.

2. 算法思路

二分查找最重要的是确认好check函数,到底是求上边界还是下边界。搞清楚!!!而且具体题目具体分析。

这道题里,需要注意的点是check函数中,这里的判断条件是小于还是小于等于。

class Solution {
    public int findKthPositive(int[] arr, int k) {
        if (arr[0] > k) {
            return k;
        }
        
        // arr[index]-index-1就是到当前为止一共缺失的数字。
        // 这里的目的是要找到一个位置i,使得到i为止缺失的数字是小于k的最大的。因此是求右边界
        // 注意,这里不能是小于等于k,如果是等于的话,比如[1,2,5,6,7,8],找第2个miss的数字,这里二分出来的数字是8。如果刚刚好是等于k的话,我们不知道前面连续的数字有多少个,往前算起来比较麻烦,因此要注意check条件。
        int l = 0, r = arr.length - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (arr[mid] - mid - 1 < k) {
                l = mid;
            } else {
                r = mid - 1;
            }
        }
        
        return arr[l] + k - (arr[l] - l - 1);
    }
}

LeetCode 69. Sqrt(x)

Problem

LeetCode 69. Sqrt(x)

1. 题目简述

给出一个数字x,计算其开方,向下取整。

2. 算法思路

最经典的几道题目之一,有很多坑。

第一,注意不要用 mid * mid,很容易越界,不好办。其次,注意问题的本质,究竟是要求左边界还是右边界!!!也就是check成立时,我们希望继续去找左侧还是右侧。

这里,我们希望找的是右边界,也就是要找到一个“最大”的y,使得y^2<=x。

class Solution {
    public int mySqrt(int x) {
        int l = 0, r = x;
        
        while (l < r) {
            // 这里应该是求右边界!!!找到一个数y,使得y^2 <= x
            int mid = (l + r + 1) / 2;
            if (mid <= x / mid) {
                l = mid;
            } else {
                r = mid - 1;
            }
        }
        
        return r;
    }
}

LeetCode 702. Search in a Sorted Array of Unknown Size

Problem

LeetCode 702. Search in a Sorted Array of Unknown Size

1. 题目简述

题目不赘述了,具体点击链接查看。

2. 算法思路

这道题很有意思,数组的大小是未知的,因此,我们需要将其分解成两个子问题。第一,确定二分查找的子区间;第二,二分查找哦啊该区间。

首先,我们定义l = 0, r = 1,直到reader.get(l) <= target <= reader.get(r)。然后二分查找该区间。算法复杂度为O(logN)

/**
 * // This is ArrayReader's API interface.
 * // You should not implement it, or speculate about its implementation
 * interface ArrayReader {
 *     public int get(int index) {}
 * }
 */

class Solution {
    public int search(ArrayReader reader, int target) {
        if (reader.get(0) == target) {
            return 0;
        }
        
        // 两个子问题,第一个是确认左右边界。这里left初始化为0,right初始化为1,然后每次get右边界。如果右边界<target,那么left=right + 1,right = right * 2。找到合适的区间后再想办法做二分。
        int l = 0, r = 1;
        while (reader.get(r) < target) {
            l = r + 1;
            r = r * 2;
        }
        
        // 此时,target一定是在[l,r]之间
        while (l < r) {
            int mid = l + r >> 1;
            if (reader.get(mid) == target) {
                return mid;
            } else if (reader.get(mid) > target) {
                r = mid - 1;
            } else {
                l = mid + 1;
            }
        }
        
        if (reader.get(l) == target) {
            return l;
        }
        
        return -1;
    }
}

LeetCode 34. Find First and Last Position of Element in Sorted Array

Problem

LeetCode 34. Find First and Last Position of Element in Sorted Array

1. 题目简述

给出一个有序数组和一个target,找到其上下边界。例如:

Example:

Input: nums = [5,7,7,8,8,10], target = 8
Output: [3,4]

2. 算法思路

二分查找的模板题目,背背背!!!!

class Solution {
    public int[] searchRange(int[] nums, int target) {
        int l = 0, r = nums.length - 1;
        int[] res = new int[2];
        
        while (l < r) {
            int mid = l + r >> 1;
            // 希望取左边界
            if (nums[mid] >= target) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        
        if (nums.length == 0 || nums[l] != target) {
            res[0] = -1;
            res[1] = -1;
            return res;
        }
        
        res[0] = l;
        
        l = 0;
        r = nums.length - 1;
        while (l < r) {
            int mid = (l + r + 1) >> 1;
            if (nums[mid] <= target) {
                l = mid;
            } else {
                r = mid - 1;
            }
        }
        res[1] = l;
        
        return res;
    }
}

LeetCode 57. Insert Interval

Problem

LeetCode 57. Insert Interval

1. 题目简述

给出一系列区间,合并它们,给出合并后的结果。例如:

Example 1:

Input: [[1,3],[2,6],[8,10],[15,18]]
Output: [[1,6],[8,10],[15,18]]
Explanation: Since intervals [1,3] and [2,6] overlaps, merge them into [1,6].

2. 算法思路

相关问题:

  1. LeetCode 57. Insert Interval
  2. LeetCode 253. Meeting Rooms II(待做,会议室问题的一系列,很重要)

也算是老经典的区间相关的问题了,可以单独整理一个小专题。慢慢添加,其实和57题基本上是一致的,都是先按照左边界进行排序,然后对右边界进行处理。

暴力做法

其实最大的问题就是如何合并两个相邻区间,每次先取一个区间,然后和后面的进行合并,直到不能够再合并为止。不能再合并的条件就是后一个的start大于前一个的end,然后先用一个List存着,然后再转为Array,注意List转为Array的方法!!!

class Solution {
    public int[][] merge(int[][] intervals) {
        if (intervals.length == 0) {
            return new int[0][0];
        }

        List<int[]> merged = new ArrayList();

        // 首先对区间进行排序
        Arrays.sort(intervals, (int[] a, int[] b) -> {
            return a[0] - b[0];
        });

        for (int i = 0; i < intervals.length; i++) {
            int[] temp = intervals[i];

            // 依次合并后面的可以合并的interval,经过排序后保证了后面的start一定是不小于当前的start的,因此只要判断后者的start和当前的end的大小
            while (i + 1 < intervals.length && intervals[i + 1][0] <= temp[1]) {
                temp[1] = Math.max(intervals[i + 1][1], temp[1]);
                i++;
            }

            merged.add(temp);
        }

        return merged.toArray(new int[merged.size()][2]);
    }
}

LeetCode 30. Substring with Concatenation of All Words

Problem

30. Substring with Concatenation of All Words

1. 题目简述

给定一个字符串 s 和一些长度相同的单词 words。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。

注意子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序。

示例 1:
输入:
s = "barfoothefoobarman",
words = ["foo","bar"]
输出:[0,9]
解释:
从索引 0 和 9 开始的子串分别是 "barfoo" 和 "foobar" 。
输出的顺序不重要, [9,0] 也是有效答案。

示例 2:
输入:
s = "wordgoodgoodgoodbestword",
words = ["word","good","best","word"]
输出:[]

2. 算法思路

相关问题:

  1. 力扣76题

这道题有两种解法,暴力解法和优化版暴力解法(双指针)。跟76题有点像。

暴力解法

第一种就是暴力求解,遍历所有可能的长度为 n * w 的子串(n是单词个数,w为每个单词的长度),使用hashmap来记录里面每个单词出现的次数,如果和words数组里的单词及出现次数刚好相同,则为一个解,否则继续遍历。这样的话时间复杂度为 o((l - n * w) * (n * w)),共计l - n * w个起始位置,每次遍历长度为 n * w。

class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        if (s.length() == 0 || words.length == 0) {
            return new ArrayList<Integer>();
        }
        
        int wordLength = words[0].length();
        int targetLength = wordLength * words.length;
        if (s.length() < targetLength) {
            return new ArrayList<Integer>();
        }
        
        Map<String, Integer> dict = new HashMap();
        // 初始化一个dict,记录一下各个word出现的频率
        for (String word : words) {
            if (!dict.containsKey(word)) {
                dict.put(word, 1);
            } else {
                dict.put(word, dict.get(word) + 1);
            }
        }
        
        List<Integer> res = new ArrayList();
        int start = 0, end = targetLength;
        while (end <= s.length()) {
            if (check(dict, s.substring(start, end), wordLength)) {
                res.add(start);
            }
            start++;
            end++;
        }
        
        return res;
    }
    
    private boolean check(Map<String, Integer> dict, String target, int wordLength) {
        Map<String, Integer> copy = new HashMap();
        copy.putAll(dict);
        
        int start = 0;
        while (start < target.length()) {
            String word = target.substring(start, start + wordLength);
            if (copy.containsKey(word) && copy.get(word) > 0) {
                copy.put(word, copy.get(word) - 1);
            } else {
                return false;
            }
            start += wordLength;
        }
        
        return true;
    }
}

双指针(滑动窗口)

我们需要注意一点,就是每个单词的长度是一致的,也就是说我们可以利用单词长度为w的特性。我们可以将s字符串分为w组,起始位置从0到w-1,以w长度为间隔。以下图为例:

LC37-1

不同颜色就表示不同的分组情况,两个不同颜色之间表示一个单词。然后我们遍历每种颜色的单词的词组,从前到后,使用一个count值来巧妙表示当前有多少合法的单词,如果count值和words里单词数相等,则为一种解。双指针,或者说滑动窗口也可以,判断每个窗口是否符合要求。

class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        List<Integer> res = new ArrayList<>();
        if (words.length == 0) {
            return res;
        }

        int w = words[0].length(), n = words.length, l = s.length(), count = 0;
        Map<String, Integer> total = new HashMap<>(), wd = new HashMap();
        for (String word : words) {
            total.put(word, total.getOrDefault(word, 0) + 1);
        }

        // 共计w中开始位置
        for (int i = 0; i < w; i++) {
            wd.clear();
            count = 0;
            // 每次开始进行遍历,步长为w
            for (int j = i; j + w <= l; j += w) {
                // 注意这里有等于,例如单词长度为5,一个单词的情况,我们第一个单词位置是[0, 4],大小为5的时候已经是下一个单词了,因此是j >=
                if (j >= i + n * w) {
                    // 此时,从wd里去掉第一个单词
                    String temp = s.substring(j - n * w, j - (n - 1) * w);
                    wd.put(temp, wd.get(temp) - 1);
                    if (wd.get(temp) < total.getOrDefault(temp, 0)) {
                        count--;
                    }
                }

                // 将当前单词插入wd中,如果满足要求,则count++,然后判断。
                String temp = s.substring(j, j + w);
                wd.put(temp, wd.getOrDefault(temp, 0) + 1);
                if (wd.get(temp) <= total.getOrDefault(temp, 0)) {
                    count++;
                }
                if (count == n) {
                    res.add(j - (n - 1) * w);
                }
            }
        }

        return res;
    }
}

LeetCode 76. Minimum Window Substring

Problem

LeetCode 76. Minimum Window Substring

1. 题目简述

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。

注意:如果 s 中存在这样的子串,我们保证它是唯一的答案。

Note:

1 <= s.length, t.length <= 105

s 和 t 由英文字母组成

2. 算法思路

相关问题:

  1. 力扣30题

所有的思路都在code的注释里了,参考30题。

滑动窗口

class Solution {
    public String minWindow(String s, String t) {
        // 初始化目标词典表
        String res = "";
        int n = t.length();
        Map<Character, Integer> total = new HashMap();
        for (int i = 0; i < n; i++) {
            total.put(t.charAt(i), total.getOrDefault(t.charAt(i), 0) + 1);
        }
        
        // 和30题的思路是一致的,滑动窗口,每次向右滑动一位,判断当前window里的所有字符是否满足要求,关于这个满足要求,可以用一个count值来表示,每次移动的时候做判断。
        // 那么什么时候移动start,什么时候移动end呢?先移动end,直到start到end满足要求后,start向后移动,如果移动一位发现当前的start到end又不满足要求了,则再次移动end,直至终结。
        // 这里能用双指针是因为单调性!!!
        int start = 0, end = 0, count = 0;
        Map<Character, Integer> window = new HashMap();
        char[] arr = s.toCharArray();
        
        for (int i = 0, j = 0; j < s.length(); j++) {
            // 关于为什么这里for循环只去考虑j呢?也就是比较靠后的指针,因为每次更新j的时候,i都会向后移动尽可能多的位数,如果移动i以后发现满足要求(比当前更小),那就更新。
            window.put(arr[j], window.getOrDefault(arr[j], 0) + 1);
            if (window.get(arr[j]) <= total.getOrDefault(arr[j], 0)) {
                count++;
            }
            
            // 如果count == t.length()并且window里面的第i个字符是多余的,那就一直将i右移。
            while (count == n && window.getOrDefault(arr[i], 0) > total.getOrDefault(arr[i], 0)) {
                window.put(arr[i], window.get(arr[i]) - 1);
                i++;
            }
            
            // 此时的i到j,如果依然是count == n,说明是符合要求的一种可能性,判断是否需要更新。这里是j - i + 1,不是j - i,因为i到j是双闭区间,不是左闭右开区间。
            if (count == n && (j - i + 1 < res.length() || res.equals(""))) {
                res = s.substring(i, j + 1);
            }
        }
        
        return res;
    }
}

LeetCode 60. Permutation Sequence

Problem

LeetCode 54. Spiral Matrix

1. 题目简述

给出一个m * n的矩阵,将其螺旋打印出来。例如:

Example 1:

Input:
[
[ 1, 2, 3 ],
[ 4, 5, 6 ],
[ 7, 8, 9 ]
]
Output: [1,2,3,6,9,8,7,4,5]
Example 2:

Input:
[
[1, 2, 3, 4],
[5, 6, 7, 8],
[9,10,11,12]
]
Output: [1,2,3,4,8,12,11,10,9,5,6,7]

2. 算法思路

相关问题:

  1. LeetCode 59. Spiral Matrix II

极其经典的螺旋打印的问题,这里的暴力做法其实是很麻烦的,用方向数组比较好!

回溯法

这里问题其实是在于,如何判断当前行所放的皇后是否合理,这里我的做法是直接将二维的棋盘表示了出来,然后用board[i - x][j - x]表示左上到右下的对角线,用board[i - x][j + x]表示右上到左下的对角线(注意边界)。

但是其实还有另外一种更为节省空间的做法(其实必要性不大),用bool型数组来表示每一行、每一列、每一个对角线是否已经共存在了一个皇后。如果已知n,那么每一组(左上到右下算一组,左下到右上算一组)对角线的个数也是已知的,2n - 1,然后用[i, j]做一个映射即可。

class Solution {
    public List<List<String>> solveNQueens(int n) {
        // 也就是说每一行,每一列,每一个斜线上都不能有任意两个皇后。每一行和每一列都还算好判断,斜线上要怎么判断呢?可以用[i][j]来做映射。但是这里我打算采用一个二维数组来做计算,写起来方便。

        int[][] board = new int[n][n];
        List<List<String>> res = new ArrayList();

        backtrack(0, n, board, res);

        return res;
    }

    private void backtrack(int line, int n, int[][] board, List<List<String>> res) {
        // 判断一下是否已经填完了最后一层,如果是,则向结果集中添加一个solution
        if (line == n) {
            List<String> solution = new ArrayList();
            for (int i = 0; i < n; i++) {
                StringBuilder sb = new StringBuilder();
                for (int j = 0; j < n; j++) {
                    sb.append(board[i][j] == 0 ? "." : "Q");
                }
                solution.add(sb.toString());
            }
            res.add(solution);
            return;
        }

        // 如果没有填完最后一层,则填line这一层,从0到n-1逐个尝试,每次尝试出一个valid的就继续向下一层出发。
        for (int i = 0; i < n; i++) {
            board[line][i] = 1;
            if (checkValid(board, line, i)) {
                backtrack(line + 1, n, board, res);
            }
            board[line][i] = 0;
        }

        return;
    }

    private boolean checkValid(int[][] board, int line, int pos) {
        // 检查第line行,pos位置放queen是否合法(同一行无须检查)
        int n = board.length;

        // 检查同一列
        for (int i = 0; i < line; i++) {
            if (board[i][pos] == 1) {
                return false;
            }
        }

        // 检查左上到右下的对角线,注意只用检查line线以上的即可
        for (int i = line - 1, j = pos - 1; i >= 0 && j >= 0; i--, j--) {
            if (board[i][j] == 1) {
                return false;
            }
        }

        // 检查左下到右上的对角线,注意只用检查line线以上的即可
        for (int i = line - 1, j = pos + 1; i >= 0 && j < n; i--, j++) {
            if (board[i][j] == 1) {
                return false;
            }
        }

        return true;
    }
}

2020-06-26

题目汇总

LeetCode 52. N-Queens II

算法思路

LeetCode 51. N-Queens一模一样,只是这个要求数量,而不是所有的解。

回溯法

class Solution {
    int count = 0;

    public int totalNQueens(int n) {
        int[][] board = new int[n][n];

        backtrack(0, n, board);

        return count;
    }

    private void backtrack(int line, int n, int[][] board) {
        // 判断一下是否已经填完了最后一层,如果是,则向结果集中添加一个solution
        if (line == n) {
            count++;
            return;
        }

        // 如果没有填完最后一层,则填line这一层,从0到n-1逐个尝试,每次尝试出一个valid的就继续向下一层出发。
        for (int i = 0; i < n; i++) {
            board[line][i] = 1;
            if (checkValid(board, line, i)) {
                backtrack(line + 1, n, board);
            }
            board[line][i] = 0;
        }

        return;
    }

    private boolean checkValid(int[][] board, int line, int pos) {
        // 检查第line行,pos位置放queen是否合法(同一行无须检查)
        int n = board.length;

        // 检查同一列
        for (int i = 0; i < line; i++) {
            if (board[i][pos] == 1) {
                return false;
            }
        }

        // 检查左上到右下的对角线,注意只用检查line线以上的即可
        for (int i = line - 1, j = pos - 1; i >= 0 && j >= 0; i--, j--) {
            if (board[i][j] == 1) {
                return false;
            }
        }

        // 检查左下到右上的对角线,注意只用检查line线以上的即可
        for (int i = line - 1, j = pos + 1; i >= 0 && j < n; i--, j++) {
            if (board[i][j] == 1) {
                return false;
            }
        }

        return true;
    }
}
class Solution {
    public Map<String, Integer> countCharacters(String s) {
        // 要求:按照字典序输出,但是hashmap本身就不保证顺序,还是使用treemap来保证有序性。
        int n = s.length();
        Map<String, Integer> res = new TreeMap<String, Integer>(
                new Comparator<String>() {
                    public int compare(String obj1, String obj2) {
                        // 升序排序
                        return obj1.compareTo(obj2);
                    }
                });

        for (int i = 0; i < n; i++) {
            String temp = s.substring(i, i + 1);
            res.put(temp, res.getorDefault(temp, 0) + 1);
        }

        return res;
    }
}

public class PrintOddEvenNumber {

    public static void main(String[] args) {
        Solution2 solution2 = new Solution2();

        // 创建两个线程,一个打印奇数,一个打印偶数
        new Thread(new Runnable() {
            @Override
            public void run() {
                Thread.currentThread().setName("打印偶数的线程 ");
                while (true) {
                    printNum.printOdd();
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                Thread.currentThread().setName("打印奇数的线程");
                while (true) {
                    printNum.printEven();
                }
            }
        }).start();
    }
}

class Solution2 {
    private int num = 0;

    // 用于打印偶数的线程
    public synchronized printEven() {
        // 判断当前打印的数字是否为偶数,如果不是,则等待
        while (num % 2 != 0) {
            try {
                this.wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        // 如果是偶数
        System.out.println(num);
        num++;

        // 通知另一个线程
        this.notify();
    }

    // 用于打印奇数的线程
    public synchronized printOdd() {
        // 判断当前打印的数字是否为奇数,如果不是,则等待
        while (num % 2 != 1) {
            try {
                this.wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        // 如果是奇数
        System.out.println(num);
        num++;

        // 通知另一个线程
        this.notify();
    }
}

LeetCode 56. Merge Intervals

Problem

LeetCode 56. Merge Intervals

1. 题目简述

给出一系列区间,合并它们,给出合并后的结果。例如:

Example 1:

Input: [[1,3],[2,6],[8,10],[15,18]]
Output: [[1,6],[8,10],[15,18]]
Explanation: Since intervals [1,3] and [2,6] overlaps, merge them into [1,6].

2. 算法思路

相关问题:

  1. LeetCode 57. Insert Interval
  2. LeetCode 253. Meeting Rooms II(待做,会议室问题的一系列,很重要)

也算是老经典的区间相关的问题了,可以单独整理一个小专题。慢慢添加,其实和57题基本上是一致的,都是先按照左边界进行排序,然后对右边界进行处理。

暴力做法

其实最大的问题就是如何合并两个相邻区间,每次先取一个区间,然后和后面的进行合并,直到不能够再合并为止。不能再合并的条件就是后一个的start大于前一个的end,然后先用一个List存着,然后再转为Array,注意List转为Array的方法!!!

class Solution {
    public int[][] merge(int[][] intervals) {
        if (intervals.length == 0) {
            return new int[0][0];
        }

        List<int[]> merged = new ArrayList();

        // 首先对区间进行排序
        Arrays.sort(intervals, (int[] a, int[] b) -> {
            return a[0] - b[0];
        });

        for (int i = 0; i < intervals.length; i++) {
            int[] temp = intervals[i];

            // 依次合并后面的可以合并的interval,经过排序后保证了后面的start一定是不小于当前的start的,因此只要判断后者的start和当前的end的大小
            while (i + 1 < intervals.length && intervals[i + 1][0] <= temp[1]) {
                temp[1] = Math.max(intervals[i + 1][1], temp[1]);
                i++;
            }

            merged.add(temp);
        }

        return merged.toArray(new int[merged.size()][2]);
    }
}

LeetCode 54. Spiral Matrix

Problem

LeetCode 54. Spiral Matrix

1. 题目简述

给出一个m * n的矩阵,将其螺旋打印出来。例如:

Example 1:

Input:
[
[ 1, 2, 3 ],
[ 4, 5, 6 ],
[ 7, 8, 9 ]
]
Output: [1,2,3,6,9,8,7,4,5]
Example 2:

Input:
[
[1, 2, 3, 4],
[5, 6, 7, 8],
[9,10,11,12]
]
Output: [1,2,3,4,8,12,11,10,9,5,6,7]

2. 算法思路

相关问题:

  1. LeetCode 59. Spiral Matrix II

极其经典的螺旋打印的问题,这里的暴力做法其实是很麻烦的,用方向数组比较好!

暴力解法——螺旋打印

注意这里的边界情况是只有一行或者一列的情况,要特殊处理,比较麻烦,建议直接去看第二写法或者解法二。

class Solution {
    public List<Integer> spiralOrder(int[][] matrix) {
        // 每次打印一圈,直至index越界直接return,写一个子函数。
        List<Integer> res = new ArrayList();

        helper(matrix, 0, 0, res);

        return res;
    }

    private void helper (int[][] matrix, int i, int j, List<Integer> res) {
        if (matrix.length == 0) {
            return;
        }

        int m = matrix.length, n = matrix[0].length;
        int height = m - 2 * i, width = n - 2 * j;

        // 越界的情况
        if (width <= 0 || height <= 0) {
            return;
        }

        // 剩下一行或一列的情况
        if (height == 1) {
            // 一行
            for (int x = 0; x < width; x++) {
                res.add(matrix[i][j + x]);
            }
            return;
        }
        if (width == 1) {
            // 一列
            for (int x = 0; x < height; x++) {
                res.add(matrix[i + x][j]);
            }
            return;
        }

        // 开始按圈添加res
        // 上方一行,例如[1,2]
        for (int x = 0; x < width - 1; x++) {
            res.add(matrix[i][j + x]);
        }
        // 右侧一列,例如[3,6]
        for (int x = 0; x < height - 1; x++) {
            res.add(matrix[i + x][j + width - 1]);
        }
        // 下方一行,例如[9,8]
        for (int x = 0; x < width - 1; x++) {
            res.add(matrix[i + height - 1][j + width - 1 - x]);
        }
        // 左侧一列,例如[7, 4]
        for (int x = 0; x < height - 1; x++) {
            res.add(matrix[i + height - 1 - x][j]);
        }

        helper(matrix, i + 1, j + 1, res);
    }
}

第二种写法和下面的解法二有异曲同工之妙,最好记一下,这样不需要去使用额外的m*n的空间,也就是isVisited数组。题解链接:题解

class Solution {
    public int[] spiralOrder(int[][] matrix) {
        if(matrix.length == 0) return new int[0];
        int l = 0, r = matrix[0].length - 1, t = 0, b = matrix.length - 1, x = 0;
        int[] res = new int[(r + 1) * (b + 1)];
        while(true) {
            for(int i = l; i <= r; i++) res[x++] = matrix[t][i]; // left to right.
            if(++t > b) break;
            for(int i = t; i <= b; i++) res[x++] = matrix[i][r]; // top to bottom.
            if(l > --r) break;
            for(int i = r; i >= l; i--) res[x++] = matrix[b][i]; // right to left.
            if(t > --b) break;
            for(int i = b; i >= t; i--) res[x++] = matrix[i][l]; // bottom to top.
            if(++l > r) break;
        }
        return res;
    }
}

方向数组巧妙解法

对于这种需要四个方向dfs的题目来说,方向数组最适合不过了,对于四个方向,只需要定义dx和dy即可。也就是说dx = [-1, 1, 0, 0], dy = [0, 0, -1, 1],分别对应“上下左右”四个方向,每次只需要判断是否越界或者已经填充即可。这么写的好处是节省脑力,不需要判断每次的长度,很省事!!!但是相应的有一丢丢浪费空间,因为需要额外的bool数组来存储每个数字是否已经打印。

class Solution {
    public List<Integer> spiralOrder(int[][] matrix) {
        if (matrix.length == 0) {
            return new ArrayList<Integer>();
        }

        int m = matrix.length, n = matrix[0].length;
        // 这里用一个boolean数组来定义是否已经访问过该数字
        boolean[][] isVisited = new boolean[m][n];
        // 定义四个方向,此题的顺序应该是右下左上,注意顺序
        int[] dx = {0, 1, 0, -1};
        int[] dy = {1, 0, -1, 0};
        // 定义一个返回的结果集
        List<Integer> res = new ArrayList();

        int x = 0, y = 0, direction = 0;

        while (res.size() < m * n) {
            res.add(matrix[x][y]);
            isVisited[x][y] = true;
            if (x + dx[direction] < 0 || x + dx[direction] >= m || y + dy[direction] < 0 || y + dy[direction] >= n || isVisited[x + dx[direction]][y + dy[direction]] == true) {
                // 此时需要变更方向
                direction = (direction + 1) % 4;
            }
            x += dx[direction];
            y += dy[direction];
        }

        return res;
    }
}

LeetCode 51. N-Queens

Problem

LeetCode 51. N-Queens

1. 题目简述

给出一个整形数n,找出”n皇后问题“的所有解。每一行,每一列以及每个皇后所在的对角线不能包含其他皇后。例如:

Example

Example:

Input: 4
Output: [
[".Q..",  // Solution 1
"...Q",
"Q...",
"..Q."],

["..Q.",  // Solution 2
"Q...",
"...Q",
".Q.."]
]
Explanation: There exist two distinct solutions to the 4-queens puzzle as shown above.

2. 算法思路

相关问题:

  1. LeetCode 52. N-Queens II

极其经典的”八皇后问题“,用回溯法。跟数独问题基本上是一样的,但是要稍微简单些。

回溯法

这里问题其实是在于,如何判断当前行所放的皇后是否合理,这里我的做法是直接将二维的棋盘表示了出来,然后用board[i - x][j - x]表示左上到右下的对角线,用board[i - x][j + x]表示右上到左下的对角线(注意边界)。

但是其实还有另外一种更为节省空间的做法(其实必要性不大),用bool型数组来表示每一行、每一列、每一个对角线是否已经共存在了一个皇后。如果已知n,那么每一组(左上到右下算一组,左下到右上算一组)对角线的个数也是已知的,2n - 1,然后用[i, j]做一个映射即可。

class Solution {
    public List<List<String>> solveNQueens(int n) {
        // 也就是说每一行,每一列,每一个斜线上都不能有任意两个皇后。每一行和每一列都还算好判断,斜线上要怎么判断呢?可以用[i][j]来做映射。但是这里我打算采用一个二维数组来做计算,写起来方便。

        int[][] board = new int[n][n];
        List<List<String>> res = new ArrayList();

        backtrack(0, n, board, res);

        return res;
    }

    private void backtrack(int line, int n, int[][] board, List<List<String>> res) {
        // 判断一下是否已经填完了最后一层,如果是,则向结果集中添加一个solution
        if (line == n) {
            List<String> solution = new ArrayList();
            for (int i = 0; i < n; i++) {
                StringBuilder sb = new StringBuilder();
                for (int j = 0; j < n; j++) {
                    sb.append(board[i][j] == 0 ? "." : "Q");
                }
                solution.add(sb.toString());
            }
            res.add(solution);
            return;
        }

        // 如果没有填完最后一层,则填line这一层,从0到n-1逐个尝试,每次尝试出一个valid的就继续向下一层出发。
        for (int i = 0; i < n; i++) {
            board[line][i] = 1;
            if (checkValid(board, line, i)) {
                backtrack(line + 1, n, board, res);
            }
            board[line][i] = 0;
        }

        return;
    }

    private boolean checkValid(int[][] board, int line, int pos) {
        // 检查第line行,pos位置放queen是否合法(同一行无须检查)
        int n = board.length;

        // 检查同一列
        for (int i = 0; i < line; i++) {
            if (board[i][pos] == 1) {
                return false;
            }
        }

        // 检查左上到右下的对角线,注意只用检查line线以上的即可
        for (int i = line - 1, j = pos - 1; i >= 0 && j >= 0; i--, j--) {
            if (board[i][j] == 1) {
                return false;
            }
        }

        // 检查左下到右上的对角线,注意只用检查line线以上的即可
        for (int i = line - 1, j = pos + 1; i >= 0 && j < n; i--, j++) {
            if (board[i][j] == 1) {
                return false;
            }
        }

        return true;
    }
}

LeetCode 49. Group Anagrams

Problem

LeetCode 49. Group Anagrams

1. 题目简述

给出一组字符串(全部由小写字母组成),将由相同字母组成的字符串按组进行返回。例如:

Example:

Input: ["eat", "tea", "tan", "ate", "nat", "bat"],
Output:
[
["ate","eat","tea"],
["nat","tan"],
["bat"]
]

2. 算法思路

其实这道题是很直接的一道题,问题就在于统计anagram,如何才能判断两个字符串啊anagram。这里我们用一个很巧妙的hash方法,记录每个字符串组成字母的哈希值。如下:

hashtable

class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> result = new HashMap<>();

        for (String str : strs){
            char[] strArray = str.toCharArray();
            char[] keyArray = new char[26];

            for (char c : strArray) {
                keyArray[c - 'a']++;
            }

            // 用一个字符串来表示一个特殊的hash key。
            String key = String.valueOf(keyArray);

            if (!result.containsKey(key)){
                result.put(key, new ArrayList<String>());
            }
            result.get(key).add(str);
        }

        return new ArrayList<>(result.values());
    }
}

LeetCode 42. Trapping Rain Water

Problem

LeetCode 42. Trapping Rain Water

1. 题目简述

给出一组非负整数表示地形,每一个竖条的宽度为1,计算下雨后,地形中能储存多少水。例如:

Example

Example:

Input: [0,1,0,2,1,0,1,3,2,1,2,1]
Output: 6

2. 算法思路

相关问题:

  1. LeetCode 503. Next Greater Element II
  2. LeetCode 556. Next Greater Element III
  3. LeetCode 739. Daily Temperatures(无解析)

monotonous stack(单调栈)

自己复习的时候直接看yxc的题解21分40秒,有图才能更清晰。这里不做过多的解释了,很难讲。

其实这道题是属于单调栈的一种不常见的case,也是属于单调栈的一种,我们每次遇到一个“洼地”的时候其实需要计算一下可以存多少雨水。

class Solution {
    public int trap(int[] height) {
        Stack<Integer> stack = new Stack();
        int n = height.length, res = 0;

        for (int i = 0; i < n; i++) {
            // last用来记录上一次的高度,这里初始化为0是为了防止边界情况,首次pop的时候不计算面积。
            int last = 0;
            while (!stack.isEmpty() && height[i] >= height[stack.peek()]) {
                // 这里首次计算的时候,i - stack.peek() - 1一定为0,这里是做了一个mapping。
                res += (height[stack.peek()] - last) * (i - stack.peek() - 1);
                last = height[stack.pop()];
            }

            // 如果pop完以后发现左侧还有一个更高的,把小尾巴计算上(以【4,2,3】为例)
            if (!stack.isEmpty()) {
                res += (i - stack.peek() - 1) * (height[i] - last);
            }

            stack.push(i);
        }

        return res;
    }
}

2020-06-22

题目汇总

LeetCode 43. Multiply Strings

算法思路

乘法直接计算

这道题怎么说呢,做过不难,但是有很多细节,见代码注释。

这里我们先对num1[i]和num2[j]进行乘积,将其暂时存到一个初始化长度为m + n的int数组num中,其中num1[i]和num2[j]相乘我们存储到num[i + j]中,对于i + j相同的值我们加起来存到num[i + j]上。因为长度m的数和长度为n的数字相乘,最多也不过m + n - 1的长度,因此我们不必担心溢出的问题。

然后再进行进位操作,一位位进行计算,细节注释如下:

class Solution {
    public String multiply(String num1, String num2) {
        if (num1.equals(zero) || num2.equals(zero)) {
            return zero;
        }

        // 这道题很巧妙,我们不需要每次乘一次就去运算各种进位什么的,不划算,直接记录每一位的进位值,画个图就懂了。
        int m = num1.length(), n = num2.length();
        int[] numA = new int[m];
        int[] numB = new int[n];
        int[] num= new int[m + n];

        // 从低位到高位存储每一位数字,计算的时候低位运算结果也要写在前面,为了防止和没用到的0相冲突!!!
        for (int i = m - 1; i >= 0; i--) {
            numA[m - 1 - i] = num1.charAt(i) - '0';
        }

        for (int i = n - 1; i >= 0; i--) {
            numB[n - 1 - i] = num2.charAt(i) - '0';
        }

        // res从低位到高位存储,index从0开始
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                num[i + j] += numA[i] * numB[j];
            }
        }

        // 从前向后计算进位
        int temp = 0;
        for (int i = 0; i < m + n; i++) {
            temp += num[i];
            num[i] = temp % 10;
            temp = temp / 10;
        }

        // 从后向前遍历,去掉多余的0,也就是多余的高位0不要
        int end = m + n - 1;
        while (end >= 0 && num[end] == 0) {
            end--;
        }

        // 从后向前依次append每一位
        StringBuilder sb = new StringBuilder();
        for (int i = end; i >= 0; i--) {
            sb.append(Integer.toString(num[i]));
        }

        return sb.toString();
    }
}

LeetCode 48. Rotate Image

算法思路

翻转做法(很tricky)

这道题算是脑筋急转弯,直接背解法即可。

将数组先沿着左上角到右下角的轴线进行对折翻转,然后沿着竖轴中线进行翻转,即可得到答案。

其实这道题还有多种对折方式,画一下,尝试一下!!!顺时针转90度,逆时针转90度等等。

class Solution {
    public void rotate(int[][] matrix) {
        // 先沿着对角线翻转,左上角到右下角的那条线;然后再沿着中轴竖着翻转;
        // 或者沿着左下角到右上角的线进行翻转,然后再沿着横中轴进行翻转。
        int n = matrix.length;

        // 注意不要翻转两遍!!!
        for (int i = 0; i < n; i++) {
            for (int j = 0; j <= i; j++) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[j][i];
                matrix[j][i] = temp;
            }
        }

        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n / 2; j++) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[i][n - 1 - j];
                matrix[i][n - 1 - j] = temp;
            }
        }
    }
}

LeetCode 1105. Filling Bookcase Shelves

Problem

LeetCode 1105. Filling Bookcase Shelves

1. 题目简述

给出一堆书和一个书架,每本书有宽和高,书架有宽度w,我们需要按照给定的顺序排列书籍,求出书架最少需要多高才能放下所有的书籍(建议去看leetcode原本题目,这里表述比较简略)。例如:

Shelves

Input: books = [[1,1],[2,3],[2,3],[1,1],[1,1],[1,1],[1,2]], shelf_width = 4

Output: 6

Explanation:
The sum of the heights of the 3 shelves are 1 + 3 + 2 = 6.
Notice that book number 2 does not have to be on the first shelf.

2. 算法思路

参考资料:

  1. 花花酱的解析

dynamic programming

这是一道十分典型的动态规划问题,没遇到很难想出来。

这里看似是一个二维的问题,实际上是一维。限制条件就是书架的宽度,这里就先不放图片了,在上面花花酱的视频里面有解释。对于任意第i本书,它有两种可能性,一种是从它自身开始重新加一层,另一种方式就是和前面的一些书一起构成一层。但是具体怎么样才算是最优解我们并不清楚,也没有判断的方法。

Greedy肯定是不可用的,因为即使我们找到了当前的最优解,也不确定它是否是全局最优解的一部分。因此我们需要遍历所有可能的书架上书的排列情况。

我们假设dp[i]是对于前i本书,最小的高度。

那么从第i本书向前搜索x本书,x本书的宽度不超过书架宽度(0<= x < i),height(i, j)表示从第i本书到第j本书最高高度。那么有:

$$dp[i] = min(dp[i - x] + height(i - x, i), dp[i]) (x本书的宽度要小于书架宽度)$$

class Solution {
    public int minHeightShelves(int[][] books, int shelf_width) {
        int n = books.length;
        int[] dp = new int[n + 1];

        // dp[i]表示前i个元素组成的书架的最小高度,对于所有的j在0到i-1之间的书,我们从后向前找x本书,使得x本书的宽度不会超过书架的width,dp[i] = min(dp[i], dp[j] + height) j在0到i-1之间。

        Arrays.fill(dp, 1000000);
        // 初始化dp[0]和dp[1]
        dp[1] = books[0][1];
        dp[0] = 0;
        for (int i = 2; i <= n; i++) {
            int width = 0, height = -1;
            for (int j = i; j > 0; j--) {
                width += books[j - 1][0];
                height = Math.max(height, books[j - 1][1]);
                if (width <= shelf_width) {
                    dp[i] = Math.min(dp[j - 1] + height, dp[i]);
                } else {
                    break;
                }
            }
        }

        return dp[n];
    }
}

2020-06-19

题目汇总

LeetCode 1048. Longest String Chain

算法思路

动态规划

用hashmap存储每个string的最长的chain length。记住先排序!!!

class Solution {
    public int longestStrChain(String[] words) {
        // 按照长度对words进行排序
        Arrays.sort(words, (word1, word2) -> word1.length() - word2.length());
        // 创建一个hashmap,记录当前每个word的最长的word chain是多少
        Map<String, Integer> dp = new HashMap();
        int longest = -1;

        for (int i = 0; i < words.length; i++) {
            String temp = words[i];
            int chainLength = -1;
            for (int j = 0; j < temp.length(); j++) {
                StringBuilder sb= new StringBuilder();
                String newStr = sb.append(temp.substring(0, j)).append(temp.substring(j + 1, temp.length())).toString();
                chainLength = Math.max(chainLength, dp.getOrDefault(newStr, 0) + 1);
            }
            dp.put(words[i], chainLength);
            longest = Math.max(longest, chainLength);
        }

        return longest;
    }
}

DFS + memorization(待完成)

LeetCode 268. Missing Number

算法思路

用一个boolean数组即可解决该问题。或者使用异或运算。

数组

class Solution {
    public int missingNumber(int[] nums) {
        int n = nums.length;
        boolean[] check = new boolean[n + 1];

        for (int num : nums) {
            check[num] = true;
        }

        for (int i = 0; i <= n; i++) {
            if (check[i] == false) {
                return i;
            }
        }

        return 0;
    }
}

异或

简单举个例子就能看懂了:

详情见: LeetCode Solution 3

class Solution {
    public int missingNumber(int[] nums) {
        int missing = nums.length;
        for (int i = 0; i < nums.length; i++) {
            missing ^= i ^ nums[i];
        }
        return missing;
    }
}

LeetCode 41. First Missing Positive

Problem

LeetCode 41. First Missing Positive

1. 题目简述

给出一个未排序的整形数组,找出最小的缺失的正整数,要求只能使用常数级别的extra space。例如:

Example 1:

Input: [1,2,0]
Output: 3

Example 2:

Input: [3,4,-1,1]
Output: 2

Example 3:

Input: [7,8,9,11,12]
Output: 1

2. 算法思路

相关问题:

  1. LeetCode 268. Missing Number

Array

这道题是属于比较smart的那种题,比较考验数学思维,做过基本上就会,没做过很难想。属于hard里难度较低的那种。

首先我们对于一个长度为n的数组,它的第一个missing number最大只可能是n+1,边界情况是n个数是由(1…n)不重复组成。

因此,对于负数,0或者大于n的数字,我们都可以把它转化成一个固定的数,这里我们设置为n+1,这样我们就能保证数组中的数都是正整数了。

现在问题在于如何使用常数级别的extra space。我们知道一共是有n个数,数组中的数字经过转化以后都变为了[1, n + 1]之间,因此,如果某个数字i有出现过,我们将第i个数(index为i - 1)置为负数即可(i != n + 1),最后判断第一个出现的正数,那么该index + 1即为所求;如果没有找到正数,那么返回n + 1。

这里有一些小tips需要注意,例如不要对一个数字两次取反,以及计算index的时候需要取绝对值,因为有可能在处理前面的数字的时候把后面的数已经置为负数了。

class Solution {
    public int firstMissingPositive(int[] nums) {
        // 直接看解法的一道题。首先,我们假设n为nums的长度,而最终的答案最多只可能是n+1,也就是说前n个数恰好是1到n。因此我们要先排除负数和0和大于n的数,我们将其都设为n+1。修改以后,数组里每个数字的范围就是1到n+1,然后我们遍历数组,用一个negative的符号来表示当前数字已经出现过了并跳过数字n+1,修改下标的符号。然后遍历数组,如果找到第一个非负数,那么返回它。如果没有找到,返回n+1.
        int n = nums.length;

        // 将不合法的数都变成n+1
        for (int i = 0; i < n; i++) {
            if (nums[i] <= 0 || nums[i] > n) {
                nums[i] = n + 1;
            }
        }

        // 遍历数组,目前数组中每个数都是在1到n+1之间,将以nums[i]的存在的数为下标的数字变为负数。
        for (int i = 0; i < n; i++) {
            // 防止nums[i]在遍历到的时候已经是负号了
            int num = Math.abs(nums[i]);
            // 防止加两次负号
            if (num <= n && nums[num - 1] > 0) {
                nums[num - 1] = -1 * nums[num - 1];
            }
        }

        // 遍历数组,找到第一个非负数
        for (int i = 0; i < n; i++) {
            if (nums[i] > 0) {
                return i + 1;
            }
        }

        // 如果没有找到,直接返回n+1
        return n + 1;
    }
}

LeetCode 37. Sudoku Solver

Problem

LeetCode 37. Sudoku Solver

1. 题目简述

给出一个9 * 9的数独游戏表格,找出它的解(保证有解)。例如:

Soduku Problem

Soduku Solution

2. 算法思路

相关问题:

  1. LeetCode 36. Valid Sudoku

参考资料:

  1. labuladong的算法小抄-数独问题

回溯法

老数独题目了,回溯法可解,就是暴力穷举,回溯法的经典题目之一。我一开始的做法是使用了hashset,其实用hashmap也OK,因为hashset的底层实现就是hashmap,本质上是一样的。

这里我们还要注意创建collections数组的方法,要先创建引用,然后再for循环创建每一个collection实例。

class Solution {
    Set<Character>[] rows = new HashSet[9];
    Set<Character>[] cols = new HashSet[9];
    Set<Character>[] boxes = new HashSet[9];

    public void solveSudoku(char[][] board) {
        for (int i = 0; i < 9; i++) {
            rows[i] = new HashSet<Character>();
            cols[i] = new HashSet<Character>();
            boxes[i] = new HashSet<Character>();
        }

        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                if (board[i][j] != '.') {
                    rows[i].add(board[i][j]);
                    cols[j].add(board[i][j]);
                    boxes[(i / 3) * 3 + j / 3].add(board[i][j]);
                }
            }
        }

        backtracking(board, 0, 0, rows, cols, boxes);
    }

    private boolean backtracking(char[][] board, int i, int j, Set<Character>[] rows, Set<Character>[] cols, Set<Character>[] boxes) {
        // 到达最后一列,开始对下一行进行回溯
        if (j == 9) {
            return backtracking(board, i + 1, 0, rows, cols, boxes);
        }

        // 完成最后一行,结束,说明找到解了
        if (i == 9) {
            return true;
        }

        int box_id = (i / 3) * 3 + j / 3;
        if (board[i][j] != '.') {
            return backtracking(board, i, j + 1, rows, cols, boxes);
        } else {
            for (char c = '1'; c <= '9'; c++) {
                // 检查当前这个值是否valid
                if (!rows[i].contains(c) && !cols[j].contains(c) && !boxes[box_id].contains(c)) {
                    board[i][j] = c;
                    rows[i].add(c);
                    cols[j].add(c);
                    boxes[box_id].add(c);
                    if (!backtracking(board, i, j + 1, rows, cols, boxes)) {
                        // 回溯
                        rows[i].remove(c);
                        cols[j].remove(c);
                        boxes[box_id].remove(c);
                        board[i][j] = '.';
                        continue;
                    } else {
                        return true;
                    }
                }
            }
            return false;
        }

    }
}

第二种写法是参考的上面参考资料,写法更为优雅,且没有用到hashset或者hashmap,写法上更加优雅,但是速度来说没有hashmap或者hashset快。

class Solution {
    public void solveSudoku(char[][] board) {
        backtrack(board, 0, 0);
    }

    boolean backtrack(char[][] board, int i, int j) {
        int m = 9, n = 9;
        if (j == n) {
            // 穷举到最后一列的话就换到下一行重新开始。
            return backtrack(board, i + 1, 0);
        }
        if (i == m) {
            // 找到一个可行解,触发 base case
            return true;
        }

        if (board[i][j] != '.') {
            // 如果有预设数字,不用我们穷举
            return backtrack(board, i, j + 1);
        }

        for (char ch = '1'; ch <= '9'; ch++) {
            // 如果遇到不合法的数字,就跳过
            if (!isValid(board, i, j, ch))
                continue;

            board[i][j] = ch;
            // 如果找到一个可行解,立即结束
            if (backtrack(board, i, j + 1)) {
                return true;
            }
            board[i][j] = '.';
        }
        // 穷举完 1~9,依然没有找到可行解,此路不通
        return false;
    }

    boolean isValid(char[][] board, int r, int c, char n) {
        for (int i = 0; i < 9; i++) {
            // 判断行是否存在重复
            if (board[r][i] == n)
                return false;
            // 判断列是否存在重复
            if (board[i][c] == n)
                return false;
            // 判断 3 x 3 方框是否存在重复
            if (board[(r/3)*3 + i/3][(c/3)*3 + i%3] == n)
                return false;
        }
        return true;
    }
}

2020-06-18

题目汇总

LeetCode 739. Daily Temperatures

算法思路

单调栈

class Solution {
    public int[] dailyTemperatures(int[] T) {
        // 典型的单调栈题目
        Stack<Integer> stack = new Stack();
        int[] res = new int[T.length];

        for (int i = 0; i < T.length; i++) {
            while (!stack.isEmpty() && T[stack.peek()] < T[i]) {
                res[stack.peek()] = i - stack.peek();
                stack.pop();
            }
            stack.push(i);
        }

        return res;
    }
}

LeetCode 274. H-Index

算法思路

274和275我觉得是很垃圾的两道题,有点像脑筋急转弯。对于274题,其实就是找出一个数x,使得数组中大于等于x的数恰好为x个。

注意,这里找到的hindex不一定是数组里的数,例如:[4,0,6,1,5]的hindex就是3。

这里有个弯需要绕一下,就是while循环那里,我们的h要从小到大去遍历,而查找citation的数量需要从后向前找,因为这是一个相遇问题而不是追击问题。需要细品一下。

class Solution {
    public int hIndex(int[] citations) {
        Arrays.sort(citations);

        // 最终的h的值一定是小于n的,看一下solution里的图就可以明白
        int n = citation.length, h = 0;
        while (h < n && citations[n - h - 1] > h) {
            h++;
        }

        return h;
    }
}

LeetCode 275. H-Index II

算法思路

和274基本一致,只是这里的array是有序的,用二分查找更方便。目标是找到一个target似的citations[i]
= n - i,如果没有这样的i,那么最终的left及其后面的数的数量就是h个。

class Solution {
  public int hIndex(int[] citations) {
    int idx = 0, n = citations.length;
    int pivot, left = 0, right = n - 1;
    while (left <= right) {
      pivot = left + (right - left) / 2;
      if (citations[pivot] == n - pivot) return n - pivot;
      else if (citations[pivot] < n - pivot) left = pivot + 1;
      else right = pivot - 1;
    }
    return n - left;
  }
}

LeetCode 36. Valid Sudoku

算法思路

就是通过多个hashmap或者hashset来对每一行、每一列以及每一个box进行查找。

class Solution {
    public boolean isValidSudoku(char[][] board) {
        // 这道题只要判断数独是否合法,而不一定有解!!!
        // 分情况讨论即可

        // 检查每一行是否valid
        for (int i = 0; i < 9; i++) {
            Set<Character> dict = new HashSet(); 
            for (int j = 0; j < 9; j++) {
                if (board[i][j] != '.') {
                    if (dict.contains(board[i][j])) {
                        return false;
                    }
                    dict.add(board[i][j]);
                }
            }
        }

        // 检查每一列是否valid
        for (int j = 0; j < 9; j++) {
            Set<Character> dict = new HashSet(); 
            for (int i = 0; i < 9; i++) {
                if (board[i][j] != '.') {
                    if (dict.contains(board[i][j])) {
                        return false;
                    }
                    dict.add(board[i][j]);
                }
            }
        }

        // 检查每一个3x3小方框是否valid
        Set<Character>[] dicts = new HashSet[9];
        for (int i = 0; i < 9; i++) {
            dicts[i] = new HashSet<Character>();
        }

        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                int id = (i / 3) * 3 + j / 3;
                if (board[i][j] != '.') {
                    if (dicts[id].contains(board[i][j])) {
                        return false;
                    }
                    dicts[id].add(board[i][j]);
                }
            }
        }

        return true;
    }
}

LeetCode 32. Longest Valid Parentheses

Problem

LeetCode 32. Longest Valid Parentheses

1. 题目简述

给出一个仅有”(“和”)”组成的字符串,找出其中最长的valid的括号匹配字符串长度。例如:

Example 1:

Input: "(()"
Output: 2
Explanation: The longest valid parentheses substring is "()"

Example 2:

Input: ")()())"
Output: 4
Explanation: The longest valid parentheses substring is "()()"

2. 算法思路

相关问题:

  1. LeetCode 20. Valid Parentheses(无解析)

一开始的想法也是动态规划,但是dp的思路出了点问题,直接看下面这个思路即可。

dynamic programming

  1. 假设dp[i]表示以第i-1个元素为结尾,最长的valid的长度;这里我们要注意,这里的dp值是以当前为结尾,而不是在此之前的最长长度!!!
  2. 那么当s[i]为”(“时,那么dp[i] = 0;
  3. 当s[i]为”)”时,分为两种情况。第一种:如果s[i - 1]为’(‘,dp[i] = dp[i - 1] + 2;
  4. 第二种情况:如果s[i - 1]为’)’,那么逻辑复杂一点,倘若dp[i - dp[i - 1] - 1] == ‘(‘,那么dp[i] = dp[i - 1] + 2 + dp[i - dp[i - 1] - 2];倘若dp[i - dp[i - 1] - 1] == ‘)’,那么说明其无法凑成对,仍然是0。
class Solution {
    public int longestValidParentheses(String s) {
        if (s.length() <= 1) {
            return 0;
        }

        int n = s.length();
        int[] dp = new int[n];
        char[] arr = s.toCharArray();
        int longest = 0;

        // 初始化dp
        if (arr[0] == '(' && arr[1] == ')') {
            dp[1] = 2;
        }
        longest = dp[1];

        // 从index为2开始遍历,只有当前字符为')'且和前面某个'('匹配时,才会更新dp值,否则都为0!
        for (int i = 2; i < n; i++) {
            if (arr[i] == ')' && arr[i - 1] == '(') {
                dp[i] = dp[i - 2] + 2;
            } else if (arr[i] == ')' && arr[i - 1] == ')') {
                if (i - dp[i - 1] - 1 >= 0 && arr[i - dp[i - 1] - 1] == '(') {
                    dp[i] = dp[i - 1] + 2 + ((i - dp[i - 1] - 2) >= 0 ? dp[i - dp[i - 1] - 2] : 0);
                }
            }
            longest = Math.max(dp[i], longest);
        }

        return longest;
    }
}

Stack

使用stack的方式十分巧妙,通过记录上一次不合法括号的最后的index来判断。每次遇到一个合法的’)’时,计算一下以当前的’)’结尾的子串的最长合法长度是多少。代码如下(直接抄的solution):

public class Solution {

    public int longestValidParentheses(String s) {
        int maxans = 0;
        Stack<Integer> stack = new Stack<>();
        stack.push(-1);
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == '(') {
                stack.push(i);
            } else {
                stack.pop();
                if (stack.empty()) {
                    stack.push(i);
                } else {
                    maxans = Math.max(maxans, i - stack.peek());
                }
            }
        }
        return maxans;
    }
}

图算法——最短路径算法

最短路径算法

最短路径算法是图计算中一种极为常见的算法,而且相对来说比较复杂,并不是因为难,而是因为情况多变。例如:有无环,边是否有权重,有无负权边等等,每种组合也可对应不同的算法,这里做一次小小的总结。

最短路径算法分类

单源最短路径

无向图的单源最短路径

有向图的单源最短路径

单源最短路径指的就是给定一个有向带权图G(V, E),给出其中的一个源点V0,求V0到其余各点的最短路径。

全图最短路径

LeetCode 556. Next Greater Element III

Problem

LeetCode 556. Next Greater Element III

1. 题目简述

给出一个32-bit的正整数,找出由它的每一位组成的下一个比它大一点的数字,如果当前已经是最大或者没有一个32位的int值可以表示,则返回-1。例如:

Example 1:

Input: 12
Output: 21


Example 2:

Input: 21
Output: -1

2. 算法思路

相关问题:

  1. LeetCode 496. Next Greater Element I
  2. LeetCode 556. Next Greater Element III
  3. LeetCode 31. Next Permutation

全排列

这道题其实和LeetCode 130. Surrounded Regions的单调栈没有任何关系,反而是和LeetCode 31. Next Permutation一模一样,目的就是找出下一个更大的数字,其难点在于注意integer的max值,如果变换后值超出int1的范围,则返回-1.

具体思路见:LeetCode 31. Next Permutation

class Solution {
    public int nextGreaterElement(int n) {
        // 我觉得这里更多需要注意的是32位整形的这个约束条件,为了不溢出,我们需要将其转化为long类型
        // 和next permutation一样的做法,从后向前找第一个降序数字x,如果找不到,说明不行;例如12386,我们找到的就是8;
        // 然后我们从x(包括x)的右侧找到第一个比“3”大的数字,也就是6,交换它们;
        // 数字变成12683,然后将x及x的右侧降序排列
        // 比较其和intMax的大小,如果更大,则还是返回-1。
        long intMax = Integer.MAX_VALUE;
        List<Integer> digitsList = new ArrayList();
        int num = n;

        // 计算数字各个位,存起来转化成数组,方便计算;从低位到高位存。
        while (num > 0) {
            digitsList.add(num % 10);
            num /= 10;
        }
        int length = digitsList.size();
        int[] digits = new int[length];
        for (int i = 0; i < length; i++) {
            digits[i] = digitsList.get(length - i - 1);
        }

        // 从后向前找第一个降序的数字
        int pos = -1;
        for (int i = length - 1; i > 0; i--) {
            if (digits[i] > digits[i - 1]) {
                pos = i;
                break;
            }
        }
        if (pos == -1) {
            return -1;
        }

        // 从后向前找第一个比digits[pos - 1]大的数字,交换它们,这个数字一定存在,最坏情况不过是pos这个数
        for (int i = length - 1; i >= pos; i--) {
            if (digits[i] > digits[pos - 1]) {
                int temp = digits[pos - 1];
                digits[pos - 1] = digits[i];
                digits[i] = temp;
                break;
            }
        }

        // 重新排列从pos到末尾的数字,但是我们知道它们都是降序的,首尾pointer交换
        int start = pos, end = length - 1;
        while (start < end) {
            int temp = digits[start];
            digits[start] = digits[end];
            digits[end] = temp;
            start++;
            end--;
        }

        // 将重新生成的数字用long看一下是否越界了
        long newNum = 0;
        long multiplier = 1;
        for (int i = length - 1; i >= 0; i--) {
            newNum += multiplier * digits[i];
            multiplier *= 10;
        }

        if (newNum > intMax) {
            return -1;
        } else {
            return (int)newNum;
        }
    }
}

LeetCode 496. Next Greater Element I

Problem

LeetCode 496. Next Greater Element I

1. 题目简述

给出两个无重复数组nums1和nums2,nums1是nums2的子集,找出nums1中所有元素在nums2中的下一个更大的元素,如果没有,则为-1。例如:

Input: nums1 = [4,1,2], nums2 = [1,3,4,2].
Output: [-1,3,-1]
Explanation:
    For number 4 in the first array, you cannot find the next greater number for it in the second array, so output -1.
    For number 1 in the first array, the next greater number for it in the second array is 3.
    For number 2 in the first array, there is no next greater number for it in the second array, so output -1.

2. 算法思路

相关问题:

  1. LeetCode 503. Next Greater Element II
  2. LeetCode 556. Next Greater Element III
  3. LeetCode 739. Daily Temperatures(无解析)

monotonous stack(单调栈)

这是单调栈,经典的数据结构之一,单调栈的目的就是为了查找下一个更大/小的元素,步骤如下:

  1. 如果栈为空,或者栈顶元素大于当前元素时,push当前元素到栈顶;
  2. 如果栈不为空,且栈顶元素小于当前元素时,一直pop,直到栈为空或者栈顶元素大于当前元素。

**注意:**上面说的“下一个更大/更小元素”并不绝对,重要的是单调栈是什么样子的,找下一个更大更小元素只是其中一个应用,其他更多的应用还有很多!!!例如LeetCode 456. 132 Pattern!!!这点一定要注意!!!要深入理解数据结构,而不是单一问题。

class Solution {
    public int[] nextGreaterElement(int[] nums1, int[] nums2) {
        // 找到nums2里所有元素的next greater element,由于无重复元素,因此用hashmap来存储,然后对nums1进行循环get即可。
        Stack<Integer> stack = new Stack();
        Map<Integer, Integer> dict = new HashMap();

        for (int num : nums2) {
            while (!stack.isEmpty() && num > stack.peek()) {
                dict.put(stack.pop(), num);
            }
            stack.push(num);
        }

        // stack里剩下的说明都是没有next greater element的数字
        while (!stack.isEmpty()) {
            dict.put(stack.pop(), -1);
        }

        for (int i = 0; i < nums1.length; i++) {
            nums1[i] = dict.get(nums1[i]);
        }

        return nums1;
    }
}

LeetCode 503. Next Greater Element II

Problem

LeetCode 503. Next Greater Element II

1. 题目简述

给出一个环形数组(首尾相连)nums,找出nums每一个元素在nums中的下一个更大的元素,如果没有,则为-1。例如:

Example 1:
Input: [1,2,1]
Output: [2,-1,2]
Explanation: The first 1's next greater number is 2; 
The number 2 can't find next greater number; 
The second 1's next greater number needs to search circularly, which is also 2.

2. 算法思路

相关问题:

  1. LeetCode 496. Next Greater Element I
  2. LeetCode 556. Next Greater Element III
  3. LeetCode 739. Daily Temperatures(无解析)

monotonous stack(单调栈)

LeetCode 496. Next Greater Element I的进阶版,这里其实算法和之前是一样的,只不过数组变成了环形,而且存在“重复”的数字,所以我们不能再使用hashmap来存储,而是**使用index**来存储,因此使用arraylist是最好的。

这里有几点需要注意:

  1. 这里我们push进stack的东西是数组的index,而不是数字,注意和前一道进行区分;
  2. 在for循环后,stack里面剩下的,不是没有next greater element的元素,这里我们需要注意。这里我们举个例子:[5,4,3,2,1],我们将其copy后变成[5,4,3,2,1,5,4,3,2,1],最终stack里面剩下的是后面数组里没有next greater element的index,也就是我们后面的54321都是没用next greater element的,但是实际上4321是哟肚饿,因为在前面已经计算完了。这里需要特别注意一下!!!
class Solution {
    public int[] nextGreaterElements(int[] nums) {
        // 和next greater element 1的区别在于这里是环形数组,之前是只能在它的后面到end之间,现在是可以绕一圈再回到自身位置,因此我们遍历到末尾后再从头遍历一次数组。
        if (nums.length == 0) {
            return nums;
        }

        Stack<Integer> stack = new Stack();
        int n = nums.length;
        int[] res = new int[n];
        Arrays.fill(res, -1);

        for (int i = 0; i < 2 * n; i++) {
            int index = i % n;
            while (!stack.isEmpty() && nums[index] > nums[stack.peek()]) {
                res[stack.pop()] = nums[index];
            }
            stack.push(index);
        }

        return res;
    }
}

LeetCode 130. Surrounded Regions

Problem

LeetCode 130. Surrounded Regions

1. 题目简述

给出一个由’O’和’X’组成的二维数组,将所有由‘X’包围的‘O’替换成‘X’。例如:

Example:

X X X X
X O O X
X X O X
X O X X

After running your function, the board should be:

X X X X
X X X X
X X X X
X O X X

2. 算法思路

相关问题:

  1. LeetCode 200. Number of Islands(无解析)

DFS || BFS

  1. 先将和边界相连的所有‘O’都替换成另一个字母‘W’,这里DFS或者BFS都可以(这里以DFS为例);
  2. 然后将board中剩余的‘O’都替换成’X‘;
  3. 将替换后的board中的’W‘再替换回去。

这里需要注意的只有我们用了一个Pair的List来记录了初始和边界相连的’O‘的位置,第三部只需要一个个遍历换回去即可。

class Solution {
    int[][] directions = {{-1, 0}, {1, 0}, {0, 1}, {0, -1}};

    public void solve(char[][] board) {
        // DFS,先将所有从边界出发的“O”替换为“W”,然后将board中所有的“O”替换成“X”,然后再替换回来

        // 判断输入为空的情况
        if (board == null || board.length == 0) {
            return;
        }

        // 遍历四周,将与边相连的“O”变为“W”
        int m = board.length, n = board[0].length;
        List<Pair<Integer, Integer>> list = new ArrayList();
        for (int i = 0; i < m; i++) {
            if (board[i][0] == 'O') {
                replace(board, i, 0, 'O', 'W', list);
            }
            if (board[i][n - 1] == 'O') {
                replace(board, i, n - 1, 'O', 'W', list);
            }
        }
        for (int j = 0; j < n; j++) {
            if (board[0][j] == 'O') {
                replace(board, 0, j, 'O', 'W', list);
            }
            if (board[m - 1][j] == 'O') {
                replace(board, m - 1, j, 'O', 'W', list);
            }
        }

        // 遍历全局,找出所有的剩余的‘O’替换为‘X’
        for (int i = 1; i < m - 1; i++) {
            for (int j = 1; j < n - 1; j++) {
                if (board[i][j] == 'O') {
                    board[i][j] = 'X';
                }
            }
        }

        // 恢复‘W’为‘O’
        for (Pair<Integer, Integer> pair : list) {
            board[pair.getKey()][pair.getValue()] = 'O';
        }

    }

    // 替换与边界相连的O为W
    private void replace(char[][] board, int row, int col, char replace, char target, List<Pair<Integer, Integer>> list) {
        if (row < 0 || row >= board.length || col < 0 || col >= board[0].length) {
            return;
        }

        if (board[row][col] == replace) {
            list.add(new Pair<Integer, Integer>(row, col));
            board[row][col] = target;
            for (int[] direction : directions) {
                replace(board, row + direction[0], col + direction[1], replace, target, list);
            }
        }
    }
}

LeetCode 673. Number of Longest Increasing Subsequence

Problem

LeetCode 673. Number of Longest Increasing Subsequence

1. 题目简述

给出一个未排序的整形数组,返回其最长递增系序列的数量。例如:

Example 1:
Input: [1,3,5,4,7]
Output: 2
Explanation: The two longest increasing subsequence are [1, 3, 4, 7] and [1, 3, 5, 7].

Note:

数据规模不会超过2000且保证最终的解是32位整形数

2. 算法思路

相关问题:

  1. LeetCode 300. Longest Increasing Subsequence

和300题不同的是,这里我们需要返回的是数量,而不是长度,也就是说仅仅找出长度已经无法满足我们的需求,我们不仅需要记录长度,还需要记录数量。

另一种思路是使用segement tree,暂时还没学到那块,等学到了在回头看!!!

动态规划

我们需要记录的有两个数值,到当前数字截止的最长的递增子序列长度和数量。因此,这是一个二维dp问题。对于300题中的第二种解法我们可以抛弃了,因为它只能找出长度,而无法找到数量。

因此,我们记录一个二维dp数组,每一维数组都有两个值,一个是长度,一个是数量。

当我们向前一个个查找的时候,记录最长的递增子序列的数量。

时间复杂度为O(n ^ 2),因为每次都要去找前面所有的数字,很慢。

class Solution {
    public int findNumberOfLIS(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }

        // dp[i][0]表示到当前的最长长度,dp[i][1]表示当前最长长度的数量;初始化dp[0][0] = 0, dp[0][1] = 1。
        int n = nums.length;
        int[][] dp = new int [n][2];
        for (int i = 0; i < n; i++) {
            dp[i][0] = 1;
        }
        dp[0][1] = 1;

        int maxLength = 1;
        for (int i = 1; i < n; i++) {
            // 这里需要注意!!!count需要初始化为1,因为假设说前面都没有比它小的,那么它就不会进入循环,它自己本身就是一个序列。
            int count = 1, longestLength = 1;
            for (int j = 0; j < i; j++) {
                // 只有当前面数字比当前数字小的时候才会进行判断
                if (nums[j] < nums[i]) {
                    if (longestLength < dp[j][0] + 1) {
                        count = dp[j][1];
                        longestLength = dp[j][0] + 1;
                    } else if (longestLength == dp[j][0] + 1) {
                        count += dp[j][1];
                    }
                }
            }
            dp[i][0] = longestLength;
            dp[i][1] = count;
            maxLength = Math.max(maxLength, longestLength);
        }

        // 根据最长长度遍历dp数组,记录总的最长长度数量
        int res = 0;
        for (int i = 0; i < n; i++) {
            if (dp[i][0] == maxLength) {
                res += dp[i][1];
            }
        }

        return res;
    }
}

LeetCode 304. Range Sum Query 2D - Immutable

Problem

LeetCode 304. Range Sum Query 2D - Immutable

1. 题目简述

给出一个二维矩阵,矩阵中每个元素都是整数。然后给出一个左上角和右下角(保证合法),计算给出的围起来的长方形的值。例如:

Example:
Given matrix = [
[3, 0, 1, 4, 2],
[5, 6, 3, 2, 1],
[1, 2, 0, 1, 5],
[4, 1, 0, 1, 7],
[1, 0, 3, 0, 5]
]

sumRegion(2, 1, 4, 3) -> 8
sumRegion(1, 1, 2, 2) -> 11
sumRegion(1, 2, 2, 4) -> 12

Note:

1. sumRegion函数会被call很多次;

2. 假设矩阵不变;

2. 算法思路

相关问题:

  1. LeetCode 308. Range Sum Query 2D - Mutable(未做——线段树)
  2. LeetCode 303. Range Sum Query - Immutable

这道题比起dp问题更像是一个数学问题。

DP(Math方法)

其实有点像求面积,用图来画一下会比较直白。我们目标就是绿色部分的面积(或者说是数字和),根据(row1, col1)和(row2, col2)我们可以清楚地知道四个部分的面积,相信一眼就能看出来这里怎么算了,我们用S(row, col)来表示从(0, 0)点到(row, col)的面积大小:

$$Target = S(row2, col2) - S(row1, col2) - S(row2, col1) + S(row1, col1)$$

时间复杂度:O(m * n),初始化时候的复杂度,其他操作为O(1)。

另外,下面的写法欧典复杂了,判断了很多边界情况,其实可以做padding的,第二种写法是solution里做了padding的写法,更加简洁,可以参考一下。

class NumMatrix {

    int[][] dp;
    int m = 0, n = 0;
    public NumMatrix(int[][] matrix) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return;
        }
        m = matrix.length;
        n = matrix[0].length;

        dp = new int[m][n];
        int sum = 0;
        for (int i = 0; i < n; i++) {
            sum += matrix[0][i];
            dp[0][i] = sum;
        }

        for (int i = 1; i < m; i++) {
            sum = 0;
            for (int j = 0; j < n; j++) {
                sum += matrix[i][j];
                dp[i][j] = sum + dp[i - 1][j];
            }
        }
    }

    public int sumRegion(int row1, int col1, int row2, int col2) {
        int sum1 = 0, sum2 = 0, sum3 = 0, sum4 = dp[row2][col2];
        // sum1是左上角,sum2是上方,sum3是左侧。
        if (row1 == 0 && col1 == 0) {
            sum1 = 0;
            sum2 = 0;
            sum3 = 0;
        } else if (row1 == 0 && col1 != 0) {
            sum1 = 0;
            sum2 = 0;
            sum3 = dp[row2][col1 - 1];
        } else if (row1 != 0 && col1 == 0 ) {
            sum1 = 0;
            sum2 = dp[row1 - 1][col2];
            sum3 = 0;
        } else {
            sum1 = dp[row1 - 1][col1 - 1];
            sum2 = dp[row1 - 1][col2];
            sum3 = dp[row2][col1 - 1];
        }

        return sum4 - sum2 - sum3 + sum1;
    }
}

/**
 * Your NumMatrix object will be instantiated and called as such:
 * NumMatrix obj = new NumMatrix(matrix);
 * int param_1 = obj.sumRegion(row1,col1,row2,col2);
 */

DP(做padding,简单写法)

class NumMatrix {
    private int[][] dp;

    public NumMatrix(int[][] matrix) {
        if (matrix.length == 0 || matrix[0].length == 0) return;
        dp = new int[matrix.length + 1][matrix[0].length + 1];
        for (int r = 0; r < matrix.length; r++) {
            for (int c = 0; c < matrix[0].length; c++) {
                dp[r + 1][c + 1] = dp[r + 1][c] + dp[r][c + 1] + matrix[r][c] - dp[r][c];
            }
        }
    }

    public int sumRegion(int row1, int col1, int row2, int col2) {
        return dp[row2 + 1][col2 + 1] - dp[row1][col2 + 1] - dp[row2 + 1][col1] + dp[row1][col1];
    }
}

LeetCode 279. Perfect Squares

Problem

LeetCode 279. Perfect Squares

1. 题目简述

给出一个正整数n,找出最少的能够组成n的完全平方数的个数。例如:

Example 1:
Input: n = 12
Output: 3 
Explanation: 12 = 4 + 4 + 4.

Example 2:
Input: n = 13
Output: 2
Explanation: 13 = 4 + 9.

Note:

最坏情况就是全都由1来组成,例如 3 = 1 + 1 + 1

2. 算法思路

参考链接:LeetCode 5种解法

其实讲道理,这道题的解法看得我头昏眼花,这里暂时只讲dp解法,关于greedy,BFS和math方法,后面有空再看,待完善。

DP

首先,我们先计算sqrt(n),向下取整为x,那么1到x都是我们的备选区间,然后我们从1到x进行遍历,找到最少的需要的数量(从后向前遍历不一定是最小值,例如:12 = 9 + 1 + 1 + 1,但是最少的是12 = 4 + 4 + 4)。我们用一个dp数组来记录当前的最小值即可,减少遍历次数。

时间复杂度为O(n * 根号n)。

class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n + 1];
        findNumSquares(n, dp);

        return dp[n];
    }

    private int findNumSquares(int n, int[] dp) {
        if (dp[n] != 0) {
            return dp[n];
        }

        if (n <= 1) {
            dp[n] = n;
            return n;
        }

        int res = n;
        // dp值为0,说明从未找过;dp值大于0,即为解;最差情况就全是1喽
        int max = (int)Math.floor(Math.sqrt((double)n));
        for (int i = 1; i <= max; i++) {
            res = Math.min(res, findNumSquares(n - i * i, dp) + 1);
        }

        dp[n] = res;

        return res;
    }
}

LeetCode 221. Maximal Square

Problem

LeetCode 221. Maximal Square

1. 题目简述

给出一个二维矩阵由0和1组成,找出其中最大的由1组成的最大正方形的面积。例如:

Input: 

1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0

Output: 4

2. 算法思路

相关问题:

  1. LeetCode 85. Maximal Rectangle

这道题的变种,85题和这道题的思路完全不同,强烈建议重新看一下。而且85题的dp思路极其难懂,用的方法很不一样。

DP

其实这道题也是用画图的方式来解决的。其实让我们找出最大的正方形面积,其实也就是找到最大的正方形边长。我们用一个dp值来记录,以当前位置为右下角的最大的正方形边长。假如说我们遍历到某个点(i, j)为1时(为0的时候不可能为正方形),那么以它为右下角的正方形的边长满足如下递推式:

$$dp[i][j] = min (dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1$$

记录全局的最大边长。遍历结束后计算出面积即可。

class Solution {
    public int maximalSquare(char[][] matrix) {
        if (matrix.length == 0 || matrix[0].length == 0) {
            return 0;
        }

        // dp[i][j]表示以[i][j]为右下角的正方形最大的边长,记得找一个全局最长的边即可。
        // 这里dp可以转化为一维!
        int m = matrix.length, n = matrix[0].length, maxEdge = 0;
        int[][] dp = new int[m + 1][n + 1];

        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (matrix[i - 1][j - 1] == '1') {
                    dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
                    maxEdge = Math.max(dp[i][j], maxEdge);
                }
            }
        }

        return maxEdge * maxEdge;
    }
}

LeetCode 85. Maximal Rectangle

DFS:
LeetCode 51 N-Queens
LeetCode 37. Sudoku Solver
LeetCode 46. Permutations
LeetCode 47. Permutations II
LeetCode 39. Combination Sum
LeetCode 40. Combination Sum II

树:
LeetCode 145. Binary Tree Postorder Traversal (循环写法,复习一下直接抄即可)
LeetCode 105. Construct Binary Tree from Preorder and Inorder Traversal
LeetCode 102. Binary Tree Level Order Traversal
LeetCode 236. Lowest Common Ancestor of a Binary Tree
LeetCode 124. Binary Tree Maximum Path Sum
LeetCode 297. Serialize and Deserialize Binary Tree

dp:
LeetCode 121. Best Time to Buy and Sell Stock
LeetCode 122. Best Time to Buy and Sell Stock II
LeetCode 123. Best Time to Buy and Sell Stock III
LeetCode 188. Best Time to Buy and Sell Stock IV
LeetCode 139. Word Break
LeetCode 174. Dungeon Game
LeetCode 198. House Robber
LeetCode 213. House Robber II
LeetCode 322. Coin Change
LeetCode 1269. Number of Ways to Stay in the Same Place After Some Steps

二分:
LeetCode 33. Search in Rotated Sorted Array
LeetCode 74. Search a 2D Matrix
LeetCode 240. Search a 2D Matrix II
LeetCode 155. Min Stack

单调栈
LeetCode 84. Largest Rectangle in Histogram
LeetCode 496. Next Greater Element I
LeetCode 503. Next Greater Element II

单调队列
LeetCode 239. Sliding Window Maximum

其他经典题目:
LeetCode 31. Next Permutation

.etc

LeetCode 378. Kth Smallest Element in a Sorted Matrix

Problem

LeetCode 378. Kth Smallest Element in a Sorted Matrix

1. 题目简述

给出一个m * n大小的矩阵,找出其第k大的数:

  1. 每一行从左到右都是递增的整形数
  2. 每一列从上至下都是递增的整形数

例如:

matrix = [
[ 1,  5,  9],
[10, 11, 13],
[12, 13, 15]
],
k = 8,

return 13.

2. 算法思路

相关问题:

  1. LeetCode 74. Search a 2D Matrix
  2. LeetCode 240. Search a 2D Matrix II

这道题和LeetCode 240. Search a 2D Matrix II特别像,基本上是一个模子里出来的,只不过这个更麻烦一些,它并不是去找某个数那样明确的目标,而是一个比较间接的值,需要做一些小改变。

按照我们正常的思路,start和end分别是左上角和右下角,但是我们的mid并不一定在matrix中存在,我们也不能用mid + 1或者mid - 1来更新start和end。因此,我们要做的有两件事:
(1)记录全局数组中小于等于mid值的个数。

(2)需要更新的值,也就是比mid小的最大值和比mid大的最小值。

二分搜索

我们还需要注意一点,数组中可能存在重复的值,就像例子里那样,13是我们的target,但是13有两个,也就是说我们找第7小的数和第8小的数都是13,如果我们找第7个,那么当我们遍历到13的时候,我们得到的个数永远都是8,8 > 7,然后我们应该让end减小,但是小于等于13的最大的数还是13,因为我们这里有两个13,就无限循环了,start是13,end也是13.

所以,我们要注意的点就是二分搜索的循环条件,这里如果l == r,则说明当前值即为所求,不要再继续循环了。这一点一定要注意!

class Solution {
    public int kthSmallest(int[][] matrix, int k) {
        // 由于横竖都有序,所以用二分搜索继续查找也是有一定规律的,我们可以从右上或者左下进行查找,可以记一个小tips。smallLargeNumber用于记录小于middle的最大值和大于middle的最小值。
        int n = matrix.length, l = matrix[0][0], r = matrix[n - 1][n - 1];

        // 注意这里的循环结束条件,不是l <=r
        while (l < r) {
            int m = l + (r - l) / 2;
            int[] smallLargeNumber = new int[2];
            int count = countLessEqual(matrix, m, smallLargeNumber);
            if (count < k) {
                l = smallLargeNumber[1];
            } else if (count > k) {
                r = smallLargeNumber[0];
            } else {
                return smallLargeNumber[0];
            }
        }

        return l;
    }

    private int countLessEqual(int[][] matrix, int middle, int[] smallLargeNumber) {
        int n = matrix.length;
        int count = 0, row = 0, col = n - 1;

        smallLargeNumber[0] = Integer.MIN_VALUE;
        smallLargeNumber[1] = Integer.MAX_VALUE;
        while (row < n && col >= 0) {
            if (matrix[row][col] > middle) {
                smallLargeNumber[1] = Math.min(smallLargeNumber[1], matrix[row][col]);
                col--;
            } else {
                smallLargeNumber[0] = Math.max(smallLargeNumber[0], matrix[row][col]);
                count += col + 1;
                row++;
            }
        }

        return count;
    }
}

LeetCode 875. Koko Eating Bananas

Problem

LeetCode 875. Koko Eating Bananas

1. 题目简述

珂珂喜欢吃香蕉。这里有 N 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 H 小时后回来。

珂珂可以决定她吃香蕉的速度 K (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 K 根。如果这堆香蕉少于 K 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。  

珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。

返回她可以在 H 小时内吃掉所有香蕉的最小速度 K(K 为整数)。

示例 1:
输入: piles = [3,6,7,11], H = 8
输出: 4

示例 2:
输入: piles = [30,11,23,4,20], H = 5
输出: 30

示例 3:
输入: piles = [30,11,23,4,20], H = 6
输出: 23

Note:

1 <= piles.length <= 10^4

piles.length <= H <= 10^9

1 <= piles[i] <= 10^9

2. 算法思路

相关问题:

  1. LeetCode 1011. Capacity To Ship Packages Within D Days(无解析)

这道题和1011特别像,但是又有所不同,看完解析后建议再去做一下1011这道题目。

这道题目的核心点在于找到一个合适的值去满足条件,而不是直接通过数值对比。也就是说我们表面上是对每小时吃多少香蕉速度K做二分搜索,但是实际作比较的却是时间,注意其中的转换关系。

而且,要注意能否进行切割!什么意思呢?拿示例3为例,如果K = 23,第一个小时可以不都吃完,只吃23个,剩下的7个下个小时再吃。对于1011题就完全不同,1011题里每个package不能进行切割。这里区别很大,要注意。

二分搜索

那么我们二分搜索的起始和结束分别是多少呢?start很明显,一个小时最少吃1根,因此start = 1;我们需要注意题里说的一点,如果当前小时的香蕉吃完了,那么就不再继续吃了,所以end的最大值就是piles[i]的最大值,也就是10 ^ 9。

class Solution {
    public int minEatingSpeed(int[] piles, int H) {
        int l = 1, r = 1000000000;

        while (l <= r) {
            int m = l + (r - l) / 2;
            int time = getHours(piles, m);
            if (time <= H) {
                r = m - 1;
            } else {
                l = m + 1;
            }
        }

        return l;
    }

    private int getHours(int[] piles, int speed) {
        int hours = 0;
        for (int pile : piles) {
            hours += (pile - 1) / speed + 1;
        }
        return hours;
    }
}

LeetCode 719. Find K-th Smallest Pair Distance

Problem

LeetCode 719. Find K-th Smallest Pair Distance

1. 题目简述

给出一个未排序的整形数组,返回其最长递增子序列的长度。

Input: [10,9,2,5,3,7,101,18]
Output: 4 
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4. 

Note:

只需要长度,而不是具体的串

time complexity要在O(n ^ 2)以下

2. 算法思路

相关问题:

  1. 673. Number of Longest Increasing Subsequence

首先要注意我们只需要返回长度即可,这里不需要考虑找出最长的那个子串,只要找到该串的长度即可。

暴力搜索

我们先来看看最暴力的思路,我们对每个节点记录一个dp值,该值表示从0到当前位置的数字中,以当前数字为结尾的最长递增子序列的长度。每次新到达一个数字后,找到其前面的所有数字中,比它小的所有数字中dp值最大的数字(不能等于,等于就不叫递增了),找出该节点的dp值,加一,即为当前节点的dp值。

$$dp[i] = max(dp[i], dp[j] + 1) (for j in 0 to n - 1 and nums[j] < nums[i])$$

时间复杂度为O(n ^ 2),因为每次都要去找前面所有的数字,很慢。

class Solution {
    public int lengthOfLIS(int[] nums) {
        if (nums.length == 0) {
            return 0;
        }

        int[] dp = new int[nums.length];
        Arrays.fill(dp, 1);

        int ret = 0;

        for (int i = 0; i < nums.length; i++) {
            int maxDP = 0;
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i] && dp[j] > maxDP) {
                    maxDP = dp[j];
                }
            }
            dp[i] = maxDP + 1;
            ret = Math.max(dp[i], ret)
        }

        return ret;
    }
}

LeetCode 240. Search a 2D Matrix II

Problem

LeetCode 240. Search a 2D Matrix II

1. 题目简述

给出一个m * n大小的矩阵,以及一个target,返回该矩阵中是否存在该数字。该矩阵有如下两条性质:

  1. 每一行从左到右都是递增的整形数
  2. 每一列从上至下都是递增的整形数

例如:

Consider the following matrix:

[
[1,   4,  7, 11, 15],
[2,   5,  8, 12, 19],
[3,   6,  9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
Given target = 5, return true.

Given target = 20, return false.

2. 算法思路

相关问题:

  1. LeetCode 74. Search a 2D Matrix
  2. LeetCode 378. Kth Smallest Element in a Sorted Matrix

这是一类很特殊的二分查找的题目,建议背下来,它并不是全局有序,而是拥有一些有序的特性,总结为下面两点:

  1. 矩阵的左上角的值全局最小;右下角的值全局最大;
  2. 对于每个L型的路径,其都是有序的,例如第一列和最后一行构成的L型或者第一行和最后一列构成的L型。

那么利用这些性质,我们应该如何去做呢?根据二分查找,我们将start设置为matrix[0][0],end设为matrix[m - 1][n - 1](m、n为行数和列数)。然后我们遍历的起点要从右上角或者左下角开始,每次舍弃一行或者一列

二分搜索

注意一下这里的起点和终点即可。

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        //这个的规律在于,第一行与最后一列连在一起是有序的,从右上角出发,如果这个数比target大,那么它就不可能在第一行,如果这个数比target小,那么它就不可能在最后一列。
        int m = matrix.length, n = 0;
        if (m != 0) {
            n = matrix[0].length;
        }
        if (m == 0 || n == 0 || target < matrix[0][0] || target > matrix[m - 1][n - 1]) {
            return false;
        }

        int i = 0, j = n - 1;
        while (i < m && j >= 0) {
            if (target < matrix[i][j]) {
                j--;
            } else if (target > matrix[i][j]) {
                i++;
            } else {
                return true;
            }
        }

        return false;
    }
}

LeetCode 300. Longest Increasing Subsequence

Problem

LeetCode 300. Longest Increasing Subsequence

1. 题目简述

给出一个未排序的整形数组,返回其最长递增子序列的长度。

Input: [10,9,2,5,3,7,101,18]
Output: 4 
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4. 

Note:

只需要长度,而不是具体的串

time complexity要在O(n ^ 2)以下

2. 算法思路

相关问题:

  1. 673. Number of Longest Increasing Subsequence

首先要注意我们只需要返回长度即可,这里不需要考虑找出最长的那个子串,只要找到该串的长度即可。

暴力搜索

我们先来看看最暴力的思路,我们对每个节点记录一个dp值,该值表示从0到当前位置的数字中,以当前数字为结尾的最长递增子序列的长度。每次新到达一个数字后,找到其前面的所有数字中,比它小的所有数字中dp值最大的数字(不能等于,等于就不叫递增了),找出该节点的dp值,加一,即为当前节点的dp值。

$$dp[i] = max(dp[i], dp[j] + 1) (for j in 0 to n - 1 and nums[j] < nums[i])$$

时间复杂度为O(n ^ 2),因为每次都要去找前面所有的数字,很慢。

class Solution {
    public int lengthOfLIS(int[] nums) {
        if (nums.length == 0) {
            return 0;
        }

        int[] dp = new int[nums.length];
        Arrays.fill(dp, 1);

        int ret = 0;

        for (int i = 0; i < nums.length; i++) {
            int maxDP = 0;
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i] && dp[j] > maxDP) {
                    maxDP = dp[j];
                }
            }
            dp[i] = maxDP + 1;
            ret = Math.max(dp[i], ret)
        }

        return ret;
    }
}

二分查找(直接背的最优解)

这道题二分查找的思路特别难想,建议直接背下来。思路如下:

  1. 我们维护一个数组,初始化为长度为nums.length,首个元素为nums[0],记录size = 1;
  2. 然后向后遍历每一个数字,二分法找到在当前数组中它应该在的位置pos(如果比当前末尾的数要大,那么就是在末尾后面一位;如果比第一个数要小,那么位置就是0);
  3. 替换掉pos位置的数字。判断:如果pos == size,那么size++;
  4. 直至循环结束,最终的size就是我们的大小。

Attention:这里只有size是我们想要的,而我们维护的那个数组并不一定是!!!

那么为什么是这样呢?size是我们想要的我们这点很清楚,因为对于任何一个数字而言,它只有两种可能性:(1)替换掉当前数组中某个数字(2)将数字加到末尾的后一位,size++。

如果替换掉当前数组中的任何一个,其实并不会改变当前size的大小。如果在末尾添加一个比较大的数,size也的确是增加了,不会存在任何影响,那么问题就在于我们为什么要替换掉数字呢?其实是为了避免下面这种情况,我们举一个极端的例子。

nums = [20, 100, 2, 3, 4, 5, 10, 80],很明显,最终答案应该是6,如果我们不进行替换,那么我们就会发现从2以后好几个数字完全插不进去整个数组,因为他们都太小了,着很明显不是我们想要的,所以只有替换了,才能保证我们对于后面的查找判断不受影响。

那么为什么我们最后得到的不一定是我们最长的子序列呢?那是因为我们不一定能够替换完全。什么意思呢?我们再举个例子:nums = [20, 100, 2]和nums = [20, 100, 2, 3],是不是好像明白了些什么?

其实,我们的目的是保证我们当前维护的这个最长递增序列的每个数的值尽可能保持小,如果后面有更小的序列我们就替换掉它,但是有可能在替换的过程中值替换了一半,所以并不保证最终得到的就是一个合法的最长递增子序列。

class Solution {
    public int lengthOfLIS(int[] nums) {
        if (nums.length == 0) {
            return 0;
        }
        int[] arr = new int[nums.length];
        int size = 1;
        arr[0] = nums[0];

        for (int num : nums) {
            // 找到num的位置,二分法找到比num大的最小的数。而且这里注意闭区间r和size的关系。
            int l = 0, r = size - 1;
            while (l <= r) {
                int m = l + (r - l) / 2;
                if (arr[m] >= num) {
                    r = m - 1;
                } else {
                    l = m + 1;
                }
            }
            arr[l] = num;
            if (l == size) {
                size++;
            }
        }

        return size;
    }
}

LeetCode 416. Partition Equal Subset Sum

Problem

LeetCode 416. Partition Equal Subset Sum

1. 题目简述

给出一个非空的正整数数组,判断该数组是否能一分为二,且两个子集的数值和相等。例如:

Input: [1, 5, 11, 5]

Output: true

Explanation: The array can be partitioned as [1, 5, 5] and [11].

2. 算法思路

相关问题:

  1. LeetCode 698. Partition to K Equal Sum Subsets(待做)

经典的子集背包问题(后面抽空会写一篇背包问题总结),完全背包问题是LeetCode 518. Coin Change 2

DP

我们首先要判断数组和是否是偶数,如果是,则OK,我们可以继续往下找;如果是基数,则pass。

然后我们算出的target = sum / 2。也就是说我们需要从数组中找出一个子集,使得其和为target。

我们写dp方程的时候先来判断一下我们有多少个变量,一个是数字个数,另一个是target大小,因此我们推测这是一个二维dp。dp[i][j]为前i个数,和为j的可能性,true或者false。

那么我们可以得出如下递推式:

$$dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]]$$

而且,我们可以知道的是每一次计算dp值的时候,只依赖于上一次的结果,因此我们可以将空间复杂度降到一维。

时间复杂度:O(n ^ 2)。因为target为 n / 2,需要遍历从0到target。

空间复杂度:O(n)。n为nums长度。

Attention:如果是使用一维数组进行dp的话,target的遍历要从后向前,因为它会依赖上一次计算前面的dp值,如果从前向后遍历,则会覆盖掉。

class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }

        // 如果和为奇数,pass
        if (sum % 2 == 1) {
            return false;
        }

        int target = sum / 2;
        boolean[] dp = new boolean[target + 1];
        dp[0] = true;

        for (int i = 1; i <= nums.length; i++) {
            for (int j = target; j > 0; j--) {
                if (j - nums[i - 1] >= 0) {
                    // 这里的dp[j]就是上一次的i-1的dp[j]
                    dp[j] = dp[j] || dp[j - nums[i - 1]];
                } else {
                    dp[j] = dp[j];
                }
            }
        }

        return dp[target];
    }
}

LeetCode 213. House Robber II

Problem

LeetCode 213. House Robber II

1. 题目简述

一个专业的抢到想要洗劫一个街区的商户,每个商户有不同数额的金钱,该街区是环形排列的(第一户和最后一户相连),当抢劫连续两个商户时,就会触发报警。问在不触发报警的情况下,如何抢劫能使获得的钱数最多,找出那个最大金额。例如:

Example 1:

Input: [2,3,2]
Output: 3
Explanation: You cannot rob house 1 (money = 2) and then rob house 3 (money = 2), because they are adjacent houses.

Example 2:

Input: [1,2,3,1]
Output: 4
Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3). Total amount you can rob = 1 + 3 = 4.

2. 算法思路

相关题目:

  1. LeetCode 198. House Robber
  2. LeetCode 337. House Robber III(待完成)
  3. paint house系列(例如:Post not found: LeetCode-1473-Paint-House-III LeetCode 1473. Paint House III(待完成)

动态规划

和House Robber 1(解析见动态规划一的例题)不同的是,这次首尾是相连的。也就是说如果按照1中的做法,找出来的最优解中包含了第一户和最后一户,那么这个就是一种不合法的最大值。

所以,我们的做法是如果选择了第一户,不要选择最后一户不就好了?反之亦然,所以说我们计算两个最大值,一个是从第1户到第n - 1户,另一个是从第2户到第n户,取二者的最大值即可。

我们假设F(i, j)计算的是从第i户到第j户的能够抢的最大值,那么有。

$$F(1, n) = max(F(1, n - 1), F(2, n))$$

至于F的求法,和之前一样:

$$ F(n) = Math.max(F(n - 1), F(n - 2) + nums[n]) (F(1) = nums[1], F(2) = Math.max(nums[1], nums[2])) $$


class Solution {
    public int rob(int[] nums) {
        if (nums.length == 0) {
            return 0;
        } else if (nums.length == 1) {
            return nums[0];
        }

        return Math.max(robFromTo(nums, 0, nums.length - 2), robFromTo(nums, 1, nums.length - 1));
    }

    private int robFromTo(int[] nums, int start, int end) {
        if (end - start == 0) {
            return nums[start];
        } else if (end - start == 1) {
            return Math.max(nums[start], nums[end]);
        }

        int pre = nums[start], cur = Math.max(nums[start], nums[]start + 1);
        for (int i = start + 2; i <= end; i++)
            int temp = cur;
            cur = Math.max(pre + nums[i], cur);
            pre = temp;
        }

        return cur;
    }
}

LeetCode 399. Evaluate Division

Problem

LeetCode 399. Evaluate Division

1. 题目简述

给出一系列等式,例如”A / B = k”,A和B都是字符串形式的字母,k是一个double数字。然后给出一些要求计算的等式,如果能根据已知的等式求出来,则计算,不能则返回-1.0。例如:

Example:
Given a / b = 2.0, b / c = 3.0.
queries are: a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ? .
return [6.0, 0.5, -1.0, 1.0, -1.0 ].

上面的例子给出的输入是如下所示:

equations = [ ["a", "b"], ["b", "c"] ],
values = [2.0, 3.0],
queries = [ ["a", "c"], ["b", "a"], ["a", "e"], ["a", "a"], ["x", "x"] ]. 

2. 算法思路

参考视频:花花酱LeetCode

经典老题目,可以用DFS也可以用union-find。

Graph + DFS

我们想一下,如果说我们将给出的所有等式当做一个图,对于每个query,我们对于这两个数的商的计算其实就是找一下这两个数之间是否存在一条路径,如果找到一条路径,则说明二者相除有结果;如果没有,则为-1。

时间复杂度:O(e + q * e),q是query的数量,e是equation数量。首次生成graph的时候是O(e)的复杂度,q * e是所有query的复杂度,因此每次都要重新做DFS。

空间复杂度:O(e),就是存储整张图的graph大小。

class Solution {
    public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
        Map<String, Map<String, Double>> graph = new HashMap();
        buildGraph(equations, values, graph);

        double[] res = new double[queries.size()];
        for (int i = 0; i < queries.size(); i++) {
            res[i] = getResult(queries.get(i).get(0), queries.get(i).get(1), new HashSet<String>(), graph);
        }

        return res;
    }

    // s1是除数,s2是被除数,找s1/s2之间的一条路径,并将所有double值相乘
    private double getResult(String s1, String s2, Set<String> visited, Map<String, Map<String, Double>> graph) {
        if (!graph.containsKey(s1) || !graph.containsKey(s2) || visited.contains(s1)) {
            return -1.0d;
        }
        if (s1.equals(s2)) {
            return 1.0d;
        }
        // 开始dfs
        visited.add(s1);
        for (Map.Entry<String, Double> nei: graph.get(s1).entrySet()) {
            if (nei.getKey().equals(s2)) {
                return nei.getValue();
            }
            double temp = getResult(nei.getKey(), s2, visited, graph);
            if (temp != -1.0) {
                return temp * nei.getValue();
            }
        }

        return -1.0d;
    }

    private void buildGraph(List<List<String>> equations, double[] values, Map<String, Map<String, Double>> graph) {
        for (int i = 0; i < equations.size(); i++) {
            String s1 = equations.get(i).get(0), s2 = equations.get(i).get(1);
            if (!graph.containsKey(s1)) {
                graph.put(s1, new HashMap<String, Double>());
            }
            if (!graph.containsKey(s2)) {
                graph.put(s2, new HashMap<String, Double>());
            }
            // value为0怎么办?
            graph.get(s1).put(s2, values[i]);
            graph.get(s2).put(s1, 1 / values[i]);
        }
    }
}

Union-Find(待完成)

上面的时间复杂度其实可以再往下降一些的,因为每次都用DFS其实某种程度上很累赘。如果我们使用Union-Find,每个字母和根节点parent相连,每次计算只需要判断二者是否在同一个森林里,如果再,则只需要计算一下每个节点与根节点相除的值,然后二者相除即可;不在同一个森林则是-1.0。

class Solution {

    public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {

    }
}

Union-Find总结

Union-Find是什么

详情参考:

  1. labuladong的算法小抄——Union-Find算法详解
  2. labuladong的算法小抄——Union-Find算法应用
  3. 花花酱的Union-Find视频

Union-Find其实是一种解决图的连通性问题的算法。或者说解决某张图里连通分量的问题。比如说最简单的Number of Islands,就是查找图里有多少个连通分量。

其实并查集的问题并不多,而且绝大多数并查集用DFS也都能够解决,union-find主要是一种思想,某些情况下会有不错的效果。

Union-Find基础思路

思想推导

基本操作Union与Find

这里推导只是简单的文字描述,作图的话时间不太充沛,以后有机会再补,觉得看不懂可以参考第一节里的参考资料,里面有很详细的分析。

首先,我们假设有n个独立的节点,我们定义每个节点i存在它的parent[i],初始化为自己本身。

然后我们定义两种操作,Union和Find。

  1. Union的过程就是将两个节点连接起来的过程,如果我们Union(a, b),那么我们就将a的parent节点设置为b。
  2. Find的过程就是找到某节点的root节点,root节点就是一级一级向上找,直到找到某个节点的parent节点是本身,该节点就是root节点。

优化

Path Regression(Find优化)

在find的时候,每次找到root节点时,可以将路径上所有节点的parent全部都指向root节点,这样的话可以保证树的高度不会很高,每次查询都可降低后续查询的复杂度。

Merge Optimization(Union优化)

在Union的时候,我们希望把比较小的树连接到比较大的树上,这样整棵树的高度不会更高。因此我们需要一个size,来记录每棵树的大小。

基础代码


class UF {
    int[] parent; // 每个连通分量的父节点
    int[] size; // 每个连通分量的大小
    int count; // 当前有多少个连通分量

    public UF (int n) {
        parent = new int[n];
        size = new int[n];
        count = n;

        for (int i = 0; i < n; i++) {
            parent[i] = i;
            size[i] = 1;
        }
    }

    // 合并的时候把size小的merge到高的上
    public void union(int a, int b) {
        int rootA = findRoot(a);
        int rootB = findRoot(b);
        if (rootA == rootB) {
            return;
        }

        if (size[rootA] >= size[rootB]) {
            parent[rootB] = rootA;
            size[rootA] += size[rootB];
        } else {
            parent[rootA] = rootB;
            size[rootB] += size[rootA];
        }
        count--;
    }

    // 找到根节点这种写法是将树的高度压缩为3以内
    // private int findRoot(int x) {
    //     while (x != parent[x]) {
    //         parent[x] = parent[parent[x]];
    //         x = parent[x];
    //     }
    //     return x;
    // }
    //这种写法是把树的高度压缩成2,一个根带多个孩子
    public int findRoot(int x) {
        if (x != parent[x]) {
            parent[x] = findRoot(parent[x]);
        }
        return parent[x];
    }
}

拓展:有向图的Union-Find

经典题目就是:LeetCode 685. Redundant Connection II

Union-Find应用题目

  1. LeetCode 547. Friend Circles
  2. LeetCode 399. Evaluate Division
  3. LeetCode 684. Redundant Connection
  4. LeetCode 685. Redundant Connection II
  5. LeetCode 130. Surrounded Regions

LeetCode 518. Coin Change 2

Problem

LeetCode 518. Coin Change 2

1. 题目简述

给出一组不重复的coins,以及目标金额amount,找出所有能够组成目标金额的硬币组合方式的数量。例如:

Example 1:

Input: amount = 5, coins = [1, 2, 5]
Output: 4
Explanation: there are four ways to make up the amount:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

2. 算法思路

动态规划中最经典的背包问题之完全背包问题,可以参考着这篇labuladong大佬的blog来看:传送门

动态规划

经典问题之一的完全背包问题,指的是在不限制每种物品的情况下,一共有多少种可能的组合,使得背包装满。

回溯

那么为什么会想到回溯呢?没有为什么,因为回溯能做出来这种需要遍历所有可能的问题。就这么简单。

对于一个数组A和某个target值,我们希望找出target所有的组合可能,假设F(int[] A, int start, int target)为从数组A中以start为起点找出target的所有可能组合,思路如下。

  1. 对A从小到大进行排序,取A[0]作为第一个数,那么我们当前的目标是F(A, 0, target - A[0]);将其结果和A[0]放在一起作为第一种解。
  2. 然后取A[1]作为第一个数,然后我们目标是F(A, 1, target - A[1])。不从0开始是因为如果取了0,就会和第一步中的某种solution重复,故不取。
  3. 终止条件为target == 0,最终结果添加一条;target < 0,回溯上去继续搞。

LeetCode 322. Coin Change

Problem

LeetCode 322. Coin Change

1. 题目简述

给出一些面值的硬币coins,和目标金额mount,找出最少需要多少枚硬币能够组成amount,如果无法组成,则返回-1。例如:

Example 1:

Input: coins = [1, 2, 5], amount = 11
Output: 3 
Explanation: 11 = 5 + 5 + 1

Example 2:

Input: coins = [2], amount = 3
Output: -1

2. 算法思路

相关问题:

  1. LeetCode 39. Combination Sum
  2. LeetCode 40. Combination Sum II
  3. LeetCode 216. Combination Sum III

经典老题目,我们需要留意的是它和combination sum的区别,combination sum是找具体的组合,而coin change只要找最小的合法组合。因此,这道题我们考虑用dp,而不是回溯法。

那么如何找到最优子问题呢?对于例子中的amount为11,它有几种组合方式:(1)10 + 1 (2)9 + 2 (3)6 + 5,找出这三者中最小的即可。其base case是当amount为0的时候,组合方式有且只有一种。递推式如下:

$$dp[i] = Math.min(dp[i - coin] + 1, dp[i]) (for coin in coins)$$

动态规划(top-down)

初始化的值没必要设置成Integer.MAX_VALUE,最大设置为amount+1即可,因为最多也就是amount个1元硬币。

class Solution {
    // 注意和combination sum的区别,combination sum是要找所有的解决方案,所以采用回溯,这里只要找最少的那个就好了。
    public int coinChange(int[] coins, int amount) {
        int[] dp = new int[amount + 1];

        return getMinCoins(coins, amount, dp, amount + 1);
    }

    private int getMinCoins(int[] coins, int amount, int[] dp, int max) {
        if (amount < 0) {
            return -1;
        } else if (amount == 0) {
            return 0;
        }

        if (dp[amount] != 0) {
            return dp[amount];
        }

        // 这里不用min,直接用dp[amount]直接操作也可以的!!!
        // dp[amount] = max + 1;
        // for (int coin : coins) {
        //     int temp = getMinCoins(coins, amount - coin, dp, max);
        //     dp[amount] = temp == -1 ? dp[amount] : Math.min(dp[amount], temp + 1);
        // }
        //dp[amount] = dp[amount] == max + 1 ? -1 : dp[amount];

        int min = max + 1;
        for (int coin : coins) {
            int temp = getMinCoins(coins, amount - coin, dp, max);
            min = temp == -1 ? min : Math.min(min, temp + 1);
        }

        dp[amount] = min == max + 1 ? -1 : min;
        return dp[amount];
    }
}

动态规划(Bottom-up)

这里我们需要注意的只有边界case,也就是dp[0] = 0,这里要和coin change 2进行区分,那个是求方式,这个是求硬币数,不一样的!!!

class Solution {
    public int coinChange(int[] coins, int amount) {
        int max = amount + 1;
        int[] dp = new int[amount + 1];
        Arrays.fill(dp, max);
        dp[0] = 0;

        for (int i = 1; i <= amount; i++) {
            for (int coin : coins) {
                if (i - coin >= 0) {
                    dp[i] = Math.min(dp[i], dp[i - coin] + 1);
                }
            }
        }

        return dp[amount] >= max ? -1 : dp[amount];
    }
}

LeetCode 47. Permutations II

Problem

LeetCode 47. Permutations II

1. 题目简述

给出一列有重复的数,找出其所有的全排列。例如:

Input: [1,1,2]
Output:
[
[1,1,2],
[1,2,1],
[2,1,1]
]

2. 算法思路

back tracking回溯法的经典题目之一,LeetCode 46. Permutations的进阶版,问题在于我们怎么去应对这个重复数字的问题。

如果按照以往的思路,对于这种有重复数字的,我们首先需要对数组进行排序,然后才能更好地去对重复数字进行处理。

那么我们能否还像permutation
1的时候,交换然后再找全排列么?不行的,这种去重方式的核心思想在于后面不会再出现比当前更小的数字,这么说可能有点抽象,我们来看个例子。

我们给出一个排好序的数组[1, 1, 2, 2, 3],按照如上思路的话:

1. 从第1个数开始,分别和1、2、3、4、5进行替换,然后寻找其后面的全排列;

2. 为了不重复,我们每次(除了首次的自我替换)寻找替换的数字的时候,都要找到和当前不一样的数字为止,例如第1个1,和第2个1就不会进行替换,以此类推;

3. 那么问题会出在哪里呢?当我们需要替换第1个1和末尾的3时,数组变为[3, 1, 2, 2, 1],然后我们找出[1, 2, 2, 1]的全排列;

4. 按照我们的逻辑,1先和第1个2替换,然后跳过第2个2,然后判断到了第2个1,第2个1和它之前的数字2是不同的,因此我们再次替换,到这里我们就会发现出现了重复,明明1和1不应该再替换的,在这里却被再次替换,所以会多出很多重复。

因此我们去重的方式其实可以更简单一点,用hashset来判断是之前是否和同样的元素交换过就可以了,而且原始数组不需要排序也可以。

回溯法

注意这里去重的方式和subset 2以及combination sum
2的去重方式完全不同,具体原因在上面已经详细描述了,建议自己尝试写一下,这里把错误的代码放在文章末尾,大家有兴趣可以自己debug一下,看看哪里出了问题。

这里我们使用hashset来去重。

class Solution {
    public List<List<Integer>> permuteUnique(int[] nums) {
        List<List<Integer>> res = new ArrayList();

        if (nums.length == 0) {
            return res;
        }

        getPermutations(nums, 0, res);

        return res;
    }

    private void getPermutations(int[] nums, int start, List<List<Integer>> res) {
        if (start == nums.length) {
            List<Integer> solution = new ArrayList();
            for (int num : nums) {
                solution.add(num);
            }
            res.add(solution);
            return;
        }

        Set<Integer> set = new HashSet();
        for (int i = start; i < nums.length; i++) {
            // 如果set中成功添加了num,则说明当前数字尚未和该数字交换过
            if (set.add(nums[i])) {
                swap(nums, start, i);
                getPermutations(nums, start + 1, res);
                swap(nums, start, i);
            }
        }
    }

    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

错误版本的代码,警戒

class Solution {
    public List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums);
        List<List<Integer>> res = new ArrayList();

        if (nums.length == 0) {
            return res;
        }

        getPermutations(nums, 0, res);

        // debug专用,找重复
        // Set<List<Integer>> set = new HashSet();
        // List<List<Integer>> newRes = new ArrayList();

        // for (List<Integer> solution : res) {
        //     if (!set.add(solution)) {
        //         newRes.add(solution);
        //     }
        // }

       return res;
    }

    private void getPermutations(int[] nums, int start, List<List<Integer>> res) {
        if (start == nums.length) {
            List<Integer> solution = new ArrayList();
            for (int num : nums) {
                solution.add(num);
            }
            res.add(solution);
            return;
        }

        for (int i = start; i < nums.length; i++) {
            //这里的去重不行,有很多重复的case,这种写法对于[1,1,2,2,3]没问题,但是对于[0,0,1,1,2,2,3]有问题,自己可以尝试一下。
            if (i > start && (nums[i] == nums[i - 1] || nums[start] == nums[i])) {
                continue;
            }
            swap(nums, start, i);
            getPermutations(nums, start + 1, res);
            swap(nums, start, i);
        }
    }

    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

LeetCode 46. Permutations

Problem

LeetCode 46. Permutations

1. 题目简述

给出一列不重复整数,找出其所有的全排列。例如:

Input: [1,2,3]
Output:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

2. 算法思路

back tracking回溯法的经典题目之一,要注意和subset以及combination sum不同的是,这里是要找排列,而不是子集合,思路有很大的不同。

首先我们先来找一下规律,对于上面的例子,不外乎就是找到以1开头的所有排列,和以2开头的所有排列以及以3开头的所有排列。

我们对于以1开头的全排列,就是“1 + 2和3的全排列”,然后我们再以同样的思路去找2和3组成的全排列,2和3的全排列就是以2开头,找3的全排列和以3开头找2的全排列。2和3都是单独的数字时,其全排列只有一种,返回。

那么我们如何去将各种不同的开头的可能性都给找出来呢?我们只需要将某数字和后面的数字轮流置换即可,找完一个以后再换回来,然后和下一个进行置换,直到首个字母和最后一个字母进行置换完成。

回溯法

回溯法置换当前数字和其后面的数字,然后递归调用。

时间复杂度:O(2 ^ n),空间复杂度:O(n)。

class Solution {
    public List<List<Integer>> permute(int[] nums) {
       List<List<Integer>> res = new ArrayList();

        if (nums.length == 0) {
            return res;
        }

       getPermutations(nums, 0, res);

       return res;
    }

    private void getPermutations(int[] nums, int start, List<List<Integer>> res) {
        if (start == nums.length - 1) {
            List<Integer> solution = new ArrayList();
            for (int num : nums) {
                solution.add(num);
            }
            res.add(solution);
            return;
        }

        for (int i = start; i < nums.length; i++) {
            swap(nums, start, i);
            getPermutations(nums, i + 1, res);
            swap(nums, start, i);
        }
    }

    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

LeetCode 31. Next Permutation

Problem

LeetCode 31. Next Permutation

1. 题目简述

给出一组数,找出其字典序的下一个序列,如果已经是字典序最后一个序列,则返回其排列组合的第一个字典序。例如:

1,2,3 → 1,3,2
3,2,1 → 1,2,3
1,1,5 → 1,5,1

2. 算法思路

相关问题:

  1. LeetCode 46. Permutations
  2. LeetCode 47. Permutations II

经典老题目,算法可以默写的那种。对于数字的字典序来说,就是比当前数大的下一个数。根据这个思路,我们可以按照如下思路来计算:

1. 从右到左寻找第一个降序的数字i,对于12354而言,第一个降序数字就是3;
2. 从i的右侧寻找出比i大的最小数字j,对于12354而言,就是4;
3. 交换两个数字的位置,变成12453;
4. 然后将4后面所有的数字按照升序排列。

硬核算法

纯碎的数学问题,对于一个数字,我们想找到比它的下一个字母序组合数字,其实就是找出它所有位组合的数字里刚刚好比它大的那个数字,,我们肯定不能从高位入手,要从低位开始寻求变化,因为高位变化影响大,不符合要求。

我们知道,对于一组数字而言,升序排列最小,降序排列最大;因此我们才要从右向左找第一个降序。

如果从左至右全是升序排列,那么说明当前数字已经是最大值了,直接将数组升序排列。

注意,不要忘了最后的交换,而且我们知道它们都是降序排列,所以直接收尾交换机可,不需要重新写排序函数。

class Solution {
    public void nextPermutation(int[] nums) {
        int i = nums.length - 1;

        // 从右到左找到第一个降序的数字,为什么从右找呢?因为前面高位的数字太大,改一个要增大很多,所以从右找。
        while (i > 0 && (nums[i] <= nums[i - 1])) {
            i--;
        }

        // 如果已经是最大数字了,排序成升序序列即可
        if (i == 0) {
            Arrays.sort(nums);
            return;
        }

        // 由于i的右侧全部降序,从右至左找到第一个比a[i - 1]更大的数字,交换他们
        int j = nums.length - 1;
        while (j >= i) {
            if (nums[j] > nums[i - 1]) {
                int temp = nums[j];
                nums[j] = nums[i - 1];
                nums[i - 1] = temp;
                break;
            }
            j--;
        }

        // 注意最后还需要将a[i - 1]右侧的数字升序排列,由于我们知道是降序的,所以只要两两互换即可
        for (int x = 0; x < (nums.length - i) / 2; x ++) {
            int temp = nums[i + x];
            nums[i + x] = nums[nums.length - 1 - x];
            nums[nums.length - 1 - x] = temp;
        }

    }
}

LeetCode 90. Subsets II

Problem

LeetCode 90. Subsets II

1. 题目简述

给出一列整数(可能重复),找出其所有不重复的子集合(包括空集)。例如:

Input: [1,2,2]
Output:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]

2. 算法思路

这道题就是LeetCode 78. Subsets的进阶版,和LeetCode 40. Combination Sum II特别像,连去重的方式都一模一样,这个必须要记住!!!

注意:这里需要排序,因为要去重!!!

class Solution {
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        // 这次就必须要sort了,为什么呢?因为我们要去重,例子中的[1,2]只能有一个。和combination sum 2的去重方式可以一致。每次遍历重复的数只用一次。
        Arrays.sort(nums);
        List<List<Integer>> res = new ArrayList();
        LinkedList<Integer> path = new LinkedList();

        res.add(new ArrayList(path));
        getAllSubsets(nums, 0, res, path);

        return res;
    }

    private void getAllSubsets(int[] nums, int start, List<List<Integer>> res, LinkedList<Integer> path) {
        for (int i = start; i < nums.length; i++) {
            // 注意,这里的去重很精髓,通过i > start这么个判断条件,使得每种重复的数字只会用一次,比如[1,2,2,2]只会出现一次。细品!!!
            if (i > start && nums[i] == nums[i - 1]) {
                continue;
            }
            path.add(nums[i]);
            res.add(new ArrayList(path));
            getAllSubsets(nums, i + 1, res, path);
            path.removeLast();
        }
    }
}

回溯法小结

回溯法基本思想

参考文章:回溯法

首先我们要明确的是回溯法使用的场景,从我个人角度来看,回溯法和没有做任何优化的递归是一样的(或者说回溯法本身就是递归),都是属于暴力搜索的一种方式,但是有些时候我们难以区分回溯法和DP的区别,在第二小节我们详细解释。

解决一个回溯问题,其实就是遍历所有可能的情况,在需要记录的时候对解决方案进行记录,在发现当前方案不符合预期的时候返回遍历下一种方案,其最经典的应用有:求子集合、全排列以及组合问题。

伪代码框架:

result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return

    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择

其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」,这里看似简单,却有很多天坑。回溯法有很多难题,后面遇到了会一一记录,而且面试考察也很多,因为回溯法经常可以和BFS、DFS一起考察。

回溯法与DP的使用区别

那么回溯法的难点在哪里呢?其实有很多小tips,例如LeetCode 377. Combination Sum IV,明明前三道combination都是在用回溯法,为什么到了第四题就用DP而不是回溯法了呢?再比如说为什么求subset的时候就不需要sort,而有些时候就需要sort呢?

我们这里先来看一下回溯法解决的问题和DP解决问题最大的不同

类目 回溯法 DP
解决问题类型 通常是求出给定条件的集合,注意,是最终的详细的解,而不是个数。 一般是求某个值,例如说最大值,最小值,或者XXX的个数,一般情况下不会给出额外的限定条件,因为DP是要写递推式的,有额外条件不方便写出普适的递推式。

这么看起来感觉比较抽象,我们来举个例子,LeetCode 39. Combination SumLeetCode 377. Combination Sum IV

注意,以下分析仅针对39和337,所谓的不同的解就用DP之类的仅针对该题目,具体问题具体分析,这里只是举例看二者的区别

这里不再赘述题目。39题和377题最大的不同点在于39题可以重复,而337不行,什么意思呢?例如[1,3]和[3,1]在39题里是同一个解,而337题里是不同的解。

那么为什么不同的解就可以用DP而不能用回溯呢?我们要注意,不是说不能用回溯,只是说DP更快。因为回溯其实是暴力搜索的一种,而DP是对暴力搜索的优化,因此对这道题而言,能用DP就自然也能用回溯(这里存在遍历选项的问题因此可以用回溯,并不是所有DP都能用回溯,要注意这里是必要不充分条件)。

将39题的递归调用backtracking那块的代码,将i变换成0就是变成找出所有解了,就会将[1,3]和[3,1]当做不同解。从0开始递归找全局组合方式。

例如:给出[1,2,3],我们的target是4,那么我们使用dp的时候,dp[4 - 1]和dp[4 - 3]我们都会去计算,因为我们不关心dp[3]和dp[1]是怎么来的,我们可以任意组合数字。

当我们使用回溯法的时候需要先对数组进行排序,然后遍历的时候只能向后遍历(包含当前节点),例如说我们到了1,我们可以从[1,2,3]中找可以组合成为3的组合;再或者我们到了2,我们就不能回去找1,就只能从[2,3]里找出所有可以组合为2的可能。这样保证了不重复

回溯法需要注意的点

什么时候回溯需要sort,而什么时候不要?

当我们原始数组里有重复数字(包括数字不重复可以重复使用)且需要不重复的解的时候需要sort。多看看下面的例子自行体会~

需要sort的例子有:LeetCode 39. Combination Sum
LeetCode 40. Combination Sum II
LeetCode 90. Subsets II

不需要sort的例子有:LeetCode 377. Combination Sum IV
LeetCode 78. Subsets

当给出的数组中存在重复数字怎么办?如何避免重复解

在循环选项之前加上这么一个if判断即可,例题:

LeetCode 40. Combination Sum II
LeetCode 90. Subsets II
for (int i = start; i < nums.length; i++) {
    if (i > start && nums[i] == nums[i - 1]) {
        continue;
    }
}

LeetCode 78. Subsets

Problem

LeetCode 78. Subsets

1. 题目简述

给出一列不重复整数,找出其所有子集合(包括空集)。例如:

Input: nums = [1,2,3]
Output:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]

2. 算法思路

back tracking回溯法的经典题目之一,和LeetCode 90. Subsets II区别在于,这道题无需考虑重复数字的问题,和combination sum很像。这里是找组合,而不是sum,所以无需排序。

回溯法

直接回溯法计算即可。固定住其中一些位置,然后从前向后找。

时间复杂度:O(n * 2 ^ n),空间复杂度:O(n)。

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList();
        List<Integer> path = new LinkedList();

        res.add(new ArrayList(path));
        backtracking(nums, 0, path, res);

        return res;
    }

    private void backtracking(int[] nums, int start, List<Integer> path, List<List<Integer>> res) {
        for (int i = start; i < nums.length; i++) {
            path.add(nums[i]);
            res.add(new ArrayList(path));
            backtracking(nums, i + 1, path, res);
            path.remove(path.size() - 1);
        }
    }
}

LeetCode 1029. Two City Scheduling

Problem

LeetCode 1029. Two City Scheduling

1. 题目简述

公司有2N个面试者,一共有两个面试地点A和B,需要保证每个地点都有N个人面试,每个面试者距离AB远近不同,导致路程花销也不一样。每个人的到达A、B地点的花销用一个二维数组来表示,求怎么样分配使得总花销最小,求出这个总花销。例如:

Input: [[10,20],[30,200],[400,50],[30,20]]
Output: 110
Explanation: 
The first person goes to city A for a cost of 10.
The second person goes to city A for a cost of 30.
The third person goes to city B for a cost of 50.
The fourth person goes to city B for a cost of 20.

The total minimum cost is 10 + 30 + 50 + 20 = 110 to have half the people interviewing in each city.

限制:

  1. 1 <= costs.length <= 100
  2. It is guaranteed that costs.length is even.
  3. 1 <= costs[i][0], costs[i][1] <= 1000

2. 算法思路

首先最直接的办法使用DFS,回溯,遍历所有可能性,时间复杂度为O(2^n),这里n为总人数,由于n在最大为100,因此这个方法一定会超时,放弃。

那么能否使用DP呢?也不行!因为要求每个面试地点都要有N个人,也就是说前面的选择对后面的选择会造成影响,说明问题不算独立子问题,所以也不行。

这里我们使用贪心法。

贪心法

公司希望总花销最小,其实这不单单是从整体的角度考虑,更要从个人的角度去看,对于每个人来说,都有两种选择,A和B,而每个人到A和到B的花销的差其实才是我们最关心的,如果差的特别多,比如说cost[i][0]很小,而cost[i][1]很大,那么我们就应该选择A地点,因为如果选择B地点的话,就会多花出很多钱。

我们定义一个变量叫做营收S,对于A的营收S(A) = cost[i][0] - cost[i][1],这个营收S可能为正,也可能为负,如果为正,就说明到A的cost比到B的cost要多,我们将他分配到A地点的可能性就更小;相反若为负,则可能性更大。

因此我们将每个数对按照对A的营收进行排序,前N个元素就为A地点的面试人,剩下的就是B地点的面试人。

时间复杂度:O(nlogn),排序的复杂度。

空间复杂度:O(1)。

class Solution {
    public int twoCitySchedCost(int[][] costs) {
        // 不能用DP,因为有N个的限制,状态有依赖关系?每个只有两种选择的可能性,使用backtracking + 剪枝?也不行,因为可能性实在是太多了,不可能全都穷举完,cost的length达到了100。考虑使用贪心。
        Arrays.sort(costs, (a1, a2) -> (a1[0] - a1[1]) - (a2[0] - a2[1]));
        int n = costs.length / 2, minCost = 0;

        for (int i = 0; i < n; i++) {
            minCost += costs[i][0] + costs[i + n][1];
        }

        return minCost;

    }
}

LeetCode 445. Add Two Numbers II

Problem

LeetCode 445. Add Two Numbers II

1. 题目简述

给出两个链表,每个链表表示一个数字,链表中的每个元素表示每一位,从高到低,求两个链表加和(有进位)。例如:

Example:

Input: (7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4)
Output: 7 -> 8 -> 0 -> 7

2. 算法思路

类似的题目还有LeetCode 2. Add Two Numbers,注意此题是从高位到低位存储,而LeetCode2中是从低到高存储。

双指针 + Stack

如果我们从头开始遍历的话,发现是从最高位开始的,但是我们计算又不能从最高位开始计算,一定要从最低位开始,因此我们需要一种能够从后向前遍历的数据结构,我们选择stack。

我们这里有一点需要着重强调,在对链表进行赋值计算的时候,每次对ptr.next进行new,然后将ptr = ptr.next。

时间复杂度:O(m + n)

空间复杂度:O(m + n)

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
     public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
         if (l1 == null) {
             return l2;
         }
         if (l2 == null) {
             return l1;
         }

         Stack<Integer> stack1 = new Stack(), stack2 = new Stack(), stack = new Stack();
         while (l1 != null) {
             stack1.push(l1.val);
             l1 = l1.next;
         }
         while (l2 != null) {
             stack1.push(l2.val);
             l2 = l2.next;
         }

         int flag = 0;
         while (!stack1.isEmpty() || !stack2.isEmpty() || flag != 0) {
             int value1 = stack1.isEmpty() ? 0 : stack1.pop();
             int value2 = stack2.isEmpty() ? 0 : stack2.pop();
             int value = (value1 + value2 + flag) % 10;
             flag = (value1 + value2 + flag) / 10;

             stack.push(value);
         }

         ListNode head = new ListNode(), ptr = head;
         while (!stack.isEmpty()) {
             ptr.next = new ListNode(stack.pop());
             ptr = ptr.next;
         }

         return head.next;
    }
}

双指针 + ArrayList

其实数字的存储不一定要用stack,用arraylist会更快。代码如下:

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        List<Integer> num1 = new ArrayList();
        List<Integer> num2 = new ArrayList();
        ListNode ptr = l1;
        while (ptr != null) {
            num1.add(ptr.val);
            ptr = ptr.next;
        }
        ptr = l2;
        while (ptr != null) {
            num2.add(ptr.val);
            ptr = ptr.next;
        }

        ListNode head = new ListNode(0);
        // form back to start
        int ptr1 = num1.size() - 1, ptr2 = num2.size() - 1, flag = 0;

        while (ptr1 >= 0 || ptr2 >= 0 || flag == 1) {
            int value1 = ptr1 < 0 ? 0 : num1.get(ptr1).intValue();
            int value2 = ptr2 < 0 ? 0 : num2.get(ptr2).intValue();
            int value = (value1 + value2 + flag) % 10;
            flag = (value1 + value2 + flag) / 10;

            // 这里注意给head赋值的方式,因为是从后向前进行遍历的。
            head.val = value;
            ListNode temp = new ListNode(0);
            temp.next = head;
            head = temp;
            ptr1--;
            ptr2--;
        }

        return head.next;
    }
}

LeetCode 24. Swap Nodes in Pairs

Problem

LeetCode 24. Swap Nodes in Pairs

1. 题目简述

给出一个链表,从头开始使其两两交换。例如:

Example:

Given 1->2->3->4, you should return the list as 2->1->4->3.

2. 算法思路

也是LeetCode中比较靠前的老题目,其进阶版本LeetCode 25. Reverse Nodes in k-Group

对于这种需要更改链表的题目,包括reverse linked list的题目,最重要的一点就是画图,画图然后去更改每一个指针的指向,然后将当前的节点向后移。否则很容易出错。

这里需要注意的是初次进行swap的时候要进行判断,是否是要和pre的指针相连,如果是初次swap,则pre是null,不需要相连。

指针

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode swapPairs(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        ListNode ret = head.next, ptr = head, pre = null;
        while (ptr != null && ptr.next != null) {
            ListNode post = ptr.next.next;
            ptr.next.next = ptr;
            if (pre != null) {
                pre.next = ptr.next;
            }
            ptr.next = post;
            pre = ptr;
            ptr = ptr.next;
        }

        return ret;
    }
}

LeetCode 92. Reverse Linked List II

Problem

LeetCode 92. Reverse Linked List II

1. 题目简述

给出一个链表,和两个正整数m、n(1 <= m <= n <= length),翻转该链表的第m和n之间的元素。例如:

Example:

Input: 1->2->3->4->5->NULL, m = 2, n = 4
Output: 1->4->3->2->5->NULL

2. 算法思路

经典题目的难度加大的版本,难度在于我们需要找到起始位置和终止位置,基本思路如下:

  1. 首先找到第m个元素ptr,以及它前面的那个元素pre,pre有可能为null;
  2. 定义tail = ptr,因为翻转后,第m个元素就是翻转部分链表的结尾;
  3. 定义con = pre,这个con是用来记录当前pre是否为null的,如果不为null值,就将con和反转后的表头相连,,为null就直接返回反转后的表头;
  4. 定义tail = ptr,因为当前未翻转部分的表头ptr其实是反转后的末尾,记录一下,因为后面while循环后ptr就不在翻转列表里了,需要重新连一下。
  5. while循环,记录post元素,将ptr的next指向pre,然后更改ptr和pre(这里有一个注意事项,就是翻转第一个元素的时候可能有人会觉得不合理,其实这只是暂存,while循环后面会修正的)。
  6. 修正tail的指向;
  7. 判断连接处con是否为空,如果不为空则连接,为空则不连接。

for循环

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode reverseBetween(ListNode head, int m, int n) {
        if (m == n) {
            return head;
        }

        ListNode ptr = head, pre = null;
        while (m > 1) {
            pre = ptr;
            ptr = ptr.next;
            m--;
            n--;
        }

        ListNode con = pre, tail = ptr;
        while (n > 0) {
            ListNode post = ptr.next;
            // 注意,对于第一个翻转的元素,这里只是暂存!!!后面ptr会变,这里其实是tail
            ptr.next = pre;
            pre = ptr;
            ptr = post;
            n--;
        }

        tail.next = ptr;
        // while循环结束时,ptr处于第n+1个元素,pre是原本的第n个元素
        if (con != null) {
            con.next = pre;
        } else {
            return pre;
        }

        return head;
    }
}

LeetCode 206. Reverse Linked List

Problem

LeetCode 206. Reverse Linked List

1. 题目简述

给出一个链表,翻转该链表。例如:

Example:

Input: 1->2->3->4->5->NULL
Output: 5->4->3->2->1->NULL

2. 算法思路

最经典的题目之一了,没啥好说的,背就完了,其进阶版本LeetCode 92. Reverse Linked List II

自己画图,找出ptr,pre和post之间的连接关系应该怎么去解决,以及边界条件,初始化的问题,稍微注意一下就好,难是不难,就是绕。

for循环

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        ListNode ptr = head, pre = null;

        while (ptr != null) {
            ListNode post = ptr.next;
            // 将后面指向前面,第一个元素反转后将会是末尾,所以指向null,没毛病
            ptr.next = pre;
            pre = ptr;
            ptr = post;
        }

        return pre;
    }
}

递归

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        ListNode reversed = reverseList(head.next);
        //这时候,head.next被置换到了末尾,所以需要将末尾指向自己
        head.next.next = head;
        // 注意置换到了末尾后,要将自己的next置为null
        head.next = null;

        return reversed;
    }
}

LeetCode 142. Linked List Cycle II

Problem

LeetCode 142. Linked List Cycle II

1. 题目简述

给出一个链表,判断链表是否存在环,如果有环,找出入环点,如果没有环,则返回null。例如:

Example

Input: head = [3,2,0,-4], pos = 1
Output: tail connects to node index 1
Explanation: There is a cycle in the linked list, where tail connects to the second node.

2. 算法思路

极其经典的老题目,类似题目:LeetCode 141. Linked List Cycle

依然采用双指针做法,但是需要一些数学推导,由于markdown中不方便编辑公式,直接在纸上写以图片的形式说明算法。

双指针

两个指针,一个slow,一个fast,slow每次走一步,fast每次走两步,如果fast先到达null值,则说明无环;如果slow和fast相遇,则说明有环,我们记录相遇点。

算法思路

时间复杂度:O(n).

空间复杂度:O(1),只有fast和slow两个指针变量。

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        // 数学推导过程记得写一下
        if (head == null || head.next == null) {
            return null;
        }
        ListNode slow = head, fast = head;

        ListNode cross = null;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow.equals(fast)) {
                cross = slow;
                break;
            }
        }

        if (cross == null) {
            return null;
        }

        // 有环,开始找环
        ListNode ptr1 = head, ptr2 = cross;
        while (ptr1 != ptr2) {
            ptr1 = ptr1.next;
            ptr2 = ptr2.next;
        }

        // 注意,这里是返回的是ptr1或者ptr2
        return ptr1;
    }
}

LeetCode 141. Linked List Cycle

Problem

LeetCode 141. Linked List Cycle

1. 题目简述

给出一个链表,判断链表是否存在环。例如:

Example

Input: head = [3,2,0,-4], pos = 1
Output: true
Explanation: There is a cycle in the linked list, where tail connects to the second node.

2. 算法思路

极其经典的老题目,类似题目:LeetCode 142. Linked List Cycle II

一种做法是用hashset来存储,每次遍历的时候去查找是否之前遍历过相同的点。这种方法看似简单,时间复杂度也不高,但是实际上调用contains函数次数也很多,时间反而较长。

这里我们使用双指针做法更好。

注意,2 pointers的代码要背下来,注意取while循环的方式,这个是死的。不要slow = head, fast = head.next,这样做步长就不是二倍关系,而是2倍-1,而且对于找入环点没有帮助。

双指针

两个指针,一个slow,一个fast,slow每次走一步,fast每次走两步,如果fast先到达null值,则说明无环,否则有环;如果slow和fast相遇,则说明有环。

时间复杂度:O(n),无环的情况下自然不用多说,我们只考虑有环情况下的复杂度。如果有环,则slow进入环以后,fast可能在某位置,slow走一圈的时间,fast必定能走两圈,所以必定会相遇,而不是白绕很多圈,所以时间复杂度为O(n).

空间复杂度:O(1),只有fast和slow两个指针变量。

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {
        if (head == null || head.next == null) {
            return false;
        }

        ListNode slow = head, fast = head;

        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                return true;
            }
        }

        return false;
    }
}

LeetCode 23. Merge k Sorted Lists

Problem

LeetCode 23. Merge k Sorted Lists

1. 题目简述

合并多个有序链表。例如:

Example:

Input:
[
1->4->5,
1->3->4,
2->6
]
Output: 1->1->2->3->4->4->5->6

2. 算法思路

极其经典的老题目,类似题目:LeetCode 21. Merge Two Sorted Lists

我们首先需要注意的是k个数组的话如果继续使用迭代的方式,会产生很多条件的判断,例如每次添加了一个节点之后,需要判断k个List中哪些已经到头了,计算起来很麻烦,而且易错。那么我们如何将问题分解成之前做过的“Merge 2 Sorted Arrays”呢?答案就是分治法。

分治法

每次二分整个lists,直到分为了两个list的合并或者一个单独的list,合并它们并返回。

时间复杂度:O(n * logk),n是所有list节点总数,k是list个数。这是因为我们其实一共需要对每个元素合并logk次,每次返回上一级都要重新合并,因此是乘法。

空间复杂度:O(logk),大部分的操作都是O(1)空间的,只有递归层数是需要压栈要空间。

//**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if (lists.length == 0) {
            return null;
        }
        if (lists.length == 1) {
            return lists[0];
        }

        return mergeLists(lists, 0, lists.length - 1);

    }

    // This function is used to divide numerous lists into smaller parts and merge them into one list.
    private ListNode mergeLists(ListNode[] lists, int start, int end) {
        if (start == end) {
            return lists[start];
        } else if (start == end - 1) {
            return mergeTwoLists(lists[start], lists[end]);
        } else {
            int mid = start + (end - start) / 2;

            ListNode left = mergeLists(lists, start, mid);
            ListNode right = mergeLists(lists, mid + 1, end);

            return mergeTwoLists(left, right);
        }
    }

    private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if (l1 == null) {
            return l2;
        }
        if (l2 == null) {
            return l1;
        }
        ListNode head = new ListNode(0);
        ListNode ptr = head;

        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                ptr.next = l1;
                ptr = ptr.next;
                l1 = l1.next;
            } else {
                ptr.next = l2;
                ptr = ptr.next;
                l2 = l2.next;
            }
        }

        ptr.next = l1 == null ? l2 : l1;

        return head.next;
    }
}

LeetCode 21. Merge Two Sorted Lists

Problem

LeetCode 21. Merge Two Sorted Lists

1. 题目简述

合并两个有序链表。例如:

Example:

Input: 1->2->4, 1->3->4
Output: 1->1->2->3->4->4

2. 算法思路

参考解法:花花酱LeetCode

极其经典的老题目,每年肯定都有考的,背下来就可以了。进阶版本:LeetCode 23. Merge k Sorted Lists

递归

当两个list有一个为空时,直接返回另一个。如果都不为空,比较两个list队首的值,选其中较小的那个,我们设为node,将该node.text设为node.next和另一个List的merge结果即可,返回node节点。

时间复杂度:O(m + n),最坏情况m+n次递归,每次O(1)。

空间复杂度:O(m + n),递归的最坏情况是二者之和。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if (l1 == null) {
            return l2;
        } else if (l2 == null) {
            return l1;
        } else if (l1.val < l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        } else {
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }
    }
}

循环

思路基本一致,只是需要注意其中一个为空的时候的情况。

时间复杂度:O(m + n),最坏情况m+n次递归,每次O(1)。

空间复杂度:O(1),不需要额外空间,merge都是直接操作指针的,没有新建链表。

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode head = new ListNode();
        ListNode ptr = head;

        while (l1 != null && l2 != null) {
            int value1 = l1.val, value2 = l2.val;
            if (value1 <= value2) {
                ptr.next = l1;
                l1 = l1.next;
                ptr = ptr.next;
            } else {
                ptr.next = l2;
                l2 = l2.next;
                ptr = ptr.next;
            }
        }

        ptr.next = l1 == null ? l2 : l1;

        return head.next;
    }
}

LeetCode 2. Add Two Numbers

Problem

LeetCode 2. Add Two Numbers

1. 题目简述

给出两个链表,每个链表表示一个数字,链表中的每个元素表示每一位,从低到高,求两个链表加和(有进位)。例如:

Example:

Input: (2 -> 4 -> 3) + (5 -> 6 -> 4)
Output: 7 -> 0 -> 8
Explanation: 342 + 465 = 807.

2. 算法思路

LeetCode 2的题目,一看就知道也是经典老题了。类似的题目还有LeetCode 445. Add Two Numbers II

这里是从低位到高位,我们需要注意的地方有两点:一是进位二茂铁,二是二者长度不相等的问题,pointer为空的时候要注意。

双指针

两个指针分别指向两个list的初始位置,然后计算是否存在进位,然后计算下一位,如果为空,则为0,继续计算,直至两个pointer均为null且进位也为0.

我们这里有一点需要着重强调,在对链表进行赋值计算的时候,每次对ptr.next进行new,然后将ptr = ptr.next。

时间复杂度:O(m + n)

空间复杂度:O(m + n)

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        if (l1 == null) {
            return l2;
        }
        if (l2 == null) {
            return l1;
        }

        ListNode head = new ListNode();
        ListNode ptr1 = l1, ptr2 = l2, ptr = head;
        int flag = 0;

        while (ptr1 != null || ptr2 != null || flag != 0) {
            int value1 = ptr1 == null ? 0 : ptr1.val;
            int value2 = ptr2 == null ? 0 : ptr2.val;
            int value = (value1 + value2 + flag) % 10;
            flag = (value1 + value2 + flag) / 10;

            ptr.next = new ListNode(value);
            ptr = ptr.next;
            ptr1 = ptr1 == null ? null : ptr1.next;
            ptr2 = ptr2 == null ? null : ptr2.next;
        }

        return head.next;
    }
}

LeetCode 226. Invert Binary Tree

Problem

226. Invert Binary Tree

1. 题目简述

翻转一棵二叉树。例如:

Invert a binary tree.

Example:

Input:

     4
   /   \
  2     7
 / \   / \
1   3 6   9
Output:

     4
   /   \
  7     2
 / \   / \
9   6 3   1

2. 算法思路

一道被大佬吐槽的easy题目。DFS或者BFS均可。

DFS

递归调用invert,前中后序均可。


/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    // 前序
    public TreeNode invertTree(TreeNode root) {
        if (root == null) {
            return root;
        }

        TreeNode temp = root.left;
        root.left = root.right;
        root.right = temp;

        if (root.left != null) {
            root.left = invertTree(root.left);
        }
        if (root.right != null) {
            root.right = invertTree(root.right);
        }

        return root;
    }

    // 中序
    public TreeNode invertTree(TreeNode root) {
        if (root == null) {
            return root;
        }

        if (root.left != null) {
            root.left = invertTree(root.left);
        }

        TreeNode temp = root.left;
        root.left = root.right;
        root.right = temp;

        if (root.right != null) {
            root.right = invertTree(root.right);
        }

        return root;
    }
    //后序
    public TreeNode invertTree(TreeNode root) {
        if (root == null) {
            return root;
        }

        if (root.left != null) {
            root.left = invertTree(root.left);
        }
        if (root.right != null) {
            root.right = invertTree(root.right);
        }

        TreeNode temp = root.left;
        root.left = root.right;
        root.right = temp;

        return root;
    }
}

BFS

class Solution {
    public TreeNode invertTree(TreeNode root) {
        if (root == null) {
            return root;
        }

        Queue<TreeNode> queue = new LinkedList();
        queue.add(root);

        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            TreeNode temp = node.left;
            node.left = node.right;
            node.right = temp;

            if (node.left != null) {
                queue.add(node.left);
            }
            if (node.right != null) {
                queue.add(node.right);
            }
        }

        return root;
    }
}

LeetCode 64. Minimum Path Sum

Problem

LeetCode 64. Minimum Path Sum

1. 题目简述

给出一个m x n的网格,每个格子有一个数字,求问从top-left到bottom-right的所有路径中数字和最小是多少。

2. 算法思路

参考解法:花花酱LeetCode

LeetCode 62. Unique Paths进阶问题,思路类似。

动态规划

和之前思路类似,只是base case小改一下,以及dp[i][j]记录的不再是走法,而是从[0, 0]到当前节点的最小path和。

时间复杂度:O(m * n)

空间复杂度:O(m * n)


class Solution {
    public int minPathSum(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        int[][] dp = new int[m][n];

        int sum = 0;
        for (int i = 0; i < m; i++) {
            sum += grid[i][0];
            dp[i][0] = sum;
        }
        sum = 0;
        for (int j = 0; j < n; j++) {
            sum += grid[0][j];
            dp[0][j] = sum;
        }

        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
            }
        }

        return dp[m - 1][n - 1];
    }
}

LeetCode 63. Unique Paths II

Problem

LeetCode 63. Unique Paths II

1. 题目简述

一个机器人坐落在一个网格中,初始点在top-left,终点在bottom-right,机器人只能向右或者向下走,而且存在一些路障(数值为1),求问有多少种走法可以到达终点。

2. 算法思路

参考解法:花花酱LeetCode

LeetCode 62. Unique Paths进阶问题,思路类似。

动态规划

和之前思路类似,只是base case中,如果出现了路障,则后面是0。而且在动态规划递推的过程中,对于grid值为1的点dp值直接置为0。

时间复杂度:O(m * n)

空间复杂度:O(m * n)


class Solution {
    // top-down
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        // without padding
        int m = obstacleGrid.length, n = obstacleGrid[0].length;
        int[][] dp = new int[m][n];

        // initialize the base cases.
        for (int i = 0; i < m; i++) {
            Arrays.fill(dp[i], -1);
        }

        int value = 1;
        for (int j = 0; j < n; j++) {
            if (obstacleGrid[0][j] == 1) {
                value = 0;
            }
            dp[0][j] = value;
        }
        value = 1;
        for (int i = 0; i < m; i++) {
            if (obstacleGrid[i][0] == 1) {
                value = 0;
            }
            dp[i][0] = value;
        }

        return helper(obstacleGrid, dp, m - 1, n - 1);

    }

    private int helper(int[][] grid, int[][] dp, int m, int n) {
        if (m < 0 || n < 0) {
            return 0;
        }
        if (grid[m][n] == 1) {
            return 0;
        }
        if (dp[m][n] != -1) {
            return dp[m][n];
        }

        dp[m][n] = 0;
        dp[m][n] += helper(grid, dp, m - 1, n);
        dp[m][n] += helper(grid, dp, m, n - 1);

        return dp[m][n];
    }
}

class Solution {
    // bottom-up
    public int uniquePaths(int m, int n) {
        int m = obstacleGrid.length, n = obstacleGrid[0].length;
        int[][] dp = new int[m][n];

        // initialize the base cases.
        for (int i = 0; i < m; i++) {
            Arrays.fill(dp[i], -1);
        }

        int value = 1;
        for (int j = 0; j < n; j++) {
            if (obstacleGrid[0][j] == 1) {
                value = 0;
            }
            dp[0][j] = value;
        }
        value = 1;
        for (int i = 0; i < m; i++) {
            if (obstacleGrid[i][0] == 1) {
                value = 0;
            }
            dp[i][0] = value;
        }

        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (grid[i][j] == 1) {
                    dp[i][j] = 0;
                    continue;
                }
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }

        return dp[m - 1][n - 1];
    }
}

LeetCode 62. Unique Paths

Problem

LeetCode 62. Unique Paths

1. 题目简述

一个机器人坐落在一个m x n的一个网格中,初始点在top-left,终点在bottom-right,机器人只能向右或者向下走,求问有多少种走法可以到达终点。

2. 算法思路

参考解法:花花酱LeetCode

也是一道经典老题目了,DP的代表问题之一。LeetCode 63. Unique Paths II

动态规划

我们定义dp[i][j]为从[0][0]到达当前点的所有可能的方式,由于只能向右或者向下走,所以有如下递推式:

$$dp[i][j] = dp[i - 1][j] + dp[i][j - 1]$$

base case就是dp[0]和dp[i][0],这两条边线的初始值都为1,只有一种走法。

时间复杂度:O(m * n)

空间复杂度:O(m * n)


class Solution {
    // top-down
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];
        for (int i = 0; i < m; i++) {
            Arrays.fill(dp[i], -1);
            dp[i][0] = 1;
        }
        Arrays.fill(dp[0], 1);

        return helper(dp, m - 1, n - 1);
    }

    private int helper(int[][] dp, int m, int n) {
        if (m < 0 || n < 0) {
            return 0;
        }
        if (dp[m][n] != -1) {
            return dp[m][n];
        }

        dp[m][n] = helper(dp, m, n - 1) + helper(dp, m - 1, n);

        return dp[m][n];
    }
}

class Solution {
    // bottom-up
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];
        for (int i = 0; i < m; i++) {
            Arrays.fill(dp[i], -1);
            dp[i][0] = 1;
        }
        Arrays.fill(dp[0], 1);

        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }

        return dp[m - 1][n - 1];
    }
}

LeetCode 10. Regular Expression Matching

Problem

LeetCode 10. Regular Expression Matching

1. 题目简述

给出两个字符串s和p,其中s是由小写字母组成,p是由小写字母以及’.’和’*‘组成,’.’和’*‘分别有如下含义:

'.' 可以表示任何单个字符
'*' 可以表示0个或者多个*前面的字符(需要组合起来看,例如:'a*'可以表示0个a或者多个a)

例如:

Example 1:
Input:
s = "aa"
p = "a"
Output: false
Explanation: "a" does not match the entire string "aa".

Example 2:
Input:
s = "aa"
p = "a*"
Output: true
Explanation: '*' means zero or more of the preceding element, 'a'. Therefore, by repeating 'a' once, it becomes "aa".

Example 3:
Input:
s = "ab"
p = ".*"
Output: true
Explanation: ".*" means "zero or more (*) of any character (.)".

Example 4:
Input:
s = "aab"
p = "c*a*b"
Output: true
Explanation: c can be repeated 0 times, a can be repeated 1 time. Therefore, it matches "aab".

2. 算法思路

相关问题:

  1. LeetCode 44. Wildcard Matching
  2. “LeetCode 72. Edit Distance”

这种题就是做过一次才能有大概思路,也不能保证完全能做出来。

动态规划

首先,对于这道题,直接用动态规划分析,没有为什么。分析p的两种特殊字符,比较麻烦的是’*’,因为我们不知道它会代表几个字符。因此我们只能逐个分析,代表0个,1个……

这里latex写公式有点问题,因此主体逻辑在注释中,其实其中最难理解的部分就是p[j]为’*’时的情况。

这里我们将其分为两种子情况:

  1. s[i] 和 p[j - 1]不匹配:那么dp[i][j] = dp[i][j - 2],这是因为既然二者不匹配的话,说明只能将dp[j - 1]dp[j]这个小pattern当做是空串;

  2. s[i] 和 p[j - 1]匹配:这里我们再去分析其可能的情况。匹配0个s[i],1个s[i],2个s[i]……但是我们仔细一想可以发现,假如说匹配2个及以上个s[i]的话,那么s的前i-1个字符的子串和p的前j个字符的子串也一定是匹配的!这里以两个为例:

    s:”acbbbd”
    p:”acb*d”

当i = 3时,也就是计算s的子串”acbb”时,当我们匹配到j = 3时,也就是说”acb*”时,我们发现”acb*”和i = 2时的”acb”也是匹配的,因为这里”b*”表示为1个b。换句话说,如果”b*”表示两个及以上的“b”的话,那么s[0, i-1]和p[0, j]也一定是匹配的,因为”b*”表示的重复有很多。

因此对于上面的情况2,我们可以将匹配2个及以上s[i]的情况递推为dp[i - 1][j],因此我们有递推式:

dp[i][j] = dp[i][j - 2] || dp[i][j - 1] || dp[i - 1][j]

三项分别对应0个,1个和多个的情况。

进一步,我们可以将其规约为两项,因为如果*的前一个字符和s[i]相匹配,那么可以匹配0个,1个,2个,3个…对于1个及以上的情况,我们可以将其规约到dp[i-1][j+1]里面,因为这次的的1个,2个,3个等于上次匹配的0个,1个,2个…

dp[i][j] = dp[i][j - 2] || dp[i - 1][j]

参考解析:LeetCode discussion区的大佬解法

class Solution {
    public boolean isMatch(String s, String p) {
        // 这里s和p可能都为空
        if (s == null || p == null) {
            return false;
        }

        int m = s.length(), n = p.length();
        boolean[][] dp = new boolean[m+1][n+1];
        dp[0][0] = true;
        char[] s_char = s.toCharArray();
        char[] p_char = p.toCharArray();


        // 初始化
        for (int i = 0; i < n; i++) {
            if (p_char[i] == '*') {
                // 相当于中间要跨一个格子,例如s=“”,p="a#b#"
                dp[0][i+1] = dp[0][i-1];
            }
        }

        // 开始dp
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                // 二者正常匹配,无#情况
                if (p_char[j] != '*') {
                    if (s_char[i] == p_char[j] || p_char[j] == '.') {
                        dp[i + 1][j + 1] = dp[i][j];
                    }
                    continue;
                }

                // 如果p[j]=='*',分情况讨论
                if (p_char[j - 1] != s_char[i] && p_char[j - 1] != '.') {
                    // *的前一个字符不匹配,那么只能当做匹配0个处理了
                    dp[i+1][j+1] = dp[i+1][j-1];
                } else {
                    // *的前一个字符和s[i]相匹配,那么可以匹配0个,1个,2个,3个...对于1个及以上的情况,我们可以将其规约到dp[i-1][j+1]里面,因为这次的的1个,2个,3个等于上次匹配的0个,1个,2个...
                    dp[i+1][j+1] = dp[i+1][j-1] || dp[i][j+1];
                }
            }
        }

        return dp[m][n];
    }
}

LeetCode 72. Edit Distance

Problem

LeetCode 72. Edit Distance

1. 题目简述

给出两个单词word1和word2,找出从word1变为word2的最少操作数。共计有如下几种操作:

  1. Insert a character

  2. Delete a character

  3. Replace a character

    Input: word1 = “horse”, word2 = “ros”
    Output: 3
    Explanation:
    horse -> rorse (replace ‘h’ with ‘r’)
    rorse -> rose (remove ‘r’)
    rose -> ros (remove ‘e’)

2. 算法思路

参考解法:花花酱LeetCode

也是一道经典老题目了,这种题和正则匹配类的题目基本上是类似的。等下写完本篇Blog再去做一下那道题,也很经典。LeetCode 10. Regular Expression Matching

这种题的做法其实并不复杂,复杂的是分情况讨论。因此实际上是hard难度的题目,真的很难想。

动态规划

我们假设两个字符串为“####a###”和”####b###”,“#”字符是什么我们不用去管,我们只知道我们比较到了一对不一样的字母,‘a’和‘b’,位置为i和j,此时,我们有三种解决方式:插入、删除、替换:

第一种,我们在word1中a的位置后插入一个b,让其和word2中的b相等,然后再去比较剩下的字符串;dp[i][j - 1]
第二种,我们删除word1中的a,然后继续比较word1和word2中剩下的字符;dp[i - 1][j]
第三种,我们将word1中的a替换成b,然后继续比较剩下的字符。dp[i - 1][j - 1]

我们应该取这三种选项中操作次数最少的。
那么我们如何把他用dp递推式的形式展现出来呢?我们假设dp[i][j]为word1的前i个字符转换成word2中前j个字符所需要的最少操作数。

$$dp[i][j] = 1 + min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1])$$

base case就是当比较的两个字符串其中一个为空时,另一个需要操作的次数等于它的长度,需要全部删除。

class Solution {
    // top - down解法
    int[][] dp;
    char[] w1, w2;
    public int minDistance(String word1, String word2) {
        int l1 = word1.length(), l2 = word2.length();
        dp = new int[l1 + 1][l2 + 1];
        w1 = word1.toCharArray();
        w2 = word2.toCharArray();
        for (int i = 0; i < dp.length; i++) {
            Arrays.fill(dp[i], -1);
        }

        return helper(l1, l2);
    }

    private int helper(int l1, int l2) {
        if (l1 == 0) {
            return l2;
        }
        if (l2 == 0) {
            return l1;
        }
        if (dp[l1][l2] != -1) {
            return dp[l1][l2];
        }

        if (w1[l1 - 1] == w2[l2 - 1]) {
            dp[l1][l2] = helper(l1 - 1, l2 - 1);
        } else {
            dp[l1][l2] = 1 + Math.min(Math.min(helper(l1 - 1, l2), helper(l1, l2 - 1)), helper(l1 - 1, l2 - 1));
        }

        return dp[l1][l2];
    }
}

LeetCode 494. Target Sum

Problem

LeetCode 494. Target Sum

1. 题目简述

给出一组非负整数nums,和一个目标数S,对每个数字前面我们可以添加正负号,求问一共有多少种添加正负号的方式使得整个数组加和为S。

注意:数组的长度不会超过20;整个数组的加和不会大于1000;返回值一定是32位整形数。

Input: nums is [1, 1, 1, 1, 1], S is 3. 
Output: 5
Explanation: 

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

There are 5 ways to assign symbols to make the sum of nums be target 3.

2. 算法思路

参考解法:花花酱LeetCode

这是一道极其经典的老题目了,target sum和0-1背包问题很是相像,并且其能够转化为0-1背包问题,这里一起记录一下整道题的思路吧。

暴力递归

首先,对于本题目,最容易想到的方式自然是暴力递归,DFS来遍历所有可能的分支,时间复杂度为O(2 ^ n),n为数组数字个数。这道题的输入大小没有超过20,所以2^20勉强在合格范围内,但是依然耗时很长。空间复杂度为O(n),n也是递归层数。

时间复杂度:O(2 ^ n),空间复杂度:O(n)。

class Solution {
    // recursion DFS
    public int findTargetSumWays(int[] nums, int S) {
        return findWays(nums, 0, nums.length - 1, S);
    }

    private int findWays(int[] nums, int start, int end, int target) {
        if (start > end) {
            return 0;
        }
        if (start == end && (nums[start] == target || nums[start] == 0 - target)) {
            if (target == 0) {
                return 2;
            }
            return 1;
        }

        return findWays(nums, start + 1, end, target - nums[start]) + findWays(nums, start + 1, end, target + nums[start]);
    }
}

DP 记忆化搜索

我们发现其实我们的重复计算有很多,因为其实对于第i个数而言,它就两种取值方式,正 or 负,前i - 1个数的所有取值种类我们只要算一次就好,不需要正负全算,因此加上一个memo数组,就可以用动态规划来解决了。

memo[i][j]:当前i个数字和为j时,从i(index为i,实际为第i+1)开始(包括i)后面数字所有排列组合满足target为S的可能性有多少种。注意! 这里的 j 并不是实际的和,因为我们的和有可能是负值,而下标不能为负,所以实际的和为j - offset,offset是nums所有元素的sum和。

base case:memo[0][offset] = 1。用0个元素,和为0(j - offset = 0)的方法有一种。

时间复杂度:O(sum * n),sum是整体数组的和,因为每个位置上的数字最多被填写一次。

空间复杂度:O(sum * n),sum是整体数组的和,大小为memo数组的大小。

class Solution {
    // DP top-down
    int[][] memo;
    public int findTargetSumWays(int[] nums, int S) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        memo = new int[nums.length][2 * sum + 1];

        for (int i = 0; i < memo.length; i++) {
            Arrays.fill(memo[i], Integer.MIN_VALUE);
        }

        return dfs(nums, 0, sum, S, 0);
    }

    private int dfs(int[] nums, int sum, int total, int S, int index) {
        if (index == nums.length) {
            if (sum == S) {
                return 1;
            } else {
                return 0;
            }
        } else {
            if (memo[index][sum + total] != Integer.MIN_VALUE) {
                return memo[index][sum + total];
            }

            int add = dfs(nums, sum + nums[index], total, S, index + 1);
            int substract = dfs(nums, sum - nums[index], total, S, index + 1);
            memo[index][sum + total] = add + substract;

            return memo[index][sum + total];
        }
    }
}

DP bottom-up

其实这道题用DP的bottom-up更符合思维一点,因为bottom-up才是正统的DP思想。

首先我们假设dp[i][j]为前i个数,和为j一共有多少种可能的方式。j有两种选择方式,一种是加上当前的第i个数,另一种是减去当前的第i个数,那么我们可以有如下递推式:

$$ dp[i][j] = dp[i - 1][j - nums[i]] + dp[i - 1][j + nums[i]] $$

这里要注意,我们的j有可能是负值,实际运算时注意要加上一个offset。

base case:dp[0][0] = 1

Attention:这里有两种递推方式,push和pull,前者是将自身的值,主动推到下一层,另一种是遍历当前值的时候向上一层去拉,二者思路上是一致的,但是需要注意的是写法,i+1还是i-1之类的.

class Solution {
    // DP bottom-up push
    int[][] dp;
    public int findTargetSumWays(int[] nums, int S) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        int offset = sum;

        // 注意边界条件的判断!!!
        if (sum < S) {
            return 0;
        }

        // 为什么这里要length+ 1而上面的top-down就不用呢?因为二者dp表示的含义根本就不同,没有可比性!!上面的top - down只是单纯的记录,而不是递推,需要边界case。
        dp = new int[nums.length + 1][2 * sum + 1];
        dp[0][offset] = 1;

        // 这里是push类型,由于会推到i+1层,所以这里i的上限要注意
        for (int i = 0; i < nums.length; i++) {
            // 这里j的遍历是因为我们需要对j-nums[i]和j+nums[i]进行查找,所以缩小一下范围,加速遍历过程。
            for (int j = nums[i]; j < 2 * sum + 1 - nums[i]; j++) {
                // 注意这里的dp方式,是推,不是拉!!!
                dp[i + 1][j + nums[i]] += dp[i][j];
                dp[i + 1][j - nums[i]] += dp[i][j];
            }
        }

        return dp[nums.length][S + offset];
    }
}
class Solution {
    // DP bottom-up pull
    int[][] dp;
    public int findTargetSumWays(int[] nums, int S) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        int offset = sum;

        // 注意边界条件的判断!!!
        if (sum < S) {
            return 0;
        }

        dp = new int[nums.length + 1][2 * sum + 1];
        dp[0][offset] = 1;

        // 这里是pull类型,因此从i = 1开始遍历,1层从0层去拉
        for (int i = 1; i <= nums.length; i++) {
            // 这里j的遍历要注意,我们不是从nums[i]开始计算,而是nums[i - 1],原因在于nums[i - 1]是第i个数,上一种push解法是从当前推以后,所以是nums[i]
            for (int j = 0; j < 2 * sum + 1; j++) {
                if (j - nums[i - 1] >= 0) {
                    dp[i][j] += dp[i - 1][j - nums[i - 1]];
                }
                if (j + nums[i - 1] < 2 * sum + 1) {
                    dp[i][j] += dp[i - 1][j + nums[i - 1]];
                }
            }
            /*这里给出一种错误写法,原因在于这次是向上去拉,拉的时候对于最底层的右边界情况来说,它应该拉的两个数中有一个是合法的,另一个是越界的,对于合法的那个我们应该去加,对于不合法的那个,我们不去加。如果按照下面的写法,则二者都不加了,肯定是错误的。*/
            // for (int j = nums[i - 1]; j < 2 * sum + 1 - nums[i - 1]; j++) {
            //     dp[i][j] += dp[i - 1][j - nums[i - 1]] + dp[i - 1][j + nums[i - 1]]
            // }
        }

        return dp[nums.length][S + offset];
    }
}

将问题转化为0-1背包问题

我们假设有P和Q两个集合,P中存放我们设置为正数的数字,Q中我们存放我们设置为负数的数字,因此我们有这样的推导:

已知:
sum(P) - sum(Q) = sum(nums)
sum(P) + sum(Q) = target
二者相加:
sum(P) - sum(Q) + sum(P) + sum(Q) = sum(nums) + target
2 * sum(P) = sum(nums) + target
sum(P) = (sum(nums) + target) / 2

因此,我们将问题转化成了找出所有的正数的集合P,使其的和为 (sum(nums) + target) / 2,这就转变成了一个0-1背包问题。

我们假设dp[i][j]表示前i个元素的子集可以加和为j的可能性有多少种。那么我们有如下递推式:

$$ dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]] $$

我们先用二维数组来表示,然后再将其降维成一维。

class Solution {
    public int findTargetSumWays(int[] nums, int S) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        if (sum < S) {
            return 0;
        }
        if ((S + sum) % 2 == 1) {
            return 0;
        }
        int target = (S + sum) / 2;

        int[][] dp = new int[nums.length + 1][target + 1];
        dp[0][0] = 1;

        for (int i = 1; i <= nums.length; i++) {
            for (int j = 0; j <= target; j++) {
                dp[i][j] += dp[i - 1][j];
                if (j - nums[i - 1] >= 0) {
                    dp[i][j] += dp[i - 1][j - nums[i - 1]];
                }
            }
        }

        return dp[nums.length][target];
    }
}

俺么如何把它降至一维呢?因为它每次计算的时候其实只和上一次计算的值有关,所以每次计算的时候,先copy一份出来做个备份,然后在这个备份上进行操作。

那么为什么直接在备份上操作呢?因为我们i是从小到大遍历的,i + 1里,和为j的可能组合一定包含了i中和为j的可能组合,我们不过是在i的基础上加了一个数而已。

class Solution {
    public int findTargetSumWays(int[] nums, int S) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        if (sum < S) {
            return 0;
        }
        if ((S + sum) % 2 == 1) {
            return 0;
        }
        int target = (S + sum) / 2;

        int[] dp = new int[sum + 1];
        dp[0] = 1;

        for (int num : nums) {
            // 注意这里初始化的方式!!!不要新建数组,然后指向dp
            int[] copy = Arrays.copyOfRange(dp, 0, dp.length);
            for (int j = 0; j <= target; j++) {
                if (j - num >= 0) {
                    dp[j] += copy[j - num];
                }
            }
            int y = 0;
        }

        // 注意,这里是target,而不是S!!!
        return dp[target];
    }
}

LeetCode 210. Course Schedule II

Problem

207. Course Schedule

1. 题目简述

一共有n门课程需要修,分别是从0到n-1。有些课程需要进行先修课程,我们以数组的形式给出,例如:[0, 1],就是说如果要修课,必须要上过1课。

现在给出numCourses门课程,和一个prerequisites数组,求给出一种拓扑排序的方式。

2. 算法思路

参考解法:花花酱LeetCode

LeetCode 207. Course Schedule类似,基本一致,这里不再赘述,直接上解析。

BFS

class Solution {
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        // BFS
        int[] inDegree = new int[numCourses];
        Stack<Integer> zeroDegreeVertex = new Stack();
        int res[] = new int[numCourses];
        ArrayList<Integer>[] relations = new ArrayList[numCourses];

        for (int i = 0; i < numCourses; i++) {
            relations[i] = new ArrayList();
        }

        for (int[] prerequisite : prerequisites) {
            inDegree[prerequisite[0]]++;
            relations[prerequisite[1]].add(prerequisite[0]);
        }

        for (int i = 0; i < numCourses; i++) {
            if (inDegree[i] == 0) {
                zeroDegreeVertex.push(i);
            }
        }

        int index = 0;
        while (!zeroDegreeVertex.isEmpty() && index != numCourses) {
            res[index] = zeroDegreeVertex.pop();
            for (int vertex : relations[res[index]]) {
                inDegree[vertex]--;
                if (inDegree[vertex] == 0) {
                    zeroDegreeVertex.push(vertex);
                }
            }
            index++;
        }

        if (index != numCourses) {
            return new int[0];
        }

        return res;
    }
}

DFS + backtracking

DFS优化

LeetCode 207. Course Schedule

Problem

LeetCode 207. Course Schedule

1. 题目简述

一共有n门课程需要修,分别是从0到n-1。有些课程需要进行先修课程,我们以数组的形式给出,例如:[0, 1],就是说如果要修课,必须要上过1课。

现在给出numCourses门课程,和一个prerequisites数组,问是否能有一种修课方式,能够使得孙俪修完n门课程。

2. 算法思路

参考解法:花花酱LeetCode

也是一道经典老题目了,拓扑排序的经典问题,这里这道题我们使用BFS,下面这道类似的题目我们来使用DFS,体会一下差别。。LeetCode 210. Course Schedule II

BFS

其实所谓的prerequesites数组就是一个有向图,我们需要做的是在这个有向图中寻找是否存在环,如果存在环,则说明不可能存在一个拓扑排序;如果不存在环,则说明存在拓扑排序。

那么我们如何去查找环呢?两种做法,DFS和BFS,这里我们使用BFS吧。

BFS寻找拓扑排序的思路如下:

1. 统计整张图所有vertex的入度(indegree);
2. 找出入度为0的点,放入一个queue或者stack中,无所谓;
3. pop或者poll出一个节点作为当前节点,然后将该节点所指向的节点的入度更改,然后将新的入度为0的顶点添加到栈顶或者队列后;
4. 重复2和3的操作,直至stack或者queue为空,如果已经遍历完所有节点,那么我们pop的顺序就是一种拓扑序。

这里只是找是否存在,所以没必要存当前的拓扑序,代码如下:


class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        ArrayList<Integer>[] graph = new ArrayList[numCourses];
        for (int i = 0; i < graph.length; i++) {
            graph[i] = new ArrayList();
        }
        int[] indegree = new int[numCourses];
        for (int[] prerequisite: prerequisites) {
            indegree[prerequisite[0]]++;
            graph[prerequisite[1]].add(prerequisite[0]);
        }

        Queue<Integer> zeroIndegrees = new LinkedList();
        for (int i = 0; i < numCourses; i++) {
            if (inDegree[i] == 0) {
                zeroIndegrees.add(i);
            }
        }

        List<Integer> topoSort = new ArrayList();
        while (!zeroIndegrees.isEmpty()) {
            int temp = zeroIndegrees.poll();
            topoSort.add(temp);
            // 减少所有它指向的节点的入度
            for (int vertex : graph[temp]) {
                indegree[vertex]--;
                if (indegree[vertex] == 0) {
                    zeroIndegrees.add(vertex);
                }
            }
        }

        return (topoSort.size() == numCourses);
    }
}

DFS

对于DFS而言,其实也有多种DFS的方式,例如 DFS + 回溯,我们可以每次从不同节点开始访问,每个节点有两种状态,访问和未访问。然后从0到n-1进行DFS遍历,每次退出的时候将节点重置为false。为什么要这样做呢?因为如果我们不将其重置为false,DFS到某节点若是已访问的状态,我们不知道它是在当前DFS的访问路径中还是说已经完全访问完毕的状态,这样做会导致大量的重复计算,如下实例,可以自行画图查看一下。

n = 4
prerequisites = [[1, 0], [1, 3], [2, 1]]

时间复杂度:O(E+ V^2);E是构建图的时间,V ^ 2是因为最坏情况下是一条线性的课程依赖关系,所以最坏是V ^ 2的复杂度。

空间复杂度:O(V+ E);构建图是O(V + E),isVisited变量是O(V),最坏情况下我们需要调用V层栈来储存我们递归函数,因此整体是O(3V + E),也就是O(v + E)。

class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        ArrayList<Integer>[] graph = new ArrayList[numCourses];
        boolean[] isVisited = new boolean[numCourses];
        for (int i = 0; i < numCourses; i++) {
            graph[i] = new ArrayList();
        }
        
        // build the whole graph
        for (int[] prerequisite: prerequisites) {
            graph[prerequisite[1]].add(prerequisite[0]);
        }
        
        //DFS确认是否有环
        for (int i = 0; i < numCourses; i++) {
            if (backtrack(graph, isVisited, i)) {
                return false;
            }
        }
        
        return true;
    }
    
    private boolean backtrack(ArrayList<Integer>[] graph, boolean[] isVisited, int vertex) {
        if (isVisited[vertex]) {
            return true;
        } else {
            isVisited[vertex] = true;
            for (int next : graph[vertex]) {
                if (backtrack(graph, isVisited, next)) {
                    return true;
                }
            }
            isVisited[vertex] = false;
            return false;
        }
    } 
}

DFS优化

看到上面的解,我们发现会有很多重复的计算,我们真的需要通过回溯来重置状态么?其实并不是这样,只要我们加一个visiting的状态即可,和unvisited分开,如果发现访问的节点是visiting状态,则说明有环,退出;若是visited,说明完全没问题,当前节点记录后直接返回;unvisited的话就正常DFS,将节点设为visiting再继续。最终结束的时候将此节点设为visited。

class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        ArrayList<Integer>[] graph = new ArrayList[numCourses];
        int[] isVisited = new int[numCourses];
        Arrays.fill(isVisited, -1);// -1 unvisited, 0 visiting, 1 visited
        for (int i = 0; i < numCourses; i++) {
            graph[i] = new ArrayList();
        }

        // build the whole graph
        for (int[] prerequisite: prerequisites) {
            graph[prerequisite[1]].add(prerequisite[0]);
        }

        //DFS确认是否有环
        for (int i = 0; i < numCourses; i++) {
            if (dfs(graph, isVisited, i)) {
                return false;
            }
        }

        return true;
    }

    // 有环return true,无环return false
    private boolean dfs(ArrayList<Integer>[] graph, int[] isVisited, int vertex) {
        if (isVisited[vertex] == 1) {
            return false;
        } else if (isVisited[vertex] == 0) {
            return true;
        } else {
            isVisited[vertex] = 0;
            for (int next : graph[vertex]) {
                if (dfs(graph, isVisited, next)) {
                    return true;
                }
            }
            isVisited[vertex] = 1;
            return false;
        }
    }
}

LeetCode 973. K Closest Points to Origin

Problem

LeetCode 973. K Closest Points to Origin

1. 题目简述

给出某二维平面上的一些坐标,例如[1, 2],找出这些坐标中距离原点最近的K个节点。例如:

Input: points = [[1,3],[-2,2]], K = 1
Output: [[-2,2]]

Note:

1 <= K <= points.length <= 10000
-10000 < points[i][0] < 10000
-10000 < points[i][1] < 10000

2. 算法思路

这道题难倒是不难,思路也很明确,其实主要是考察数据结构的熟悉程度以及API的调用,否则写起来很麻烦。

暴力解法

首先我们需要记录每个节点与原点的距离,然后进行排序,找出前k个,并把这k个的对应的pair找出来。

用最笨的办法的话就是用个map记录一下distance和对应的index之间的关系,然后排序结束以后找出前k的distance,然后再从map中找出index,再进行遍历。这个方法很麻烦,而且耗时长。

使用Arrays.sort方法自定义排序

我们知道Arrays.sort函数可以默认排序数字类型,但是其更广泛的用法其实是自定义排序任意类型,这点和Collections.sort一个道理。

详情见:Java知识点记录——第一项

使用PriorityQueue自定义比较方式

同上,只不过queue也是一种List,可以用Comparator来自定义比较方式从而初始化一个小顶堆。这里有两种方式,多种写法。

分治法

找top k问题的常见套路,写一个helper函数来找一个合适的mid,其实并不需要全局有序,只要部分有序就可以了。

3. 解法

暴力解法代码

class Solution {
    public int[][] kClosest(int[][] points, int K) {
        Map<Integer, List<Integer>> distanceMap = new HashMap();
        List<Integer> distances = new ArrayList();

        for (int i = 0; i < points.length; i++) {
            int distance = points[i][0] * points[i][0] + points[i][1] * points[i][1];
            distances.add(distance);
            if (!distanceMap.containsKey(distance)) {
                distanceMap.put(distance, new ArrayList<Integer>());
            }
            distanceMap.get(distance).add(i);
        }

        Collections.sort(distances);

        int[][] res = new int[K][2];
        int count = 0, index = 0;
        while (count < K) {
            int target = distances.get(index);
            List<Integer> targetIndexes = distanceMap.get(target);
            for (int i = 0; i < targetIndexes.size(); i++) {
                int x = targetIndexes.get(i);
                res[count][0] = points[x][0];
                res[count][1] = points[x][1];
                count++;
            }
            index++;
        }

        return res;
    }
}

使用Arrays.sort()

class Solution {
    // 解法1
    public int[][] kClosest(int[][] points, int K) {
        int[][] res = new int[K][2];
        int index = 0;

        Arrays.sort(points, (point1, point2) -> point1[0] * point1[0] + point1[1] * point1[1] - point2[0] * point2[0] - point2[1] * point2[1]);

        while (index < K) {
            res[index] = points[index];
            index++;
        }

        return res;
    }

    // 解法2
    public int[][] kClosest(int[][] points, int K) {
        Arrays.sort(points, Comparator.comparing(p -> p[0] * p[0] + p[1] * p[1]));
        return Arrays.copyOfRange(points, 0, K);
    }
}

使用PriorityQueu+Comparator

class Solution {
    public int[][] kClosest(int[][] points, int K) {
        // 要大顶堆,所以返回p2 - p1
        PriorityQueue<int[]> queue = new PriorityQueue<int[]>((p1, p2) -> p2[0] * p2[0] + p2[1] * p2[1] - p1[0] * p1[0] - p1[1] * p1[1]);

        for (int[] p : points) {
            queue.offer(p);
            if (queue.size() > K) {
                queue.poll();
            }
        }

        int[][] res = new int[K][2];
        while (K > 0) {
            res[--K] = queue.poll();
        }

        return res;
    }

    public int[][] kClosest(int[][] points, int K) {
        // 要大顶堆,所以返回p2 - p1
        PriorityQueue<int[]> queue = new PriorityQueue<int[]>((p1, p2) -> p1[0] * p1[0] + p1[1] * p1[1] - p2[0] * p2[0] - p2[1] * p2[1]);

        queue.addAll(Arrays.asList(points));

        int[][] res = new int[K][2];
        while (K > 0) {
            res[--K] = queue.poll();
        }

        return res;
    }
}

使用分治法找top k


class Solution {
    public int[][] kClosest(int[][] points, int K) {
        int l = 0, r = points.length - 1,

        while (l <= r) {
            int mid = helper(points, l, r);
            if (mid == K) {
                break;
            } else if (mid < K) {
                l = mid + 1;
            } else if (mid > K) {
                r = mid - 1;
            }
        }

        return Arrays.copyOfRange(points, 0, K);
    }

    private int helper(int[][] points, int l, int r) {
        int[] pivot = points[l];
        while (l < r) {
            while (l < r && compare(points[r], pivot)) {
                r--;
            }
            points[l] = points[r];
            while (l < r && compare(pivot, points[l])) {
                l++;
            }
            points[r] = points[l];
        }
        points[l] = pivot;
        return l;
    }

    // p1是否大于p2
    private boolean compare(int[] p1, int[] p2) {
        return (p1[0] * p1[0] + p1[1] * p1[1] - p2[0] * p2[0] - p2[1] * p2[1]) >= 0;
    }
}

LeetCode 14. Longest Common Prefix

Problem

LeetCode 14. Longest Common Prefix

1. 题目简述

给出一组字符串,找出这些字符串的最长common前缀。

2. 算法思路

水平比较

首先,最容易想到的思路就是从前往后,一个个进行比较找common suffix,直到找到最后一个字符串。

这种方法时间复杂度为O(S),S是所有字符串的字符数总和,最坏情况就是所有字符串相等,一直比较下去。

垂直比较

垂直的意思是从第一位开始比较,比较所有的字符串的第一位,如果相同,则比较下一位;如果不同或者到达某字符串的最大长度,则返回当前的suffix。

这种方法时间复杂度为O(S),S是所有字符串的字符数总和,最坏情况就是所有字符串相等,一直比较下去。

分治法

可以将原本的数组不断拆分成子数组,然后获取子数组的最长公共前缀,然后再进行合并。

时间复杂度为O(S),空间复杂度为O(m * logn)。空间复杂度是因为我们需要调用函数logn词,每次空间为m。

3. 解法

水平比较


public class Solution {
    public String longestCommonPrefix(String[] strs) {
        if (strs.length == 0) return "";
        String prefix = strs[0];
        for (int i = 1; i < strs.length; i++)
            while (strs[i].indexOf(prefix) != 0) {
                prefix = prefix.substring(0, prefix.length() - 1);
                if (prefix.isEmpty()) return "";
            }
        return prefix;
    }
}

垂直比较

public class Solution {
    public String longestCommonPrefix(String[] strs) {
        if (strs == null || strs.length == 0) return "";
        for (int i = 0; i < strs[0].length() ; i++){
            char c = strs[0].charAt(i);
            for (int j = 1; j < strs.length; j ++) {
                if (i == strs[j].length() || strs[j].charAt(i) != c)
                    return strs[0].substring(0, i);             
            }
        }
        return strs[0];
    }
}

分治法

class Soluton{
    public String longestCommonPrefix(String[] strs) {
        if (strs == null || strs.length == 0) return "";    
            return longestCommonPrefix(strs, 0 , strs.length - 1);
    }

    private String longestCommonPrefix(String[] strs, int l, int r) {
        if (l == r) {
            return strs[l];
        }
        else {
            int mid = (l + r)/2;
            String lcpLeft =   longestCommonPrefix(strs, l , mid);
            String lcpRight =  longestCommonPrefix(strs, mid + 1,r);
            return commonPrefix(lcpLeft, lcpRight);
    }
    }

    String commonPrefix(String left,String right) {
        int min = Math.min(left.length(), right.length());       
        for (int i = 0; i < min; i++) {
            if ( left.charAt(i) != right.charAt(i) )
                return left.substring(0, i);
        }
        return left.substring(0, min);
    }
}

LeetCode 28. Implement strStr()

Problem

LeetCode 28. Implement strStr()

1. 题目简述

给出两个字符串haystack和needle,找出needle在haystack中出现的首次位置,如果没有出现则返回-1。needle为空的时候返回0,例如:

Example 1:

Input: haystack = "hello", needle = "ll"
Output: 2

Example 2:

Input: haystack = "aaaaa", needle = "bba"
Output: -1

2. 算法思路

2 pointers暴力搜索

两个指针分别指向s1和s2,如果遇到不匹配,则将s2的指针重置,s1指针向后移动一位。

KMP算法

KMP算法的详解可以参考:

  1. 花花酱的KMP算法视频
  2. 某大佬的blog

或者这篇字符串汇总的Blog中:传送门

3. 解法

2 pointers

class Solution {
    public int strStr(String haystack, String needle) {
        int h = haystack.length(), n = needle.length();
        if (n == 0) {
            return 0;
        }
        if (n > h) {
            return -1;
        }

        int p1 = 0, p2 = 0;
        while (p1 < h - n + 1) {
            int currLen = 0;
            while (p1 < h && p2 < n && haystack.charAt(p1) == needle.charAt(p2)) {
                p1++;
                p2++;
                currLen++;
            }

            if (currLen == n) {
                return p1 - n;
            } else {
                p1 = p1 - currLen + 1;
                p2 = 0;
            }
        }

        return -1;
    }
}

KMP字符型匹配算法

class Solution {
    public int strStr(String haystack, String needle) {
        if (needle.length() == 0) {
            return 0;
        }

        int[] next = buildKMPTable(needle);
        for (int i = 0, j = 0; i < haystack.length(); i++) {
            while (j > 0 && haystack.charAt(i) != needle.charAt(j)) {
                j = next[j];
            }
            if (haystack.charAt(i) == needle.charAt(j)) {
                j++;
            }
            if (j == needle.length()) {
                // 这里不一定是要返回,也可以继续搜索,直接记录个什么数,然后j继续跳回去
                return i - needle.length() + 1;
            }
        }
        return -1;
    }

    // 这个是大重点!!!KMP表的实现
    private int[] buildKMPTable(String s) {
        int length = s.length();
        int[] next = new int[length + 1];
        for (int i = 1, j = 0; i < length; i++) {
            while (j > 0 && s.charAt(i) != s.charAt(j)) {
                j = next[j];
            }
            if (s.charAt(i) == s.charAt(j)) {
                j++;
            }
            next[i + 1] = j;
        }
        return next;
    }
}

LeetCode 647. Palindromic Substrings

Problem

LeetCode 647. Palindromic Substrings

1. 题目简述

找出一个字符串的所有回文子字符串数量。例如:

Input: "aaa"
Output: 6
Explanation: Six palindromic strings: "a", "a", "a", "aa", "aa", "aaa".

2. 算法思路

思路和LeetCode 5是一样的,都是找回文子字符串,只不过5是找最长的回文子字符串,这里是找所有回文子字符串的数量。

其实就是相当于每次找到回文字符串的时候count++就OK了,记得在dp[i][i]赋值的时候+1。

Dynamic Programming

“LeetCode 5”是一样的解析,这里不多做解释了。

中心扩散法

这里同理,注意count++的时机。

3. 解法

Dynamic Programming bottom-up


public class Solution {
    public int countSubstrings(String s) {
        if (s.length() <= 1) {
            return s.length();
        }

        int n = s.length(), count = 0;
        boolean[][] dp = new boolean[n][n];

        for (int i = n - 1; i >= 0; i--) {
            dp[i][i] = true;
            count++;
            for (int j = i + 1; j < n; j++) {
                if (s.charAt(i) == s.charAt(j) && ((j - i <= 2 )|| dp[i + 1][j - 1])) {
                    dp[i][j] = true;
                    count++;
                }
            }
        }

        return count;
    }
}

中心扩散法

public class Solution {
    public int countSubstrings(String s) {
        if (s.length() <= 1) {
            return s.length();
        }

        int n = s.length(), count = 0;
        for (int i = 0; i < n; i++) {
            count += findPalindrome(s, i, i) + findPalindrome(s, i , i+ 1);
        }

        return count;
    }

    private int findPalindrome(String s, int l, int r) {
        int count = 0;
        while (l >=0 && r < s.length() && s.charAt(l) == s.charAt(r)) {
            count++;
            l--;
            r++;
        }
        return count;
    }
}

LeetCode 516. Longest Palindromic Subsequence

Problem

LeetCode 516. Longest Palindromic Subsequence

1. 题目简述

找出一个字符串的最长回文子序列。例如:

Example 1:
Input:

"bbbab"
Output:
4
One possible longest palindromic subsequence is "bbbb".

2. 算法思路

和它相似的题目还有两道,LeetCode 5 最长回文子字符串和LeetCode 647 回文子字符串。

这里我们要注意,这里是子序列,而不是子字符串,子字符串是要求连续的,而子序列不是。

Dynamic Programming

对于求子序列的题目,而且还是最大值的,我们第一反应肯定还是DP,思路和 LeetCode 5 最长回文子字符串 不同的是,dp策略有所改变。

我们回忆一下,对于5,我们只考虑dp[i][j]的时候,如果s[i] == s[j],需要考虑dp[i + 1][j - 1]是否是回文字符串,dp[i][j]表示i到j是否是回文子字符串;那么对于这道题来说,如果s[i] == s[j],则需要将dp[i + 1][j - 1] + 2,其中dp[i][j]表示i到j的最长回文子序列的长度。

那么如果s[i] != s[j]时,对于5来说,dp[i][j]直接为false;而对于本题来说,是一个int值,dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])。

base case的情况是dp[i][i] = 1,不需要考虑j - i之类的,因,如果对于j - i = 2的case来说,dp[i + 1][j - 1] = dp[i + 1][i + 1] = 1,可以正常计算的,不受影响;如果 j - i = 1,那么说明两个数挨着,dp[i + 1][j - 1]看似是不合法的,但实际上是0,直接加2也是OK的,所以边界情况无需再考虑。

所以递推式如下

\begin{equation}
dp[i][j] = dp[i + 1][j - 1] + 2 \text{ if } s[i] = s[j]
\end{equation}

\begin{equation}
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]) \text{ if } s[i] \neq s[j]
\end{equation}

遍历顺序依旧按照自身喜好来,但是这里依然是和5做同样选择,因为递推式的前项依赖的需求。

时间复杂度为O(n ^ 2),空间复杂度也是O(n ^ 2)

补充

这里用中心扩散法就明显行不通了,因为是非连续的,怎么扩散嘛,没辙。

3. 解法

Dynamic Programming bottom-up


class Solution {
    public int longestPalindromeSubseq(String s) {
        if (s.length() <= 1) {
            return s.length();
        }

        int length = s.length(), longest = 1;
        int[][] dp = new int[length][length];

        for (int i = length - 1; i >= 0; i--) {
            dp[i][i] = 1;
            for (int j = i + 1; j < length; j++) {
                if (s.charAt(i) == s.charAt(j)) {
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                    longest = Math.max(longest, dp[i][j]);
                } else {
                    dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
                }
            }
        }

        return longest;
    }
}

LeetCode 5. Longest Palindromic Substring

Problem

LeetCode 5. Longest Palindromic Substring

1. 题目简述

找出一个字符串的最长回文子字符串。例如:

Input: "babad"
Output: "bab"
Note: "aba" is also a valid answer.

2. 算法思路

一道经典老题目了,也不分析了,直接背答案吧,和它相似的题目还有两道,“LeetCode 516 最长回文子序列”“LeetCode 647 回文子字符串”

Dynamic Programming

首先,对于这种求最值的问题,第一反应就应该是DP,确实这道题是一个经典的斜着DP的题目。

我们定义dp[i][j]表示从index为i到j的子字符串是否是回文序列。

base case是当i == j时,dp[i][j] = true;对于其他的任意一对i和j,如果s[i]和s[j]相同,则只要dp[i + 1][j - 1]为true,dp[i][j]就为true,或者 j - i <= 2 也可满足要求,若 j-i == 1,则是两个相邻的字符,类似于“aa”;如果j - i == 2,则有可能是“aba”这种类型的字符串,也是回文字符串。故有如下递推式

\begin{equation}
dp[i][j] = dp[i + 1][j - 1] || j - i \leq 2 \text{ if } s[i] = s[j]
\end{equation}

\begin{equation}
dp[i][j] = false \text{ if } s[i] \neq s[j]
\end{equation}

遍历顺序根据自身喜好来,注意j是要大于i的,所以整张dp表只有一半会被遍历到。而且要注意,当计算dp[i][j]的时候,dp[i + 1][j - 1]必须是已经计算完毕的了,因此建议i从后向前计算,也就是从n - 1开始,到0。然后每次j从i + 1到n - 1.

时间复杂度为O(n ^ 2),空间复杂度也是O(n ^ 2)

中心扩散法

对于每个回文序列来说,它的特性是镜像对称的,也就是说它存在一个中心点(长度为奇数)或者中轴(长度为偶数)。

对于0到n - 1中任意一个字符i,它可能是中心点,也可能是在某中轴的左右侧。如果他是中心点,那么从i分别向左右扩散,会找到以i为中心的回文子串,直到两端或者不匹配;如果中轴刚好在它的右侧,那么从i向左和i+1向右出发(包括i和i+1),逐渐找到回文字符串。

3. 解法

Dynamic Programming bottom-up


public class Solution {
    public String longestPalindrome(String s) {
        if (s.length() <= 1) {
            return s;
        }

        int n = s.length(), longest = 0, start = 0, end = 0;
        boolean[][] dp = new boolean[n][n];

        for (int i = n - 1; i >= 0; i--) {
            dp[i][i] = true;
            for (int j = i + 1; j < n; j++) {
                if (s.charAt(i) == s.charAt(j) && ((dp[i + 1][j - 1]) || (j - i <= 2))) {
                    dp[i][j] = true;
                    if (j - i + 1 > longest) {
                        start = i;
                        end = j;
                        longest = j - i + 1;
                    }
                }
            }
        }

        return s.substring(start, end + 1);
    }
}

中心扩散法

public class Solution {
    public String longestPalindrome(String s) {
        if (s.length() <= 1) {
            return s;
        }

        int longest = 0, start = 0;

        for (int i = 0; i < s.length(); i++) {
            int currentLongest = Math.max(findPalindrome(s, i, i),findPalindrome(s, i, i + 1));
            if (currentLongest > longest) {
                longest = currentLongest;
                start = i - (longest - 1)/ 2;
            }
        }

        return s.substring(start, start + longest);

    }

    private int findPalindrome(String s, int l, int r) {
        while (l >=0 && r < s.length() && s.charAt(l) == s.charAt(r)) {
            l--;
            r++;
        }
        return r - l - 1;
    }
}

字符串总结

String总结

String常用api

方法 String
String concat(String str): 将指定字符串连接到此字符串的结尾。(不建议使用)
字符串不能随便删,trim除外,放到下面部分了。
char[] toCharArray(): 将此字符串转换为一个新的字符数组。

String substring(int beginIndex): 返回一个新的字符串,从beginIndex到结尾,它是此字符串的一个子字符串。

String substring(int beginIndex, int endIndex): 返回一个新的字符串,下标从beginIndex到endIndex - 1,是一个左闭右开的区间,它是此字符串的一个子字符串。

String trim(): 返回字符串的副本,忽略前导空白和尾部空白。

String toUpperCase(): 将此 String 中的所有字符都转换为大写。

String toLowerCase(): 将此 String 中的所有字符都转换为小写。

String replace(char oldChar, char newChar): 返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。

String replaceAll(String regex, String replacement): 使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。

**String replaceFirst(String regex, String replacement):**使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。
int length(): 返回此字符串的长度。int indexOf(int ch)

char charAt(int index):返回指定索引处的 char 值

int indexOf(int ch, int fromIndex): 返回在此字符串中第一次出现指定字符处的索引,从指定的索引开始搜索(包含),没有则返回-1。

int indexOf(String str): 返回指定子字符串在此字符串中第一次出现处的索引,没有则-1。

int indexOf(String str, int fromIndex): 返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始,没有则-1。

int lastIndexOf(int ch): 返回指定子字符在此字符串中最右边出现处的索引,没有则-1。

int lastIndexOf(int ch, int fromIndex): 返回指定字符在此字符串中最后一次出现处的索引,从指定的索引处开始进行反向搜索,没有则-1。

int lastIndexOf(String str): 返回指定子字符串在此字符串中最右边出现处的索引,没有则-1。

int lastIndexOf(String str, int fromIndex): 返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索,没有则-1。

boolean startsWith(String prefix, int toffset): 测试此字符串从指定索引开始的子字符串是否以指定前缀开始,offset是空出字符的数量,也是开始查找的index。

boolean startsWith(String prefix): 测试此字符串是否以指定的前缀开始。

String经典算法

KMP算法

KMP算法是用于字符串匹配的经典算法,但是实现起来比较复杂,故不推荐使用而已,它的最坏时间复杂度是O(m + n),m和n分别是待匹配字符串和目标字符串的长度。

KMP算法解释起来很麻烦,可以结合以下资料来观看(因为懒得画图,这里需要很土图示来解释):

  1. 花花酱的KMP算法视频
  2. 某大佬的blog

其实它的核心思想就是对于某次错误的字符串匹配后,不要再进行重新从头匹配,而是跳转到某个合适的位置。例如:

s1: "aaaabababba"
s2: "ababb"
希望在s1中找到s2首次出现的位置

假设我们某次匹配到了s1字符串的子串,aaaabababba,和s2的(ababb)只差了一位,按照传统做法,我们这时候会将s1的指针右移一位,然后重新开始匹配。

但是我们细心一点可以发现,s1红色的那个子串,最后不匹配的a的前面有ab,而ab又恰好是s2的开头两个字符,也就是说我们可以从s2的第三位开始比较起来,也就是说我们下一个希望比较的s1的子字符串是aaaabababba,我们可以发现刚好符合条件;如果使用老的方法则是将s1的pointer右移一位,比较babab和s2,KMP算法可以直接跨越这一步。这就是KMP算法的核心思想。

KMP算法实现:

LeetCode 28. Implement strStr()

String经典题目汇总

回文系列

  1. 125. Valid Palindrome(无解析)
  2. 最长回文子字符串
  3. “LeetCode 516 最长回文子序列”
  4. “LeetCode 647 回文子字符串数”

最长common前缀

  1. LeetCode 14. Longest Common Prefix

翻转字符串

  1. LeetCode 344. Reverse String(无解析)

LeetCode 338. Counting Bits

Problem

LeetCode 338. Counting Bits

1. 题目简述

给出一个非负整数num,计算从0到num每个数字二进制表示中1的个数,并以数组形式返回。

Input: 5
Output: [0,1,1,2,1,2]

2. 算法思路

Brute Force

首先最暴力的解法肯定是每次向右移动一位,计算当前数字除以2的余数是多少,然后加和。

暴力解法(进阶版)

比如说对于32,二进制表示为“100000”,需要移动5次,计算6次,这很明显是不合理的嘛,因此就用到了一个十分巧妙的方法去掉末尾的0。

$$n = n & (n - 1)$$

以32举例,31的二进制表示是“11111”,和32做&操作后,n变为0,将从右至左第一个1给消除掉了,减少了很多不必要的运算。

Dynamic Programming

这种方法比较巧妙,我们其实可以找规律。我们假设从1找到8的二进制写法:

0 ->    0
1 ->    1
2 ->   10
3 ->   11
4 ->  100
5 ->  101
6 ->  110
7 ->  111
8 -> 1000

我们找一下规律,从4到7,不外乎就是把0、1、2、3四个数的二进制数前面加个1,多了一个1;我们可以想象,从8到15,也就是比从0到7每个数多了一个1,因此我们用一个lower变量表示比8小的每个数的1的个数,当到达下一个节点时,更新lower为1.

3. 解法

Dynamic Programming


class Solution {
    public int[] countBits(int num) {
        int pow = 1;
        int lower = 1;
        int[] res = new int[num + 1];
        Arrays.fill(res, 0);

        for (int i = 1; i <= num; i++) {
            if (i == pow) {
                pow = pow << 1;
                res[i] = 1;
                lower = 1;
            } else {
                res[i] = res[lower] + 1;
                lower++;
            }
        }

        return res;
    }
}

暴力解法进阶版:

class Solution {
    public int[] countBits(int num) {
        int[] result = new int[num + 1];
        for (int i = 1; i <= num; i++) {
            result[i] = fastCountBits(i);
        }
        return result;
    }

    private int fastCountBits(int num) {
        int i;

        for (i = 0; num != 0; i++) {
            num = num & (num - 1);
        }

        return i;
    }
}

LeetCode 138. Copy List with Random Pointer

Problem

LeetCode 138. Copy List with Random Pointer

1. 题目简述

给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。

要求返回这个链表的 深拷贝。

我们用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:

val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为  null 。

Examples:

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

2. 算法思路

哈希表(要绕个弯)

首先,这道题的难点在于那个random指针,我们根本无从知晓这个random究竟会指向哪个节点,究竟是已经被我们创建还是未被我们创建。而且每个节点的value值与自己的index顺序也不是对应的,所以不能用传统的数组寻址的方式,而是hashmap。

对于每个节点,我们用原始的node作为key,新建的node作为value,这样,我们只需要先从前向后遍历一次创建所有节点,再从前向后将所有的next和random指向对应节点即可。

O(1)空间复杂度的方法

可以对原链表进行修改,解法见:传送门

3. 解法

HashMap


/*
// Definition for a Node.
class Node {
    int val;
    Node next;
    Node random;

    public Node(int val) {
        this.val = val;
        this.next = null;
        this.random = null;
    }
}
*/

class Solution {
    public Node copyRandomList(Node head) {
        Map<Node, Node> clone = new HashMap();
        Node ptr = head;

        // create all nodes
        while (ptr != null) {
            clone.put(ptr, new Node(ptr.val));
            ptr = ptr.next;
        }

        // make the next and random pointer to the pright place
        ptr = head;
        while (ptr != null) {
            Node node = clone.get(ptr);
            node.next = clone.get(ptr.next);
            node.random = clone.get(ptr.random);
            ptr = ptr.next;
        }

        return clone.get(head);

    }
}

LeetCode 133. Clone Graph

Problem

LeetCode 138. Copy List with Random Pointer

1. 题目简述

给出无向连通图中一个节点的引用,请你返回该图的 深拷贝(克隆)。

图中的每个节点都包含它的值 val(int) 和其邻居的列表(list[Node])。

class Node {
    public int val;
    public List<Node> neighbors;
}

测试用例格式:

简单起见,每个节点的值都和它的索引相同。例如,第一个节点值为 1(val = 1),第二个节点值为 2(val = 2),以此类推。该图在测试用例中使用邻接列表表示。

邻接列表 是用于表示有限图的无序列表的集合。每个列表都描述了图中节点的邻居集。

给定节点将始终是图中的第一个节点(值为 1)。必须将 给定节点的拷贝 作为对克隆图的引用返回。

Examples:
Example

2. 算法思路

这道题和LeetCode 138. Copy List with Random Pointer是基本一致的,稍微有所变形,而且涉及到DFS和BFS。

目前的困境是一样的,我们每次在创建节点后,对neighbor进行遍历时,无法直接找到对应的节点,依然用hashMap来存储是比较直接的方式。

哈希表+DFS

哈希表+BFS

双队列+DFS

O(1)空间复杂度的方法

3. 解法

HashMap + DFS


/*
// Definition for a Node.
class Node {
    public int val;
    public List<Node> neighbors;

    public Node() {
        val = 0;
        neighbors = new ArrayList<Node>();
    }

    public Node(int _val) {
        val = _val;
        neighbors = new ArrayList<Node>();
    }

    public Node(int _val, ArrayList<Node> _neighbors) {
        val = _val;
        neighbors = _neighbors;
    }
}
*/

class Solution {
    Map<Node, Node> clone = new HashMap();

    public Node cloneGraph(Node node) {
        if (clone.containsKey(node)) {
            return clone.get(node);
        }

        Node newNode = new Node(node.val);
        clone.put(node, newNode);

        for (Node neighbor : node.neighbors) {
            newNode.neighbors.add(cloneGraph(neighbor));
        }

        return clone.get(node);
    }
}

HashMap + BFS

class Solution {
    public Node cloneGraph(Node node) {
        if (node == null) {
            return node;
        }

        Map<Node, Node> clone = new HashMap();
        Node newNode = new Node(node.val);
        Queue<Node> queue = new LinkedList();
        queue.offer(node);
        clone.put(node, newNode);

        while (!queue.isEmpty()) {
            Node temp = queue.poll();
            for (Node neighbor : temp.neighbors) {
                if (!clone.containsKey(neighbor)) {
                    clone.put(neighbor, new Node (neighbor.val));
                    queue.offer(neighbor));
                }
                clone.get(temp).neighbors.add(clone.get(neighbor));
            }
        }

        return newNode;
    }
}

双Queue


   // 这种双queue的做法一开始不行,原因在于,每次BFS遍历到一个老节点时,遍历其neighbors,对于我们copy的那个节点,我们没办法从newQueue里面获取到,所以不能用简单的isVisited的boolean数组来替代,而是用Node数组来访问。
class Solution {
    public Node cloneGraph(Node node) {
        if (node == null) {
            return node;
        }

        Node[] allNodes = new Node[101];
        Arrays.fill(allNodes, null);

        Queue<Node> oldQueue = new LinkedList();
        Queue<Node> newQueue = new LinkedList();

        oldQueue.offer(node);
        Node ret = new Node(node.val);
        newQueue.offer(ret);
        allNodes[1] = ret;

        while (!oldQueue.isEmpty()) {
            Node oldOne = oldQueue.poll();
            Node newOne = newQueue.poll();
            for (Node neighbor : oldOne.neighbors) {
                if (allNodes[neighbor.val] == null) {
                    // Create new node.
                    Node newNode = new Node(neighbor.val);
                    allNodes[neighbor.val] = newNode;
                    oldQueue.offer(neighbor);
                    newQueue.offer(newNode);
                }
                newOne.neighbors.add(allNodes[neighbor.val]);
            }
        }

        return ret;
    }
}

LeetCode 1042. Flower Planting With No Adjacent

Problem

LeetCode 1042. Flower Planting With No Adjacent

1. 题目简述

给出N个花园(1…N),给出花园中的路径paths,每个花园最多有三条连通路径。一共可以种四种花(1, 2, 3, 4),每条路径两段的花的种类不能一致,给出一种符合条件的解。

Input: N = 3, paths = [[1,2],[2,3],[3,1]]
Output: [1,2,3]

条件限制:

1 <= N <= 10000
0 <= paths.size <= 20000
No garden has 4 or more paths coming into or leaving it.
It is guaranteed an answer exists.

2. 算法思路

这道题很多人点了不喜欢,一开始我也没做出来,看了解析之后才发现是自己想复杂了。

和二分图不同,这里像是四分图,且保证一定有解,这里不是非黑即白的选择,例如和1连通的有2,3,和4,我们假设四种颜色为红黄蓝白,1为红色,2、3、4可选的颜色种类太多了,如果都为黄色,那么遍历到[2, 3]这条路径的时候,我们又发现不行,不能都为黄色,又需要调整,容易陷入怪圈。

我们细心点可以发现这里有一个条件是每个花园最多有三条路径相连,也就是说每个花园一定能够被赋予某种颜色,而不会有冲突,一定存在解,所以现在的问题就在于如何找这种解法。

贪心法

稍微再想一想,我们完全没必要关心邻居究竟是怎么样的颜色分布,只要当前节点的颜色符合规定不就Ok了嘛,题中都保证一定有解了。

对于此题,我们需要先将整个图给遍历出来,然后,对于每个顶点,我们遍历其neighbors,找到其邻居的所有染色,然后随便选一种尚未出现的颜色对当前节点上色即可。

因此这是一种保证当前节点和之前节点最优解的方法,可以认为是贪心法。

注意

这里同样要注意ArrayList数组的初始化方法,不能使用List[]作为数组,因为List是抽象接口,不能这么做,需要用ArrayList。

3. 解法

Greedy


class Solution {
    public int[] gardenNoAdj(int N, int[][] paths) {
        int[] res = new int[N];
        Arrays.fill(res, 0);
        ArrayList<Integer>[] graph = new ArrayList[N];

        for (int i = 0; i < N; i++) {
            graph[i] = new ArrayList();
        }

        for (int[] path : paths) {
            graph[path[0] - 1].add(path[1] - 1);
            graph[path[1] - 1].add(path[0] - 1);
        }

        for (int i = 0; i < N; i++) {
            int[] color = new int[5];

            for (int neighbor : graph[i]) {
                color[res[neighbor]] = 1;
            }

            for (int j = 1; j < 5; j++) {
                if (color[j] != 1) {
                    res[i] = j;
                    break;
                }
            }
        }

        return res;
    }
}

LeetCode 785. Is Graph Bipartite?

Problem

LeetCode 785. Is Graph Bipartite?

1. 题目简述

给出一个图,判断这个图是否为二分图。

Example 2:
Input: [[1,2,3], [0,2], [0,1,3], [0,2]]
Output: false
Explanation: 
The graph looks like this:
0----1
| \  |
|  \ |
3----2
We cannot find a way to divide the set of nodes into two independent subsets.

2. 算法思路

基础的染色二分图问题,但是要注意,对于整张图而言,它可能不是连通图,而是分为多个子图的,所以不能只遍历一种。

DFS

BFS

3. 解法

DFS


class Solution {
    public boolean isBipartite(int[][] graph) {
        int[] color = new int[graph.length];
        // -1是未染色,0是红色,1是黑色
        Arrays.fill(color, -1);

        for (int i = 0; i < graph.length; i++) {
            if (color[i] == -1) {
                color[i] = 0;
                if (!dfs(graph, i, color)) {
                    return false;
                }
            }
        }

        return true;
    }

    private boolean dfs(int[][] graph, int index, int[] color) {
        for (int i = 0; i < graph[index].length; i++) {
            if (color[graph[index][i]] == -1) {
                color[graph[index][i]] = color[index] ^ 1;
            } else if (color[graph[index][i]] != color[index] ^ 1) {
                return false;
            }
        }

        return true;
    }
}

BFS


class Solution {
    public boolean isBipartite(int[][] graph) {
        int n = graph.length;
        int[] color = new int[graph.length];
        Arrays.fill(color, -1);

        for (int i = 0; i < n; i++) {
            // 这里for循环是因为这里也可能是非连通图,再判断是否遍历过,如果遍历过则跳过;
            // 没有则说明到了新的一个连通图
            if (color[i] == -1) {
                Queue<Integer> queue = new LinkedList();
                queue.offer(i);
                color[i] = 0;

                while (!queue.isEmpty()) {
                    int node = queue.poll();
                    for (int j = 0; j < graph[node].length; j++) {
                        if (color[graph[node][j]] == -1) {
                            color[graph[node][j]] = color[node] ^ 1;
                            queue.offer(graph[node][j]);
                        } else if (color[graph[node][j]] == color[node]) {
                            return false;
                        }
                    }
                }
            }
        }

        return true;
    }
}

LeetCode 886. Possible Bipartition

Problem

LeetCode 886. Possible Bipartition

1. 题目简述

给出n个人,编号从(1…n),我们可以将他们分成任意大小的两组,但是他们之间会有讨厌某些人的关系,那么他们就不用管被分到同一个组里。

用公式化的形式来讲,就是如果dislikes[i] = [a, b],那么a和b就不会被分到同一个组里。我们需要判断能否做到合适的分组,满足所有dislike条件。

Input: N = 4, dislikes = [[1,2],[1,3],[2,4]]
Output: true
Explanation: group1 [1,4], group2 [2,3]

2. 算法思路

经典的二分图算法,和785是一样的,只不过本题需要先把整张dislike图给construct出来。染色,然后判断是否ok。

DFS

BFS

注意

首先将整张dislike图build出来,因为我们并不知道每个人究竟有多少个dislike的对象,所以可以用ArrayList的数组来存储。这里需要注意ArrayList数组的初始化问题!!!ArrayList数组应该这样初始化,注释中的做法不可用!!!

原因在于foreach不能用于元素赋值或初始化,而是将数组中元素copy一份给临时变量。


ArrayList<Integer>[] dislikeGraph = new ArrayList[N + 1];

// for (List<Integer> list : dislikeGraph) {
//     list = new ArrayList();
// }

for (int i = 0; i <= N; i++) {
    dislikeGraph[i] = new ArrayList();
}

3. 解法

DFS


class Solution {
        // 注意List数组的创建!!!
        ArrayList<Integer>[] dislikeGraph = new ArrayList[N + 1];

        for (int i = 0; i <= N; i++) {
            dislikeGraph[i] = new ArrayList();
        }

        for (int[] dislike : dislikes) {
            dislikeGraph[dislike[0]].add(dislike[1]);
            dislikeGraph[dislike[1]].add(dislike[0]);
        }

        // DFS, -1是未访问,0是red,1是black
        int[] isVisited = new int[N + 1];
        Arrays.fill(isVisited, -1);

        for (int i = 1; i <= N; i++) {
            if (isVisited[i] != -1) {
                continue;
            }
            // 注意这里给上的颜色,如果已经遍历过,有颜色,则说明该节点没问题,跳过即可
            isVisited[i] = 0;
            if (!dfs(dislikeGraph, isVisited, i)) {
                return false;
            }
        }

        return true;
    }

    private boolean dfs(List<Integer>[] dislikeGraph, int[] isVisited, int index) {
        for (int i : dislikeGraph[index]) {
            if (isVisited[i] == -1) {
                isVisited[i] = isVisited[index] ^ 1;
                if (!dfs(dislikeGraph, isVisited, i)) {
                    return false;
                }
            } else if (isVisited[i] != (isVisited[index] ^ 1)) {
                return false;
            }
        }

        return true;
    }
}

BFS:
下次看到这里再留着练手吧

图算法总结

图的基本概念

基本概念

  • 顶点
  • 顶点的度
  • 相邻
  • 完全图: 所有顶都相邻
  • 二分图: 对于一个图G, V(G)=X∪Y, X∩Y=∅, X中, Y 中任两顶不相邻
  • 路径

图的表示方式

两种,紧接矩阵和邻接表,如下。

邻接矩阵

邻接表

图的相关问题

其实对于很多非图的问题,也是可以把其看做图的,使用图遍历的方式其进行遍历。例如 LeetCode 138. Copy List with Random Pointer,以图遍历的方式遍历链表;又或者是jump game III,将数组用图遍历的方式展现出来。

图的搜索

BFS

用Queue来遍历吧,先写个样例,以邻接表为例:

/**
 * 模拟一下,假设给出所有的边,然后重新construct一个图,进行bfs,例如[2,3]就表示2和3之间存在一条边。

 注意!!! 这里的BFS是考虑连通图的情况下,如果是非连通图,则在while循环外面再套一层for循环。
*/

public void bfs (int[][] edges, int n) {
    // 将边构建成邻接表,用arrayList数组存储
    ArrayList<Integer>[] graph = new ArrayList[n + 1];
    for (int i = 1; i < n + 1; i++) {
        graph[i] = new ArrayList();
    }
    for (int[] edge : edges) {
        graph[edge[0]].add(edge[1]);
        graph[edge[1]].add(edge[0]);
    }

    // BFS遍历
    boolean[] visited = new boolean[n + 1];
    Queue<Integer> queue = new LinkedList();
    queue.add(1);
    isVisited[1] = true; // 将初始设为已经遍历

    while (!queue.isEmpty()) {
        int node = queue.pop();
        for (int i = 0; i < graph[node].size(); i++) {
            if (!isVisited[graph[node].get(i)]) {
                doSomething();
                isVisited[graph[node].get(i)] = true;
                queue.offer(graph[node.get[i]]);
            }
        }
    }

    return;
}

DFS

一般情况下是递归调用DFS较多。而且需要注意的是!!!

Attention: DFS遍历时也要注意preorder还是postorder遍历(有向图的情况下要注意),前者是先遍历当前节点,再去遍历neighbor;后者是先遍历neighbor,再遍历自身。为什么无向图不需要考虑postorder的可能呢?因为无向图都是双向的,如果一个无向图存在环,那么postorder就会引起无限循环,所以对于无向图,都是preorder。

那么对于有向图的postorder遍历有什么区别呢?如果都是在遍历完neighbor以后才将其置为visited的话,那么一旦存在一个有向环,就会无限循环的,和postorder一个道理,跟无向图没有postorder遍历一个道理。因此,我们就需要设置三种状态,unvisited,visiting和visited,当遇到unvisited的时候,我们继续DFS;发现visiting节点,则说明存在环;发现visited节点,则返回,因为已经遍历过了。

下面遍历方式都是对于无向图,有向图的遍历参考:LeetCode 207,最经典的就是拓扑排序。

/**
 *这里用二维数组的邻接表来表示整个图,vertex即为顶点id,和数组的顺序一致,从0开始。
*/
public void testDFS(int[][] graph){
        boolean[] isVisited = new int[graph.length];
        Arrays.fill(isVisited, false);

        // 这里有可能是非连通图,对于不同的节点都要尝试。
        for (int i = 0; i < graph.length; i++) {
            if (isVisited[i] == false) {
                isVisited[i] = true;
                dfs(graph, i, isVisited);
            }
        }
}

private void dfs(int[][] graph, int vertex, boolean isVisited) {
    for (int i = 0; i < graph[i].length; i++) {
        // 如果邻居未被访问,则遍历邻居节点
        if (isVisited[graph[vertex][i]] == false) {
            isVisited[graph[vertex][i]] = true;
            dfs(graph, graph[vertex][i], isVisited);
        } else if (isVisited[graph[vertex][i]] == true) {
            // 遍历到一条路径的终点了,做点啥或者直接返回?
            doSomething();
        }
    }
}

二分图

二分图的问题本质上是相当于染色问题,有点像红黑树的感觉。如果能够顺利将整个图染成2种不同颜色,则是二分图,否则不是。实现可以用BFS或DFS均可,下面是三个例题链接。

  1. LeetCode-785-Is-Graph-Bipartite
  2. LeetCode 886. Possible Bipartition
  3. LeetCode-1042-Flower-Planting-With-No-Adjacent

深拷贝图

  1. LeetCode 138. Copy List with Random Pointer
  2. LeetCode 133. Clone Graph

拓扑排序

判断环

最小生成树

最小生成树是无向图里的概念!!!

Kruskal Algorithm

其核心思想在于按边查找。

  1. 首先将所有的边从小到大进行排序;
  2. 依次从前到后遍历所有的边,如果该条边不会使得已有的边形成“环”,加入这条边到MST里;否则,跳过;
  3. 直到有n-1条边被选出或者无边可选, MST生成。

O(T) = O(ElogE)

O(S) = O(V)

例题:LeetCode 1584

class Solution {
    
    class UF {
        int size;
        int[] parent;
        int[] weight;
        public UF(int n) {
            size = n;
            parent = new int[n];
            weight = new int[n];
            for (int i = 0; i < n; i++) {
                parent[i] = i;
                weight[i] = 1;
            }
        }
        
        public int find(int x) {
            if (x == parent[x]) {
                return x;
            }
            parent[x] = find(parent[x]);
            return parent[x];
        }
        
        public void union(int x, int y) {
            int rootX = find(x), rootY = find(y);
            if (rootX == rootY) {
                return;
            }
            
            if (weight[rootX] <= weight[rootY]) {
                parent[rootX] = rootY;
                weight[rootY] += weight[rootX];
            } else {
                parent[rootY] = rootX;
                weight[rootX] += weight[rootY];
            }
            
            size--;
        }
    }
    
    class Edge{
        int x, y, l;
        public Edge(int _x, int _y, int _l){
            x = _x;
            y = _y;
            l = _l;
        }
    }
    
    public int minCostConnectPoints(int[][] points) {
        int n = points.length;
        PriorityQueue<Edge> pq = new PriorityQueue<>((a, b) -> a.l - b.l);
        
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                Edge e = new Edge(i, j, Math.abs(points[i][0] - points[j][0]) + Math.abs(points[i][1] - points[j][1]));
                pq.add(e);
            }
        }
        
        UF uf = new UF(n);
        int cnt = 0, res = 0;
        while (cnt < n - 1) {
            Edge e = pq.poll();
            if (uf.find(e.x) == uf.find(e.y)) {
                continue;
            }
            
            uf.union(e.x, e.y);
            res += e.l;
            cnt++;
        }
        
        return res;
    }
}

Prim Algorithm

按顶点来进行查找。

  1. 将顶点分为两个set,visited和unvisited;
  2. visited中初始化放入一个顶点,剩下的则放入unvisited中;
  3. 每次找到一条从visited到unvisited的最短的边,将其加入MST,并将该顶点加入MST(该顶点一定在unvisited中,因为边是从visited到unvisited的);
  4. 重复3,知道全部顶点已经加入visited或者无边可加。

单源最短路径

Dijkstra

LeetCode

class Solution {
    public int networkDelayTime(int[][] times, int n, int k) {
        // 普通版本的dijkstra
        int[] dists = new int[n + 1];
        Arrays.fill(dists, Integer.MAX_VALUE);
        Map<Integer, List<int[]>> graph = new HashMap<>();
        dists[k] = 0;
        
        for (int[] edge : times) {
            graph.putIfAbsent(edge[0], new ArrayList<int[]>());
            graph.get(edge[0]).add(new int[]{edge[1], edge[2]});
        }
        
        // 定义一个visited数组表示该点是否有被遍历过
        boolean[] visited = new boolean[n + 1];
        // visited[k] = true;
        
        while (true) {
            // 遍历去找当前距离node k最近的点为candidate
            int candidateId = -1, minD = Integer.MAX_VALUE;
            for (int i = 1; i <= n; i++) {
                if (!visited[i] && dists[i] < minD) {
                    candidateId = i;
                    minD = dists[i];
                }
            }

            // 如果不存在,则跳出
            if (candidateId < 0) break;

            visited[candidateId] = true;

            // 这里,注意要先判断一下图里有没有candidate,因为有的点只有入度没有出度。
            if (graph.containsKey(candidateId)) {
                for (int[] nei : graph.get(candidateId)) {
                    dists[nei[0]] = Math.min(dists[candidateId] + nei[1], dists[nei[0]]);
                }
            }
        }
        
        int res = 0;
        for (int i = 1; i <= n; i++) {
            if (dists[i] == Integer.MAX_VALUE) {
                return -1;
            }
            res = Math.max(res, dists[i]);
        }
        
        return res;
    }
}

堆优化版Dijkstra


class Solution {
    public int networkDelayTime(int[][] times, int n, int k) {
        // 堆优化版Dijkstra算法
        Map<Integer, List<int[]>> graph = new HashMap<>();
        for (int[] edge : times) {
            graph.putIfAbsent(edge[0], new ArrayList<>());
            graph.get(edge[0]).add(new int[]{edge[1], edge[2]});
        }
        
        // 初始化距离
        int[] dist = new int[n + 1];
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[k] = 0;
        boolean[] seen = new boolean[n + 1];
        
        PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> (a[1] - b[1]));
        pq.add(new int[]{k, 0});
        
        while (!pq.isEmpty()) {
            int[] cand = pq.poll();
            int candidate = cand[0], minD = cand[1];
            
            // 根据candidate更新dist数组
            if (seen[candidate]) {
                continue;
            }
            dist[candidate] = minD;
            seen[candidate] = true;
            if (graph.containsKey(candidate)) {
                for (int[] nei : graph.get(candidate)) {
                    // 这里加判断是为了减少已经找到最短路的顶点,减少heap处理的数据量大小
                    if (!seen[nei[0]])
                        pq.offer(new int[]{nei[0], nei[1] + minD});
                }
            }
        }
        
        int res = 0;
        for (int i = 1; i <= n; i++) {
            if (dist[i] == Integer.MAX_VALUE) {
                return -1;
            }
            res = Math.max(dist[i], res);
        }
        return res;
    }
}

最短路径问题

欧拉路径

最大流

LeetCode 525. Contiguous Array

Problem

LeetCode 525. Contiguous Array

1. 题目简述

给出一个二进制数组,由0和1组成,找出最长的子数组,使得里面的0和1的数量相等。例如:

Example 1:
Input: [0,1]
Output: 2
Explanation: [0, 1] is the longest contiguous subarray with equal number of 0 and 1.

2. 算法思路

这道题的思路很特别,我们需要记录一下。在这里将1看做是1,0看做是-1,然后我们记录前缀和(prefix sum),将每次前缀和都记录下来,如果发现有重复的前缀和,那么上一次出现的前缀和的位置到当前位置中0和1的数量应该是相等的,且我们只需要记录第一次出现的前缀和即可。

Hash Table

class Solution {
    public int findMaxLength(int[] nums) {
        int res = 0, sum = 0;
        Map<Integer, Integer> prefixSum = new HashMap();
        // 注意,这里要预先放一个sum为0的值,要不然后面计算和为0的时候会少算。
        prefixSum.put(0 , -1);

        for (int i = 0; i < nums.length; i++) {
            if (nums[i] == 1) {
                sum++;
            } else {
                sum--;
            }

            if (!prefixSum.containsKey(sum)) {
                prefixSum.put(sum, i);
            } else {
                res = Math.max(res, i - prefixSum.get(sum));
            }
        }

        return res;
    }
}

LeetCode 74. Search a 2D Matrix

Problem

LeetCode 74. Search a 2D Matrix

1. 题目简述

写出一个函数来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:

1. 每行中的整数从左到右按升序排列。
2. 每行的第一个整数大于前一行的最后一个整数。

Input:
matrix = [
[1,   3,  5,  7],
[10, 11, 16, 20],
[23, 30, 34, 50]
]
target = 3
Output: true

2. 算法思路

Binary Search

这道题的矩阵其实是一个完全有序的矩阵,有两种做法:第一种是先找行数,在找列数;第二种做法是只做一次二分查找,l = 0,r = m * n - 1,然后通过算式来计算middle的横纵坐标来进行下一步计算。

3. 解法

1.分别计算横纵坐标:需要注意的是第一次查找完以后,如果发现这个数比全局最小都小,也就是l = 0,则直接返回-1.

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        // 两次二分,先确定行,再确定列
        
        int m = matrix.length, n = matrix[0].length, row = 0, col = 0;
        
        if (matrix[0][0] > target || matrix[m - 1][n - 1] < target) {
            return false;
        }
        
        // 确定行,这里是要找右边界,也就是小于等于target的最大的那个队首。
        int l = 0, r = m - 1;
        while (l < r) {
            int mid = (l + r + 1) / 2;
            if (matrix[mid][0] <= target) {
                l = mid;
            } else {
                r = mid - 1;
            }
        }
        
        row = l;
        
        // 确定列,这里是找出最准确的那个数,左右边界无所谓
        l = 0;
        r = n - 1;
        while (l < r) {
            int mid = (l + r) / 2;
            if (matrix[row][mid] == target) {
                return true;
            } else if (matrix[row][mid] > target) {
                r = mid - 1;
            } else {
                l = mid + 1;
            }
        }
        
        return matrix[row][l] == target;
        
    }
}
  1. 一次计算二分搜索
class Solution {
  public boolean searchMatrix(int[][] matrix, int target) {
    int m = matrix.length;
    if (m == 0) return false;
    int n = matrix[0].length;

    // binary search
    int left = 0, right = m * n - 1;
    int pivotIdx, pivotElement;
    while (left <= right) {
      pivotIdx = (left + right) / 2;
      pivotElement = matrix[pivotIdx / n][pivotIdx % n];
      if (target == pivotElement) return true;
      else {
        if (target < pivotElement) right = pivotIdx - 1;
        else left = pivotIdx + 1;
      }
    }
    return false;
  }
}

二分搜索

二分搜索

这里有一篇总结二分搜索非常好的Blog,链接贴在这,一定要看!!

传送门

一. 二分搜索基本应用场景

当一道题目出现了“有序N维数组中查找某个东西或最接近的值”时,大概率用二分搜索;或者输入的数字特别大,达到2^31 - 1左右的这种,也很有可能使用二分搜索。

其核心思想就是每次找一半,然后舍弃另一半,其中最恶心的莫过于有重复数字的二分搜索,太难顶了。例如:LeetCode 81的“Search in Rotated Sorted Array II”,到现在也不懂,真的难受,先记录着,等以后能搞懂了再回来看。

二. 二分搜索模板

基本模板

关于查找区间的问题,究竟是“左闭右开”还是“左右闭区间”,我在这里直接就用左右全闭区间的方法了。

while (l <= r) {
    m = l + (r - l) / 2;

    if g(m) == target :
        return;
    else if g(m) > target :
        r = m - 1;
    else if g(m) < target :
        l = m + 1;
}

最终返回的时候,l一定是r右面的那个数,这是由while的循环条件决定的,二者最后只能差1。最后看情况是需要l还是r。

左边界 & 右边界

参考题目:下面例题 3.4 LeetCode 34. Find First and Last Position of Element in Sorted Array

关于左边界和右边界的问题只要简单说一下就懂了,举个例子,我们有一个数组nums:[1, 2, 6, 6, 10, 13, 17],我们想要找出数字6所在的位置,那么我们返回2还是返回3呢?虽然值是不同,但是对于实际应用的时候有很大不同。

比如说对于查找的时候,可能需要查找大于某个数的最小值,或者小于某个数的最大值,前者就是要找右边界,后者就是要找左边界(这里不敢保证写的绝对对啊,只是个人理解,有可能是错的)。

对于左右边界的处理其实不同点就在于等于的情况下,是更新left还是right。

左边界 -> 等于的时候更新右边界

while (l <= r) {
    m = l + (r - l) / 2;

    if g(m) >= target :
        r = m - 1;
    else if g(m) < target :
        l = m + 1;
}

右边界 -> 等于的时候更新左边界

while (l <= r) {
    m = l + (r - l) / 2;

    if g(m) > target :
        r = m - 1;
    else if g(m) <= target :
        l = m + 1;
}

返回l?返回r?

我们需要care的一点就是到底是返回l还是返回r呢?结论是:就题论题,九成以上返回l,但是不排除返回r的可能,例如上面的数组[1, 2, 6, 6, 10, 13, 17],找出比6小的最大值,自己写一下代码,很容易发现返回的是r,而不是l,因此如果不确定的话,最好的办法是找个例子尝试一下。

三. 二分搜索简单题目

其实二分搜索,难的是真滴难,简单的也有很多坑,慢慢积累经验吧,有时候边界条件搞不懂就特别烦。

3.1 LeetCode 69. Sqrt(x)

**注意:**需要注意的点就是输入的值可能会特别大,二分后的值,如果直接平方的话有可能会越界,所以可以考虑使用除法或者用long来实现。这里用除法。

class Solution {
    public int mySqrt(int x) {

        if (x <= 1) {
            return x;
        }

        int l = 0, m = 0, r = x;

        while (l <= r) {
            m = l + (r - l) / 2;
            if (m == x / m) {
                return m;
            } else if (m > x / m) {
                r = m - 1;
            } else if (m < x / m) {
                l = m + 1;
            }
        }

        return l - 1;
    }
}

3.2 LeetCode 367. Valid Perfect Square

**注意:**需要注意的是这里不能用除法,和69题不一样,因为除法会损失精度,导致计算错误,所以这里用long类型来辅助计算。

class Solution {
    public boolean isPerfectSquare(int num) {
        int start = 1, end = num;
        long target = 0;

        while (start <= end) {
            target = (start + end) / 2;
            if (target * target > num) {
                end = (int)target - 1;
            } else if (target * target < num) {
                start = (int)target + 1;
            } else {
                return true;
            }
        }

        return false;
    }
}

3.3 LeetCode 35. Search Insert Position

**注意:**这道是最传统的二分搜索,基础中的基础。

class Solution {
    public int searchInsert(int[] nums, int target) {
        int l = 0, m = 0, r = nums.length - 1;

        while (l <= r) {
            m = l + (r - l) / 2;
            if (nums[m] == target) {
                return m;
            } else if (nums[m] < target) {
                l = m + 1;
            } else if (nums[m] > target) {
                r = m - 1;
            }
        }

        return l;
    }
}

3.4 LeetCode 34. Find First and Last Position of Element in Sorted Array

**注意:**先找到一个pivot,再分别计算左右有多少。

class Solution {
    public int[] searchRange(int[] nums, int target) {
        int l = 0, r = nums.length - 1, m = 0, pivot = -1;
        int[] res = new int[2];

        while (l <= r) {
            m = l + (r - l) / 2;
            if (nums[m] < target) {
                l = m + 1;
            } else if (nums[m] >= target) {
                r = m - 1;
            }
        }

        if (l == nums.length || nums[l] != target) {
            return new int[]{-1, -1};
        }

        // 这里l是第一个target的位置
        res[0] = l;

        r = nums.length - 1;
        while (l <= r) {
            m = l + (r - l) / 2;
            if (nums[m] > target) {
                r = m - 1;
            } else {
                l = m + 1;
            }
        }
        res[1] = l - 1;

        return res;
    }
}

3.5 LeetCode 50. Pow(x, n)

**注意:**这道题要用二分法来减少运算,次数,而且负数除以2余数为-1,注意取反。

class Solution {
    public double myPow(double x, int n) {
        if (x == 0d) {
            return 0;
        }

        if (n == 0) {
            return 1;
        } else if (n > 0) {
            double ret = myPow(x, n / 2);
            if (n % 2 == 1) {
                return x * ret * ret;
            } else {
                return ret * ret;
            }
        } else {
            double ret = myPow(x, n / 2);
            if ((0 - n) % 2 == 1) {
                return ret * ret / x;
            } else {
                return ret * ret;
            }
        }
    }
}

LeetCode 1035. Uncrossed Lines

Problem

LeetCode 1035. Uncrossed Lines

1. 题目简述

我们在两条独立的平行线线上按给定的顺序写下 A 和 B 数组。

我们可以绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且我们绘制的直线不与任何其他连线(非水平线)相交。

以这种方法绘制线条,返回我们可以绘制的最大连线数。

Note:
1 <= A.length <= 500
1 <= B.length <= 500
1 <= A[i], B[i] <= 2000

Examples:

Input: A = [1,4,2], B = [1,2,4]
Output: 2
Explanation: We can draw 2 uncrossed lines as in the diagram.
We cannot draw 3 uncrossed lines, because the line from A[1]=4 to B[2]=4 will intersect the line from A[2]=2 to B[1]=2.

2. 算法思路

Dynamic Programming

又是二维数组,又是计算最大值,大概率是用DP来做。

我们假设**dp[i][j]**表示从A[0]到A[i]和B[0]到B[j]的子数组的最大连线数。

那么我们分为两种情况,第一种是A[i]和B[j]有连线,那么dp[i][j]就等于dp[i - 1][j - 1] + 1;如果没有连线,则dp[i][j]和max(dp[i - 1][j], dp[i][j - 1])相等,递推式如下:

\begin{equation}
dp[i][j] = dp[i - 1][j - 1] \text{ if } A[i] = B[j]
\end{equation}

\begin{equation}
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) \text{ if } A[i] \neq B[j]
\end{equation}

3. 解法

Dynamic Programming bottom-up


class Solution {
    public int maxUncrossedLines(int[] A, int[] B) {
        int n1 = A.length, n2 = B.length;
        int[][] dp = new int[n1 + 1][n2 + 1];

        for (int[] row : dp) {
            Arrays.fill(row, 0);
        }

        for (int i = 1; i <= n1; i++) {
            for (int j = 1; j <= n2; j++) {
                if (A[i - 1] == B[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }

        return dp[n1][n2];
    }
}

LeetCode 1218. Longest Arithmetic Subsequence of Given Difference

Problem

LeetCode 1218. Longest Arithmetic Subsequence of Given Difference

1. 题目简述

给出一个整形数组arr和一个整形数difference,找出arr所有子序列中以difference为间隔的最大长度。例如:

Examples:

Example 1:
Input: arr = [1,2,3,4], difference = 1
Output: 4
Explanation: The longest arithmetic subsequence is [1,2,3,4].

Example 2:
Input: arr = [1,3,5,7], difference = 1
Output: 1
Explanation: The longest arithmetic subsequence is any single element.

Example 3:
Input: arr = [1,5,7,8,5,3,4,2,1], difference = -2
Output: 4
Explanation: The longest arithmetic subsequence is [7,5,3,1].

Constraints:

1 <= arr.length <= 10^5
-10^4 <= arr[i], difference <= 10^4

2. 算法思路

Dynamic Programming

首先我们看一下给出的输入大小,基本上确定了不能使用暴力搜索,最多支持O(nlogn)级别的复杂度。所以我们判断使用动态规划来节约时间。

我们来看一下题目,要求找子序列,且difference确定,base case我们设定为长度为1的single element,maxLength就是1。然后对于第n个数,我们需要找到从第1个数到第 $ n - 1 $ 个数存不存在num[n] - difference,如果不存在,则说明以当前数字为结尾的最长子序列长度为1,也就是本身;如果存在,则为dp[n - difference] + 1,视情况更新maxLength。

对于判断是否存在num[n] - difference,有一个最好用的数据结构就是hashmap,key为数字,value为最后一次出现的index,对于重复的数,后出现的那个一定是包含先出现的情况。

dp[n] = dp[n - difference] + 1 (如果n - difference出现过)
dp[n] = 1 (如果n - difference没出现过)

3. 解法

Dynamic Programming bottom-up:数组最长为10^5,则dp数组长度为100001.


class Solution {
    public int longestSubsequence(int[] arr, int difference) {
        int[] dp = new int[100000];
        // key是数值,value是last index
        Map<Integer, Integer> dict = new HashMap();
        int longest = 1;

        for (int i = 0; i < arr.length; i++) {
            int targetIndex = dict.getOrDefault(arr[i] - difference, -1);
            dict.put(arr[i], i);
            if (targetIndex == -1) {
                dp[i] = 1;
                continue;
            } else {
                dp[i] = dp[targetIndex] + 1;
                longest = Math.max(longest, dp[i]);
            }
        }

        return longest;
    }
}

动态规划一

动态规划(一)

一. 动态规划基础

此小节参考:动态规划解题套路

1.1 什么是动态规划

我们首先需要知道我们为什么需要动态规划,动态规划的核心思想其实就是用空间换时间,而省时间的方式就是减少Brute Force或者递归中重复的子问题。

动态规划的基础是最优子结构,什么是最优子结构?其实就是从一个小规模问题推导出大规模问题的解。并且子问题之间不能相互影响,所以找出最优子结构,写出递推式是DP问题的最关键的部分,同时也是最难的部分。用花花酱的话来说,DP问题做多少都不嫌多。

最直接的例子就是斐波那契数列:

$$ f(n) = f(n - 1) + f(n - 2) $$

有可能要计算很多重复的子问题,如果使用一个数组记录已经计算过的f(n),计算f(n + 1)时用到f(n)时可以直接返回,而不用再次递归。

1.2 动态规划思路

动态规划问题大多数都是求最值的,从比较小规模的最值,到当前规模的最值,一步步扩张。

1.2.1 top-down

递归 + memorization

自顶向下:其实就是普通的递归加上memorization,上面提到的斐波那契数列的例子就是top-down,简单来说就是从需要计算的解,从后向前推导,记录每一步的值,直到base case结束,也就是递归终点。

1.2.2 bottom-up

Loop + memorization

自底向上:从base case去递归求解下一个解,直到最终的目标解。以斐波那契额为例:

dp[1] = 1
dp[2] = 2
i = 3
while (i <= n) {
    dp[i] = dp[i - 1] + dp[i - 2];
    i++;
}

return dp[n];

1.2.3 注意事项

动态规划还需要注意的很重要的一点是边界,往往边界是递归返回的base case,或者用于padding。

1.3 动态规划分类

这里分类是向花花酱学习的分类方式,用数据规模,时间复杂度和空间复杂度进行分类。这里是花花酱整理的不同类型DP问题分类链接

本次讨论的所有动态规划问题都是属于入门级难度,大部分是:“I: O(n), S = O(n), T = O(n)”,其中I表示的是输入量级,S表示空间复杂度,T表示时间复杂度。

注意,这里S的O(n)可能是O(2n),O(3n),T也是如此,对于2n和3n的情况后面还会讨论,需要多做题熟悉。

二. 动态规划题目总结(2020.05.25)

本次的题目大都比较简单,比较简单的题目直接放链接和递推式,然后直接给出Java Code,需要额外写文章的这里给出链接。

2.1 LeetCode 53. Maximum Subarray

递推式:假设F(n)为从第1到n个数中包含当前数字的子序列的最大值。

$$ F(n) = max(F(n - 1) + num[n], num[n]) $$

class Solution {
    public int maxSubArray(int[] nums) {
        int resultMax = nums[0], currentMax = nums[0];

        for (int i = 1; i < nums.length; i++){
            currentMax = Math.max(currentMax + nums[i], nums[i]);
            resultMax = Math.max(resultMax, currentMax);
        }

        return resultMax;
    }
}

2.2 LeetCode 70. Climbing Stairs

递推式:假设F(n)为从第1到n级台阶的走法最大值。

$$ F(n) = F(n - 1) + F(n - 2) (F(1) = 1, F(2) = 2)$$

class Solution {
    public int climbStairs(int n) {
        if (n < 3) {
            return n;
        }

        int pre = 1, cur = 2;

        while (n-- > 2) {
            int temp = pre + cur;
            pre = cur;
            cur = temp;
        }

        return cur;
    }
}

2.3 LeetCode 121. Best Time to Buy and Sell Stock

递推式:假设F(n)为从第1到n天,买卖股票赚钱最多赚多少钱;tempMin(1, n)为从1到n的最最低价。

$$ F(n) = Math.max(F(n - 1), prices[n] - tempMin(1, n))$$

class Solution {
    public int maxProfit(int[] prices) {
        if (prices.length < 2){
            return 0;
        }

        int result = prices[1] - prices[0] > 0 ? prices[1] - prices[0] : 0;
        int tempMin = prices[1] > prices[0] ? prices[0] : prices[1];

        for (int i = 2; i < prices.length; i++){
            result = prices[i] - tempMin > result ? prices[i] - tempMin : result;
            tempMin = prices[i] < tempMin ? prices[i] : tempMin;
        }

        return result;
    }
}

2.4 LeetCode 198. House Robber

递推式:假设F(n)为从第1家到第n家最多能获得的财富数目,nums[n]为第n家的财富。

$$ F(n) = Math.max(F(n - 1), F(n - 2) + nums[n]) (F(1) = nums[1], F(2) = Math.max(nums[1], nums[2])) $$

class Solution {
    public int rob(int[] nums) {
        if (nums.length == 0) {
            return 0;
        }
        if (nums.length == 1) {
            return nums[0];
        }

        int pre = nums[0], cur = Math.max(nums[0], nums[1]);
        for (int i = 2; i < nums.length; i++) {
            int temp = cur;
            cur = Math.max(pre + nums[i], cur);
            pre = temp;
        }

        return cur;
    }
}

2.5 LeetCode 303. Range Sum Query - Immutable

这道题我不觉得算是一道DP题目,最多算caching,硬要说的话也的确有dp减少重复子问题的思想在。

class NumArray {

    int[] sums;

    public NumArray(int[] nums) {
        if (nums.length > 0) {
            sums = new int[nums.length];
            sums[0] = nums[0];

            for (int i = 1; i < nums.length; i++) {
                sums[i] = nums[i] + sums[i - 1];
            }
        }
    }

    public int sumRange(int i, int j) {
        return i - 1 < 0 ? sums[j] : sums[j] - sums[i - 1];
    }
}

/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray obj = new NumArray(nums);
 * int param_1 = obj.sumRange(i,j);
 */

2.6 LeetCode 746. Min Cost Climbing Stairs

注意:这道题里,登上最终台阶的意思不是数组的最后一级,而是数组的最后一级再向后数一级,也就是说要越过它,到达n+1起始位置我们可以理解为从0级台阶开始,cost为0。

递推式:假设F(n)为从第1级到跨越n级所需要的最小cost(从n-1跳就不需要加上cost[n], 从n-2跳就需要)。

$$ F(n) = Math.min(F(n - 1), F(n - 2) + cost[n - 1]) (F(1) = nums[0], F(2) = Math.min(nums[0], nums[1]))$$

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int[] dp = new int[cost.length];
        int n = cost.length;
        Arrays.fill(dp, 0);
        dp[0] = cost[0];
        dp[1] = cost[1];

        for (int i = 2; i < n; i++) {
            dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i] ;
        }

        return Math.min(dp[n - 1], dp[n - 2]);
    }
}

2.7 LeetCode 1137. N-th Tribonacci Number

递推式:已经很明显地告知递推式了,且0 <= n <= 37无需多言。

$$ F(n) = F(n - 1) + F(n - 2) + F(n - 3) (n >= 3, F(1) = 0, F(1) = 1, F(2) = 1)$$

class Solution {
    int[] res = new int[38];
    public int tribonacci(int n) {
        res[0] = 0;
        res[1] = 1;
        res[2] = 1;

        if (n < 3 || res[n] > 0) {
            return res[n];
        }

        res[n] = tribonacci(n - 1) + tribonacci(n - 2) + tribonacci(n - 3);

        return res[n];
    }
}

LeetCode 1008. Construct Binary Search Tree from Preorder Traversal

Problem

LeetCode 1008. Construct Binary Search Tree from Preorder Traversal

1. 题目简述

给出一棵BST树的前序遍历的顺序,恢复这棵树。例如:

Input: [8,5,1,7,10,12]
Output: [8,5,10,1,7,null,12]

BST树

Constraints:

1 <= preorder.length <= 100
1 <= preorder[i] <= 10^8
The values of preorder are distinct.

2. 算法思路

中序遍历+前序遍历

这道题和LeetCode 105很像,只不过105中给出的是一棵二叉树的前序和中序遍历,对于BST树来说,其中序遍历就是其所有元素从小到大的排列。

第一种做法更加简洁一些。中序遍历的顺序是”中->左子树->右子树”,也就是说从第二个元素开始,到左子树结束,其中间的数都比“中”要小,所以并不需要通过中序遍历找左右子树。

第二种做法就是重排列一下数组,然后以前序遍历和中序遍历的顺序重新construct整棵树。这种做法麻烦的地方在于需要注意很多index,一不小心就会出很大的错。

3. 解法

  1. 简洁版,不找中序遍历序列

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public TreeNode bstFromPreorder(int[] preorder) {
        return buildTree(preorder, 0, preorder.length - 1);
    }

    private TreeNode buildTree(int[] preorder, int start, int end) {
        if (start > end) {
            return null;
        }

        int temp = end;
        while (preorder[temp] > preorder[start]) {
            temp--;
        }

        TreeNode node = new TreeNode(preorder[start]);
        node.left = buildTree(preorder, start + 1, temp);
        node.right = buildTree(preorder, temp + 1, end);

        return node;
    }
}
  1. 麻烦版本(代码见LeetCode 105

LeetCode 1458. Max Dot Product of Two Subsequences

Problem

LeetCode 1458. Max Dot Product of Two Subsequences

1. 题目简述

给出两个数组 nums1 和 nums2 。

请返回 nums1 和 nums2 中两个长度相同的 非空 子序列的最大点积。

数组的非空子序列是通过删除原数组中某些元素(可能一个也不删除)后剩余数字组成的序列,但不能改变数字间相对顺序。比方说,[2,3,5] 是 [1,2,3,4,5] 的一个子序列而 [1,5,3] 不是。例如:

Example 1:
Input: nums1 = [2,1,-2,5], nums2 = [3,0,-6]
Output: 18
Explanation: Take subsequence [2,-2] from nums1 and subsequence [3,-6] from nums2.
Their dot product is (2*3 + (-2)*(-6)) = 18.

Example 2:
Input: nums1 = [-1,-1], nums2 = [1,1]
Output: -1
Explanation: Take subsequence [-1] from nums1 and subsequence [1] from nums2.
Their dot product is -1.

Constraints:
1 <= nums1.length, nums2.length <= 500
-1000 <= nums1[i], nums2[i] <= 1000

2. 算法思路

Dynamic Programming

这道题和 LeetCode 72. Edit Distance很类似,今天等下去做一下试试。还有GeekForGeeks的这个Find Maximum dot product of two arrays with insertion of 0’s基本一致。

首先根据数据规模,判断复杂度最多只能是O(mn),不会更多,因此使用DP(做多了应该就一眼看出来了)。

我们设 dp[i][j] 为使用nums1[0 ~ i]和nums2[0 ~ j]的非空子序列的最大点积,那么有如下四种情况。

1. nums1的第i个元素被纳入了最终解中,而nums2的第j个元素没有被纳入最终解中,dp[i][j] = dp[i][j - 1];
2. nums1的第i个元素没有被纳入了最终解中,而nums2的第j个元素被纳入最终解中,dp[i][j] = dp[i - 1][j];
3. nums1的第i个元素和nums2的第j个元素都被纳入到最终解的计算中,dp[i][j] = max(dp[i - 1][j - 1], 0) + nums[i] * nums[j]。
4. nums1的第i个元素和nums2的第j个元素都没有被纳入到最终解的计算中,dp[i][j] = dp[i - 1][j - 1];

但是我们其实会发现,第四种情况根本就是不需要考虑的情况,因为dp[i - 1][j]和dp[i][j - 1]一定是大于等于dp[i - 1][j - 1]的,所以第四种情况我们pass掉,就变成了从三种情况中取最大值。

那么base case是什么样的呢?我们有两种方式进行选择,进行padding或者不进行padding。不进行padding的话可能会多出一些判断条件。padding的意思就是定义一些额外的情况,从而不需要判断边界。

Attention:

这里有几个需要注意的点:

  1. 首先,这里数字有正有负,所以有可能出现前面dp的值都为负数的情况,所以在上面的第三种情况中,如果我们需要nums[i] * nums[j],则需要判断前面是否小于0,如果小于0,则抛弃前面的dp值。

3. 解法

  1. Dynamic Programming (with padding)

class Solution {
    public int maxDotProduct(int[] nums1, int[] nums2) {
        int n1 = nums1.length, n2 = nums2.length;
        int[][] dp = new int[n1 + 1][n2 + 1];
        for (int[] row : dp) {
            Arrays.fill(row, Integer.MIN_VALUE);
        }

        for (int i = 1; i <= n1; i++) {
            for (int j = 1; j <= n2; j++) {
                dp[i][j] = Math.max(Math.max(dp[i][j - 1], dp[i - 1][j]), Math.max(0, dp[i - 1][j - 1]) + nums1[i - 1] * nums2[j - 1];
            }
        }

        return dp[n1][n2];
    }
}
  1. Dynamic Programming (without padding)

class Solution {
    public int maxDotProduct(int[] nums1, int[] nums2) {
        int n1 = nums1.length, n2 = nums2.length;
        int[][] dp = new int[n1][n2];

        for (int i = 0; i < n1; i++) {
            for (int j = 0; j < n2; j++) {
                dp[i][j] = nums1[i] * nums2[j];
                if (i > 0 && j > 0) {
                    dp[i][j] = Math.max(dp[i - 1][j - 1], 0) + dp[i][j];
                }
                if (i > 0) {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j]);
                }
                if (j > 0) {
                    dp[i][j] = Math.max(dp[i][j - 1], dp[i][j]);
                }
            }
        }

        return dp[n1 - 1][n2 - 1];
    }
}

LeetCode 1457. Pseudo-Palindromic Paths in a Binary Tree

Problem

LeetCode 1457. Pseudo-Palindromic Paths in a Binary Tree

1. 题目简述

给你一棵二叉树,每个节点的值为 1 到 9 。我们称二叉树中的一条路径是 「伪回文」的,当它满足:路径经过的所有节点值的排列中,存在一个回文序列。

返回从根到叶子节点的所有路径中“伪回文”路径的数目。例如:

伪回文路径数

Input: root = [2,3,1,3,1,null,1]
Output: 2 
Explanation: 上图为给定的二叉树。总共有 3 条从根到叶子的路径:红色路径 [2,3,3] ,绿色路径 [2,1,1] 和路径 [2,3,1] 。

在这些路径中,只有红色和绿色的路径是伪回文路径,因为红色路径 [2,3,3] 存在回文排列 [3,2,3] ,绿色路径 [2,1,1] 存在回文排列 [1,2,1] 。

2. 算法思路

奇偶性

需要计算每条从root到根节点路径中每种数字的奇偶性,然后到达根节点时判断个数为奇数的数字是否最多只有一个。如果是,则res++;否则直接返回。

奇偶性 + bit manipulation

其实我们并不需要知道每种数字的具体个数,只要知道奇偶性即可。因此我们用一个10位bit来记录,如果为奇数,则bit位为1,偶数则bit位为0。最终判断bit位为1的个数。

3. 解法(LeetCode 105 & 106)

  1. 奇偶性

使用Arrays.copyOfRange(int[] data, int start, int end)函数,写起来简洁,但是有点慢,而且耗空间大,因为要截取数组。


/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    int res = 0;
    public int pseudoPalindromicPaths (TreeNode root) {
        int[] path = new int[10];

        Arrays.fill(path, 0);
        dfs(root, path);

        return res;
    }

    private void dfs(TreeNode root, int[] path) {
        if (root == null) {
            return;
        }

        path[root.val]++;

        if (root.left == null && root.right == null) {
            int countOdd = 0;
            for (int i = 1; i < 10; i++) {
                if (path[i] % 2 == 1) {
                    countOdd++;
                }
            }
            if (countOdd <= 1) {
                res++;
            }
        }

        dfs(root.left, path);
        dfs(root.right, path);

        path[root.val]--;
    }
}
  1. 奇偶性 + bit manipulation,注意二进制里1个数的计算。另外,java不支持默认参数,所以需要另写一个辅助函数。

class Solution {
    public int pseudoPalindromicPaths (TreeNode root) {
        return getPseudo(root, 0);
    }

    private int getPseudo (TreeNode root, int s) {
        if (root == null) {
            return 0;
        }

        s ^= (1 << root.val);

        int ans = 0;
        if (root.left == null && root.right == null) {
            // 注意这里是怎么计算某数字二进制里1的个数的!
            int c = 0, x = s;
            while (x > 0) {
                x &= (x - 1); //每次去除最低位的1,直至全部为0
                c++;
            }
            ans += c <= 1 ? 1 : 0;
        }

        ans += getPseudo(root.left, s);
        ans += getPseudo(root.right, s);

        return ans;
    }
}

LeetCode 1456. Maximum Number of Vowels in a Substring of Given Length

Problem

LeetCode 1456. Maximum Number of Vowels in a Substring of Given Length

1. 题目简述

给出一个字符串s和一个正整数k,返回以k长度的所有子字符串包含元音字母的最大值。例如:

Input: s = "abciiidef", k = 3
Output: 3
Explanation: The substring "iii" contains 3 vowel letters.

Constraints:
1 <= s.length <= 10^5
s consists of lowercase English letters.
1 <= k <= s.length

2. 算法思路

Sliding Window

这道题一眼就能看出来是sliding window,初始化以后每次移动一位,记录历史最大值。

3. 解法

  1. Sliding Window

使用Arrays.copyOfRange(int[] data, int start, int end)函数,写起来简洁,但是有点慢,而且耗空间大,因为要截取数组。


class Solution {
    public int maxVowels(String s, int k) {
        int tempVowels = 0, maxVowels = 0, startIndex = 0, endIndex = k;
        int length = s.length();

        for (int i = 0; i < k; i++) {
            if (isVowel(s.charAt(i))) {
                tempVowels++;
            }
        }

        maxVowels = tempVowels;
        while (endIndex < length) {
            if (isVowel(s.charAt(endIndex))) {
                tempVowels++;
            }
            if (isVowel(s.charAt(startIndex))) {
                tempVowels--;
            }
            endIndex++;
            startIndex++;
            maxVowels = Math.max(maxVowels, tempVowels);
        }

        return maxVowels;
    }

    private boolean isVowel(char c) {
        return (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u');
    }
}

LeetCode 986. Interval List Intersections

Problem

LeetCode 986. Interval List Intersections

1. 题目简述

给出两个闭区间的列表,每个闭区间列表都是以数对的形式顺序排列,且闭区间之间不相连。例如:

Input: A = [[0,2],[5,10],[13,23],[24,25]], B = [[1,5],[8,12],[15,24],[25,26]]
Output: [[1,2],[5,5],[8,10],[15,23],[24,24],[25,25]]
Reminder: The inputs and the desired output are lists of Interval objects, and not arrays or lists.

1. 0 <= A.length < 1000
2. 0 <= B.length < 1000
3. 0 <= A[i].start, A[i].end, B[i].start, B[i].end < 10^9

2. 算法思路

双指针

首先我们看到两个列表的区间很大。。。算了,直接看出来双指针能解决就不假装分析了。

3. 解法

  1. 双指针

class Solution {
    public int[][] intervalIntersection(int[][] A, int[][] B) {
        List<int[]> intersections = new ArrayList();
        int sizeA = A.length, sizeB = B.length, indexA = 0, indexB = 0;

        while (indexA < sizeA && indexB < sizeB) {
            int lo = Math.max(A[indexA][0], B[indexB][0]);
            int hi = Math.min(A[indexA][1], B[indexB][1]);
            if (lo <= hi) {
                intersections.add(new int[]{lo, hi});
            }
            if (hi == A[indexA][1]) {
                indexA++;
            } else {
                indexB++;
            }
        }

        int[][] result = new int[intersections.size()][2];
        for (int i = 0; i < intersections.size(); i++) {
            result[i] = intersections.get(i);
        }

        return result;
    }
}

LeetCode 377. Combination Sum IV

Problem

LeetCode 377. Combination Sum IV

1. 题目简述

给出一个无重复的正整数数组nums,一个目标数target,找出和为target的所有可能的组合,每个数字可以取多个。例如:

Input: candidates = [2,3,6,7], target = 7,
A solution set is:
[
[7],
[2,2,3]
]

2. 算法思路

回溯

那么我们应该使用什么算法呢?还是回溯么?貌似是可行的,只要将循环时的startIndex不设置为i,始终设置为0即可。代码放在下面(实际上会TLE)。

DP

这道题是combination sum系列的第四题,又是不重复的数字,每个数字可以多次,和combination sum I很像,但是1中要求不能有重复的组合([1, 2, 3]和[3, 2, 1]算一种),而4中即使是相同的数字,不同的排列组合也算不同。例如,我们希望用[1, 2, 3]组合得到7,对于[1, 1]和[2]来说,其实目标都是找出组合为5的所有可能的排列组合的可能性,重复计算,数字越大,重复计算越多,所以DP才是相对较好的解。

3. 解法

  1. 回溯。会TLE,原因在于这相当于是一个排列组合问题,由[1, 2, 3]组成30的可能性已经有53798080种,而这其中的每一种都是由好多层递归来实现的,重叠子问题计算得太多。

class Solution {
    int res = 0;
    public int combinationSum4(int[] candidates, int target) {
        Arrays.sort(candidates);

        backtracking(candidates, target, 0, 0);

        return res;
    }

    private void backtracking(int[] candidates, int target, int sum, int startIndex) {
        for (int i = startIndex; i < candidates.length; i++){
            sum += candidates[i];

            if (sum < target) {
                backtracking(candidates, target, sum, 0);
                sum -= candidates[i];
                continue;
            } else if (sum == target) {
                res++;
            }
            return;
        }
    }
}
  1. DP。顺利通过。

class Solution {

    int[] dp;
    public int combinationSum4(int[] candidates, int target) {
        // 这里无需像之前那样进行排序,因为这次不需要对不同的顺序有要求,不同顺序也被当成不同个解。
        // Arrays.sort(candidates);
        dp = new int[target + 1];
        Arrays.fill(dp, -1);
        dp[0] = 1;

        return findSum(candidates, target);
    }

    private int findSum(int candicates, int target) {
        if (target < 0) {
            return 0;
        }
        if (dp[target] != -1) {
            return dp[target];
        }

        for (int num : candidates) {
            dp[target] += find(candidates, target - num);
        }

        dp[target]++;// 注意,由于初始值是-1.所以这里要加1

        return dp[target];
    }
}

LeetCode 216. Combination Sum II

Problem

LeetCode 216. Combination Sum II

1. 题目简述

给出一个目标数n,在1到9中找k个数,使得k个数之和为n(每个数字至多选择一次)。例如:

Input: k = 3, n = 9
Output: [[1,2,6], [1,3,5], [2,3,4]]

2. 算法思路

回溯

这道题是combination sum系列的第三题,又是不重复的数字,且每个数字至多只能选一次,而且要求一定是k个。

限制条件变多,但依然是用回溯法做,每次判断下一层的时候记得k-1,且最终add结果时判断k是否为k-1是否为0。数字为1-9,这次不需要传数组进去了,直接用数字就好。

3. 解法

  1. 回溯,记得如果更改了k的话也要将k复原,回溯法一定注意复原!

class Solution {

    List<List<Integer>> res = new ArrayList();

    public List<List<Integer>> combinationSum3(int k, int n) {
        LinkedList<Integer> nums = new LinkedList();

        backtracking(n, 0, 1, nums, k);

        return res;
    }

    private void backtracking(int target, int sum, int start, LinkedList<Integer> nums, int k) {
        for (int i = start; i < 10; i++) {
            sum += i;
            nums.add(i);

            if (sum < target) {
                backtracking(target, sum, i + 1, nums, k - 1);
                nums.removeLast();
                sum -= i;
                continue;
            } else if (sum == target && k - 1 == 0) {
                res.add(new ArrayList(nums));
            }

            nums.removeLast();
            return;
        }
    }
}

LeetCode 40. Combination Sum II

Problem

LeetCode 40. Combination Sum II

1. 题目简述

给出一个正整数数组A(可能存在重复),给出一个target,找出和为target的所有不重复的可能组合(每个数字最多只能使用一次)。例如:

Input: candidates = [10,1,2,7,6,1,5], target = 8,
A solution set is:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]

2. 算法思路

这道题是combination sum系列的第二题,区别在于这次不是无限适应数字了,而是每个数字都最多使用一次。其实算法类似,这道题还是不能用DP,因为每个数字只能用一次,子问题相互干涉,使用不是DP问题(下周做DP的背包问题吧,要不然太烦了,感觉跟这个combination sum好像啊)。

回溯

此题我们仍然考虑用回溯法,其实可以用combination sum I的代码,只需要在递归调用backtracking的时候将startIndex从i变为i+1而已,最后还需要将整个结果集去重(因为存在存在重复数字,假设说我们有三个1,有一种解法需要两个1和其他数字,如果不去重,这一种解法就会膨胀成三个)。

当然,还有一种更巧妙的去重方法,如果临时想的话不一定想得到,直接记忆吧,放在解法2里。

3. 解法

  1. 回溯,使用hashset去重法,巨慢

class Solution {

    List<List<Integer>> res;
    Set<List<Integer>> hashset;

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        hashset = new HashSet();
        LinkedList<Integer> nums = new LinkedList();

        backtracking(candidates, target, 0, 0, nums);

        res = new ArrayList(hashset);
        return res;
    }

    private void backtracking(int[] candidates, int target, int sum, int startIndex, LinkedList<Integer> nums) {
        for (int i = startIndex; i < candidates.length; i++) {
            sum += candidates[i];
            nums.add(candidates[i]);

            if (sum < target) {
                backtracking(candidates, target, sum, i + 1, nums);
                nums.removeLast();
                sum -= candidates[i];
                continue;
            } else if (sum == target) {
                hashset.add(new ArrayList(nums));
            }

            nums.removeLast();
            return;
        }
    }
}
  1. 回溯,循环判断去重,优雅。

class Solution {

    List<List<Integer>> res;

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        LinkedList<Integer> nums = new LinkedList();
         res = new ArrayList();

        backtracking(candidates, target, 0, 0, nums);

        return res;
    }

    private void backtracking(int[] candidates, int target, int sum, int startIndex, LinkedList<Integer> nums) {
        for (int i = startIndex; i < candidates.length; i++) {

            // 优雅的去重方式,记住,以后可能用得到!
            if (i > startIndex && candidates[i - 1] == candidates[i]) {
                continue;
            }

            sum += candidates[i];
            nums.add(candidates[i]);

            if (sum < target) {
                backtracking(candidates, target, sum, i + 1, nums);
                nums.removeLast();
                sum -= candidates[i];
                continue;
            } else if (sum == target) {
                res.add(new ArrayList(nums));
            }

            nums.removeLast();
            return;
        }
    }
}

LeetCode 39. Combination Sum

Problem

LeetCode 39. Combination Sum

1. 题目简述

给出一个无重复的正整数数组A,给出一个target,找出和为target的所有不重复的可能组合(每个数字使用次数不限)。例如:

Input: candidates = [2,3,5], target = 8,
A solution set is:
[
[2,2,2,2],
[2,3,3],
[3,5]
]

2. 算法思路

这道题是combination sum系列的第一题,看似有点像背包问题,实际上又不是。这道题问的是所有组合,某些DP问题也是求所有的组合之类的,但是本题要求不能有相同元素组成的组合,且找不到所谓的最优子问题结构,因此所以并不是DP问题(等复习到背包问题再回过来看)。

Brute Force(行不通)

首先,最容易想到的就是能不能用暴力破解法呢?答案肯定是不行的,由于每个数字取的次数不限,所以直接暴力破解不可取,需要优化。

回溯

那么为什么会想到回溯呢?没有为什么,因为回溯能做出来这种需要遍历所有可能的问题。就这么简单。

对于一个数组A和某个target值,我们希望找出target所有的组合可能,假设F(int[] A, int start, int target)为从数组A中以start为起点找出target的所有可能组合,思路如下。

  1. 对A从小到大进行排序,取A[0]作为第一个数,那么我们当前的目标是F(A, 0, target - A[0]);将其结果和A[0]放在一起作为第一种解。
  2. 然后取A[1]作为第一个数,然后我们目标是F(A, 1, target - A[1])。不从0开始是因为如果取了0,就会和第一步中的某种solution重复,故不取。
  3. 终止条件为target == 0,最终结果添加一条;target < 0,回溯上去继续搞。

3. 解法

  1. 暴力解法(Pass)
  2. 回溯

class Solution {
    List<List<Integer>> res;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        res = new ArrayList();
        LinkedList<Integer> nums = new LinkedList(); //删除last回溯的时候方便
        Arrays.sort(candidates);

        backtracking(candidates, target, 0, 0, nums);

        return res;
    }

    private void backtracking(int[] candidates, int target, int sum, int startIndex, LinkedList<Integer> nums) {
        for (int i = startIndex; i < candidates.length; i++){
            sum += candidates[i];
            nums.add(candidates[i]);

            if (sum < target) {
                backtracking(candidates, target, sum, i, nums);
                sum -= candidates[i];
                nums.removeLast();
                continue;
            } else if (sum == target) {
                res.add(new ArrayList(nums));
            }

            nums.removeLast();
            return;
        }
    }
}

LeetCode 968. Binary Tree Cameras

Problem

LeetCode 968. Binary Tree Cameras

1. 题目简述

给出一棵二叉树,我们向节点中加入camera,每个节点可以监控其父节点和左右孩子,求最少需要多少个camera才能监控整棵树(节点不大于1000个,每个节点的value都是0)。例如:

最少camera数
Input: [0,0,null,0,null,0,null,null,0]
Output: 2
Explanation: At least two cameras are needed to monitor all nodes of the tree. The above image shows one of the valid configurations of camera placement.

2. 算法思路

Greedy:

这道题完全没思路,直接看了solution,DP解法没看懂,直接看的Greedy,逻辑思路如下。

对于任何一个节点,它都有两种状态,有camera和没camera,其中没camera又分为两种,一种是它已经被covered了,另一种是还没有。所以一共是有三种状态。我们假设某节点和其左右孩子都需要被cover,那么我们首选肯定是在父节点放入camera,这样它才能辐射范围更广,而不是需要两个孩子放camera,camera更多。所以我们的核心策略就是尽可能把camera设置到上层,而不是叶子节点。

首先我们的定义一个节点的三种状态:

  1. 有camera – 2
  2. 无camera但是被cover了 – 1
  3. 无camera并且需要父亲节点carry – 0

对于null节点,我们不需要考虑,所以其状态为1。

所以逻辑如下:

  1. 如果当前节点的左右孩子有一个为2(有camera),则当前节点为1;
  2. 如果当前节点的左右孩子有一个为0(无camera,需要父亲带飞),则当前节点为2;
  3. 如果当前节点的左右孩子都为1(都不用父亲carry),则当前节点期待父节点carry,当前节点为0;
  4. 如果根节点为0(没被孩子带飞),则result+1。

时间复杂度为O(n),空间复杂度O(h)。h为树的高度。

3. 解法

  1. Greedy
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    int res = 0;
    public int minCameraCover(TreeNode root) {
        return getCameraState(root) == 0 ? res + 1: res;
    }

    private int getCameraState(TreeNode node) {
        if (node == null) {
            return 1;
        }

        int leftState = getCameraState(node.left);
        int rightState = getCameraState(node.right);
        if (leftState == 1 && rightState == 1) {
            return 0; // 当前节点不放camera,期待被父亲carry
        } else if (leftState == 0 || rightState == 0) {
            res++;
            return 2; // 当前节点的孩子节点存在0状态,所以需要放一个camera
        } else { // 当前节点的孩子至少有一个2
            return 1;
        }
    }
}

LeetCode 451. Sort Characters By Frequency

Problem

LeetCode 451. Sort Characters By Frequency

1. 题目简述

给出一个字符串,将其字母按照字母出现频率进行降序排列。例如:

Input:
"Aabb"

Output:
"bbAa"

Explanation:
"bbaA" is also a valid answer, but "Aabb" is incorrect.
Note that 'A' and 'a' are treated as two different characters.

2. 算法思路

HashTable:

一考虑到频率的问题,首先就应该想到hashmap来进行计算。然后比较麻烦的是对hashmap的value进行排序,这个需要重写一个comparator,用来排序Map.Entry,写的比较少,多练练。然后按照顺序输出就可以了。

时间复杂度为O(nlogn),这个是排序的时间复杂度;空间复杂度最大为O(n).

Bucket Sort:

第二种做法是用bucket sort,将字符放入hashmap后,计算出最大频率freq,然后简历一个长度为freq+1的list,对于不同频率的字符放入其中。

3. 解法

  1. HashTable

class Solution {
    public String frequencySort(String s) {
        Map<Character, Integer> dict = new HashMap();
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            dict.put(c, dict.getOrDefault(c, 0) + 1);
        }

        // 重点在这里!!!Comparator的写法,以及如何对hashmap转为list后排序
        List<Map.Entry<Character, Integer>> list = new ArrayList(dict.entrySet());
        Collections.sort(list, new Comparator<Map.Entry<Character, Integer>>(){
            public int compare(Map.Entry<Character, Integer> o1, Map.Entry<Character, Integer> o2) {
                return o2.getValue() - o1.getValue();
            }
        });

        StringBuilder sb = new StringBuilder();
        for (Map.Entry<Character, Integer> entry : list) {
            int copy = entry.getValue();
            char c = entry.getKey();
            while (copy > 0) {
                sb.append(c);
                copy--;
            }
        }

        return sb.toString();
    }
}
  1. Bucket Sort

这里要注意三个地方:见注释。

class Solution {
    public String frequencySort(String s) {
        Map<Character, Integer> dict = new HashMap();
        int maxFrequency = 0;
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            dict.put(c, dict.getOrDefault(c, 0) + 1);
            maxFrequency = Math.max(maxFrequency, dict.get(c));
        }

        int size = dict.size();
        // 注意1:这里定义数组时不用初始化,抽象定义
        List<Character>[] bucket = new List[maxFrequency + 1];

        for (Map.Entry<Character, Integer> entry : dict.entrySet()) {
            // 注意2:这路记得初始化bucket数组
            if (bucket[entry.getValue()] == null) {
                bucket[entry.getValue()] = new ArrayList();
            }
            bucket[entry.getValue()].add(entry.getKey());
        }

        StringBuilder sb = new StringBuilder();
        for (int i = maxFrequency; i > 0; i--) {
            // 注意3:bucket数组可能未被初始化,这里需要判断是否为空
            for (Character character : bucket[i]) {
                for (int j = i; j > 0; j--) {
                    sb.append(character.charValue());
                }
            }
        }

        return sb.toString();
    }
}

LeetCode 814. Binary Tree Pruning

Problem

LeetCode 814. Binary Tree Pruning

1. 题目简述

给出一颗二叉树,其每个节点都由0或1组成,删除其所有的节点都为0的子树,返回新的树的root节点。例如:

Input: [1,0,1,0,0,0,1]
Output: [1,null,1,null,1]

Example

2. 算法思路

递归:

和以前的一些求子树和的题目很像,例如LeetCode 508. Most Frequent Subtree Sum,我们也可以求子树和,如果和为0,则说明将当前节点置为null即可。与返回布尔值的效果一致。仍然使用后序遍历。

3. 解法

  1. 如果节点为null,直接返回0.
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public TreeNode pruneTree(TreeNode root) {
        int rootSum = subtreeSum(root);
        if (rootSum == 0) {
            return null;
        }

        return root;
    }

    private int subtreeSum(TreeNode root){
        if (root == null) {
            return 0;
        }

        int leftSum = subtreeSum(root.left);
        int rightSum = subtreeSum(root.right);

        if (leftSum == 0) {
            root.left = null;
        }
        if (rightSum == 0) {
            root.right = null;
        }

        return root.val + leftSum + rightSum;
    }
}

LeetCode 508. Most Frequent Subtree Sum

Problem

LeetCode 508. Most Frequent Subtree Sum

1. 题目简述

给出一颗二叉树,找出其所有子树并求子树和,找出频率最高的子树和(如果有频率一致的,返回多个)。例如:

Examples 1
Input:

   5
  /  \
 2   -3
return [2, -3, 4], since all the values happen only once, return all of them in any order.

Examples 2
Input:

   5
  /  \
 2   -5
return [2], since 2 happens twice, however -5 only occur once.

2. 算法思路

后序遍历 + HashMap:

很明显的后序遍历,对于每一个节点,都计算它的左子树和和右子树和,和自己本身的val相加,就是自己的子树和,统计完成后,找出最高频率,然后再次遍历HashMap,找出符合条件的sum和,最后再将储存结果的list转为array。

3. 解法

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {

    Map<Integer, Integer> res = new HashMap();
    public int[] findFrequentTreeSum(TreeNode root) {
        int[] ret = new int[0];
        if (root == null) {
            return ret;
        }

        getSum(root);

        int maxFrequency = 0;
        for (Map.Entry<Integer, Integer> entry : res.entrySet()) {
            maxFrequency = Math.max(maxFrequency, entry.getValue());
        }

        List<Integer> list = new ArrayList();
        for (Map.Entry<Integer, Integer> entry : res.entrySet()) {
            if (maxFrequency == entry.getValue()) {
                list.add(entry.getKey());
            }
        }

        ret = new int[list.size()];
        for (int i = 0; i < list.size(); i++) {
            ret[i] = list.get(i).intValue();
        }

        return ret;
    }

    private int getSum(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int sum = getSum(root.left) + getSum(root.right) + root.val;
        res.put(sum, (res.containsKey(sum) ? res.get(sum) : 0) + 1);

        return sum;
    }
}

LeetCode 450. Delete Node in a BST

Problem

LeetCode 450. Delete Node in a BST

1. 题目简述

给出一棵BST树,删除其中一个值为key的节点,返回新的树的根节点。例如:

root = [5,3,6,2,4,null,7]
key = 3

    5
   / \
  3   6
 / \   \
2   4   7

Given key to delete is 3. So we find the node with value 3 and delete it.

One valid answer is [5,4,6,2,null,null,7], shown in the following BST.

    5
   / \
  4   6
 /     \
2       7

Another valid answer is [5,2,6,null,4,null,7].

    5
   / \
  2   6
   \   \
    4   7

2. 算法思路

中序遍历

对于一棵BST树,其最重要的性质就是其有序性。删除一个节点后,整棵树仍然保持有序。在这里我们需要明确一个节点的前驱结点(Predecessor)后继节点(Successor)。某节点的前驱结点就是中序遍历的情况下在该节点之前的一个节点,后继节点就是该节点之后的一个节点。

删除一个节点时,需要将它的前驱结点或后继节点填补到它原本的位置上。那么逻辑就很清晰了,有几种不同的节点情况,我们分情况讨论。

  1. 删除的节点为叶子节点:直接删除,毫无压力。
  2. 删除的节点存在右子树:找到右子树的最小节点(后继节点),将该节点替换到删除节点的位置。
  3. 删除的节点为非叶子节点,且不存在右子树:找到左子树的最大节点(前驱节点),将该节点替换到删除节点的位置

或者我们可以遵循另一种思路:

  1. 删除的节点存在右子树:找到右子树的最小节点,替换为当前节点。
  2. 删除的节点不存在右子树,直接将左孩子替换为当前节点。

3. 解法

  1. 解法1:递归
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
  /*
  One step right and then always left
  */
  public int successor(TreeNode root) {
    root = root.right;
    while (root.left != null) root = root.left;
    return root.val;
  }

  /*
  One step left and then always right
  */
  public int predecessor(TreeNode root) {
    root = root.left;
    while (root.right != null) root = root.right;
    return root.val;
  }

  public TreeNode deleteNode(TreeNode root, int key) {
    if (root == null) return null;

    // delete from the right subtree
    if (key > root.val) root.right = deleteNode(root.right, key);
    // delete from the left subtree
    else if (key < root.val) root.left = deleteNode(root.left, key);
    // delete the current node
    else {
      // the node is a leaf
      if (root.left == null && root.right == null) root = null;
      // the node is not a leaf and has a right child
      else if (root.right != null) {
        root.val = successor(root);
        root.right = deleteNode(root.right, root.val);
      }
      // the node is not a leaf, has no right child, and has a left child 
      else {
        root.val = predecessor(root);
        root.left = deleteNode(root.left, root.val);
      }
    }
    return root;
  }
}
  1. 解法2:记录前驱结点
class Solution {
    public TreeNode deleteNode(TreeNode root, int key) {
        // 这里我分为两种情况讨论吧,看blog都是三种情况,不知道可行与否
        // 1. 存在右子树:将右子树中最小值替换掉当前值
        // 2. 不存在右子树:将左子树替换掉

        TreeNode pre = null, node = root;
        while (node != null && node.val != key) {
            if (node.val < key) {
                pre = node;
                node = node.right;
            } else if (node.val > key) {
                pre = node;
                node = node.left;
            }
        }
        if (node == null) {
            return root;
        }

        if (node.right == null) {
            // 如果是根节点
            if (pre == null) {
                return node.left;
            }
            // 右子树为空
            if (pre.val < node.val) {
                pre.right = node.left;
            } else {
                pre.left = node.left;
            }
        } else {
            // 右子树不为空
            TreeNode rightRoot = node.right, rightPre = null;
            while (rightRoot.left != null) {
                rightPre = rightRoot;
                rightRoot = rightRoot.left;
            }
            node.val = rightRoot.val;
            if (rightPre != null) {
                rightPre.left = rightRoot.right;
            } else {
                node.right = rightRoot.right;
            }
        }

        return root;
    }
}

LeetCode 1277. Count Square Submatrices with All Ones

Problem

LeetCode 1277. Count Square Submatrices with All Ones

1. 题目简述

给出一个m x n的矩阵,问有多少个正方形子矩阵是都由1组成的。例如:

Input: matrix =
[
[0,1,1,1],
[1,1,1,1],
[0,1,1,1]
]
Output: 15
Explanation: 
There are 10 squares of side 1.
There are 4 squares of side 2.
There is  1 square of side 3.
Total number of squares = 10 + 4 + 1 = 15. 

2. 算法思路

Brute Force:

暴力解法,这种方式就是遍历所有的可能的正方形的长度,范围从1到min(m, n),然后再遍历所有的可能的位置,如果符合条件,则count+1,最后返回count。这种方式显然速度很慢,时间复杂度为O(mn * min(m, n))。

Dynamic Programming:

这道题的DP思路我也是没有想到的,我们假设F(i, j)为以(i, j)为右下角的正方形的个数,则有如下推导:

$$F(i, j) = min(F(i - 1, j), F(i - 1, j - 1), F(i, j - 1)) + 1$$

记住吧就,想象一下很简单就能得出该公式是正确的,但是没遇过很难想到。

3. 解法

  1. DP大法好
class Solution {
    public int countSquares(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length, res = 0;
        int[][] dp = new int[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] > 0 && i > 0 && j > 0) {
                    dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i - 1][j]), dp[i][j - 1]) + 1;
                } else {
                    dp[i][j] = matrix[i][j];
                }
                res += dp[i][j];
            }
        }

        return res;
    }
}

LeetCode 666. Path Sum IV

Problem

LeetCode 666. Path Sum IV

1. 题目简述

如果一棵树的高度不超过5,则这棵树可以被表示为一列三位数字的数组(数组从小到大排列),对于每位数字都有其表示的含义:

  1. 百位D表示该节点的深, 1 <= D <= 4
  2. 十位P表示这个数字在该层的位置,1 <= P <= 8
  3. 个位V表示该节点的值,0 <= V <= 9

求所有的从root到leaf的路径和。例如:

Input: [113, 215, 221]
Output: 12
Explanation: 
The tree that the list represents is:
    3
   / \
  5   1

The path sum is (3 + 5) + (3 + 1) = 12.

2. 算法思路

DFS

此题的算法并不难,之前也有算过类似的题目,难点在于如何将一棵树完整地复原。一种做法是自己定义TreeNode,然后完整复原整棵树;另一种是使用HashMap来巧妙存储树,用HashMap的时候,前两位为key,个位为value,D和P加在一起能够唯一确定一个节点,且其子节点和右节点的key也能过通过数学公式算出来;第三种就是使用数组来储存,因为高度最高也就是4,建立一个二维数组来存储。

3. 解法

  1. 自己定义TreeNode来存储,然后遍历

class Solution {

    class Node {
        Node left, right;
        int val;
        Node(int v) {val = v;
    }

    int res = 0;
    public int pathSum(int[] nums) {
        Node root = new Node(nums[0] % 10);

        // 这个构建方法中,判断左右子树那块没太搞懂,直接抄了
        for (int num: nums) {
            if (num == nums[0]) continue;
            int depth = num / 100, pos = num / 10 % 10, val = num % 10;
            pos--;
            Node cur = root;
            for (int d = depth - 2; d >= 0; --d) {
                if (pos < 1<<d) {
                    if (cur.left == null) cur.left = new Node(val);
                    cur = cur.left;
                } else {
                    if (cur.right == null) cur.right = new Node(val);
                    cur = cur.right;
                }
                pos %= 1<<d;
            }
        }

        dfs(root, 0);
        return res;
    }

    private void dfs(TreeNode root, int currSum) {
        if (root == null) {
            return;
        }

        currSum += root.val;
        if (root.left == null && root.right == null) {
            res += currSum;
            return;
        }

        dfs(root.left, currSum);
        dfs(root.right, currSum);
    }
}

  1. 使用HashMap来遍历整棵树(直接抄了solution)

class Solution {
    int ans = 0;
    Map<Integer, Integer> values;
    public int pathSum(int[] nums) {
        values = new HashMap();
        for (int num: nums)
            values.put(num / 10, num % 10);

        dfs(nums[0] / 10, 0);
        return ans;
    }

    public void dfs(int node, int sum) {
        if (!values.containsKey(node)) return;
        sum += values.get(node);

        int depth = node / 10, pos = node % 10;
        int left = (depth + 1) * 10 + 2 * pos - 1;
        int right = left + 1;

        if (!values.containsKey(left) && !values.containsKey(right)) {
            ans += sum;
        } else {
            dfs(left, sum);
            dfs(right, sum);
        }
    }
}
  1. 我的原始做法,使用数组来存储
class Solution {
    int res = 0;
    public int pathSum(int[] nums) {
        int[][] tree = new int[4][];

        for (int i = 0; i < 4; i++) {
            tree[i] = new int[(int)Math.pow(2, i)];
            Arrays.fill(tree[i], -1);
        }

        for (int x : nums) {
            int d = x / 100;
            int p = (x % 100) / 10;
            int v = x % 10;
            tree[d - 1][p - 1] = v;
        }

        dfs(tree, 0, 0, 0);

        return res;
    }

    private void dfs(int[][] tree, int d, int p, int sum) {
        if (d >= 4 || p >= (int)Math.pow(2, d) || tree[d][p] == -1) {
            return;
        }
        sum += tree[d][p];
        if (d == 3 || (tree[d + 1][2 * p] == -1 && tree[d + 1][2 * p + 1] == -1)) {
            res += sum;
            return;
        }
        dfs(tree, d + 1, 2 * p, sum);
        dfs(tree, d + 1, 2 * p + 1, sum);
    }
}

LeetCode 437. Path Sum III

Problem

LeetCode 437. Path Sum III

1. 题目简述

给出一棵二叉树和一个sum值,根节点为root,找出所有路path为sum的路径的数量,这里的path不一定要从根节点到叶子节点,但是一定要从父亲节点到孩子节点,不能回溯。例如:

root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8

      10
     /  \
    5   -3
   / \    \
  3   2   11
 / \   \
3  -2   1

Return 3. The paths that sum to 8 are:

1.  5 -> 3
2.  5 -> 2 -> 1
3. -3 -> 11

2. 算法思路

DFS

此题难度为easy是我没想到的,其实感觉逻辑上还是有点难理解的,而且存在一种巧妙的HashMap的做法,作为解法2。

第一种解法。对于这个和之前最大的不同就是需要考虑不是从根节点出发的path了,对于某节点p来说我们需要计算的是包含p节点的以p为start出发的path;然后再加上从p的左右孩子出发的path,左子树和右子树分别计算。勉强算是前序遍历?完全取决于return的加法顺序。

第二种解法。逆向思维,第一种解法中考虑了从某节点p出发的path和为sum的路径;在这个基础上,我们是否可以换个思路,考虑了以某节点p结尾的path和为sum的路径。因此,对于任意一个节点p,我们需要计算的是以p为结尾的路径和为sum的path数,然后将其加和。

3. 解法

  1. 使用LinkedList保存当前路径,遍历后从currPath中删除当前节点,一定要删除!而且要注意,add到result里的时候,记得new一个对象,否则currPath会变。
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public int pathSum(TreeNode root, int sum) {
        return findPathSum(root, sum) + pathSum(root.left, sum) + pathSum(root.right, sum)
    }

    // 经过以root节点为start节点的path,和为sum的路径个数
    private int findPathSum(TreeNode root, int sum) {
        if (root == null) {
            return 0;
        }
        return (root.val == sum ? 1 : 0) + findPathSum(root.left, sum - root.val) + findPathSum(root.right, sum - root.val);
    }
}
  1. 使用HashMap来记录到某节点p之前的,从root节点到其中任意一个节点的路径之和,方法很巧妙,需要硬记。记得最后要删除当前这个currSum的记录。

class Solution {
    int count = 0;
    public int pathSum(TreeNode root, int sum) {
        Map<Integer, Integer> preSumMap = new HashMap();
        preSumMap.put(0, 1);
        findPathSum(root, 0, sum, preSumMap);
        return count;
    }

    private void findPathSum(TreeNode root, int currSum, int target, Map<Integer, Integer> preSumMap) {
        if (root == null) {
            return;
        }

        currSum += root.val;

        if (preSumMap.containsKey(currSum - target)) {
            count += preSumMap.get(currSum - target);
        }

        preSumMap.put(currSum, preSumMap.containsKey(currSum) ?(preSumMap.get(currSum) + 1) : 1);

        findPathSum(root.left, currSum, target, preSumMap);
        findPathSum(root.right, currSum, target, preSumMap);

        preSumMap.put(currSum, preSumMap.get(currSum) - 1);
    }
}

LeetCode 129. Sum Root to Leaf Numbers

Problem

LeetCode 129. Sum Root to Leaf Numbers

1. 题目简述

给出一棵只含有value值为0-9的二叉树,根节点为root,每一条从root到leaf节点的路径都代表一个数字,求这些所有数字之和。例如:

Input: [4,9,0,5,1]
    4
   / \
  9   0
 / \
5   1
Output: 1026
Explanation:
The root-to-leaf path 4->9->5 represents the number 495.
The root-to-leaf path 4->9->1 represents the number 491.
The root-to-leaf path 4->0 represents the number 40.
Therefore, sum = 495 + 491 + 40 = 1026.

2. 算法思路

DFS

很简单的DFS的思路,前序遍历的思路,到达根节点时做一次判断。

3. 解法

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    int res = 0;

    public int sumNumbers(TreeNode root) {
        dfs(root, 0);
        return res;
    }

    private void dfs(TreeNode root, int currSum) {
        if  (root == null) {
            return;
        }
        currSum = currSum * 10 + root.val;
        if (root.left == null && root.right == null) {
            res += currSum;
        }

        dfs(root.left, currSum);
        dfs(root.right, currSum);
    }
}

LeetCode 113. Path Sum II

Problem

LeetCode 113. Path Sum II

1. 题目简述

给出一棵二叉树和一个sum值,根节点为root,找出所有从根节点到叶子节点的和为sum的路径。例如:

Given the below binary tree and sum = 22,

      5
     / \
    4   8
   /   / \
  11  13  4
 /  \      \
7    2      1

Return:
[
[5,4,11,2],
[5,8,4,5]
]

2. 算法思路

DFS

此题难度为medium,思路和Path Sum 1差不多,也是DFS遍历所有路径,这里有两种做法,一种是保留根节点到当前节点的路径,遍历完成后从list中删除该节点,需要写一个辅助函数;另一种做法是返回左孩子和右孩子到根节点和为sum-root.val的路径,然后拼接。

第一次做的时候用的是解法2,存在较多的拼接操作,运行速度较慢,放在下面的解法2.

3. 解法

  1. 使用LinkedList保存当前路径,遍历后从currPath中删除当前节点,一定要删除!而且要注意,add到result里的时候,记得new一个对象,否则currPath会变。
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {

    public List<List<Integer>> pathSum(TreeNode root, int sum) {
        List<List<Integer>> result = new ArrayList<List<Integer>>();
        List<Integer> currPath = new LinkedList();
        pathSum(root, sum, currPath, result);
        return result;
    }

    private void pathSum(TreeNode root, int sum, List<Integer> currPath, List<List<Integer>> result) {
        if (root == null) {
            return;
        }

        currPath.add(new TreeNode(root.val));
        if (root.left == null && root.right == null && root.val == sum) {
            result.add(new LinkedList(currPath));
        } else {
            pathSum(root.left, sum - root.val, currPath, result);
            pathSum(root.right, sum - root.val, currPath, result);
        }
        currPath.remove(currPath.size() - 1);

    }
}
  1. 不适用辅助函数,将左右节点返回的符合条件的list拼接起来
class Solution {
    public List<List<Integer>> pathSum(TreeNode root, int sum) {
        List<List<Integer>> result = new ArrayList<List<Integer>>();

        if (root == null || (root.left == null && root.right == null && root.val != sum)) {
            return result;
        }

        if (root.left == null && root.right == null && root.val == sum) {
            List<Integer> path = new ArrayList();
            path.add(root.val);
            result.add(path);
            return result;
        }

        List<List<Integer>> leftList = pathSum(root.left, sum - root.val);
        List<List<Integer>> rightList = pathSum(root.right, sum - root.val);

        if (leftList.size() == 0 && rightList.size() == 0) {
            return result;
        }

        if (leftList.size() > 0) {
            for (List<Integer> list : leftList) {
                List<Integer> path = new ArrayList();
                path.add(root.val);
                path.addAll(list);
                result.add(path);
            }
        }

        if (rightList.size() > 0) {
            for (List<Integer> list : rightList) {
                List<Integer> path = new ArrayList();
                path.add(root.val);
                path.addAll(list);
                result.add(path);
            }
        }

        return result;

    }
}

LeetCode 112. Path Sum

Problem

LeetCode 112. Path Sum

1. 题目简述

给出一棵二叉树和一个sum值,根节点为root,求从root节点到叶子节点是否存在一条路径其节点之和为sum。例如:

Given the below binary tree and sum = 22,

      5
     / \
    4   8
   /   / \
  11  13  4
 /  \      \
7    2      1
return true, as there exist a root-to-leaf path 5->4->11->2 which sum is 22.

2. 算法思路

DFS

很简单的DFS的思路,中序遍历的思路,记录从根节点到当前节点的和,到达根节点时做一次判断。

3. 解法

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public boolean hasPathSum(TreeNode root, int sum) {
        if (root == null) {
            return false;
        }
        if (root.left == null && root.right == null && sum == root.val) {
            return true;
        }

        return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val);
    }
}

LeetCode 1306. Jump Game III

Problem

LeetCode 1306. Jump Game III

1. 题目简述

给出一个整形非负数组,初始在index为start的位置。当处于i位置时,可以跳跃到i - arr[i]或者i + arr[i]的位置。判断是否能够到达一个值为0的位置。例如:

Example 1:

Input: arr = [4,2,3,0,3,1,2], start = 5
Output: true
Explanation: 
All possible ways to reach at index 3 with value 0 are: 
index 5 -> index 4 -> index 1 -> index 3 
index 5 -> index 6 -> index 4 -> index 1 -> index 3 

Example 2:

Input: arr = [3,0,2,1,2], start = 2
Output: false
Explanation: There is no way to reach at index 1 with value 0.

2. 算法思路

BFS或DFS

虽然这道题叫jump game,但是和之前的jump game完全不同。理由在于jump game1和2都是可以看做是动态规划问题,然后进一步简化使用Greedy。但是这道题并不是一个求极值的问题,所以不要被迷惑,这道题不能用DP,而是其他方法。

那么其实就两种情况,一种是可以顺利到达节点为0,另一种不能。如果能够到达,则最终的case则是当前节点值为0;如果不能到达,则表示全局陷入了一个循环怪圈,屡次循环却无法到达,也就是说我们某种跳跃策略一旦发现了循环的迹象,立刻终止。

与其说是图,更像是一棵树,每个节点的两个子节点为i+arr[i]和i - arr[i],因此BFS和DFS其实都是行得通的。这里两种算法都写出来。

Tips:
这里运用了一个小技巧,因为arr[i]未遍历时一定大于0(除去最终的0节点),我们遍历完某节点后将其置为-arr[i],下次再遍历到的时候,如果发现其为负值,则立刻返回false。

3. 解法

  1. DFS
class Solution {
    public boolean canReach(int[] arr, int start) {
        if (strat < 0 || start > arr.length - 1 || arr[start] < 0) {
            return false;
        }
        if (arr[start] == 0) {
            return true;
        }
        arr[start] = -arr[start];

        return canReach(arr, start + arr[start]) || canReach(arr, start - arr[start]);
    }
}
  1. BFS
class Solution {
    public boolean canReach(int[] arr, int start) {
        Queue<Integer> queue = new LinkedList();
        queue.add(start);

        while (!queue.isEmpty()) {
            int index = queue.poll();
            if (arr[index] == 0) {
                return true;
            }
            if (arr[index] > 0 && index - arr[index] >= 0) {
                queue.add(index - arr[index]);
            }
            if (arr[index] > 0 && index + arr[index] < arr.length) {
                queue.add(index + arr[index]);
            }
            arr[index] = -arr[index];
        }

        return false;
    }
}

LeetCode 45. Jump Game II

Problem

LeetCode 45. Jump Game II

1. 题目简述

给出一个整形非负数组,初始在index为0的位置,每个位置上的数代表其所能跳跃的最大长度(不是只能跳跃该长度),保证一定能够跳到末尾节点,求从0节点跳到末尾节点的最少跳跃次数。例如:

Input: [2,3,1,1,4]
Output: 2
Explanation: The minimum number of jumps to reach the last index is 2.
    Jump 1 step from index 0 to 1, then 3 steps to the last index.

2. 算法思路

Brute Force

首先,暴力求解法直接pass,我们需要遍历所有的步长,我们假设F(x)为从index为x的节点到末尾节点的最短jump数。暴力求解法伪代码:

F(x):
    if x >= n - 1
        return 0;
    maxStep = nums[x];
    for i from 1 to maxStep:
        memo[x] = min(memo[x], F(x + i) + 1)
    return memo[x]

Dynamic Programming

这里我们也可以用DP来减少重复计算,对于每个节点到末尾节点最少跳跃次数设为函数F(x),用memo数组来进行记录并更新。下面是top-down和bottom-up的两种算法。

DP1 top-down:

记录一个memo数组,长度为n,每个元素初始化为n(因为数组长度为n,最多跳跃次数也不过是n-1),计算F(0),对于每个0节点能到达的步长,进行比较,记录,找出最小的那个+1即为0节的最少跳跃数。

DP2 bottom-up:
记录一个memo数组,bottom-up算法就是要从base case开始进行计算,一直到目标target,本题中,base case是memo[n-1]=1,所以我们要从n-2开始往回遍历,判断n-2节点能否到达n-1节点,如果能,则memo[n-2]=1,不能则为0,一直遍历到memo[0],返回memo[0]。

两种DP算法时间复杂度均为O(n^2),空间复杂度为O(n)。

Greedy

本题是要找最少的跳跃步数,也就是说我们希望每一步都尽可能要跳得远,步子迈得大。举个例子:
[2,3,1,1,4],我们在0节点时,最大步长为2,因此最元能到达1和2两个节点,其最大步长分别为3和1,如果我们想走得远,我们当然希望能够第一次走到1节点,它的最大步长为3,可以到达4节点。

因此我们可以看出,对于初始节点0,它能够到达的最远为farthest为end,然后我们从0到end遍历找出其中x节点能跳到最远距离新的farthest,本次跳跃就跳到x节点,jump++,新的end为farthest,以此类推。直到最后,返回jump值。

3. 解法

  1. 暴力解法(Pass,上面有伪代码)
  2. DP top-down

class Solution{
    public int jump(int[] nums) {
        int n = nums.length;
        int[] memo = new int[nums.length];
        Arrays.fill(memo, n);
        return minimumJump(nums, 0, memo);
    }

    private int minimumJump(int[] nums, int x, int[] memo) {
        int n = nums.length;
        if (x >= n - 1) {
            return 0;
        }
        if (memo[x] < n) {
            return memo[x];
        }

        int maxStep = nums[x];
        for (int i = 1; i <= maxStep; i++) {
            memo[x] = Math.min(memo[x], minimumJump(nums, x + i, memo) + 1);
        }

        return memo[x];
    }
}
  1. DP bottom-up
class Solution{
    public int jump(int[] nums) {
        int n = nums.length;
        int[] memo = new int[nums.length];
        Arrays.fill(memo, n);
        memo[n - 1] = 0;

        for (int i = n - 2; i >= 0; i--) {
            int maxStep = nums[i];
            for (int j = 1; j <= maxStep; j++) {
                if (i + j > n - 1) {
                    break;
                }
                memo[i] = Math.min(memo[i], memo[i + j] + 1);
            }
        }

        return memo[0];
    }
}
  1. Greedy
class Solution{
    public int jump(int[] nums) {
        int currEnd = 0, farthest = 0, jump = 0;

        for (int i = 0; i < nums.length - 1; i++) {
            farthest = Math.max(farthest, i + nums[i]);
            if (i == currEnd) {
                currEnd = farthest;
                jump++;
            }
        }

        return jump;
    }
}

LeetCode 55. Jump Game

Problem

LeetCode 55. Jump Game

1. 题目简述

给出一个整形非负数组,初始在index为0的位置,每个位置上的数代表其所能跳跃的最大长度(不是只能跳跃该长度),问是否能够跳到达数组末尾位置。例如:

Example 1:

Input: nums = [2,3,1,1,4]
Output: true
Explanation: Jump 1 step from index 0 to 1, then 3 steps to the last index.

Example 2:

Input: nums = [3,2,1,0,4]
Output: false
Explanation: You will always arrive at index 3 no matter what. Its maximum jump length is 0, which makes it impossible to reach the last index.

2. 算法思路

Brute Force

首先,我们能想到的最傻的办法就是暴力求解。设函数F(x)为从x节点能否到达尾结点,我们假设从0开始,0节点的最大jump值为5,然后我们将步长step从1到5进行遍历,如果能够达到末尾节点,则为0,伪代码如下:

F(x) :
    if x >= n - 1
        return true;
    step = nums[x]
    for i from 1 to step:
        if F(x + i):
            return true;
        else:
            continue;
    return false;

最坏情况,时间复杂度为O(2^n),空间复杂度O(n)。

对于暴力破解法显然是不优雅的,每次只遍历一步,而且存在大量的重复计算,比如对于这种情况:

Index 0 1 2 3 4 5 6
nums 5 4 3 2 1 0 0
从index为0开始时,首次步长为1,跳转到index为1,再到index为2,以此类推,最终返回的时候我们知道步长为1时,无法到达末尾位置;然后我们调整步长为2,此时,我们直接到index为2的节点,判断位置为2的节点能否到达末尾,但是实际上,在我们计算步长为1的时候,index为2位置能否到达末尾节点我们已经知晓了,是false的,其实没必要计算,这个就是我们需要DP来优化的子问题。

Dynamic Programming

DP1 top-down:

记录一个memo数组,长度为n,表示index处能否到达末尾位置,memo[n-1]为1,其余初始为0。然后开始递归调用,top-down的算法都是要递归的。

DP2 bottom-up:
记录一个memo数组,bottom-up算法就是要从base case开始进行计算,一直到目标target,本题中,base case是memo[n-1]=1,所以我们要从n-2开始往回遍历,判断n-2节点能否到达n-1节点,如果能,则memo[n-2]=1,不能则为0,一直遍历到memo[0],返回memo[0]。

两种DP算法时间复杂度均为O(n^2),空间复杂度为O(n)。

Greedy

本题的最优解为贪心法,那么什么情况下应该使用贪心法呢?其实贪心法算是动态规划的一种特殊形式,贪心法是要找出当前的最优解,而当前的最优解却不一定是全局最优解,这个在后面的题目中再慢慢体会。

所以我们记录一个farthest表示从0到当前节点,所能到达的最大index。
$$farthest=max(i+nums[i],farthest) (0<=i<n-1)$$

这里我们需要注意一种情况,正如之前的例子,如果不能到达末尾节点,中间一定有某个为0的节点,且该节点的所有前置节点最远只能到达该节点。所以每次遍历时,需要判断farthest和i的关系,如果 farthest<=i 则代表i节点为0且无法到达后面的节点了。

3. 解法

  1. 暴力解法(直接抄leetcode上的solution了)

public class Solution {
    public boolean canJumpFromPosition(int position, int[] nums) {
        if (position == nums.length - 1) {
            return true;
        }

        int furthestJump = Math.min(position + nums[position], nums.length - 1);
        for (int nextPosition = position + 1; nextPosition <= furthestJump; nextPosition++) {
            if (canJumpFromPosition(nextPosition, nums)) {
                return true;
            }
        }

        return false;
    }

    public boolean canJump(int[] nums) {
        return canJumpFromPosition(0, nums);
    }
}
  1. DP top-down
enum Index {
    GOOD, BAD, UNKNOWN
}

public class Solution {
    Index[] memo;

    public boolean canJumpFromPosition(int position, int[] nums) {
        if (memo[position] != Index.UNKNOWN) {
            return memo[position] == Index.GOOD ? true : false;
        }

        int furthestJump = Math.min(position + nums[position], nums.length - 1);
        for (int nextPosition = position + 1; nextPosition <= furthestJump; nextPosition++) {
            if (canJumpFromPosition(nextPosition, nums)) {
                memo[position] = Index.GOOD;
                return true;
            }
        }

        memo[position] = Index.BAD;
        return false;
    }

    public boolean canJump(int[] nums) {
        memo = new Index[nums.length];
        for (int i = 0; i < memo.length; i++) {
            memo[i] = Index.UNKNOWN;
        }
        memo[memo.length - 1] = Index.GOOD;
        return canJumpFromPosition(0, nums);
    }
}
  1. DP bottom-up
enum Index {
    GOOD, BAD, UNKNOWN
}

public class Solution {
    public boolean canJump(int[] nums) {
        Index[] memo = new Index[nums.length];
        for (int i = 0; i < memo.length; i++) {
            memo[i] = Index.UNKNOWN;
        }
        memo[memo.length - 1] = Index.GOOD;

        for (int i = nums.length - 2; i >= 0; i--) {
            int furthestJump = Math.min(i + nums[i], nums.length - 1);
            for (int j = i + 1; j <= furthestJump; j++) {
                if (memo[j] == Index.GOOD) {
                    memo[i] = Index.GOOD;
                    break;
                }
            }
        }

        return memo[0] == Index.GOOD;
    }
}
  1. Greedy

class Solution {

    public boolean canJump(int[] nums) {
        int farthest = 0;
        for (int i = 0; i < nums.length - 1; i++) {
            farthest = Math.max(farthest, i + nums[i]);
            if (farthest >= nums.length-1) {
                return true;
            }
            if (farthest <= i) {
                return false;
            }
        }
        return farthest >= nums.length-1;
    }
}

LeetCode 236. Lowest Common Ancestor of a Binary Tree

Problem

LeetCode 236. Lowest Common Ancestor of a Binary Tree

1. 题目简述

给出一棵二叉树树,给出其两个节点p和q,找出p和q的高度最低公共ancestor节点,本身也是自己的祖先节点。例如:

二叉树

Input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
Output: 3
Explanation: The LCA of nodes 5 and 1 is 3.

2. 算法思路

recursion 或 stack + hash table

和二叉搜索树的问题不同,二叉树并没有其良好的有序性,不能取巧剪枝很多,所以要完全抛弃掉之前的思路。

那么如何进行查找呢?这里有两种思路。一种是递归,另一种是stack + hashmap。

第一种思路。对于某节点node,我们定义left,mid和right三个int变量,left变量表示在node的左节点上是否存在p或q节点(0不存在,1存在),mid表示node节点本身是否存在p或q节点,right表示node的右子树上是否存在p或q节点。对于我们要找的目标节点target,left+mid+right一定是等于2的,而对于其他节点来说,left+mid+right最多为1,遍历整棵树,可得到目标节点。

第二种思路。我们先存储p的所有祖先节点,然后q节点不断向上找父节点,直到找到某个节点也在p的祖先节点列表中。我们用map来存储<node, father>键值对,进行遍历,直至map中的key包含了p和q。然后,将p一级级向上查找父节点,直至为null,存储到一个set中;随后对q进行同样操作,直至q包含在set中,此时,q记为所求。

注意,这里一层层遍历时,只能选择前序遍历或者BFS,不能选择中序遍历或者后续遍历,因为有”map.containsKey(p) && map.containsKey(q)”作为终止条件,后面两种遍历方式有可能导致ancestors的关系记录不完全!!!!有反例。

Corner Cases:
注意lowestAncestor是p或q本身的情况。

3. 解法

  1. 递归

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    TreeNode lowestAncestor;

    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        findPQ(root, p, q);
        return lowestAncestor;
    }

    private boolean findPQ(TreeNode node, TreeNode p, TreeNode q) {
        if (lowestAncestor != null) {
            return false; // 剪枝,更快一点点
        }
        if (node == null) {
            return false;
        }

        int left = findPQ(node.left, p, q) ? 1 : 0;
        int right = findPQ(node.right, p, q) ? 1 : 0;
        int mid = ((node == p) || (node == q)) ? 1 : 0;

        if  (left + mid + right == 2) {
            lowestAncestor = node;
        }

        if (left + mid + right > 0) {
            return true;
        }
        return false;
    }
}
  1. 记录父节点

class Solution {
    TreeNode lowestAncestor;

    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        Map<TreeNode, TreeNode> map = new HashMap();
        Set<TreeNode> ancestorP = new HashSet();
        Stack<TreeNode> stack = new Stack();

        map.put(root, null);
        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            if (node.right != null) {
                stack.push(node.right);
                map.put(node.right, node);
            }
            if (node.left != null) {
                stack.push(node.left);
                map.put(node.left, node);
            }
            if (map.containsKey(p) && map.containsKey(q)) {
                break;
            }
        }

        while (p != null) {
            ancestorP.add(p);
            p = map.get(p);
        }

        while (q != null) {
            if (ancestorP.contains(q)){
                return q;
            }
            q = map.get(q);
        }

        return null;
    }
}

LeetCode 235. Lowest Common Ancestor of a Binary Search Tree

Problem

LeetCode 235. Lowest Common Ancestor of a Binary Search Tree

1. 题目简述

给出一棵BST树,给出其两个节点p和q,找出p和q的高度最低公共ancestor节点,本身也是自己的祖先节点。例如:

平衡二叉树

Input: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
Output: 6
Explanation: The LCA of nodes 2 and 8 is 6.

2. 算法思路

BST树的特性

这道题和LeetCode 236看起来很像,但是做法其实完全不同,本题为easy,用到BST树的特性秒解。

对于任意一个节点,其左子树的所有值必定小于其右子树的值。我们假设p.val < q.val。因此,对于某节点n,如果n.val < p.val < q.val,则p和q一定都在其右子树,左子树可以全部剪枝;p.val < q.val < n.val,则p和q一定都在其左子树,右子树可以全部剪枝;如果p和q分别在其左子树和右子树上,则它就是那个高度最低的公共祖先节点。

3. 解法

  1. 利用BST数的节点特性

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    TreeNode lowestAncestor;

    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        while ((p.val - root.val) * (q.val - root.val) > 0) {
            if (p.val < root.val) {
                // p和q都在左子树
                root = root.left;
            } else {
                root = root.right;
            }
        }

        return root;
    }
}

LeetCode 270. Closest Binary Search Tree Value

Problem

LeetCode 270. Closest Binary Search Tree Value

1. 题目简述

给出一颗二叉树,给出一个目标double类型的target,找出与其最近的节点值。例如:

Input: root = [4,2,5,1,3], target = 3.714286, and k = 2

    4
   / \
  2   5
 / \
1   3

Output: 4

2. 算法思路

中序遍历

这道题和LeetCode 272看起来很像,只不过272是要求k个最接近的数。

这道题只用找一个最接近的数,很简单,直接中序遍历,愿意加一个判断本次和上一次差值不同剪枝条件也可以,我没加。

3. 解法

  1. 中序遍历,用abs函数记录绝对值,closestValue记录最接近的值

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public int closestValue(TreeNode root, double target) {
        double abs = Double.MAX_VALUE;
        int closestValue = 0;

        while (root != null) {
            double diff = target - (double)root.val;
            if (Math.abs(diff) < abs) {
                closestValue = root.val;
                abs = Math.abs(diff);
            }
            if (diff > 0) {
                root = root.right;
            } else if (diff < 0) {
                root = root.left;
            } else {
                return root.val;
            }
        }

        return closestValue;
    }
}

LeetCode 272. Closest Binary Search Tree Value II

Problem

LeetCode 272. Closest Binary Search Tree Value II

1. 题目简述

给出一颗二叉树,给出一个目标double类型的target,找出k个与其最近的节点值。例如:

Input: root = [4,2,5,1,3], target = 3.714286, and k = 2

    4
   / \
  2   5
 / \
1   3

Output: [4,3]

2. 算法思路

中序遍历 + Stack + Queue

这道题和LeetCode 270看起来很像,只不过270是只要求一个最接近的数。

其实算法就是在一个有序数组里找出最接近target的k个数。在这个数组中和target的关系有两种,一种是大于,一种是小于等于。对于小于等于target的数,我们将其按顺序压入stack中;对于大于target的数,我们将其按顺序放入queue中。然后我们比较栈顶和队首的值,哪个更接近target,就取出哪个,直至k个。

Corner Cases:
注意栈和队列为空的情况!

3. 解法

  1. LeetCode 105 preorder + inorder
    使用Arrays.copyOfRange(int[] data, int start, int end)函数,写起来简洁,但是有点慢,而且耗空间大,因为要截取数组。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public List<Integer> closestKValues(TreeNode root, double target, int k) {
        Queue<Integer> larger = new LinkedList<Integer>();
        List<Integer> result = new ArrayList<Integer>();
        Stack<Integer> smaller = new Stack<Integer>();
        Stack<TreeNode> stack = new Stack<TreeNode>();

        while (!stack.isEmpty() || root != null) {
            if (root == null) {
                root = stack.pop();
                if (root.val <= target) {
                    smaller.push(root.val);
                } else {
                    larger.add(root.val);
                }
                root = root.right;
            } else {
                stack.push(root);
                root = root.left;
            }
        }

        while (k-- > 0) {
            if (smaller.isEmpty()) {
                result.add(larger.poll());
                continue;
            }
            if (larger.peek() == null) {
                result.add(smaller.pop());
                continue;
            }
            int x1 = smaller.peek();
            int x2 = larger.peek();
            if (Math.abs(x1 - target) < Math.abs(x2 - target)) {
                result.add(x1);
                smaller.pop();
            } else {
                result.add(x2);
                larger.poll();
            }
        }

        return result;
    }
}

LeetCode 105. Construct Binary Tree from Preorder and Inorder Traversal

Problem

LeetCode 105. Construct Binary Tree from Preorder and Inorder Traversal

1. 题目简述

给出一棵树的前序遍历和中序遍历的顺序,恢复这棵树。例如:

For example, given
preorder = [3,9,20,15,7]
inorder = [9,3,15,20,7]

Return the following binary tree:
     3
    / \
   9  20
    /  \
   15   7

2. 算法思路

DFS

这道题和LeetCode 106很像,只不过106是给出中序遍历和后序遍历,要求恢复整棵树。

先拿这道题来说,先序遍历的顺序是“中->左->右”,也就是说第一个元素是整棵树的根节点,后面是它的左子树和右子树的节点,但是我们不知道哪些是左子树,哪些是右子树。因此我们需要借助中序遍历,中序遍历的顺序是“左->中->右”,找到根节点后,中序遍历数组中,根节点左侧的节点就是其左子树的所有节点,右侧就是其右子树的所有节点;至此,我们知道了左子树的节点个数l和右子树节点个数r,前序遍历中第1到l个元素就是根节点左子树的前序遍历的顺序,右子树同理,随后递归构建其左右子树。

3. 解法(LeetCode 105 & 106)

  1. LeetCode 105 preorder + inorder

使用Arrays.copyOfRange(int[] data, int start, int end)函数,写起来简洁,但是有点慢,而且耗空间大,因为要截取数组。


class Solution {
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        int length = preorder.length;
        if (length == 0) {
            return null;
        }

        int rootVal = preorder[0];
        int rootIndex = 0;

        for (int i = 0; i < length; i++) {
            if (inorder[i] == rootVal) {
                rootIndex = i;
                break;
            }
        }

        TreeNode root = new TreeNode(rootVal);
        root.left = buildTree(Arrays.copyOfRange(preorder, 1, rootIndex + 1), Arrays.copyOfRange(inorder, 0, rootIndex));
        root.right = buildTree(Arrays.copyOfRange(preorder, rootIndex + 1, length), Arrays.copyOfRange(inorder, rootIndex + 1, length));

        return root;
    }
}
  1. LeetCode 106 postorder + inorder

第二种方式可以重新构建一个函数,直接传入原始的两个数组,以及其对应的start和end,以及一个用于查找目标value的map,且代码也更加复杂,涉及到的变量很多,但是速度相对更快一点。


/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
c/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
    Map<Integer, Integer> inMap = new HashMap<Integer, Integer>();
    
    for(int i = 0; i < inorder.length; i++) {
        inMap.put(inorder[i], i);
    }

    TreeNode root = buildTree(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1, inMap);
    return root;
}

public TreeNode buildTree(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd, Map<Integer, Integer> inMap) {
    if(preStart > preEnd || inStart > inEnd) return null;
    
    TreeNode root = new TreeNode(preorder[preStart]);
    int inRoot = inMap.get(root.val);
    int numsLeft = inRoot - inStart;
    
    root.left = buildTree(preorder, preStart + 1, preStart + numsLeft, inorder, inStart, inRoot - 1, inMap);
    root.right = buildTree(preorder, preStart + numsLeft + 1, preEnd, inorder, inRoot + 1, inEnd, inMap);
    
    return root;
}
}

LeetCode 567. Permutation in String

Problem

LeetCode 567. Permutation in String

1. 题目简述

给出两个字符串s1和s2,判断s2中是否含有s1所有字母的一组排列。例如:

Input: s1 = "ab" s2 = "eidbaooo"
Output: True
Explanation: s2 contains one permutation of s1 ("ba").

2. 算法思路

sliding window

这道题和LeetCode-438基本一致,算法也一模一样,只是返回的内容不同,这道题还性对简单点,同样是使用两个数组来记录字母个数,每次滑动窗口时判断是否和目标字母一致。

3. 解法

  1. 双数组

class Solution {
    public boolean checkInclusion(String s1, String s2) {
        if (s1.length() > s2.length()) {
            return false;
        }

        int[] dict = new int[26];
        int[] window = new int[26]; 

        for (int i = 0; i < s1.length(); i++) {
            char c = s1.charAt(i);
            dict[c - 'a']++;
        }

        int start = 0, end = 0;
        for (; end < s1.length(); end++) {
            char c = s2.charAt(end);
            window[c - 'a']++;
        }

        boolean flag = true;
        for (int i = 0; i < 26; i++) {
            if (dict[i] != window[i]) {
                flag = false;
                break;
            }
        }

        if (flag) {
            return true;
        }

        for (start = 1; end < s2.length(); start++, end++) {
            window[s2.charAt(start - 1) - 'a']--;
            window[s2.charAt(end) - 'a']++;
            flag = true;
            for (int i = 0; i < 26; i++) {
                if (dict[i] != window[i]) {
                    flag = false;
                    break;
                }
            }
            if (flag) {
                return true;
            }
        }

        return false;
    }
}

LeetCode 438. Find All Anagrams in a String

Problem

LeetCode 438. Find All Anagrams in a String

1. 题目简述

给出两个字符串s和p,p非空,找出s中所有p的同字母异序词的索引。例如:

Input:
s: "cbaebabacd" p: "abc"

Output:
[0, 6]

Explanation:
The substring with start index = 0 is "cba", which is an anagram of "abc".
The substring with start index = 6 is "bac", which is an anagram of "abc".

2. 算法思路

sliding window

首先我们能想到的肯定是暴力破解,找出所有的长度和p相等的子串,然后判断是否和p的组成字符一致。可以AC,但是耗时过长,620ms,不太可取,因此尝试别的思路。

如何优化呢?其实我们每次去移动这个子串的时候,其实只移动了一位,剩余的完全没动,但是暴力搜索子串会做很多无意义的计算,因此耗时会变长,所以更加友好的方法应该去计算变化值。

3. 解法

  1. 暴力破解法,很多重复计算

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        List<Integer> result = new ArrayList();
        if (p.length() > s.length()) {
            return result;
        }

        int s_length = s.length(), p_length = p.length();
        char[] s_array = s.toCharArray();
        int[] src;
        int[] dest = new int[26];

        for (int i = 0; i < p_length; i++) {
            dest[p.charAt(i) - 'a']++;
        }

        for (int i = 0; i < s_length - p_length + 1; i++) {
            src = new int[26];
            for (int j = 0; j < p_length; j++) {
                src[s_array[i + j] - 'a']++;
            }
            int index = 0;
            boolean flag = true;
            while (index < 26 && flag) {
                if (src[index] != dest[index]) {
                    flag = false;
                    break;
                }
                index++;
            }
            if (flag){
                result.add(i);
            }
        }

         return result;
    }
}
  1. 记录变化值

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        if (p.length() > s.length()) {
            return new ArrayList<Integer>();
        }

        List<Integer> result = new ArrayList<Integer>();
        int s_length = s.length(), p_length = p.length();
        char[] char_s = s.toCharArray();
        char[] char_p = p.toCharArray();
        int[] arr_s = new int[26], arr_p = new int[26];

        for (int i = 0; i < p_length; i++) {
            arr_s[char_s[i] - 'a']++;
            arr_p[char_p[i] - 'a']++;
        }

        boolean flag = true;
        for (int i = 0; i < 26; i++) {
            if (arr_s[i] != arr_p[i]) {
                flag = false;
                break;
            }
        }
        if (flag) {
            result.add(0);
        }

        int start = 0, end = p_length - 1;
        while (end < s_length - 1) {
            arr_s[char_s[start] - 'a']--;
            start++;
            end++;
            arr_s[char_s[end] - 'a']++;
            flag = true;
            for (int i = 0; i < 26; i++) {
                if (arr_s[i] != arr_p[i]) {
                    flag = false;
                    break;
                }
            }
            if (flag) {
                result.add(start);
            }
        }

        return result;
    }
}

LeetCode 173. Binary Search Tree Iterator

Problem

LeetCode 173. Binary Search Tree Iterator

1. 题目简述

实现一个二叉搜索树(BST树)的iterator类,其中包含next函数和hasNext函数。例如:

Example:

    7 
  /   \ 
 3    15 
     /  \   
    9   20 

BSTIterator iterator = new BSTIterator(root);
iterator.next();    // return 3
iterator.next();    // return 7
iterator.hasNext(); // return true
iterator.next();    // return 9
iterator.hasNext(); // return true
iterator.next();    // return 15
iterator.hasNext(); // return true
iterator.next();    // return 20
iterator.hasNext(); // return false

2. 算法思路

这又是一道关于设计的题目,很明显,最直接的方法,就是使用List按照中序遍历的顺序存储所有的节点,然后记录当前的index。

根据讨论区大佬的解法,这里还可以用到stack来帮助我们动态地来计算next节点,而不是一次性遍历所有。

为什么想到要用栈呢?因为就像非递归形式的中序遍历一样的想法,需要用到栈来帮助回溯到父节点。

或许还可以用Morris遍历?

3. 解法:

  1. 使用List,很笨,但很直接

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class BSTIterator {

    List<Integer> values;
    int index;

    private void getValues(TreeNode root, List list) {
        if (root == null) {
            return;
        }

        // fe非递归形式中序遍历i
        // Stack<TreeNode> stack = new Stack<TreeNode>();
        // stack.push(root);

        // while (!stack.isEmpty() || root == null) {
        //     if (root == null) {
        //         root = stack.pop();
        //         values.add(root.val);
        //         root = root.right;
        //     } else {
        //         stack.push(root);
        //         root = root.left;
        //     }
        // }

        if (root.left != null) {
            getValues(root.left, list);
        }
        list.add(root.val);
        if (root.right != null) {
            getValues(root.right, list);
        }
    }

    public BSTIterator(TreeNode root) {
        values = new ArrayList();
        getValues(root, values);
        index = -1;
    }

    /** @return the next smallest number */
    public int next() {
        int size = values.size();
        if (++index < size) {
            return values.get(index);
        } else {
            return Integer.MIN_VALUE;
        }
    }

    /** @return whether we have a next smallest number */
    public boolean hasNext() {
        if (index + 1 < values.size()) {
            return true;
        } else {
            return false;
        }
    }
}

/**
 * Your BSTIterator object will be instantiated and called as such:
 * BSTIterator obj = new BSTIterator(root);
 * int param_1 = obj.next();
 * boolean param_2 = obj.hasNext();
 */
  1. 使用Stack来帮助动态进行找next节点
class BSTIterator {

    Stack<TreeNode> stack = new Stack<TreeNode>();

    private void pushNodes(TreeNode root) {
        while (root != null) {
            stack.push(root);
            root = root.left;
        }
    }

    public BSTIterator(TreeNode root) {
        pushNodes(root);
    }

    /** @return the next smallest number */
    public int next() {
        TreeNode node = stack.pop();
        pushNodes(node.right);
        return node.val;
    }

    /** @return whether we have a next smallest number */
    public boolean hasNext() {
        return stack.isEmpty();
    }
}

LeetCode 328. Odd Even Linked List

Problem

LeetCode 328. Odd Even Linked List

1. 题目简述

给出一个链表,将所有奇数个节点串起来,然后接上所有偶数位的节点。这里说的是节点的index而不是节点的value。例如:

Example 1:
Input: 1->2->3->4->5->NULL
Output: 1->3->5->2->4->NULL

2. 算法思路

链表+双指针

这道题不知道为什么划分到medium中,相当于将链表重新排序。这里要用到两个指针,一个指向当前odd的结尾,一个指向even的结尾,注意赋值顺序即可。

3. 解法

注意推导,过程,算法本身并不难


/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode oddEvenList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        ListNode odd = head, even = head.next;

        while (even != null && even.next != null) {
            ListNode odd_next_node = even.next;
            ListNode even_next_node = odd_next_node.next;
            ListNode even_start_node = odd.next;

            odd.next = odd_next_node;
            odd = odd.next;
            even.next = even_next_node;
            even = even.next;
            odd.next = even_start_node;
        }

        return head;
    }
}

LeetCode 637. Average of Levels in Binary Tree

Problem

LeetCode 637. Average of Levels in Binary Tree

1. 题目简述

给出一棵树,每个节点都是signed int类型,计算出每一层的平均值(double类型),将其放入一个list中后返回。例如:

Input:
    3
   / \
  9  20
    /  \
   15   7
Output: [3, 14.5, 11]
Explanation:
The average value of nodes on level 0 is 3,  on level 1 is 14.5, and on level 2 is 11. Hence return [3, 14.5, 11].

2. 算法思路

Queue + BFS

首先,我们要想好用什么方式对树进行遍历,BFS or DFS。这里涉及到层数,一层一层来计算,因此BFS相对更好一些。DFS则需要用Queue来对节点进行存储,问题是怎么样对不同层级来计算,这里在循环每一层之前,用一个size变量来记录当前Queue大小,这个大小就是这一层层级的节点数。

3. 解法

  1. 注意使用变量记录当前界定个数,方便循环。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public List<Double> averageOfLevels(TreeNode root) {
        List<Double> result = new ArrayList<Double>();

        if (root == null) {
            return result;
        }

        Queue<TreeNode> queue1 = new LinkedList<TreeNode>();
        queue1.add(root);

        while (!queue1.isEmpty()) {
            int size = queue1.size();
            double sum = 0.0;

            for (int i = 0; i < size; i++) {
                TreeNode temp = queue1.poll();
                if (temp.left != null) {
                    queue1.offer(temp.left);
                }
                if (temp.right != null) {
                    queue1.offer(temp.right);
                }
                sum += temp.val;
            }

            result.add(new Double(sum / size));
        }

        return result;
    }
}

LeetCode 532. K-diff Pairs in an Array

Problem

LeetCode 532. K-diff Pairs in an Array

1. 题目简述

给出一个数组nums,一个整数k,找出数组内差绝对值为k的数组对的个数,(i,j)和(j, i)算同一个。例如:

Input: [3, 1, 4, 1, 5], k = 2
Output: 2
Explanation: There are two 2-diff pairs in the array, (1, 3) and (3, 5).
Although we have two 1s in the input, we should only return the number of unique pairs.

2. 算法思路

2.1 HashSet

首先,我们要想好用什么样的数据结构来存储。因为重复时,只计算一对,所以这种数据结构要有去重的效果,我们首先考虑Set,与此同时我们又发现,如果k=0的时候,我们又需要计算有重复的数有多少个,且2个重复的数和10个重复的数是一样的,在k=0时都只能算一对。所以单一的值存储不满足我们的需求,这时我们想到要用Map来存储,Key是值,Value是出现的次数。

Corner Cases:

  1. k<0,nums为空数组或者null。
  2. k=0,nums为(1, 1, 1, 2, 2)之类的有多组重复。

2.2 数组排序

如果我们仅利用数组的话,逻辑相对复杂,但速度会快一些,逻辑如下。

  1. 首先将数组从小到大排序;
  2. 定义lo和hi为两个pointer,初始值为0;n为数组长度
  3. 当hi < n时,循环:如果lo >= hi,hi = lo + 1,循环继续;如果nums[hi] - nums[lo] < k,hi++;如果nums[hi] - nums[lo] > k,lo++;如果nums[hi] - nums[lo] = k,count++,lo++,且循lo++,直至下一个不同的值。

3. 解法

  1. 注意hashMap的遍历方式

class Solution {
    public int findPairs(int[] nums, int k) {

        if ((nums == null) || (nums.length == 0) || (k < 0)) {
            return 0;
        }

        int count = 0;
        Map<Integer, Integer> hashMap = new HashMap<Integer, Integer>();

        for (int i = 0; i < nums.length; i++) {
            if (hashMap.containsKey(nums[i])) {
                int value = hashMap.get(nums[i]);
                hashMap.put(nums[i], ++value);
            } else {
                hashMap.put(nums[i], 1);
            }
        }

        if (k == 0) {
            for (Map.Entry<Integer, Integer> entry : hashMap.entrySet()) {
                if (entry.getValue() > 1) {
                    count++;
                }
            }
        } else {
            for (Map.Entry<Integer, Integer> entry : hashMap.entrySet()) {
                if (hashMap.containsKey(entry.getKey() + k)) {
                    count++;
                }
            }  
        }

        return count;
    }
}

LeetCode 402:Remove K Digits

Problem

LeetCode 402:Remove K Digits

1. 题目简述

给出一个数字组成的字符串num,从中去除k位后使其得到的数字最小。注意,要将首位的0全部去掉,举例:

Input: num = "1432219", k = 3
Output: "1219"
Explanation: Remove the three digits 4, 3, and 2 to form the new number 1219 which is the smallest.

Input: num = "10200", k = 1
Output: "200"
Explanation: Remove the leading 1 and the number is 200. Note that the output must not contain leading zeroes.  

2. 算法思路

贪心法 + Stack:

先看去除一位的情况,去除一位时,一位一位从前向后数,找到第一个逆序数对时,即第k + 1位小于第k位,此时,删除第k位。去除多位的情况重复以上操作,直至满足要求。

Corner Case:

  1. 首先注意如果剩余数字的位数为空,则应该返回“0”,而不是空字符串。
  2. 在最后去除首位0的时候,注意数组不能越界,存在去除数字后为“0000…0”的情况。

3. 解法

这里参考了讨论区的大佬的做法,用数组替代了stack,使用了一个top变量俩控制栈顶,十分巧妙。

class Solution {
    public String removeKdigits(String num, int k) {
        int length = num.length();
        int digits = length - k;
        char[] stack = new char[length];
        int top = 0;
  
        for(int i = 0; i < length; i++) {
            char temp = num.charAt(i);
            while(k > 0 && top > 0 && stack[top - 1] > temp) {
                top--;
                k--;
            }
            stack[top++] = temp;
        }
        // attention : corner case
        int startZeros = 0;
        while(startZeros < digits && stack[startZeros] == '0') {
            startZeros++;
        }
        // attention : corner case
        return startZeros == digits ? "0" : new String(stack, startZeros, digits - startZeros);
  
    }
}

LeetCode 297. Serialize and Deserialize Binary Tree

Problem

LeetCode 297. Serialize and Deserialize Binary Tree

1. 题目简述

设计一组加密/解密二叉树的方式,加密的结果返回为字符串,解密的结果返回重建后二叉树的根节点。方法不一,尽可能发挥想象力。例如:

Example: 

You may serialize the following tree:

     1
    / \
   2   3
      / \
     4   5

as "[1,2,3,null,null,4,5]"

2. 算法思路

这是一道关于设计的题目。

3. 解法


/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Codec {

    String NULL_C = "#", SPLITER = ",";

    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        StringBuilder sb = new StringBuilder();
        serial(root, sb);
        return sb.toString();
    }

    private void serial(TreeNode root, StringBuilder sb) {
        if (root == null) {
            sb.append(NULL_C).append(SPLITER);
        } else {
            sb.append(root.val).append(SPLITER);
            serial(root.left, sb);
            serial(root.right, sb);
        }
    }

    public TreeNode deserialize(String data) {
        Queue<String> queue = new LinkedList<String>();
        queue.addAll(Arrays.asList(data.split(SPLITER)));
        return deserial(queue);
    }

    private TreeNode deserial(Queue<String> queue) {
        String temp = queue.poll();
        if (temp.equals(NULL_C)) {
            return null;
        } else {
            TreeNode node = new TreeNode(Integer.parseInt(temp));
            node.left = deserial(queue);
            node.right = deserial(queue);
            return node;
        }
    }

}

// Your Codec object will be instantiated and called as such:
// Codec codec = new Codec();
// codec.deserialize(codec.serialize(root));

LeetCode 144. Binary Tree Preorder Traversal

Problem

LeetCode 144. Binary Tree Preorder Traversal

1. 题目简述:

给出一颗二叉树,返回其前序遍历的顺序(List形式),且尽量使用非递归的形式。例如:

Input: [1,null,2,3]
1
 \
  2
 /
3
Output: [1,2,3]  

2. 算法思路

Stack:

递归的形式很好写,普通的前序遍历,这里不再赘述。问题在于非递归(迭代)的形式怎么办。

我们都知道递归其实就是计算完某节点后不断返回上一级的方法,如果用递归达到同样效果,一般都需要使用栈Stack!

遍历过程:

  1. 如果节点不为空,访问某节点。
  2. 如果某节点存在右子树,压栈;如果存在左子树,压栈(这样弹出时是先左后右)。

Condition Statement:

  1. 注意,第一种解法中这里要先把root给压栈。
  2. 第一种解法中,要注意root为null的情况,所以while判断条件中加了node != null的条件,否则会出现NullPointerException。
  3. 第二种解法中,压栈的目的只是为了回溯找右节点,而不是弹出时访问。第一种解法更清晰,是弹出时访问。二者思路有很大不同。

3. 解法

这里提供两种遍历思路:

  1. 遍历时先压右子树,再压左子树。
class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        Stack<TreeNode> stack = new Stack<TreeNode>();
        List<Integer> list = new ArrayList<Integer>();
        TreeNode node = root;
        stack.push(node);

        while (!stack.isEmpty() && node != null) {
            node = stack.pop();
            list.add(node.val);
            if (node.right != null) {
                stack.push(node.right);
            }
            if (node.left != null) {
                stack.push(node.left);
            }
        }

        return list;
    }
}
  1. 压栈的目的只是为了回溯找右节点,不是压左右子树,而是访问过的节点。
class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        Stack<TreeNode> stack = new Stack<TreeNode>();
        List<Integer> list = new ArrayList<Integer>();
        TreeNode node = root;

        while (!stack.isEmpty() || node != null) {
            if (node != null) {
                list.add(node.val);
                stack.push(node);
                node = node.left;
            } else {
                node = stack.pop().right;
            }
        }

        return list;
    }
}

LeetCode 96. Unique Binary Search Trees

Problem

LeetCode 96. Unique Binary Search Trees

1. 题目简述

给一个正整数n,节点为(1, 2, 3…n),计算出所有可能存在的BST树的个数。例如:

Input: 3
Output: 5
Explanation:
Given n = 3, there are a total of 5 unique BST's:

 1         3     3      2      1
  \       /     /      / \      \
   3     2     1      1   3      2
  /     /       \                 \
 2     1         2                 3

2. 算法思路

动态规划

我们假设函数G(n)为所有可能存在的BST树的个数;函数F(i,n)为以i为根节点,存在的所有BST的个数;我们已知G(0)=1,G(1)=1。

那么我们如何求G(n)呢?对于任意的i为根节点时,其中1<= i <= n,节点[1, i-1]必定都在i节点的左子树;节点[i+1, n]必定都在i节点的右子树,左右子树可以为空;此时F(i,n)=G(i-1)*G(n-i),i取值从1到n,所以如果要计算G(n),G(n)的根节点有n种可能,需要将这n种都加起来。

$$ G(n)=F(1,n)+F(2,n)+…+F(n,n)=G(0)\times G(n-1)+G(1)\times G(n-2)+…+G(n-1)\times G(0) $$

因此,计算G(n)需要计算出G(1)到G(n-1)。例如:计算G(3)

$$G(3)=F(1,3)+F(2,3)+F(3,3)=G(0)\times G(2)+G(1)\times G(1)+G(2)\times G(0)=2+1+2=5$$

3. 解法

注意推导,过程,算法本身并不难


class Solution {
    public int numTrees(int n) {

        int[] G = new int[n + 1];
        G[0] = G[1] = 1;

        for (int i = 2; i <= n; i++) {
            for (int j = 1; j <= i; j++) {
                G[i] += G[j - 1] * G[i - j];
            }
        }

        return G[n];
    }
}

LeetCode 95. Unique Binary Search Trees II

Problem

LeetCode 95. Unique Binary Search Trees II

1. 题目简述

给一个正整数n,节点为(1, 2, 3…n),返回所有可能的BST树的list。例如:

Input: 3
Output:
[
[1,null,3,2],
[3,2,null,1],
[3,1,null,null,2],
[2,1,3],
[1,null,2,null,3]
]
Explanation:
The above output corresponds to the 5 unique BST's shown below:

   1         3     3      2      1
    \       /     /      / \      \
     3     2     1      1   3      2
    /     /       \                 \
   2     1         2                 3

2. 算法思路

动态规划

思路和96题类似,只不过96题是求个数,这里是求所有树。

我们需要构造一个函数,该函数的目的是返回送start到end的所有可能子树的根节点的list(闭区间)。

3. 解法:

注意推导,过程,算法本身并不难,在genTrees方法中,不需要判断start==end,因为start是可以和end相等的,表示此时到达根节点,下一次就该返回null了。注意,在start>end时需要将null值放入list中,否则根节点的子节点无法指向null。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public List<TreeNode> generateTrees(int n) {
        if (n == 0) {
            return new ArrayList<TreeNode>();
        }
        return genTrees(1, n);
    }
    private List<TreeNode> genTrees(int start, int end) {
        List<TreeNode> result = new ArrayList<TreeNode>();

        // if (start == end) {
        //     TreeNode node = new TreeNode(start);
        //     result.add(node);
        //     return result;
        // } else
        if (start > end) {
            result.add(null);
            return result;
        }

        for (int i = start; i <= end; i++) {
            List<TreeNode> leftList = genTrees(start, i - 1);
            List<TreeNode> rightList = genTrees(i + 1, end);

            for (TreeNode leftNode : leftList) {
                for (TreeNode rightNode : rightList) {
                    TreeNode node = new TreeNode(i);
                    node.left = leftNode;
                    node.right = rightNode;
                    result.add(node);
                }
            }

        }

        return result;
    }
}

LeetCode 94. Binary Tree Inorder Traversal

Problem

LeetCode 94. Binary Tree Inorder Traversal

1. 题目简述

给出一颗二叉树,返回其中序遍历的顺序(List形式),且尽量使用非递归的形式。例如:

Input: [1,null,2,3]
1
 \
  2
 /
3
Output: [1,3,2]  

2. 算法思路

Stack:

递归的形式很好写,普通的中序遍历,这里不再赘述。问题在于非递归(迭代)的形式怎么办。

我们都知道递归其实就是计算完某节点后不断返回上一级的方法,如果用递归达到同样效果,一般都需要使用栈Stack!

遍历过程:

  1. 如果某节点存在左子树,压栈,访问左子树(这里压栈的目的是一级一级可以回溯)。
  2. 如果节点没有左子树,说明该次访问已经到最底层,出栈,访问该节点,然后判断右子树是否为空,如果为空,继续pop,直至栈为空结束或者右子树不为空,将指针指向该节点的右子树。

Condition Statement:

  1. 当且仅当node为null且Stack为空时,循环结束(初次进入循环时,栈为空,但node不为空;当node为空时,一般都是要pop,除了最后)。

3. 解法:

这里提供两种遍历思路:个人倾向于第二种,比较简洁。

  1. 判断节点的左子树是否为空
class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        Stack<TreeNode> stack = new Stack<TreeNode>();
        List<Integer> list = new ArrayList<Integer>();
        TreeNode node = root;

        while (node != null) { // 下面的循环里保证了node在遍历未结束时不为null
            if (node.left != null) {
                stack.push(node);
                node = node.left;
            } else {
                list.add(node.val);
                while (node.right == null && !stack.isEmpty()) {
                    node = stack.pop();
                    list.add(node.val);
                }
                node = node.right;
            }
        }

        return list;
    }
}
  1. 判断节点是否为空
class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        Stack<TreeNode> stack = new Stack<TreeNode>();
        List<Integer> list = new ArrayList<Integer>();
        TreeNode node = root;

        while (node != null || !stack.isEmpty()) {
            if (node != null) {
                stack.push(node);
                node = node.left;
            } else {
                node = stack.pop();
                list.add(node.val);
                node = node.right;
            }
        }

        return list;
    }
}

LeetCode 918. Maximum Sum Circular Subarray

Problem

LeetCode 918. Maximum Sum Circular Subarray

1. 题目简述

给出一个环形数组A,找出其非空子序列的最大sum值。例如:

Example 1:
Input: [1,-2,3,-2]
Output: 3
Explanation: Subarray [3] has maximum sum 3

Example 2:
Input: [5,-3,5]
Output: 10
Explanation: Subarray [5,5] has maximum sum 5 + 5 = 10

Example 3:
Input: [3,-1,2,-1]
Output: 4
Explanation: Subarray [2,-1,3] has maximum sum 2 + (-1) + 3 = 4

Example 4:
Input: [3,-2,2,-3]
Output: 3
Explanation: Subarray [3] and [3,-2,2] both have maximum sum 3

Example 5:
Input: [-2,-3,-1]
Output: -1
Explanation: Subarray [-1] has maximum sum -1

2. 算法思路

Greedy & Dynamic Programming

首先,这道题和LeetCode 53 Maximum Subarray 有异曲同工之妙,我们先来回忆一下如何求一个数组的子序列的最大值。

假设某数组arr长度为n,对于中间某位置第i个元素,设函数F(i)为元素0到i的包含元素i的最大子序列之和,则F(i+1)有如下推导:

$$F(i+1)=max(F(i)+arr[i+1],F(i))(0< i< n)$$

每次循环都记录F(i)和全局maxValue值的比较,循环结束,maxValue即为所求。

当我们确定了如何求最大值时,也就意味着我们如何求最小子序列值,与上同理。

对于一个环形序列来说,最大子序列有两种可能,一种是和普通的数组一样,并没有从数组的队尾绕回队首;第二种可能性是最大子序列从队尾绕回了队首。

对于前者来说,计算方法和53题一致,对于后者来说就有点麻烦了,如果还是用常规思路比较复杂。我们反过来想,如果一个数组和为sum,其最小子序列之和为minSubValue,那么它的环形最大子序列之和就是sum-minSubValue。

Corner Case

Attention:这里有一个很可怕的corner case,如果数组全部为负数(Example 5),那么其全局最小子序列值为所有值之和,环形最大子序列之和就为0,这种情况应当直接返回连续最大子序列之和,其实也就是整个数组的最大值,也是负数(但凡有一个正数都不会这样)。

3. 解法

  1. 注意使用变量记录当前界定个数,方便循环。

class Solution {
    public int maxSubarraySumCircular(int[] A) {
        int contiguousMaxValue = contiguousMax(A);
        int cicularMaxValue = cicularMax(A);

        return contiguousMaxValue < 0 ? contiguousMaxValue : Math.max(contiguousMaxValue, cicularMaxValue);
    }

    private int contiguousMax (int[] A) {
        int maxValue = Integer.MIN_VALUE, currentMax = 0;

        for (int i = 0; i < A.length; i++) {
            currentMax += A[i];
            maxValue = currentMax > maxValue ? currentMax : maxValue;
            currentMax = currentMax < 0 ? 0 : currentMax;
        }

        return maxValue;
    }

    private int cicularMax (int[] A) {
        int minValue = Integer.MAX_VALUE, sum = 0, currentMin = 0;

        for (int i = 0; i < A.length; i++) {
            sum += A[i];
            currentMin += A[i];
            minValue = currentMin < minValue ? currentMin : minValue;
            currentMin = currentMin > 0 ? 0 : currentMin;
        }

        return sum - minValue;
    }
}