算法思想
二分查找
public int search(int key, int[] array) {
int l = 0, h = array.length - 1;
while (l <= h) {
int mid = l + (h - l) / 2;
if (key == array[mid]) return mid;
if (key < array[mid]) h = mid - 1;
else l = mid + 1;
}
return -1;
}
实现时需要注意以下细节:
在计算 mid 时不能使用 mid = (l + h) / 2 这种方式,因为 l + h 可能会导致加法溢出,应该使用 mid = l + (h - l) / 2。
对 h 的赋值和循环条件有关,当循环条件为 l <= h 时,h = mid - 1;当循环条件为 l < h 时,h = mid。解释如下:在循环条件为 l <= h 时,如果 h = mid,会出现循环无法退出的情况,例如 l = 1,h = 1,此时 mid 也等于 1,如果此时继续执行 h = mid,那么就会无限循环;在循环条件为 l < h,如果 h = mid - 1,会错误跳过查找的数,例如对于数组 [1,2,3],要查找 1,最开始 l = 0,h = 2,mid = 1,判断 key < arr[mid] 执行 h = mid - 1 = 0,此时循环退出,直接把查找的数跳过了。
l 的赋值一般都为 l = mid + 1。
求开方
Input: 4
Output: 2
Input: 8
Output: 2
Explanation: The square root of 8 is 2.82842..., and since we want to return an integer, the decimal part will be truncated.
一个数 x 的开方 sqrt 一定在 0 ~ x 之间,并且满足 sqrt == x / sqrt 。可以利用二分查找在 0 ~ x 之间查找 sqrt。
public int mySqrt(int x) {
if(x <= 1) return x;
int l = 1, h = x;
while(l <= h){
int mid = l + (h - l) / 2;
int sqrt = x / mid;
if(sqrt == mid) return mid;
else if(sqrt < mid) h = mid - 1;
else l = mid + 1;
}
return h;
}
摆硬币
n = 8
The coins can form the following rows:
¤
¤ ¤
¤ ¤ ¤
¤ ¤
Because the 4th row is incomplete, we return 3.
题目描述:第 i 行摆 i 个,统计能够摆的行数。
返回 h 而不是 l,因为摆的硬币最后一行不能算进去。
public int arrangeCoins(int n) {
int l = 0, h = n;
while(l <= h){
int m = l + (h - l) / 2;
long x = m * (m + 1L) / 2;
if(x == n) return m;
else if(x < n) l = m + 1;
else h = m - 1;
}
return h;
}
可以不用二分查找,更直观的解法如下:
public int arrangeCoins(int n) {
int level = 1;
while (n > 0) {
n -= level;
level++;
}
return n == 0 ? level - 1 : level - 2;
}
有序数组的 Single Element
Input: [1,1,2,3,3,4,4,8,8]
Output: 2
题目描述:一个有序数组只有一个数不出现两次,找出这个数。
public int singleNonDuplicate(int[] nums) {
int l = 0, h = nums.length - 1;
while(l < h) {
int m = l + (h - l) / 2;
if(m % 2 == 1) m--; // 保证 l/h/m 都在偶数位,使得查找区间大小一直都是奇数
if(nums[m] == nums[m + 1]) l = m + 2;
else h = m;
}
return nums[l];
}
贪心思想
贪心思想保证每次操作都是局部最优的,并且最后得到的结果是全局最优的。
分配饼干
Input: [1,2], [1,2,3]
Output: 2
Explanation: You have 2 children and 3 cookies. The greed factors of 2 children are 1, 2.
You have 3 cookies and their sizes are big enough to gratify all of the children,
You need to output 2.
题目描述:每个孩子都有一个满足度,每个饼干都有一个大小,只有饼干的大小大于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。
因为最小的孩子最容易得到满足,因此先满足最小孩子。给一个孩子的饼干应当尽量小又能满足该孩子,这样大饼干就能拿来给满足度比较大的孩子。
证明:假设在某次选择中,贪心策略选择给第 i 个孩子分配第 m 个饼干,并且第 i 个孩子满足度最小,第 m 个饼干为可以满足第 i 个孩子的最小饼干,利用贪心策略最终可以满足 k 个孩子。假设最优策略在这次选择中给 i 个孩子分配第 n 个饼干,并且这个饼干大于第 m 个饼干。我们发现使用第 m 个饼干去替代第 n 个饼干完全不影响后续的结果,因此不存在比贪心策略更优的策略,即贪心策略就是最优策略。
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int i = 0, j = 0;
while(i < g.length && j < s.length){
if(g[i] <= s[j]) i++;
j++;
}
return i;
}
投飞镖刺破气球
Input:
[[10,16], [2,8], [1,6], [7,12]]
Output:
2
题目描述:气球在一个水平数轴上摆放,可以重叠,飞镖垂直射向坐标轴,使得路径上的气球都会刺破。求解最小的投飞镖次数使所有气球都被刺破。
从左往右投飞镖,并且在每次投飞镖时满足以下条件:
左边已经没有气球了;
本次投飞镖能够刺破最多的气球。
public int findMinArrowShots(int[][] points) {
if(points.length == 0) return 0;
Arrays.sort(points,(a,b) -> (a[1] - b[1]));
int curPos = points[0][1];
int ret = 1;
for (int i = 1; i < points.length; i++) {
if(points[i][0] <= curPos) {
continue;
}
curPos = points[i][1];
ret++;
}
return ret;
}
股票的最大收益
题目描述:一次交易包含买入和卖出,多个交易之间不能交叉进行。
对于 [a, b, c, d],如果有 a <= b <= c <= d ,那么最大收益为 d - a。而 d - a = (d - c) + (c - b) + (b - a) ,因此当访问到一个 prices[i] 且 prices[i] - prices[i-1] > 0,那么就把 prices[i] - prices[i-1] 添加加到收益中,从而在局部最优的情况下也保证全局最优。
public int maxProfit(int[] prices) {
int profit = 0;
for(int i = 1; i < prices.length; i++){
if(prices[i] > prices[i-1]) profit += (prices[i] - prices[i-1]);
}
return profit;
}
种植花朵
Input: flowerbed = [1,0,0,0,1], n = 1
Output: True
题目描述:花朵之间至少需要一个单位的间隔。
public boolean canPlaceFlowers(int[] flowerbed, int n) {
int cnt = 0;
for(int i = 0; i < flowerbed.length; i++){
if(flowerbed[i] == 1) continue;
int pre = i == 0 ? 0 : flowerbed[i - 1];
int next = i == flowerbed.length - 1 ? 0 : flowerbed[i + 1];
if(pre == 0 && next == 0) {
cnt++;
flowerbed[i] = 1;
}
}
return cnt >= n;
}
修改一个数成为非递减数组
Input: [4,2,3]
Output: True
Explanation: You could modify the first 4 to 1 to get a non-decreasing array.
题目描述:判断一个数组能不能只修改一个数就成为非递减数组。
在出现 nums[i] < nums[i - 1] 时,需要考虑的是应该修改数组的哪个数,使得本次修改能使 i 之前的数组成为非递减数组,并且 不影响后续的操作 。优先考虑令 nums[i - 1] = nums[i],因为如果修改 nums[i] = nums[i - 1] 的话,那么 nums[i] 这个数会变大,那么就有可能比 nums[i + 1] 大,从而影响了后续操作。还有一个比较特别的情况就是 nums[i] < nums[i - 2],只修改 nums[i - 1] = nums[i] 不能令数组成为非递减,只能通过修改 nums[i] = nums[i - 1] 才行。
public boolean checkPossibility(int[] nums) {
int cnt = 0;
for(int i = 1; i < nums.length; i++){
if(nums[i] < nums[i - 1]){
cnt++;
if(i - 2 >= 0 && nums[i - 2] > nums[i]) nums[i] = nums[i-1];
else nums[i - 1] = nums[i];
}
}
return cnt <= 1;
}
判断是否为子串
s = "abc", t = "ahbgdc"
Return true.
public boolean isSubsequence(String s, String t) {
for (int i = 0, pos = 0; i < s.length(); i++, pos++) {
pos = t.indexOf(s.charAt(i), pos);
if(pos == -1) return false;
}
return true;
}
分隔字符串使同种字符出现在一起
Input: S = "ababcbacadefegdehijhklij"
Output: [9,7,8]
Explanation:
The partition is "ababcbaca", "defegde", "hijhklij".
This is a partition so that each letter appears in at most one part.
A partition like "ababcbacadefegde", "hijhklij" is incorrect, because it splits S into less parts.
public List partitionLabels(String S) {
List ret = new ArrayList<>();
int[] lastIdxs = new int[26];
for(int i = 0; i < S.length(); i++) lastIdxs[S.charAt(i) - 'a'] = i;
int startIdx = 0;
while(startIdx < S.length()) {
int endIdx = startIdx;
for(int i = startIdx; i < S.length() && i <= endIdx; i++) {
int lastIdx = lastIdxs[S.charAt(i) - 'a'];
if(lastIdx == i) continue;
if(lastIdx > endIdx) endIdx = lastIdx;
}
ret.add(endIdx - startIdx + 1);
startIdx = endIdx + 1;
}
return ret;
}
根据身高和序号重组队列
Input:
[[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]]
Output:
[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]]
题目描述:一个学生用两个分量 (h, k) 描述,h 表示身高,k 表示排在前面的有 k 个学生的身高比他高或者和他一样高。
为了在每次插入操作时不影响后续的操作,身高较高的学生应该先做插入操作,否则身高较小的学生原先正确插入第 k 个位置可能会变成第 k+1 个位置。
身高降序、k 值升序,然后按排好序的顺序插入队列的第 k 个位置中。
public int[][] reconstructQueue(int[][] people) {
if(people == null || people.length == 0 || people[0].length == 0) return new int[0][0];
Arrays.sort(people, new Comparator() {
public int compare(int[] a, int[] b) {
if(a[0] == b[0]) return a[1] - b[1];
return b[0] - a[0];
}
});
int n = people.length;
List tmp = new ArrayList<>();
for(int i = 0; i < n; i++) {
tmp.add(people[i][1], new int[]{people[i][0], people[i][1]});
}
int[][] ret = new int[n][2];
for(int i = 0; i < n; i++) {
ret[i][0] = tmp.get(i)[0];
ret[i][1] = tmp.get(i)[1];
}
return ret;
}
双指针
双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。
从一个已经排序的数组中查找出两个数,使它们的和为 0
Input: numbers={2, 7, 11, 15}, target=9
Output: index1=1, index2=2
使用双指针,一个指针指向元素较小的值,一个指针指向元素较大的值。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。
如果两个指针指向元素的和 sum == target,那么得到要求的结果;如果 sum > target,移动较大的元素,使 sum 变小一些;如果 sum < target,移动较小的元素,使 sum 变大一些。
public int[] twoSum(int[] numbers, int target) {
int i = 0, j = numbers.length - 1;
while (i < j) {
int sum = numbers[i] + numbers[j];
if (sum == target) return new int[]{i + 1, j + 1};
else if (sum < target) i++;
else j--;
}
return null;
}
反转字符串中的元音字符
Given s = "leetcode", return "leotcede".
使用双指针,指向待反转的两个元音字符,一个指针从头向尾遍历,一个指针从尾到头遍历。
private HashSet vowels = new HashSet<>(Arrays.asList('a','e','i','o','u','A','E','I','O','U'));
public String reverseVowels(String s) {
if(s.length() == 0) return s;
int i = 0, j = s.length() - 1;
char[] result = new char[s.length()];
while(i <= j){
char ci = s.charAt(i);
char cj = s.charAt(j);
if(!vowels.contains(ci)){
result[i] = ci;
i++;
} else if(!vowels.contains(cj)){
result[j] = cj;
j--;
} else{
result[i] = cj;
result[j] = ci;
i++;
j--;
}
}
return new String(result);
}
两数平方和
Input: 5
Output: True
Explanation: 1 * 1 + 2 * 2 = 5
题目描述:判断一个数是否为两个数的平方和,例如 5 = 12 + 22。
public boolean judgeSquareSum(int c) {
int left = 0, right = (int) Math.sqrt(c);
while(left <= right){
int powSum = left * left + right * right;
if(powSum == c) return true;
else if(powSum > c) right--;
else left++;
}
return false;
}
回文字符串
Input: "abca"
Output: True
Explanation: You could delete the character 'c'.
题目描述:字符串可以删除一个字符,判断是否能构成回文字符串。
public boolean validPalindrome(String s) {
int i = 0, j = s.length() -1;
while(i < j){
if(s.charAt(i) != s.charAt(j)){
return isPalindrome(s, i, j - 1) || isPalindrome(s, i + 1, j);
}
i++;
j--;
}
return true;
}
private boolean isPalindrome(String s, int l, int r){
while(l < r){
if(s.charAt(l) != s.charAt(r))
return false;
l++;
r--;
}
return true;
}
归并两个有序数组
题目描述:把归并结果存到第一个数组上
public void merge(int[] nums1, int m, int[] nums2, int n) {
int i = m - 1, j = n - 1; // 需要从尾开始遍历,否则在 nums1 上归并得到的值会覆盖还未进行归并比较的值
int idx = m + n - 1;
while(i >= 0 || j >= 0){
if(i < 0) nums1[idx] = nums2[j--];
else if(j < 0) nums1[idx] = nums1[i--];
else if(nums1[i] > nums2[j]) nums1[idx] = nums1[i--];
else nums1[idx] = nums2[j--];
idx--;
}
}
判断链表是否存在环
使用双指针,一个指针每次移动一个节点,一个指针每次移动两个节点,如果存在环,那么这两个指针一定会相遇。
public boolean hasCycle(ListNode head) {
if(head == null) return false;
ListNode l1 = head, l2 = head.next;
while(l1 != null && l2 != null){
if(l1 == l2) return true;
l1 = l1.next;
if(l2.next == null) break;
l2 = l2.next.next;
}
return false;
}
最长子序列
Input:
s = "abpcplea", d = ["ale","apple","monkey","plea"]
Output:
"apple"
题目描述:可以删除 s 中的一些字符,使得它成为字符串列表 d 中的一个字符串。要求在 d 中找到满足条件的最长字符串。
public String findLongestWord(String s, List d) {
String ret = "";
for (String str : d) {
for (int i = 0, j = 0; i < s.length() && j < str.length(); i++) {
if (s.charAt(i) == str.charAt(j)) j++;
if (j == str.length()) {
if (ret.length() < str.length()
|| (ret.length() == str.length() && ret.compareTo(str) > 0)) {
ret = str;
}
}
}
}
return ret;
}
排序
快速选择
一般用于求解 Kth Element 问题,可以在 O(n) 时间复杂度,O(1) 空间复杂度完成求解工作。
与快速排序一样,快速选择一般需要先打乱数组,否则最坏情况下时间复杂度为 O(n2)。
堆排序
堆排序用于求解 TopK Elements 问题,通过维护一个大小为 K 的堆,堆中的元素就是 TopK Elements。当然它也可以用于求解 Kth Element 问题,因为最后出堆的那个元素就是 Kth Element。快速选择也可以求解 TopK Elements 问题,因为找到 Kth Element 之后,再遍历一次数组,所有小于等于 Kth Element 的元素都是 TopK Elements。可以看到,快速选择和堆排序都可以求解 Kth Element 和 TopK Elements 问题。
Kth Element
排序 :时间复杂度 O(nlgn),空间复杂度 O(1) 解法
public int findKthLargest(int[] nums, int k) {
int N = nums.length;
Arrays.sort(nums);
return nums[N - k];
}
堆排序 :时间复杂度 O(nlgk),空间复杂度 O(k)
public int findKthLargest(int[] nums, int k) {
PriorityQueue pq = new PriorityQueue<>();
for(int val : nums) {
pq.offer(val);
if(pq.size() > k) {
pq.poll();
}
}
return pq.peek();
}
快速选择 :时间复杂度 O(n),空间复杂度 O(1)
public int findKthLargest(int[] nums, int k) {
k = nums.length - k;
int lo = 0;
int hi = nums.length - 1;
while (lo < hi) {
final int j = partition(nums, lo, hi);
if(j < k) {
lo = j + 1;
} else if (j > k) {
hi = j - 1;
} else {
break;
}
}
return nums[k];
}
private int partition(int[] a, int lo, int hi) {
int i = lo;
int j = hi + 1;
while(true) {
while(i < hi && less(a[++i], a[lo]));
while(j > lo && less(a[lo], a[--j]));
if(i >= j) {
break;
}
exch(a, i, j);
}
exch(a, lo, j);
return j;
}
private void exch(int[] a, int i, int j) {
final int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
private boolean less(int v, int w) {
return v < w;
}
}
桶排序
找出出现频率最多的 k 个数
Given [1,1,1,2,2,3] and k = 2, return [1,2].
public List topKFrequent(int[] nums, int k) {
List ret = new ArrayList<>();
Map map = new HashMap<>();
for(int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
List[] bucket = new List[nums.length + 1];
for(int key : map.keySet()) {
int frequency = map.get(key);
if(bucket[frequency] == null) {
bucket[frequency] = new ArrayList<>();
}
bucket[frequency].add(key);
}
for(int i = bucket.length - 1; i >= 0 && ret.size() < k; i--) {
if(bucket[i] != null) {
ret.addAll(bucket[i]);
}
}
return ret;
}
搜索
深度优先搜索和广度优先搜索广泛运用于树和图中,但是它们的应用远远不止如此。
BFS
广度优先搜索的搜索过程有点像一层一层地进行遍历:从节点 0 出发,遍历到 6、2、1 和 5 这四个新节点。
继续从 6 开始遍历,得到节点 4 ;从 2 开始遍历,没有下一个节点;从 1 开始遍历,没有下一个节点;从 5 开始遍历,得到 3 和 4 节点。这一轮总共得到两个新节点:4 和 3 。
反复从新节点出发进行上述的遍历操作。
可以看到,每一轮遍历的节点都与根节点路径长度相同。设 di 表示第 i 个节点与根节点的路径长度,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 di<=dj。利用这个结论,可以求解最短路径 最优解 问题:第一次遍历到目的节点,其所经过的路径为最短路径,如果继续遍历,之后再遍历到目的节点,所经过的路径就不是最短路径。
在程序实现 BFS 时需要考虑以下问题:
队列:用来存储每一轮遍历的节点
标记:对于遍历过得节点,应该将它标记,防止重复遍历;
计算在网格中从原点到特定点的最短路径长度
[[1,1,0,1],
[1,0,1,0],
[1,1,1,1],
[1,0,1,1]]
public int minPathLength(int[][] grids, int tr, int tc) {
int[][] next = { {1, 0}, {-1, 0}, {0, 1}, {0, -1}};
int m = grids.length, n = grids[0].length;
Queue queue = new LinkedList<>();
queue.add(new Position(0, 0, 1));
while (!queue.isEmpty()) {
Position pos = queue.poll();
for (int i = 0; i < 4; i++) {
Position nextPos = new Position(pos.r + next[i][0], pos.c + next[i][1], pos.length + 1);
if (nextPos.r < 0 || nextPos.r >= m || nextPos.c < 0 || nextPos.c >= n) continue;
if (grids[nextPos.r][nextPos.c] != 1) continue;
grids[nextPos.r][nextPos.c] = 0;
if (nextPos.r == tr && nextPos.c == tc) return nextPos.length;
queue.add(nextPos);
}
}
return -1;
}
private class Position {
int r, c, length;
public Position(int r, int c, int length) {
this.r = r;
this.c = c;
this.length = length;
}
}
DFS
广度优先搜索一层一层遍历,每一层遍历到的所有新节点,要用队列先存储起来以备下一层遍历的时候再遍历;而深度优先搜索在遍历到一个新节点时立马对新节点进行遍历:从节点 0 出发开始遍历,得到到新节点 6 时,立马对新节点 6 进行遍历,得到新节点 4;如此反复以这种方式遍历新节点,直到没有新节点了,此时返回。返回到根节点 0 的情况是,继续对根节点 0 进行遍历,得到新节点 2,然后继续以上步骤。
从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 可达性 问题。
在程序实现 DFS 时需要考虑以下问题:
栈:用栈来保存当前节点信息,当遍历新节点返回时能够继续遍历当前节点。也可以使用递归栈。
标记:和 BFS 一样同样需要对已经遍历过得节点进行标记。
查找最大的连通面积
[[0,0,1,0,0,0,0,1,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,1,1,0,1,0,0,0,0,0,0,0,0],
[0,1,0,0,1,1,0,0,1,0,1,0,0],
[0,1,0,0,1,1,0,0,1,1,1,0,0],
[0,0,0,0,0,0,0,0,0,0,1,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,0,0,0,0,0,0,1,1,0,0,0,0]]
public int maxAreaOfIsland(int[][] grid) {
int m = grid.length, n = grid[0].length;
int max = 0;
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(grid[i][j] == 1) max = Math.max(max, dfs(grid, i, j));
}
}
return max;
}
private int dfs(int[][] grid, int i, int j){
int m = grid.length, n = grid[0].length;
if(i < 0 || i >= m || j < 0 || j >= n) return 0;
if(grid[i][j] == 0) return 0;
grid[i][j] = 0;
return dfs(grid, i + 1, j) + dfs(grid, i - 1, j) + dfs(grid, i, j + 1) + dfs(grid, i, j - 1) + 1;
}
图的连通分量
Input:
[[1,1,0],
[1,1,0],
[0,0,1]]
Output: 2
Explanation:The 0th and 1st students are direct friends, so they are in a friend circle.
The 2nd student himself is in a friend circle. So return 2.
public int findCircleNum(int[][] M) {
int n = M.length;
int ret = 0;
boolean[] hasFind = new boolean[n];
for(int i = 0; i < n; i++) {
if(!hasFind[i]) {
dfs(M, i, hasFind);
ret++;
}
}
return ret;
}
private void dfs(int[][] M, int i, boolean[] hasFind) {
hasFind[i] = true;
int n = M.length;
for(int k = 0; k < n; k++) {
if(M[i][k] == 1 && !hasFind[k]) {
dfs(M, k, hasFind);
}
}
}
矩阵中的连通区域数量
11110
11010
11000
00000
Answer: 1
private int m, n;
private int[][] direction = { {0, 1}, {0, -1}, {1, 0}, {-1, 0}};
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) return 0;
m = grid.length;
n = grid[0].length;
int ret = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '1') {
dfs(grid, i, j);
ret++;
}
}
}
return ret;
}
private void dfs(char[][] grid, int i, int j) {
if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] == '0') return;
grid[i][j] = '0';
for (int k = 0; k < direction.length; k++) {
dfs(grid, i + direction[k][0], j + direction[k][1]);
}
}
输出二叉树中所有从根到叶子的路径
1
/ \
2 3
\
5
["1->2->5", "1->3"]
public List binaryTreePaths(TreeNode root) {
List ret = new ArrayList();
if(root == null) return ret;
dfs(root, "", ret);
return ret;
}
private void dfs(TreeNode root, String prefix, List ret){
if(root == null) return;
if(root.left == null && root.right == null){
ret.add(prefix + root.val);
return;
}
prefix += (root.val + "->");
dfs(root.left, prefix, ret);
dfs(root.right, prefix, ret);
}
填充封闭区域
For 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
题目描述:使得被 'X' 的 'O' 转换为 'X'。
先填充最外侧,剩下的就是里侧了。
private int[][] direction = { {0, 1}, {0, -1}, {1, 0}, {-1, 0}};
private int m, n;
public void solve(char[][] board) {
if (board == null || board.length == 0) return;
m = board.length;
n = board[0].length;
for (int i = 0; i < m; i++) {
dfs(board, i, 0);
dfs(board, i, n - 1);
}
for (int i = 0; i < n; i++) {
dfs(board, 0, i);
dfs(board, m - 1, i);
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == 'T') board[i][j] = 'O';
else if (board[i][j] == 'O') board[i][j] = 'X';
}
}
}
private void dfs(char[][] board, int r, int c) {
if (r < 0 || r >= m || c < 0 || c >= n || board[r][c] != 'O') return;
board[r][c] = 'T';
for (int i = 0; i < direction.length; i++) {
dfs(board, r + direction[i][0], c + direction[i][1]);
}
}
从两个方向都能到达的区域
Given the following 5x5 matrix:
Pacific ~ ~ ~ ~ ~
~ 1 2 2 3 (5) *
~ 3 2 3 (4) (4) *
~ 2 4 (5) 3 1 *
~ (6) (7) 1 4 5 *
~ (5) 1 1 2 4 *
* * * * * Atlantic
Return:
[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (positions with parentheses in above matrix).
题目描述:左边和上边是太平洋,右边和下边是大西洋,内部的数字代表海拔,海拔高的地方的水能够流到低的地方,求解水能够流到太平洋和大西洋的所有位置。
private int m, n;
private int[][] matrix;
private int[][] direction = { {0, 1}, {0, -1}, {1, 0}, {-1, 0}};
public List pacificAtlantic(int[][] matrix) {
List ret = new ArrayList<>();
if (matrix == null || matrix.length == 0) return ret;
this.m = matrix.length;
this.n = matrix[0].length;
this.matrix = matrix;
boolean[][] canReachP = new boolean[m][n];
boolean[][] canReachA = new boolean[m][n];
for (int i = 0; i < m; i++) {
dfs(i, 0, canReachP);
dfs(i, n - 1, canReachA);
}
for (int i = 0; i < n; i++) {
dfs(0, i, canReachP);
dfs(m - 1, i, canReachA);
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (canReachP[i][j] && canReachA[i][j]) {
ret.add(new int[]{i, j});
}
}
}
return ret;
}
private void dfs(int r, int c, boolean[][] canReach) {
if(canReach[r][c]) return;
canReach[r][c] = true;
for (int i = 0; i < direction.length; i++) {
int nextR = direction[i][0] + r;
int nextC = direction[i][1] + c;
if (nextR < 0 || nextR >= m || nextC < 0 || nextC >= n
|| matrix[r][c] > matrix[nextR][nextC]) continue;
dfs(nextR, nextC, canReach);
}
}
N 皇后
题目描述:在 n*n 的矩阵中摆放 n 个皇后,并且每个皇后不能在同一行,同一列,同一对角线上,要求解所有的 n 皇后解。
一行一行地摆放,在确定一行中的那个皇后应该摆在哪一列时,需要用三个标记数组来确定某一列是否合法,这三个标记数组分别为:列标记数组、45 度对角线标记数组和 135 度对角线标记数组。
45 度对角线标记数组的维度为 2*n - 1,通过下图可以明确 (r,c) 的位置所在的数组下标为 r + c。
135 度对角线标记数组的维度也是 2*n - 1,(r,c) 的位置所在的数组下标为 n - 1 - (r - c)。
private List> ret;
private char[][] nQueens;
private boolean[] colUsed;
private boolean[] diagonals45Used;
private boolean[] diagonals135Used;
private int n;
public List> solveNQueens(int n) {
ret = new ArrayList<>();
nQueens = new char[n][n];
Arrays.fill(nQueens, '.');
colUsed = new boolean[n];
diagonals45Used = new boolean[2 * n - 1];
diagonals135Used = new boolean[2 * n - 1];
this.n = n;
backstracking(0);
return ret;
}
private void backstracking(int row) {
if (row == n) {
List list = new ArrayList<>();
for (char[] chars : nQueens) {
list.add(new String(chars));
}
ret.add(list);
return;
}
for (int col = 0; col < n; col++) {
int diagonals45Idx = row + col;
int diagonals135Idx = n - 1 - (row - col);
if (colUsed[col] || diagonals45Used[diagonals45Idx] || diagonals135Used[diagonals135Idx]) {
continue;
}
nQueens[row][col] = 'Q';
colUsed[col] = diagonals45Used[diagonals45Idx] = diagonals135Used[diagonals135Idx] = true;
backstracking(row + 1);
colUsed[col] = diagonals45Used[diagonals45Idx] = diagonals135Used[diagonals135Idx] = false;
nQueens[row][col] = '.';
}
}
Backtracking
回溯是 DFS 的一种,它不是用在遍历图的节点上,而是用于求解 排列组合 问题,例如有 { 'a','b','c' } 三个字符,求解所有由这三个字符排列得到的字符串。
在程序实现时,回溯需要注意对元素进行标记的问题。使用递归实现的回溯,在访问一个新元素进入新的递归调用,此时需要将新元素标记为已经访问,这样才能在继续递归调用时不用重复访问该元素;但是在递归返回时,需要将该元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,而在不同的递归链是可以访问已经访问过但是不在当前递归链中的元素。
数字键盘组合
Input:Digit string "23"
Output: ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
private static final String[] KEYS = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
public List letterCombinations(String digits) {
List ret = new ArrayList<>();
if (digits != null && digits.length() != 0) {
combination("", digits, 0, ret);
}
return ret;
}
private void combination(String prefix, String digits, int offset, List ret) {
if (offset == digits.length()) {
ret.add(prefix);
return;
}
String letters = KEYS[digits.charAt(offset) - '0'];
for (char c : letters.toCharArray()) {
combination(prefix + c, digits, offset + 1, ret);
}
}
在矩阵中寻找字符串
For example,
Given board =
[
['A','B','C','E'],
['S','F','C','S'],
['A','D','E','E']
]
word = "ABCCED", -> returns true,
word = "SEE", -> returns true,
word = "ABCB", -> returns false.
private static int[][] shift = { {1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private static boolean[][] visited;
private int m;
private int n;
public boolean exist(char[][] board, String word) {
if (word == null || word.length() == 0) return true;
if (board == null || board.length == 0 || board[0].length == 0) return false;
m = board.length;
n = board[0].length;
visited = new boolean[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (dfs(board, word, 0, i, j)) return true;
}
}
return false;
}
private boolean dfs(char[][] board, String word, int start, int r, int c) {
if (start == word.length()) {
return true;
}
if (r < 0 || r >= m || c < 0 || c >= n || board[r][c] != word.charAt(start) || visited[r][c] ) {
return false;
}
visited[r][c] = true;
for (int i = 0; i < shift.length; i++) {
int nextR = r + shift[i][0];
int nextC = c + shift[i][1];
if (dfs(board, word, start + 1, nextR, nextC)) return true;
}
visited[r][c] = false;
return false;
}
IP 地址划分
Given "25525511135",
return ["255.255.11.135", "255.255.111.35"].
private List ret;
public List restoreIpAddresses(String s) {
ret = new ArrayList<>();
doRestore(0, "", s);
return ret;
}
private void doRestore(int k, String path, String s) {
if (k == 4 || s.length() == 0) {
if (k == 4 && s.length() == 0) {
ret.add(path);
}
return;
}
for (int i = 0; i < s.length() && i <= 2; i++) {
if (i != 0 && s.charAt(0) == '0') break;
String part = s.substring(0, i + 1);
if (Integer.valueOf(part) <= 255) {
doRestore(k + 1, path.length() != 0 ? path + "." + part : part, s.substring(i + 1));
}
}
}
排列
[1,2,3] have the following permutations:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
public List> permute(int[] nums) {
List> ret = new ArrayList<>();
List permuteList = new ArrayList<>();
boolean[] visited = new boolean[nums.length];
backtracking(permuteList, visited, nums, ret);
return ret;
}
private void backtracking(List permuteList, boolean[] visited, int[] nums, List> ret){
if(permuteList.size() == nums.length){
ret.add(new ArrayList(permuteList));
return;
}
for(int i = 0; i < visited.length; i++){
if(visited[i]) continue;
visited[i] = true;
permuteList.add(nums[i]);
backtracking(permuteList, visited, nums, ret);
permuteList.remove(permuteList.size() - 1);
visited[i] = false;
}
}
含有相同元素求排列
[1,1,2] have the following unique permutations:
[[1,1,2], [1,2,1], [2,1,1]]
题目描述:数组元素可能含有相同的元素,进行排列时就有可能出先重复的排列,要求重复的排列只返回一个。
在实现上,和 Permutations 不同的是要先排序,然后在添加一个元素时,判断这个元素是否等于前一个元素,如果等于,并且前一个元素还未访问,那么就跳过这个元素。
public List> permuteUnique(int[] nums) {
List> ret = new ArrayList<>();
List permuteList = new ArrayList<>();
Arrays.sort(nums);
boolean[] visited = new boolean[nums.length];
backtracking(permuteList, visited, nums, ret);
return ret;
}
private void backtracking(List permuteList, boolean[] visited, int[] nums, List> ret) {
if (permuteList.size() == nums.length) {
ret.add(new ArrayList(permuteList));
return;
}
for (int i = 0; i < visited.length; i++) {
if (i != 0 && nums[i] == nums[i - 1] && !visited[i - 1]) continue;
if (visited[i]) continue;
visited[i] = true;
permuteList.add(nums[i]);
backtracking(permuteList, visited, nums, ret);
permuteList.remove(permuteList.size() - 1);
visited[i] = false;
}
}
组合
If n = 4 and k = 2, a solution is:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
public List> combine(int n, int k) {
List> ret = new ArrayList<>();
List combineList = new ArrayList<>();
backtracking(1, n, k, combineList, ret);
return ret;
}
private void backtracking(int start, int n, int k, List combineList, List> ret){
if(k == 0){
ret.add(new ArrayList(combineList)); // 这里要重新构造一个 List
return;
}
for(int i = start; i <= n - k + 1; i++){ // 剪枝
combineList.add(i); // 把 i 标记为已访问
backtracking(i + 1, n, k - 1, combineList, ret);
combineList.remove(combineList.size() - 1); // 把 i 标记为未访问
}
}
组合求和
given candidate set [2, 3, 6, 7] and target 7,
A solution set is:
[[7],[2, 2, 3]]
private List> ret;
public List> combinationSum(int[] candidates, int target) {
ret = new ArrayList<>();
doCombination(candidates, target, 0, new ArrayList<>());
return ret;
}
private void doCombination(int[] candidates, int target, int start, List list) {
if (target == 0) {
ret.add(new ArrayList<>(list));
return;
}
for (int i = start; i < candidates.length; i++) {
if (candidates[i] <= target) {
list.add(candidates[i]);
doCombination(candidates, target - candidates[i], i, list);
list.remove(list.size() - 1);
}
}
}
含有相同元素的求组合求和
For example, given candidate set [10, 1, 2, 7, 6, 1, 5] and target 8,
A solution set is:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
private List> ret;
public List> combinationSum2(int[] candidates, int target) {
ret = new ArrayList<>();
Arrays.sort(candidates);
doCombination(candidates, target, 0, new ArrayList<>(), new boolean[candidates.length]);
return ret;
}
private void doCombination(int[] candidates, int target, int start, List list, boolean[] visited) {
if (target == 0) {
ret.add(new ArrayList<>(list));
return;
}
for (int i = start; i < candidates.length; i++) {
if (i != 0 && candidates[i] == candidates[i - 1] && !visited[i - 1]) continue;
if (candidates[i] <= target) {
list.add(candidates[i]);
visited[i] = true;
doCombination(candidates, target - candidates[i], i + 1, list, visited);
visited[i] = false;
list.remove(list.size() - 1);
}
}
}
子集
题目描述:找出集合的所有子集,子集不能重复,[1, 2] 和 [2, 1] 这种子集算重复
private List> ret;
private List subsetList;
public List> subsets(int[] nums) {
ret = new ArrayList<>();
subsetList = new ArrayList<>();
for (int i = 0; i <= nums.length; i++) {
backtracking(0, i, nums);
}
return ret;
}
private void backtracking(int startIdx, int size, int[] nums) {
if (subsetList.size() == size) {
ret.add(new ArrayList(subsetList));
return;
}
for (int i = startIdx; i < nums.length; i++) {
subsetList.add(nums[i]);
backtracking(i + 1, size, nums); // startIdx 设为下一个元素,使 subset 中的元素都递增排序
subsetList.remove(subsetList.size() - 1);
}
}
含有相同元素求子集
For example,
If nums = [1,2,2], a solution is:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
private List> ret;
private List subsetList;
private boolean[] visited;
public List> subsetsWithDup(int[] nums) {
ret = new ArrayList<>();
subsetList = new ArrayList<>();
visited = new boolean[nums.length];
Arrays.sort(nums);
for (int i = 0; i <= nums.length; i++) {
backtracking(0, i, nums);
}
return ret;
}
private void backtracking(int startIdx, int size, int[] nums) {
if (subsetList.size() == size) {
ret.add(new ArrayList(subsetList));
return;
}
for (int i = startIdx; i < nums.length; i++) {
if (i != 0 && nums[i] == nums[i - 1] && !visited[i - 1]) continue;
subsetList.add(nums[i]);
visited[i] = true;
backtracking(i + 1, size, nums);
visited[i] = false;
subsetList.remove(subsetList.size() - 1);
}
}
分割字符串使得每部分都是回文数
private List> ret;
public List> partition(String s) {
ret = new ArrayList<>();
doPartion(new ArrayList<>(), s);
return ret;
}
private void doPartion(List list, String s) {
if (s.length() == 0) {
ret.add(new ArrayList<>(list));
return;
}
for (int i = 0; i < s.length(); i++) {
if (isPalindrome(s, 0, i)) {
list.add(s.substring(0, i + 1));
doPartion(list, s.substring(i + 1));
list.remove(list.size() - 1);
}
}
}
private boolean isPalindrome(String s, int begin, int end) {
while (begin < end) {
if (s.charAt(begin++) != s.charAt(end--)) return false;
}
return true;
}
数独
private boolean[][] rowsUsed = new boolean[9][10];
private boolean[][] colsUsed = new boolean[9][10];
private boolean[][] cubesUsed = new boolean[9][10];
private char[][] board;
public void solveSudoku(char[][] board) {
this.board = board;
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] == '.') continue;
int num = board[i][j] - '0';
rowsUsed[i][num] = true;
colsUsed[j][num] = true;
cubesUsed[cubeNum(i, j)][num] = true;
}
}
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
backtracking(i, j);
}
}
}
private boolean backtracking(int row, int col) {
while (row < 9 && board[row][col] != '.') {
row = col == 8 ? row + 1 : row;
col = col == 8 ? 0 : col + 1;
}
if (row == 9) {
return true;
}
for (int num = 1; num <= 9; num++) {
if (rowsUsed[row][num] || colsUsed[col][num] || cubesUsed[cubeNum(row, col)][num]) continue;
rowsUsed[row][num] = colsUsed[col][num] = cubesUsed[cubeNum(row, col)][num] = true;
board[row][col] = (char) (num + '0');
if (backtracking(row, col)) return true;
board[row][col] = '.';
rowsUsed[row][num] = colsUsed[col][num] = cubesUsed[cubeNum(row, col)][num] = false;
}
return false;
}
private int cubeNum(int i, int j) {
int r = i / 3;
int c = j / 3;
return r * 3 + c;
}
分治
给表达式加括号
Input: "2-1-1".
((2-1)-1) = 0
(2-(1-1)) = 2
Output : [0, 2]
public List diffWaysToCompute(String input) {
int n = input.length();
List ret = new ArrayList<>();
for (int i = 0; i < n; i++) {
char c = input.charAt(i);
if (c == '+' || c == '-' || c == '*') {
List left = diffWaysToCompute(input.substring(0, i));
List right = diffWaysToCompute(input.substring(i + 1));
for (int l : left) {
for (int r : right) {
switch (c) {
case '+': ret.add(l + r); break;
case '-': ret.add(l - r); break;
case '*': ret.add(l * r); break;
}
}
}
}
}
if (ret.size() == 0) ret.add(Integer.valueOf(input));
return ret;
}
动态规划
递归和动态规划都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存了子问题的解。
分割整数
分割整数的最大乘积
题目描述:For example, given n = 2, return 1 (2 = 1 + 1); given n = 10, return 36 (10 = 3 + 3 + 4).
public int integerBreak(int n) {
int[] dp = new int[n + 1];
dp[1] = 1;
for(int i = 2; i <= n; i++) {
for(int j = 1; j <= i - 1; j++) {
dp[i] = Math.max(dp[i], Math.max(j * dp[i - j], j * (i - j)));
}
}
return dp[n];
}
按平方数来分割整数
题目描述:For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9.
public int numSquares(int n) {
List squares = new ArrayList<>(); // 存储小于 n 的平方数
int diff = 3;
while(square <= n) {
squares.add(square);
square += diff;
diff += 2;
}
int[] dp = new int[n + 1];
for(int i = 1; i <= n; i++) {
int max = Integer.MAX_VALUE;
for(int s : squares) {
if(s > i) break;
max = Math.min(max, dp[i - s] + 1);
}
dp[i] = max;
}
return dp[n];
}
分割整数构成字母字符串
题目描述:Given encoded message "12", it could be decoded as "AB" (1 2) or "L" (12).
public int numDecodings(String s) {
if(s == null || s.length() == 0) return 0;
int n = s.length();
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = s.charAt(0) == '0' ? 0 : 1;
for(int i = 2; i <= n; i++) {
int one = Integer.valueOf(s.substring(i - 1, i));
if(one != 0) dp[i] += dp[i - 1];
if(s.charAt(i - 2) == '0') continue;
int two = Integer.valueOf(s.substring(i - 2, i));
if(two <= 26) dp[i] += dp[i - 2];
}
return dp[n];
}
矩阵路径
矩阵的总路径数
题目描述:统计从矩阵左上角到右下角的路径总数,每次只能向左和向下移动。
public int uniquePaths(int m, int n) {
int[] dp = new int[n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if(i == 0) dp[j] = 1;
else if(j != 0) dp[j] = dp[j] + dp[j - 1];
}
}
return dp[n - 1];
}
矩阵的最小路径和
题目描述:求从矩阵的左上角到右下角的最小路径和,每次只能向左和向下移动。
public int minPathSum(int[][] grid) {
if(grid.length == 0 || grid[0].length == 0) return 0;
int m = grid.length, n = grid[0].length;
int[] dp = new int[n];
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
if(j == 0) dp[0] = dp[0] + grid[i][0];
else if(i == 0) dp[j] = dp[j - 1] + grid[0][j];
else dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];
}
}
return dp[n - 1];
}
斐波那契数列
爬楼梯
题目描述:有 N 阶楼梯,每次可以上一阶或者两阶,求有多少种上楼梯的方法。
定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始),dp[i] 表示走到第 i 个楼梯的方法数目。第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。
dp[N] 即为所求。
考虑到 dp[i] 只与 dp[i - 1] 和 dp[i - 2] 有关,因此可以只用两个变量来存储 dp[i - 1] 和 dp[i - 2] 即可,使得原来的 O(n) 空间复杂度优化为 O(1) 复杂度。
public int climbStairs(int n) {
if(n == 1) return 1;
if(n == 2) return 2;
// 前一个楼梯、后一个楼梯
int pre1 = 2, pre2 = 1;
for(int i = 2; i < n; i++){
int cur = pre1 + pre2;
pre2 = pre1;
pre1 = cur;
}
return pre1;
}
母牛生产
题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。
第 i 年成熟的牛的数量为:
强盗抢劫
题目描述:抢劫一排住户,但是不能抢邻近的住户,求最大抢劫量。
定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。由于不能抢劫邻近住户,因此如果抢劫了第 i 个住户那么只能抢劫 i - 2 和 i - 3 的住户,所以
O(n) 空间复杂度实现方法:
public int rob(int[] nums) {
int n = nums.length;
if(n == 0) return 0;
if(n == 1) return nums[0];
if(n == 2) return Math.max(nums[0], nums[1]);
int[] dp = new int[n];
dp[0] = nums[0];
dp[1] = nums[1];
dp[2] = nums[0] + nums[2];
for(int i = 3; i < n; i++){
dp[i] = Math.max(dp[i -2], dp[i - 3]) + nums[i];
}
return Math.max(dp[n - 1], dp[n - 2]);
}
O(1) 空间复杂度实现方法:
public int rob(int[] nums) {
int n = nums.length;
if(n == 0) return 0;
if(n == 1) return nums[0];
if(n == 2) return Math.max(nums[0], nums[1]);
int pre3 = nums[0], pre2 = nums[1], pre1 = nums[2] + nums[0];
for(int i = 3; i < n; i++){
int cur = Math.max(pre2, pre3) + nums[i];
pre3 = pre2;
pre2 = pre1;
pre1 = cur;
}
return Math.max(pre1, pre2);
}
强盗在环形街区抢劫
public int rob(int[] nums) {
if(nums == null || nums.length == 0) return 0;
int n = nums.length;
if(n == 1) return nums[0];
return Math.max(rob(nums, 0, n - 2), rob(nums, 1, n - 1));
}
private int rob(int[] nums, int s, int e) {
int n = nums.length;
if(e - s == 0) return nums[s];
if(e - s == 1) return Math.max(nums[s], nums[s + 1]);
int[] dp = new int[n];
dp[s] = nums[s];
dp[s + 1] = nums[s + 1];
dp[s + 2] = nums[s] + nums[s + 2];
for (int i = s + 3; i <= e; i++) {
dp[i] = Math.max(dp[i - 2], dp[i - 3]) + nums[i];
}
return Math.max(dp[e], dp[e - 1]);
}
信件错排
题目描述:有 N 个 信 和 信封,它们被打乱,求错误装信的方式数量。
定义一个数组 dp 存储错误方式数量,dp[i] 表示前 i 个信和信封的错误方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据 i 和 k 是否相等,有两种情况:
① i==k,交换 i 和 k 的信后,它们的信和信封在正确的位置,但是其余 i-2 封信有 dp[i-2] 种错误装信的方式。由于 j 有 i-1 种取值,因此共有 (i-1)*dp[i-2] 种错误装信方式。
② i != k,交换 i 和 j 的信后,第 i 个信和信封在正确的位置,其余 i-1 封信有 dp[i-1] 种错误装信方式。由于 j 有 i-1 种取值,因此共有 (n-1)*dp[i-1] 种错误装信方式。
综上所述,错误装信数量方式数量为:
dp[N] 即为所求。
和上楼梯问题一样,dp[i] 只与 dp[i-1] 和 dp[i-2] 有关,因此也可以只用两个变量来存储 dp[i-1] 和 dp[i-2]。
最长递增子序列
已知一个序列 {S1, S2,...,Sn} ,取出若干数组成新的序列 {Si1, Si2,..., Sim},其中 i1、i2 ... im 保持递增,即新序列中各个数仍然保持原数列中的先后顺序,称新序列为原序列的一个 子序列 。
如果在子序列中,当下标 ix > iy 时,Six > Siy,称子序列为原序列的一个 递增子序列 。
定义一个数组 dp 存储最长递增子序列的长度,dp[n] 表示以 Sn 结尾的序列的最长递增子序列长度。对于一个递增子序列 {Si1, Si2,...,Sim},如果 im < n 并且 Sim < Sn ,此时 {Si1, Si2,..., Sim, Sn} 为一个递增子序列,递增子序列的长度增加 1。满足上述条件的递增子序列中,长度最长的那个递增子序列就是要找的,在长度最长的递增子序列上加上 Sn 就构成了以 Sn 为结尾的最长递增子序列。因此 dp[n] = max{ dp[i]+1 | Si < Sn && i < n} 。
因为在求 dp[n] 时可能无法找到一个满足条件的递增子序列,此时 {Sn} 就构成了递增子序列,因此需要对前面的求解方程做修改,令 dp[n] 最小为 1,即:
对于一个长度为 N 的序列,最长子序列并不一定会以 SN 为结尾,因此 dp[N] 不是序列的最长递增子序列的长度,需要遍历 dp 数组找出最大值才是所要的结果,即 max{ dp[i] | 1 <= i <= N} 即为所求。
最长递增子序列
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
for(int i = 0; i < n; i++){
int max = 1;
for(int j = 0; j < i; j++){
if(nums[i] > nums[j]) max = Math.max(max, dp[j] + 1);
}
dp[i] = max;
}
int ret = 0;
for(int i = 0; i < n; i++){
ret = Math.max(ret, dp[i]);
}
return ret;
}
以上解法的时间复杂度为 O(n2) ,可以使用二分查找使得时间复杂度降低为 O(nlogn)。定义一个 tails 数组,其中 tails[i] 存储长度为 i + 1 的最长递增子序列的最后一个元素,例如对于数组 [4,5,6,3],有
len = 1 : [4], [5], [6], [3] => tails[0] = 3
len = 2 : [4, 5], [5, 6] => tails[1] = 5
len = 3 : [4, 5, 6] => tails[2] = 6
对于一个元素 x,如果它大于 tails 数组所有的值,那么把它添加到 tails 后面;如果 tails[i-1] < x <= tails[i],那么更新 tails[i] = x 。
可以看出 tails 数组保持有序,因此在查找 Si 位于 tails 数组的位置时就可以使用二分查找。
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] tails = new int[n];
int size = 0;
for(int i = 0; i < n; i++){
int idx = binarySearch(tails, 0, size, nums[i]);
tails[idx] = nums[i];
if(idx == size) size++;
}
return size;
}
private int binarySearch(int[] nums, int sIdx, int eIdx, int key){
while(sIdx < eIdx){
int mIdx = sIdx + (eIdx - sIdx) / 2;
if(nums[mIdx] == key) return mIdx;
else if(nums[mIdx] > key) eIdx = mIdx;
else sIdx = mIdx + 1;
}
return sIdx;
}
最长摆动子序列
要求:使用 O(n) 时间复杂度求解。
使用两个状态 up 和 down。
public int wiggleMaxLength(int[] nums) {
int len = nums.length;
if (len == 0) return 0;
int up = 1, down = 1;
for (int i = 1; i < len; i++) {
if (nums[i] > nums[i - 1]) up = down + 1;
else if (nums[i] < nums[i - 1]) down = up + 1;
}
return Math.max(up, down);
}
最长公共子系列
对于两个子序列 S1 和 S2,找出它们最长的公共子序列。
定义一个二维数组 dp 用来存储最长公共子序列的长度,其中 dp[i][j] 表示 S1 的前 i 个字符与 S2 的前 j 个字符最长公共子序列的长度。考虑 S1i 与 S2j 值是否相等,分为两种情况:
① 当 S1i==S2j 时,那么就能在 S1 的前 i-1 个字符与 S2 的前 j-1 个字符最长公共子序列的基础上再加上 S1i 这个值,最长公共子序列长度加 1 ,即 dp[i][j] = dp[i-1][j-1] + 1。
② 当 S1i != S2j 时,此时最长公共子序列为 S1 的前 i-1 个字符和 S2 的前 j 个字符最长公共子序列,与 S1 的前 i 个字符和 S2 的前 j-1 个字符最长公共子序列,它们的最大者,即 dp[i][j] = max{ dp[i-1][j], dp[i][j-1] }。
综上,最长公共子系列的状态转移方程为:
对于长度为 N 的序列 S1 和 长度为 M 的序列 S2,dp[N][M] 就是序列 S1 和序列 S2 的最长公共子序列长度。
与最长递增子序列相比,最长公共子序列有以下不同点:
① 针对的是两个序列,求它们的最长公共子序列。
② 在最长递增子序列中,dp[i] 表示以 Si 为结尾的最长递增子序列长度,子序列必须包含 Si ;在最长公共子序列中,dp[i][j] 表示 S1 中前 i 个字符与 S2 中前 j 个字符的最长公共子序列长度,不一定包含 S1i 和 S2j 。
③ 由于 2 ,在求最终解时,最长公共子序列中 dp[N][M] 就是最终解,而最长递增子序列中 dp[N] 不是最终解,因为以 SN 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。
public int lengthOfLCS(int[] nums1, int[] nums2) {
int n1 = nums1.length, n2 = nums2.length;
int[][] dp = new int[n1 + 1][n2 + 1];
for (int i = 1; i <= n1; i++) {
for (int j = 1; j <= n2; j++) {
if (nums1[i - 1] == nums2[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];
}
0-1 背包
有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。
定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示体积不超过 j 的情况下,前 i 件物品能达到的最大价值。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论:
① 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j]。
② 第 i 件物品添加到背包中,dp[i][j] = dp[i-1][j-w] + v。
第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。
综上,0-1 背包的状态转移方程为:
public int knapsack(int W, int N, int[] weights, int[] values) {
int[][] dp = new int[N][W];
for (int i = W - 1; i >= 0; i--) {
dp[0][i] = i > weights[0] ? values[0] : 0;
}
for (int i = 1; i < N; i++) {
for (int j = W - 1; j >= weights[i]; j--) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i]] + values[i]);
}
for (int j = weights[i - 1] - 1; j >= 0; j--) {
dp[i][j] = dp[i - 1][j];
}
}
return dp[N - 1][W - 1];
}
空间优化
在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅由前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此时,
因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w] 防止将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。
无法使用贪心算法的解释
0-1 背包问题无法使用贪心算法来求解,也就是说不能按照先添加性价比最高的物品来达到最优,这是因为这种方式可能造成背包空间的浪费,从而无法达到最优。考虑下面的物品和一个容量为 5 的背包,如果先添加物品 0 再添加物品 1,那么只能存放的价值为 16,浪费了大小为 2 的空间。最优的方式是存放物品 1 和物品 2,价值为 22.
id
w
v
v/w
0
1
6
6
1
2
10
5
2
3
12
4
变种
完全背包:物品可以无限个,可以转换为 0-1 背包,令每种物品的体积和价值变为 1/2/4... 倍数,把它们都当成一个新物品,然后一种物品只能添加一次。
多重背包:物品数量有限制,同样可以转换为 0-1 背包。
多维费用背包:物品不仅有重量,还有体积,同时考虑这两种限制。
其它:物品之间相互约束或者依赖。
划分数组为和相等的两部分
可以看成一个背包大小为 sum/2 的 0-1 背包问题,但是也有不同的地方,这里没有价值属性,并且背包必须被填满。
以下实现使用了空间优化。
public boolean canPartition(int[] nums) {
int sum = 0;
for (int num : nums) {
sum += num;
}
if (sum % 2 != 0) {
return false;
}
int W = sum / 2;
boolean[] dp = new boolean[W + 1];
int n = nums.length;
for(int i = 0; i <= W; i++) {
if(nums[0] == i) dp[i] = true;
}
for(int i = 1; i < n; i++) {
for(int j = W; j >= nums[i]; j--) {
dp[j] = dp[j] || dp[j - nums[i]];
}
}
return dp[W];
}
字符串按单词列表分割
s = "leetcode",
dict = ["leet", "code"].
Return true because "leetcode" can be segmented as "leet code".
public boolean wordBreak(String s, List wordDict) {
int n = s.length();
boolean[] dp = new boolean[n + 1];
dp[0] = true;
for (int i = 1; i <= n; i++) {
for (String word : wordDict) {
if (word.length() <= i
&& word.equals(s.substring(i - word.length(), i))) {
dp[i] = dp[i] || dp[i - word.length()];
}
}
}
return dp[n];
}
改变一组数的正负号使得它们的和为一给定数
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.
该问题可以转换为 subset sum 问题,从而使用 0-1 背包的方法来求解。可以将这组数看成两部分,P 和 N,其中 P 使用正号,N 使用负号,有以下推导:
sum(P) - sum(N) = target
sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N)
2 * sum(P) = target + sum(nums)
因此只要找到一个子集,令它们都取正号,并且和等于 (target + sum(nums))/2,就证明存在解。
public int findTargetSumWays(int[] nums, int S) {
int sum = 0;
for (int num : nums) {
sum += num;
}
if (sum < S || (sum + S) % 2 == 1) {
return 0;
}
return subsetSum(nums, (sum + S) >>> 1);
}
private int subsetSum(int[] nums, int targetSum) {
Arrays.sort(nums);
int[] dp = new int[targetSum + 1];
dp[0] = 1;
for (int i = 0; i < nums.length; i++) {
int num = nums[i];
for (int j = targetSum; j >= num; j--) {
dp[j] = dp[j] + dp[j - num];
}
}
return dp[targetSum];
}
01 字符构成最多的字符串
Input: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
Output: 4
Explanation: This are totally 4 strings can be formed by the using of 5 0s and 3 1s, which are “10,”0001”,”1”,”0”
这是一个多维费用的 0-1 背包问题,有两个背包大小,0 的数量和 1 的数量。
public int findMaxForm(String[] strs, int m, int n) {
if (strs == null || strs.length == 0) return 0;
int l = strs.length;
int[][] dp = new int[m + 1][n + 1];
for (int i = 0; i < l; i++) {
String s = strs[i];
int ones = 0, zeros = 0;
for (char c : s.toCharArray()) {
if (c == '0') zeros++;
else if (c == '1') ones++;
}
for (int j = m; j >= zeros; j--) {
for (int k = n; k >= ones; k--) {
if (zeros <= j && ones <= k) {
dp[j][k] = Math.max(dp[j][k], dp[j - zeros][k - ones] + 1);
}
}
}
}
return dp[m][n];
}
找零钱
题目描述:给一些面额的硬币,要求用这些硬币来组成给定面额的钱数,并且使得硬币数量最少。硬币可以重复使用。
这是一个完全背包问题,完全背包问题和 0-1 背包问题在实现上唯一的不同是,第二层循环是从 0 开始的,而不是从尾部开始。
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.length; j++) {
if (coins[j] <= i) {
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
组合总和
nums = [1, 2, 3]
target = 4
The possible combination ways are:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
Note that different sequences are counted as different combinations.
Therefore the output is 7.
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1];
dp[0] = 1;
for (int i = 1; i <= target; i++) {
for (int j = 0; j < nums.length; j++) {
if(nums[j] <= i) {
dp[i] += dp[i - nums[j]];
}
}
}
return dp[target];
}
只能进行两次的股票交易
public int maxProfit(int[] prices) {
int firstBuy = Integer.MIN_VALUE, firstSell = 0;
int secondBuy = Integer.MIN_VALUE, secondSell = 0;
for (int curPrice : prices) {
if (firstBuy < -curPrice) firstBuy = -curPrice;
if (firstSell < firstBuy + curPrice) firstSell = firstBuy + curPrice;
if (secondBuy < firstSell - curPrice) secondBuy = firstSell - curPrice;
if (secondSell < secondBuy + curPrice) secondSell = secondBuy + curPrice;
}
return secondSell;
}
只能进行 k 次的股票交易
dp[i, j] = max(dp[i, j-1], prices[j] - prices[jj] + dp[i-1, jj]) { jj in range of [0, j-1] } = max(dp[i, j-1], prices[j] + max(dp[i-1, jj] - prices[jj]))
public int maxProfit(int k, int[] prices) {
int n = prices.length;
if (k >= n/2) {
int maxPro = 0;
for (int i = 1; i < n; i++) {
if (prices[i] > prices[i-1])
maxPro += prices[i] - prices[i-1];
}
return maxPro;
}
int[][] dp = new int[k + 1][n];
for (int i = 1; i <= k; i++) {
int localMax = dp[i - 1][0] - prices[0];
for (int j = 1; j < n; j++) {
dp[i][j] = Math.max(dp[i][j - 1], prices[j] + localMax);
localMax = Math.max(localMax, dp[i - 1][j] - prices[j]);
}
}
return dp[k][n - 1];
}
数组区间
数组区间和
求区间 i ~ j 的和,可以转换为 sum[j] - sum[i-1],其中 sum[i] 为 0 ~ j 的和。
class NumArray {
int[] nums;
public NumArray(int[] nums) {
for(int i = 1; i < nums.length; i++)
nums[i] += nums[i - 1];
this.nums = nums;
}
public int sumRange(int i, int j) {
return i == 0 ? nums[j] : nums[j] - nums[i - 1];
}
}
子数组最大的和
令 sum[i] 为以 num[i] 为结尾的子数组最大的和,可以由 sum[i-1] 得到 sum[i] 的值,如果 sum[i-1] 小于 0,那么以 num[i] 为结尾的子数组不能包含前面的内容,因为加上前面的部分,那么和一定会比 num[i] 还小。
public int maxSubArray(int[] nums) {
int n = nums.length;
int[] sum = new int[n];
sum[0] = nums[0];
int max = sum[0];
for(int i = 1; i < n; i++){
sum[i] = (sum[i-1] > 0 ? sum[i-1] : 0) + nums[i];
max = Math.max(max, sum[i]);
}
return max;
}
空间复杂度可以优化成 O(1) 空间复杂度
public int maxSubArray(int[] nums) {
int max = nums[0];
int oldsum = nums[0];
for (int i = 1; i < nums.length; i++) {
oldsum = (oldsum > 0 ? oldsum: 0) + nums[i];
max = Math.max(max, oldsum);
}
return max;
}
数组中等差递增子区间的个数
A = [1, 2, 3, 4]
return: 3, for 3 arithmetic slices in A: [1, 2, 3], [2, 3, 4] and [1, 2, 3, 4] itself.
对于 (1,2,3,4),它有三种组成递增子区间的方式,而对于 (1,2,3,4,5),它组成递增子区间的方式除了 (1,2,3,4) 的三种外还多了一种,即 (1,2,3,4,5),因此 dp[i] = dp[i - 1] + 1。
public int numberOfArithmeticSlices(int[] A) {
int n = A.length;
int[] dp = new int[n];
for(int i = 2; i < n; i++) {
if(A[i] - A[i - 1] == A[i - 1] - A[i - 2]) {
dp[i] = dp[i - 1] + 1;
}
}
int ret = 0;
for(int cnt : dp) {
ret += cnt;
}
return ret;
}
字符串编辑
删除两个字符串的字符使它们相等
可以转换为求两个字符串的最长公共子序列问题。
public int minDistance(String word1, String word2) {
int m = word1.length(), n = word2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 0; i <= m; i++) {
for (int j = 0; j <= n; j++) {
if (i == 0 || j == 0) continue;
dp[i][j] = word1.charAt(i - 1) == word2.charAt(j - 1) ? dp[i - 1][j - 1] + 1
: Math.max(dp[i][j - 1], dp[i - 1][j]);
}
}
return m + n - 2 * dp[m][n];
}
修改一个字符串称为另一个字符串 // TODO
其它问题
需要冷却期的股票交易
题目描述:交易之后需要有一天的冷却时间。
s0[i] = max(s0[i - 1], s2[i - 1]); // Stay at s0, or rest from s2
s1[i] = max(s1[i - 1], s0[i - 1] - prices[i]); // Stay at s1, or buy from s0
s2[i] = s1[i - 1] + prices[i]; // Only one way from s1
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) return 0;
int n = prices.length;
int[] s0 = new int[n];
int[] s1 = new int[n];
int[] s2 = new int[n];
s0[0] = 0;
s1[0] = -prices[0];
s2[0] = Integer.MIN_VALUE;
for (int i = 1; i < n; i++) {
s0[i] = Math.max(s0[i - 1], s2[i - 1]);
s1[i] = Math.max(s1[i - 1], s0[i - 1] - prices[i]);
s2[i] = Math.max(s2[i - 1], s1[i - 1] + prices[i]);
}
return Math.max(s0[n - 1], s2[n - 1]);
}
统计从 0 ~ n 每个数的二进制表示中 1 的个数
对于数字 6(110),它可以看成是数字 2(10) 前面加上一个 1 ,因此 dp[i] = dp[i&(i-1)] + 1;
public int[] countBits(int num) {
int[] ret = new int[num + 1];
for(int i = 1; i <= num; i++){
ret[i] = ret[i&(i-1)] + 1;
}
return ret;
}
一组整数对能够构成的最长链
对于 (a, b) 和 (c, d) ,如果 b < c,则它们可以构成一条链。
public int findLongestChain(int[][] pairs) {
if(pairs == null || pairs.length == 0) {
return 0;
}
Arrays.sort(pairs, (a, b) -> (a[0] - b[0]));
int n = pairs.length;
int[] dp = new int[n];
Arrays.fill(dp, 1);
for(int i = 0; i < n; i++) {
for(int j = 0; j < i; j++) {
if(pairs[i][0] > pairs[j][1]){
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
int ret = 0;
for(int num : dp) {
ret = Math.max(ret, num);
}
return ret;
}
买入和售出股票最大的收益
只进行一次交易。
只要记录前面的最小价格,将这个最小价格作为买入价格,然后将当前的价格作为售出价格,查看这个价格是否是当前的最大价格。
public int maxProfit(int[] prices) {
int n = prices.length;
if(n == 0) return 0;
int soFarMin = prices[0];
int max = 0;
for(int i = 1; i < n; i++){
if(soFarMin > prices[i]) soFarMin = prices[i];
else max = Math.max(max, prices[i] - soFarMin);
}
return max;
}
复制粘贴字符
public int minSteps(int n) {
int[] dp = new int[n + 1];
for (int i = 2; i <= n; i++) {
dp[i] = i;
for (int j = i - 1; j >= 0; j--) {
if (i % j == 0) {
dp[i] = dp[j] + dp[i / j];
break;
}
}
}
return dp[n];
}
public int minSteps(int n) {
if (n == 1) return 0;
for (int i = 2; i <= Math.sqrt(n); i++) {
if (n % i == 0) return i + minSteps(n / i);
}
return n;
}
数学
素数
素数分解
每一个数都可以分解成素数的乘积,例如 84 = 22 * 31 * 50 * 71 * 110 * 130 * 170 * …
整除
令 x = 2m0 * 3m1 * 5m2 * 7m3 * 11m4 * …
令 y = 2n0 * 3n1 * 5n2 * 7n3 * 11n4 * …
如果 x 整除 y(y mod x == 0),则对于所有 i,mi <= ni。
x 和 y 的 最大公约数 为:gcd(x,y) = 2min(m0,n0) * 3min(m1,n1) * 5min(m2,n2) * ...
x 和 y 的 最小公倍数 为:lcm(x,y) = 2max(m0,n0) * 3max(m1,n1) * 5max(m2,n2) * ...
生成素数序列
埃拉托斯特尼筛法在每次找到一个素数时,将能被素数整除的数排除掉。
public int countPrimes(int n) {
boolean[] notPrimes = new boolean[n + 1];
int cnt = 0;
for(int i = 2; i < n; i++){
if(notPrimes[i]) continue;
cnt++;
// 从 i * i 开始,因为如果 k < i,那么 k * i 在之前就已经被去除过了
for(long j = (long) i * i; j < n; j += i){
notPrimes[(int) j] = true;
}
}
return cnt;
}
最大公约数
int gcd(int a, int b) {
if (b == 0) return a;
return gcd(b, a % b);
}
最大公倍数为两数的乘积除以最大公约数。
int lcm(int a, int b){
return a * b / gcd(a, b);
}
对于最大公约数问题,因为需要计算 a % b ,而这个操作是比较耗时的,可以使用 编程之美:2.7 的方法,利用减法和移位操作来替换它。
对于 a 和 b 的最大公约数 f(a, b),有:
1. 如果 a 和 b 均为偶数,f(a, b) = 2*f(a/2, b/2);
2. 如果 a 是偶数 b 是奇数,f(a, b) = f(a/2, b);
3. 如果 b 是偶数 a 是奇数,f(a, b) = f(a, b/2);
4. 如果 a 和 b 均为奇数,f(a, b) = f(a, a-b);
乘 2 和除 2 都可以转换为移位操作。
进制转换
Java 中 static String toString(int num, int radix) 可以将一个整数装换为 redix 进制表示的字符串。
7 进制
public String convertToBase7(int num) {
if (num < 0) {
return '-' + convertToBase7(-num);
}
if (num < 7) {
return num + "";
}
return convertToBase7(num / 7) + num % 7;
}
16 进制
public String toHex(int num) {
char[] map = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
if(num == 0) return "0";
String ret = "";
while(num != 0){
ret = map[(num & 0b1111)] + ret;
num >>>= 4;
}
return ret;
}
阶乘
统计阶乘尾部有多少个 0
尾部的 0 由 2 * 5 得来,2 的数量明显多于 5 的数量,因此只要统计有多少个 5 即可。
对于一个数 N,它所包含 5 的个数为:N/5 + N/52 + N/53 + ...,其中 N/5 表示不大于 N 的数中 5 的倍数贡献一个 5,N/52 表示不大于 N 的数中 52 的倍数再贡献一个 5 ...。
public int trailingZeroes(int n) {
return n == 0 ? 0 : n / 5 + trailingZeroes(n / 5);
}
如果统计的是 N! 的二进制表示中最低位 1 的位置,只要统计有多少个 2 即可,该题目出自 编程之美:2.2 。和求解有多少个 5 一样,2 的个数为 N/2 + N/22 + N/23 + ...
字符串加法减法
二进制加法
a = "11"
b = "1"
Return "100".
public String addBinary(String a, String b) {
int i = a.length() - 1, j = b.length() - 1, carry = 0;
String str = "";
while(i >= 0 || j >= 0){
if(i >= 0 && a.charAt(i--) == '1') carry++;
if(j >= 0 && b.charAt(j--) == '1') carry++;
str = (carry % 2) + str;
carry /= 2;
}
if(carry == 1) str = "1" + str;
return str;
}
字符串加法
字符串的值为非负整数
public String addStrings(String num1, String num2) {
StringBuilder sb = new StringBuilder();
int carry = 0;
for(int i = num1.length() - 1, j = num2.length() - 1; i >= 0 || j >= 0 || carry == 1; i--, j--){
int x = i < 0 ? 0 : num1.charAt(i) - '0';
int y = j < 0 ? 0 : num2.charAt(j) - '0';
sb.append((x + y + carry) % 10);
carry = (x + y + carry) / 10;
}
return sb.reverse().toString();
}
相遇问题
改变数组元素使所有的数组元素都相等
Input:
[1,2,3]
Output:
2
Explanation:
Only two moves are needed (remember each move increments or decrements one element):
[1,2,3] => [2,2,3] => [2,2,2]
每次可以对一个数组元素加一或者减一,求最小的改变次数。
这是个典型的相遇问题,移动距离最小的方式是所有元素都移动到中位数。理由如下:
设 m 为中位数。a 和 b 是 m 两边的两个元素,且 b > a。要使 a 和 b 相等,它们总共移动的次数为 b - a,这个值等于 (b - m) + (m - a),也就是把这两个数移动到中位数的移动次数。
设数组长度为 N,则可以找到 N/2 对 a 和 b 的组合,使它们都移动到 m 的位置。
解法 1
先排序,时间复杂度:O(NlgN)
public int minMoves2(int[] nums) {
Arrays.sort(nums);
int ret = 0;
int l = 0, h = nums.length - 1;
while(l <= h) {
ret += nums[h] - nums[l];
l++;
h--;
}
return ret;
}
解法 2
使用快速排序找到中位数,时间复杂度 O(N)
public int minMoves2(int[] nums) {
int ret = 0;
int n = nums.length;
int median = quickSelect(nums, 0, n - 1, n / 2 + 1);
for(int num : nums) ret += Math.abs(num - median);
return ret;
}
private int quickSelect(int[] nums, int start, int end, int k) {
int l = start, r = end, privot = nums[(l + r) / 2];
while(l <= r) {
while(nums[l] < privot) l++;
while(nums[r] > privot) r--;
if(l >= r) break;
swap(nums, l, r);
l++; r--;
}
int left = l - start + 1;
if(left > k) return quickSelect(nums, start, l - 1, k);
if(left == k && l == r) return nums[l];
int right = r - start + 1;
return quickSelect(nums, r + 1, end, k - right);
}
private void swap(int[] nums, int i, int j) {
int tmp = nums[i]; nums[i] = nums[j]; nums[j] = tmp;
}
多数投票问题
数组中出现次数多于 n / 2 的元素
先对数组排序,最中间那个数出现次数一定多于 n / 2
public int majorityElement(int[] nums) {
Arrays.sort(nums);
return nums[nums.length / 2];
}
可以利用 Boyer-Moore Majority Vote Algorithm 来解决这个问题,使得时间复杂度为 O(n)。可以这么理解该算法:使用 cnt 来统计一个元素出现的次数,当遍历到的元素和统计元素不想等时,令 cnt--。如果前面查找了 i 个元素,且 cnt == 0 ,说明前 i 个元素没有 majority,或者有 majority,但是出现的次数少于 i / 2 ,因为如果多于 i / 2 的话 cnt 就一定不会为 0 。此时剩下的 n - i 个元素中,majority 的数目依然多于 (n - i) / 2,因此继续查找就能找出 majority。
public int majorityElement(int[] nums) {
int cnt = 0, majority = 0;
for(int i = 0; i < nums.length; i++){
if(cnt == 0) {
majority = nums[i];
cnt++;
}
else if(majority == nums[i]) cnt++;
else cnt--;
}
return majority;
}
其它
平方数
Input: 16
Returns: True
平方序列:1,4,9,16,..
间隔:3,5,7,...
间隔为等差数列,使用这个特性可以得到从 1 开始的平方序列。
public boolean isPerfectSquare(int num) {
int subNum = 1;
while (num > 0) {
num -= subNum;
subNum += 2;
}
return num == 0;
}
3 的 n 次方
public boolean isPowerOfThree(int n) {
return n > 0 && (1162261467 % n == 0);
}
找出数组中的乘积最大的三个数
Input: [1,2,3,4]
Output: 24
public int maximumProduct(int[] nums) {
int max1 = Integer.MIN_VALUE, max2 = Integer.MIN_VALUE, max3 = Integer.MIN_VALUE, min1 = Integer.MAX_VALUE, min2 = Integer.MAX_VALUE;
for (int n : nums) {
if (n > max1) {
max3 = max2;
max2 = max1;
max1 = n;
} else if (n > max2) {
max3 = max2;
max2 = n;
} else if (n > max3) {
max3 = n;
}
if (n < min1) {
min2 = min1;
min1 = n;
} else if (n < min2) {
min2 = n;
}
}
return Math.max(max1*max2*max3, max1*min1*min2);
}
乘积数组
For example, given [1,2,3,4], return [24,12,8,6].
题目描述:给定一个数组,创建一个新数组,新数组的每个元素为原始数组中除了该位置上的元素之外所有元素的乘积。
题目要求:时间复杂度为 O(n),并且不能使用除法。
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] ret = new int[n];
ret[0] = 1;
for(int i = 1; i < n; i++) {
ret[i] = ret[i - 1] * nums[i - 1];
}
int right = 1;
for(int i = n - 1; i >= 0; i--) {
ret[i] *= right;
right *= nums[i];
}
return ret;
}
数据结构相关
栈和队列
用栈实现队列
一个栈实现:
class MyQueue {
private Stack st = new Stack();
public void push(int x) {
Stack temp = new Stack();
while(!st.isEmpty()){
temp.push(st.pop());
}
st.push(x);
while(!temp.isEmpty()){
st.push(temp.pop());
}
}
public int pop() {
return st.pop();
}
public int peek() {
return st.peek();
}
public boolean empty() {
return st.isEmpty();
}
}
两个栈实现:
class MyQueue {
private Stack in = new Stack();
private Stack out = new Stack();
public void push(int x) {
in.push(x);
}
public int pop() {
in2out();
return out.pop();
}
public int peek() {
in2out();
return out.peek();
}
private void in2out(){
if(out.isEmpty()){
while(!in.isEmpty()){
out.push(in.pop());
}
}
}
public boolean empty() {
return in.isEmpty() && out.isEmpty();
}
}
用队列实现栈
class MyStack {
private Queue queue;
public MyStack() {
queue = new LinkedList<>();
}
public void push(int x) {
queue.add(x);
for(int i = 1; i < queue.size(); i++){ // 翻转
queue.add(queue.remove());
}
}
public int pop() {
return queue.remove();
}
public int top() {
return queue.peek();
}
public boolean empty() {
return queue.isEmpty();
}
}
最小值栈
用两个栈实现,一个存储数据,一个存储最小值。
class MinStack {
private Stack dataStack;
private Stack minStack;
private int min;
public MinStack() {
dataStack = new Stack<>();
minStack = new Stack<>();
min = Integer.MAX_VALUE;
}
public void push(int x) {
dataStack.add(x);
if(x < min) {
min = x;
}
minStack.add(min);
}
public void pop() {
dataStack.pop();
minStack.pop();
if(!minStack.isEmpty()) {
min = minStack.peek();
} else{
min = Integer.MAX_VALUE;
}
}
public int top() {
return dataStack.peek();
}
public int getMin() {
return min;
}
}
对于实现最小值队列问题,可以先将队列使用栈来实现,然后就将问题转换为最小值栈,这个问题出现在 编程之美:3.7。
用栈实现括号匹配
"()[]{}"
Output : true
public boolean isValid(String s) {
Stack stack = new Stack<>();
for(int i = 0; i < s.length(); i++){
char c = s.charAt(i);
if(c == '(' || c == '{' || c == '[') stack.push(c);
else{
if(stack.isEmpty()) return false;
char cStack = stack.pop();
if(c == ')' && cStack != '(' ||
c == ']' && cStack != '[' ||
c == '}' && cStack != '{' ) {
return false;
}
}
}
return stack.isEmpty();
}
数组中元素与下一个比它大的元素之间的距离
Input: [73, 74, 75, 71, 69, 72, 76, 73]
Output: [1, 1, 4, 2, 1, 1, 0, 0]
在遍历数组时用 Stack 把数组中的数存起来,如果当前遍历的数比栈顶元素来的大,说明栈顶元素的下一个比它大的数就是当前元素。
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] ret = new int[n];
Stack stack = new Stack<>();
for(int i = 0; i < n; i++) {
while(!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
int idx = stack.pop();
ret[idx] = i - idx;
}
stack.add(i);
}
return ret;
}
在另一个数组中比当前元素大的下一个元素
Input: nums1 = [4,1,2], nums2 = [1,3,4,2].
Output: [-1,3,-1]
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
Map map = new HashMap<>();
Stack stack = new Stack<>();
for(int num : nums2){
while(!stack.isEmpty() && num > stack.peek()){
map.put(stack.pop(), num);
}
stack.add(num);
}
int[] ret = new int[nums1.length];
for(int i = 0; i < nums1.length; i++){
if(map.containsKey(nums1[i])) ret[i] = map.get(nums1[i]);
else ret[i] = -1;
}
return ret;
}
循环数组中比当前元素大的下一个元素
public int[] nextGreaterElements(int[] nums) {
int n = nums.length, next[] = new int[n];
Arrays.fill(next, -1);
Stack stack = new Stack<>();
for (int i = 0; i < n * 2; i++) {
int num = nums[i % n];
while (!stack.isEmpty() && nums[stack.peek()] < num)
next[stack.pop()] = num;
if (i < n) stack.push(i);
}
return next;
}
哈希表
利用 Hash Table 可以快速查找一个元素是否存在等问题,但是需要一定的空间来存储。在优先考虑时间复杂度的情况下,可以利用 Hash Table 这种空间换取时间的做法。
Java 中的 HashSet 用于存储一个集合,并以 O(1) 的时间复杂度查找元素是否在集合中。
如果元素有穷,并且范围不大,那么可以用一个布尔数组来存储一个元素是否存在,例如对于只有小写字符的元素,就可以用一个长度为 26 的布尔数组来存储一个字符集合,使得空间复杂度降低为 O(1)。
Java 中的 HashMap 主要用于映射关系,从而把两个元素联系起来。
在对一个内容进行压缩或者其它转换时,利用 HashMap 可以把原始内容和转换后的内容联系起来。例如在一个简化 url 的系统中(Leetcdoe : 535. Encode and Decode TinyURL (Medium)),利用 HashMap 就可以存储精简后的 url 到原始 url 的映射,使得不仅可以显示简化的 url,也可以根据简化的 url 得到原始 url 从而定位到正确的资源。
HashMap 也可以用来对元素进行计数统计,此时键为元素,值为计数。和 HashSet 类似,如果元素有穷并且范围不大,可以用整型数组来进行统计。
数组中的两个数和为给定值
可以先对数组进行排序,然后使用双指针方法或者二分查找方法。这样做的时间复杂度为 O(nlgn),空间复杂度为 O(1)。
用 HashMap 存储数组元素和索引的映射,在访问到 nums[i] 时,判断 HashMap 中是否存在 target - nums[i] ,如果存在说明 target - nums[i] 所在的索引和 i 就是要找的两个数。该方法的时间复杂度为 O(n),空间复杂度为 O(n),使用空间来换取时间。
public int[] twoSum(int[] nums, int target) {
HashMap map = new HashMap<>();
for(int i = 0; i < nums.length; i++){
if(map.containsKey(target - nums[i])) return new int[]{map.get(target - nums[i]), i};
else map.put(nums[i], i);
}
return null;
}
最长和谐序列
Input: [1,3,2,2,5,2,3,7]
Output: 5
Explanation: The longest harmonious subsequence is [3,2,2,2,3].
和谐序列中最大数和最小数只差正好为 1。
public int findLHS(int[] nums) {
Map map = new HashMap<>();
for (long num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
int result = 0;
for (long key : map.keySet()) {
if (map.containsKey(key + 1)) {
result = Math.max(result, map.get(key + 1) + map.get(key));
}
}
return result;
}
字符串
两个字符串包含的字符是否完全相同
s = "anagram", t = "nagaram", return true.
s = "rat", t = "car", return false.
字符串只包含小写字符,总共有 26 个小写字符。可以用 Hash Table 来映射字符与出现次数,因为键值范围很小,因此可以使用长度为 26 的整型数组对字符串出现的字符进行统计,比较两个字符串出现的字符数量是否相同。
public boolean isAnagram(String s, String t) {
int[] cnts = new int[26];
for(int i = 0; i < s.length(); i++) cnts[s.charAt(i) - 'a'] ++;
for(int i = 0; i < t.length(); i++) cnts[t.charAt(i) - 'a'] --;
for(int i = 0; i < 26; i++) if(cnts[i] != 0) return false;
return true;
}
字符串同构
Given "egg", "add", return true.
Given "foo", "bar", return false.
Given "paper", "title", return true.
记录一个字符上次出现的位置,如果两个字符串中某个字符上次出现的位置一样,那么就属于同构。
public boolean isIsomorphic(String s, String t) {
int[] m1 = new int[256];
int[] m2 = new int[256];
for(int i = 0; i < s.length(); i++){
if(m1[s.charAt(i)] != m2[t.charAt(i)]) {
return false;
}
m1[s.charAt(i)] = i + 1;
m2[t.charAt(i)] = i + 1;
}
return true;
}
计算一组字符集合可以组成的回文字符串的最大长度
Input : "abccccdd"
Output : 7
Explanation : One longest palindrome that can be built is "dccaccd", whose length is 7.
使用长度为 128 的整型数组来统计每个字符出现的个数,每个字符有偶数个可以用来构成回文字符串。因为回文字符串最中间的那个字符可以单独出现,所以如果有单独的字符就把它放到最中间。
public int longestPalindrome(String s) {
int[] cnts = new int[128]; // ascii 码总共 128 个
for(char c : s.toCharArray()) cnts[c]++;
int ret = 0;
for(int cnt : cnts) ret += (cnt / 2) * 2;
if(ret < s.length()) ret++; // 这个条件下 s 中一定有单个未使用的字符存在,可以把这个字符放到回文的最中间
return ret;
}
判断一个整数是否是回文数
要求不能使用额外空间,也就不能将整数转换为字符串进行判断。
将整数分成左右两部分,右边那部分需要转置,然后判断这两部分是否相等。
public boolean isPalindrome(int x) {
if(x == 0) return true;
if(x < 0) return false;
if(x % 10 == 0) return false;
int right = 0;
while(x > right){
right = right * 10 + x % 10;
x /= 10;
}
return x == right || x == right / 10;
}
回文子字符串
Input: "aaa"
Output: 6
Explanation: Six palindromic strings: "a", "a", "a", "aa", "aa", "aaa".
解决方案是从字符串的某一位开始,尝试着去扩展子字符串。
private int cnt = 0;
public int countSubstrings(String s) {
for(int i = 0; i < s.length(); i++) {
extendSubstrings(s, i, i); // 奇数长度
extendSubstrings(s, i, i + 1); // 偶数长度
}
return cnt;
}
private void extendSubstrings(String s, int start, int end) {
while(start >= 0 && end < s.length() && s.charAt(start) == s.charAt(end)) {
start--;
end++;
cnt++;
}
}
统计二进制字符串中连续 1 和连续 0 数量相同的子字符串个数
Input: "00110011"
Output: 6
Explanation: There are 6 substrings that have equal number of consecutive 1's and 0's: "0011", "01", "1100", "10", "0011", and "01".
public int countBinarySubstrings(String s) {
int preLen = 0, curLen = 1, ret = 0;
for(int i = 1; i < s.length(); i++){
if(s.charAt(i) == s.charAt(i-1)) curLen++;
else{
preLen = curLen;
curLen = 1;
}
if(preLen >= curLen) ret++;
}
return ret;
}
字符串循环移位包含
s1 = AABCD, s2 = CDAA
Return : true
给定两个字符串 s1 和 s2 ,要求判定 s2 是否能够被 s1 做循环移位得到的字符串包含。
s1 进行循环移位的结果是 s1s1 的子字符串,因此只要判断 s2 是否是 s1s1 的子字符串即可。
字符串循环移位
将字符串向右循环移动 k 位。
例如 abcd123 向右移动 3 位 得到 123abcd
将 abcd123 中的 abcd 和 123 单独逆序,得到 dcba321,然后对整个字符串进行逆序,得到 123abcd。
字符串中单词的翻转
例如将 "I am a student" 翻转成 "student a am I"
将每个单词逆序,然后将整个字符串逆序。
数组与矩阵
把数组中的 0 移到末尾
For example, given nums = [0, 1, 0, 3, 12], after calling your function, nums should be [1, 3, 12, 0, 0].
public void moveZeroes(int[] nums) {
int n = nums.length;
int idx = 0;
for(int i = 0; i < n; i++){
if(nums[i] != 0) nums[idx++] = nums[i];
}
while(idx < n){
nums[idx++] = 0;
}
}
1-n 分布
一个数组元素在 [1, n] 之间,其中一个数被替换为另一个数,找出丢失的数和重复的数
Input: nums = [1,2,2,4]
Output: [2,3]
最直接的方法是先对数组进行排序,这种方法时间复杂度为 O(nlogn).本题可以以 O(n) 的时间复杂度、O(1) 空间复杂度来求解。
主要思想是让通过交换数组元素,使得数组上的元素在正确的位置上
遍历数组,如果第 i 位上的元素不是 i + 1 ,那么就交换第 i 位 和 nums[i] - 1 位上的元素,使得 num[i] - 1 的元素为 nums[i] ,也就是该位的元素是正确的。交换操作需要循环进行,因为一次交换没办法使得第 i 位上的元素是正确的。但是要交换的两个元素可能就是重复元素,那么循环就可能永远进行下去,终止循环的方法是加上 nums[i] != nums[nums[i] - 1 条件。
类似题目:
public int[] findErrorNums(int[] nums) {
for(int i = 0; i < nums.length; i++){
while(nums[i] != i + 1 && nums[i] != nums[nums[i] - 1]) {
swap(nums, i, nums[i] - 1);
}
}
for(int i = 0; i < nums.length; i++){
if(i + 1 != nums[i]) {
return new int[]{nums[i], i + 1};
}
}
return null;
}
private void swap(int[] nums, int i, int j){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
找出数组中重复的数,数组值在 [0, n-1] 之间
二分查找解法:
public int findDuplicate(int[] nums) {
int l = 1, h = nums.length - 1;
while (l <= h) {
int mid = l + (h - l) / 2;
int cnt = 0;
for (int i = 0; i < nums.length; i++) {
if (nums[i] <= mid) cnt++;
}
if (cnt > mid) h = mid - 1;
else l = mid + 1;
}
return l;
}
双指针解法,类似于有环链表中找出环的入口:
public int findDuplicate(int[] nums) {
int slow = nums[0], fast = nums[nums[0]];
while (slow != fast) {
slow = nums[slow];
fast = nums[nums[fast]];
}
fast = 0;
while (slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
有序矩阵
有序矩阵指的是行和列分别有序的矩阵。一般可以利用有序性使用二分查找方法。
[
[ 1, 5, 9],
[10, 11, 13],
[12, 13, 15]
]
有序矩阵查找
public boolean searchMatrix(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) return false;
int m = matrix.length, n = matrix[0].length;
int row = 0, col = n - 1;
while (row < m && col >= 0) {
if (target == matrix[row][col]) return true;
else if (target < matrix[row][col]) col--;
else row++;
}
return false;
}
有序矩阵的 Kth Element
matrix = [
[ 1, 5, 9],
[10, 11, 13],
[12, 13, 15]
],
k = 8,
return 13.
二分查找解法:
public int kthSmallest(int[][] matrix, int k) {
int m = matrix.length, n = matrix[0].length;
int lo = matrix[0][0], hi = matrix[m - 1][n - 1];
while(lo <= hi) {
int mid = lo + (hi - lo) / 2;
int cnt = 0;
for(int i = 0; i < m; i++) {
for(int j = 0; j < n && matrix[i][j] <= mid; j++) {
cnt++;
}
}
if(cnt < k) lo = mid + 1;
else hi = mid - 1;
}
return lo;
}
堆解法:
public int kthSmallest(int[][] matrix, int k) {
int m = matrix.length, n = matrix[0].length;
PriorityQueue pq = new PriorityQueue();
for(int j = 0; j < n; j++) pq.offer(new Tuple(0, j, matrix[0][j]));
for(int i = 0; i < k - 1; i++) { // 小根堆,去掉 k - 1 个堆顶元素,此时堆顶元素就是第 k 的数
Tuple t = pq.poll();
if(t.x == m - 1) continue;
pq.offer(new Tuple(t.x + 1, t.y, matrix[t.x + 1][t.y]));
}
return pq.poll().val;
}
class Tuple implements Comparable {
int x, y, val;
public Tuple(int x, int y, int val) {
this.x = x; this.y = y; this.val = val;
}
@Override
public int compareTo(Tuple that) {
return this.val - that.val;
}
}
链表
判断两个链表的交点
A: a1 → a2
↘
c1 → c2 → c3
↗
B: b1 → b2 → b3
要求:时间复杂度为 O(n) 空间复杂度为 O(1)
设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a。
当访问 A 链表的指针访问到链表尾部时,令它从链表 B 的头部开始访问链表 B;同样地,当访问 B 链表的指针访问到链表尾部时,令它从链表 A 的头部开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA == null || headB == null) return null;
ListNode l1 = headA, l2 = headB;
while(l1 != l2){
l1 = (l1 == null) ? headB : l1.next;
l2 = (l2 == null) ? headA : l2.next;
}
return l1;
}
如果只是判断是否存在交点,那么就是另一个问题,即 编程之美:3.6 的问题。有两种解法:把第一个链表的结尾连接到第二个链表的开头,看第二个链表是否存在环;或者直接比较第一个链表最后一个节点和第二个链表最后一个节点是否相同。
链表反转
头插法能够按逆序构建链表。
public ListNode reverseList(ListNode head) {
ListNode newHead = null; // 设为 null,作为新链表的结尾
while(head != null){
ListNode nextNode = head.next;
head.next = newHead;
newHead = head;
head = nextNode;
}
return newHead;
}
归并两个有序的链表
链表和树一样,可以用递归方式来定义:链表是空节点,或者有一个值和一个指向下一个链表的指针。因此很多链表问题可以用递归来处理。
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1 == null) return l2;
if(l2 == null) return l1;
ListNode newHead = null;
if(l1.val < l2.val){
newHead = l1;
newHead.next = mergeTwoLists(l1.next, l2);
} else{
newHead = l2;
newHead.next = mergeTwoLists(l1, l2.next);
}
return newHead;
}
从有序链表中删除重复节点
public ListNode deleteDuplicates(ListNode head) {
if(head == null || head.next == null) return head;
head.next = deleteDuplicates(head.next);
return head.next != null && head.val == head.next.val ? head.next : head;
}
回文链表
切成两半,把后半段反转,然后比较两半是否相等。
public boolean isPalindrome(ListNode head) {
if(head == null || head.next == null) return true;
ListNode slow = head, fast = head.next;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
if(fast != null){ // 偶数节点,让 slow 指向下一个节点
slow = slow.next;
}
cut(head, slow); // 切成两个链表
ListNode l1 = head, l2 = slow;
l2 = reverse(l2);
return isEqual(l1, l2);
}
private void cut(ListNode head, ListNode cutNode){
while( head.next != cutNode ) head = head.next;
head.next = null;
}
private ListNode reverse(ListNode head){
ListNode newHead = null;
while(head != null){
ListNode nextNode = head.next;
head.next = newHead;
newHead = head;
head = nextNode;
}
return newHead;
}
private boolean isEqual(ListNode l1, ListNode l2){
while(l1 != null && l2 != null){
if(l1.val != l2.val) return false;
l1 = l1.next;
l2 = l2.next;
}
return true;
}
链表元素按奇偶聚集
Example:
Given 1->2->3->4->5->NULL,
return 1->3->5->2->4->NULL.
public ListNode oddEvenList(ListNode head) {
if (head == null) {
return head;
}
ListNode odd = head, even = head.next, evenHead = even;
while (even != null && even.next != null) {
odd.next = odd.next.next;
odd = odd.next;
even.next = even.next.next;
even = even.next;
}
odd.next = evenHead;
return head;
}
树
递归
一棵树要么是空树,要么有两个指针,每个指针指向一棵树。树是一种递归结构,很多树的问题可以使用递归来处理。
树的高度
public int maxDepth(TreeNode root) {
if(root == null) return 0;
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
翻转树
public TreeNode invertTree(TreeNode root) {
if(root == null) return null;
TreeNode left = root.left; // 后面的操作会改变 left 指针,因此先保存下来
root.left = invertTree(root.right);
root.right = invertTree(left);
return root;
}
归并两棵树
Input:
Tree 1 Tree 2
1 2
/ \ / \
3 2 1 3
/ \ \
5 4 7
Output:
Merged tree:
3
/ \
4 5
/ \ \
5 4 7
public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
if(t1 == null && t2 == null) return null;
if(t1 == null) return t2;
if(t2 == null) return t1;
TreeNode root = new TreeNode(t1.val + t2.val);
root.left = mergeTrees(t1.left, t2.left);
root.right = mergeTrees(t1.right, t2.right);
return root;
}
判断路径和是否等于一个数
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.
路径和定义为从 root 到 leaf 的所有节点的和
public boolean hasPathSum(TreeNode root, int sum) {
if(root == null) return false;
if(root.left == null && root.right == null && root.val == sum) return true;
return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val);
}
统计路径和等于一个数的路径数量
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
路径不一定以 root 开头并以 leaf 结尾,但是必须连续
public int pathSum(TreeNode root, int sum) {
if(root == null) return 0;
int ret = pathSumStartWithRoot(root, sum) + pathSum(root.left, sum) + pathSum(root.right, sum);
return ret;
}
private int pathSumStartWithRoot(TreeNode root, int sum){
if(root == null) return 0;
int ret = 0;
if(root.val == sum) ret++;
ret += pathSumStartWithRoot(root.left, sum - root.val) + pathSumStartWithRoot(root.right, sum - root.val);
return ret;
}
树的对称
1
/ \
2 2
/ \ / \
3 4 4 3
public boolean isSymmetric(TreeNode root) {
if(root == null) return true;
return isSymmetric(root.left, root.right);
}
private boolean isSymmetric(TreeNode t1, TreeNode t2){
if(t1 == null && t2 == null) return true;
if(t1 == null || t2 == null) return false;
if(t1.val != t2.val) return false;
return isSymmetric(t1.left, t2.right) && isSymmetric(t1.right, t2.left);
}
平衡树
3
/ \
9 20
/ \
15 7
平衡树左右子树高度差都小于等于 1
private boolean result = true;
public boolean isBalanced(TreeNode root) {
maxDepth(root);
return result;
}
public int maxDepth(TreeNode root) {
if (root == null) return 0;
int l = maxDepth(root.left);
int r = maxDepth(root.right);
if (Math.abs(l - r) > 1) result = false;
return 1 + Math.max(l, r);
}
最小路径
树的根节点到叶子节点的最小路径长度
public int minDepth(TreeNode root) {
if(root == null) return 0;
int left = minDepth(root.left);
int right = minDepth(root.right);
if(left == 0 || right == 0) return left + right + 1;
return Math.min(left, right) + 1;
}
统计左叶子节点的和
3
/ \
9 20
/ \
15 7
There are two left leaves in the binary tree, with values 9 and 15 respectively. Return 24.
public int sumOfLeftLeaves(TreeNode root) {
if(root == null) {
return 0;
}
if(isLeaf(root.left)) {
return root.left.val + sumOfLeftLeaves(root.right);
}
return sumOfLeftLeaves(root.left) + sumOfLeftLeaves(root.right);
}
private boolean isLeaf(TreeNode node){
if(node == null) {
return false;
}
return node.left == null && node.right == null;
}
修剪二叉查找树
Input:
3
/ \
0 4
\
2
/
1
L = 1
R = 3
Output:
3
/
2
/
1
二叉查找树(BST):根节点大于等于左子树所有节点,小于等于右子树所有节点。
只保留值在 L ~ R 之间的节点
public TreeNode trimBST(TreeNode root, int L, int R) {
if(root == null) return null;
if(root.val > R) return trimBST(root.left, L, R);
if(root.val < L) return trimBST(root.right, L, R);
root.left = trimBST(root.left, L, R);
root.right = trimBST(root.right, L, R);
return root;
}
子树
Given tree s:
3
/ \
4 5
/ \
1 2
Given tree t:
4
/ \
1 2
Return true, because t has the same structure and node values with a subtree of s.
public boolean isSubtree(TreeNode s, TreeNode t) {
if(s == null && t == null) return true;
if(s == null || t == null) return false;
if(s.val == t.val && isSame(s, t)) return true;
return isSubtree(s.left, t) || isSubtree(s.right, t);
}
private boolean isSame(TreeNode s, TreeNode t){
if(s == null && t == null) return true;
if(s == null || t == null) return false;
if(s.val != t.val) return false;
return isSame(s.left, t.left) && isSame(s.right, t.right);
}
从有序数组中构造二叉查找树
public TreeNode sortedArrayToBST(int[] nums) {
return toBST(nums, 0, nums.length - 1);
}
private TreeNode toBST(int[] nums, int sIdx, int eIdx){
if(sIdx > eIdx) return null;
int mIdx = (sIdx + eIdx) / 2;
TreeNode root = new TreeNode(nums[mIdx]);
root.left = toBST(nums, sIdx, mIdx - 1);
root.right = toBST(nums, mIdx + 1, eIdx);
return root;
}
两节点的最长路径
Input:
1
/ \
2 3
/ \
4 5
Return 3, which is the length of the path [4,2,1,3] or [5,2,1,3].
private int max = 0;
public int diameterOfBinaryTree(TreeNode root) {
depth(root);
return max;
}
private int depth(TreeNode root) {
if (root == null) return 0;
int leftDepth = depth(root.left);
int rightDepth = depth(root.right);
max = Math.max(max, leftDepth + rightDepth);
return Math.max(leftDepth, rightDepth) + 1;
}
找出二叉树中第二小的节点
Input:
2
/ \
2 5
/ \
5 7
Output: 5
一个节点要么具有 0 个或 2 个子节点,如果有子节点,那么根节点是最小的节点。
public int findSecondMinimumValue(TreeNode root) {
if(root == null) return -1;
if(root.left == null && root.right == null) return -1;
int leftVal = root.left.val;
int rightVal = root.right.val;
if(leftVal == root.val) leftVal = findSecondMinimumValue(root.left);
if(rightVal == root.val) rightVal = findSecondMinimumValue(root.right);
if(leftVal != -1 && rightVal != -1) return Math.min(leftVal, rightVal);
if(leftVal != -1) return leftVal;
return rightVal;
}
二叉查找树的最近公共祖先
_______6______
/ \
___2__ ___8__
/ \ / \
0 _4 7 9
/ \
3 5
For example, the lowest common ancestor (LCA) of nodes 2 and 8 is 6. Another example is LCA of nodes 2 and 4 is 2, since a node can be a descendant of itself according to the LCA definition.
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root.val > p.val && root.val > q.val) return lowestCommonAncestor(root.left, p, q);
if(root.val < p.val && root.val < q.val) return lowestCommonAncestor(root.right, p, q);
return root;
}
二叉树的最近公共祖先
_______3______
/ \
___5__ ___1__
/ \ / \
6 _2 0 8
/ \
7 4
For example, the lowest common ancestor (LCA) of nodes 5 and 1 is 3. Another example is LCA of nodes 5 and 4 is 5, since a node can be a descendant of itself according to the LCA definition.
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
return left == null ? right : right == null ? left : root;
}
相同节点值的最大路径长度
1
/ \
4 5
/ \ \
4 4 5
Output : 2
private int path = 0;
public int longestUnivaluePath(TreeNode root) {
dfs(root);
return path;
}
private int dfs(TreeNode root){
if(root == null) return 0;
int left = dfs(root.left);
int right = dfs(root.right);
int leftPath = root.left != null && root.left.val == root.val ? left + 1 : 0;
int rightPath = root.right != null && root.right.val == root.val ? right + 1 : 0;
path = Math.max(path, leftPath + rightPath);
return Math.max(leftPath, rightPath);
}
间隔遍历
3
/ \
2 3
\ \
3 1
Maximum amount of money the thief can rob = 3 + 3 + 1 = 7.
public int rob(TreeNode root) {
if (root == null) return 0;
int val1 = root.val;
if (root.left != null) {
val1 += rob(root.left.left) + rob(root.left.right);
}
if (root.right != null) {
val1 += rob(root.right.left) + rob(root.right.right);
}
int val2 = rob(root.left) + rob(root.right);
return Math.max(val1, val2);
}
层次遍历
使用 BFS 进行层次遍历。不需要使用两个队列来分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。
一棵树每层节点的平均数
public List averageOfLevels(TreeNode root) {
List ret = new ArrayList<>();
if(root == null) return ret;
Queue queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
int cnt = queue.size();
double sum = 0;
for(int i = 0; i < cnt; i++){
TreeNode node = queue.poll();
sum += node.val;
if(node.left != null) queue.add(node.left);
if(node.right != null) queue.add(node.right);
}
ret.add(sum / cnt);
}
return ret;
}
得到左下角的节点
Input:
1
/ \
2 3
/ / \
4 5 6
/
7
Output:
7
public int findBottomLeftValue(TreeNode root) {
Queue queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
root = queue.poll();
if(root.right != null) queue.add(root.right);
if(root.left != null) queue.add(root.left);
}
return root.val;
}
前中后序遍历
1
/ \
2 3
/ \ \
4 5 6
层次遍历顺序:[1 2 3 4 5 6]
前序遍历顺序:[1 2 4 5 3 6]
中序遍历顺序:[4 2 5 1 3 6]
后序遍历顺序:[4 5 2 6 3 1]
层次遍历使用 BFS 实现,利用的就是 BFS 一层一层遍历的特性;而前序、中序、后序遍历利用了 DFS 实现。
前序、中序、后序遍只是在对节点访问的顺序有一点不同,其它都相同。
① 前序
void dfs(TreeNode root){
visit(root);
dfs(root.left);
dfs(root.right);
}
② 中序
void dfs(TreeNode root){
dfs(root.left);
visit(root);
dfs(root.right);
}
③ 后序
void dfs(TreeNode root){
dfs(root.left);
dfs(root.right);
visit(root);
}
非递归实现二叉树的前序遍历
public List preorderTraversal(TreeNode root) {
List ret = new ArrayList<>();
if (root == null) return ret;
Stack stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
ret.add(node.val);
if (node.right != null) stack.push(node.right);
if (node.left != null) stack.push(node.left); // 先添加右子树再添加左子树,这样是为了让左子树在栈顶
}
return ret;
}
非递归实现二叉树的后序遍历
前序遍历为 root -> left -> right,后序遍历为 left -> right -> root,可以修改前序遍历成为 root -> right -> left,那么这个顺序就和后序遍历正好相反。
public List postorderTraversal(TreeNode root) {
List ret = new ArrayList<>();
if (root == null) return ret;
Stack stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
ret.add(node.val);
if (node.left != null) stack.push(node.left);
if (node.right != null) stack.push(node.right);
}
Collections.reverse(ret);
return ret;
}
非递归实现二叉树的中序遍历
public List inorderTraversal(TreeNode root) {
List ret = new ArrayList<>();
Stack stack = new Stack<>();
TreeNode cur = root;
while(cur != null || !stack.isEmpty()) {
while(cur != null) { // 模拟递归栈的不断深入
stack.add(cur);
cur = cur.left;
}
TreeNode node = stack.pop();
ret.add(node.val);
cur = node.right;
}
return ret;
}
BST
主要利用 BST 中序遍历有序的特点。
在 BST 中寻找两个节点,使它们的和为一个给定值
Input:
5
/ \
3 6
/ \ \
2 4 7
Target = 9
Output: True
使用中序遍历得到有序数组之后,再利用双指针对数组进行查找。
应该注意到,这一题不能用分别在左右子树两部分来处理这种思想,因为两个待求的节点可能分别在左右子树中。
public boolean findTarget(TreeNode root, int k) {
List nums = new ArrayList<>();
inOrder(root, nums);
int i = 0, j = nums.size() - 1;
while(i < j){
int sum = nums.get(i) + nums.get(j);
if(sum == k) return true;
if(sum < k) i++;
else j--;
}
return false;
}
private void inOrder(TreeNode root, List nums){
if(root == null) return;
inOrder(root.left, nums);
nums.add(root.val);
inOrder(root.right, nums);
}
在 BST 中查找两个节点之差的最小绝对值
Input:
1
\
3
/
2
Output:
1
利用 BST 的中序遍历为有序的性质,计算中序遍历中临近的两个节点之差的绝对值,取最小值。
private int minDiff = Integer.MAX_VALUE;
private int preVal = -1;
public int getMinimumDifference(TreeNode root) {
inorder(root);
return minDiff;
}
private void inorder(TreeNode node){
if(node == null) return;
inorder(node.left);
if(preVal != -1) minDiff = Math.min(minDiff, Math.abs(node.val - preVal));
preVal = node.val;
inorder(node.right);
}
把 BST 每个节点的值都加上比它大的节点的值
Input: The root of a Binary Search Tree like this:
5
/ \
2 13
Output: The root of a Greater Tree like this:
18
/ \
20 13
先遍历右子树。
private int sum = 0;
public TreeNode convertBST(TreeNode root) {
traver(root);
return root;
}
private void traver(TreeNode root) {
if (root == null) {
return;
}
if (root.right != null) {
traver(root.right);
}
sum += root.val;
root.val = sum;
if (root.left != null) {
traver(root.left);
}
}
寻找 BST 中出现次数最多的节点
1
\
2
/
2
return [2].
private int cnt = 1;
private int maxCnt = 1;
private TreeNode preNode = null;
private List list;
public int[] findMode(TreeNode root) {
list = new ArrayList<>();
inorder(root);
int[] ret = new int[list.size()];
int idx = 0;
for(int num : list){
ret[idx++] = num;
}
return ret;
}
private void inorder(TreeNode node){
if(node == null) return;
inorder(node.left);
if(preNode != null){
if(preNode.val == node.val) cnt++;
else cnt = 1;
}
if(cnt > maxCnt){
maxCnt = cnt;
list.clear();
list.add(node.val);
} else if(cnt == maxCnt){
list.add(node.val);
}
preNode = node;
inorder(node.right);
}
寻找 BST 的第 k 个元素
递归解法:
public int kthSmallest(TreeNode root, int k) {
int leftCnt = count(root.left);
if(leftCnt == k - 1) return root.val;
if(leftCnt > k - 1) return kthSmallest(root.left, k);
return kthSmallest(root.right, k - leftCnt - 1);
}
private int count(TreeNode node) {
if(node == null) return 0;
return 1 + count(node.left) + count(node.right);
}
中序遍历解法:
private int cnt = 0;
private int val;
public int kthSmallest(TreeNode root, int k) {
inorder(root, k);
return val;
}
private void inorder(TreeNode node, int k) {
if(node == null) return;
inorder(node.left, k);
cnt++;
if(cnt == k) {
val = node.val;
return;
}
inorder(node.right, k);
}
Trie
Trie,又称前缀树或字典树,用于判断字符串是否存在或者是否具有某种字符串前缀。
实现一个 Trie
class Trie {
private class Node{
Node[] childs = new Node[26];
boolean isLeaf;
}
private Node root = new Node();
/** Initialize your data structure here. */
public Trie() {
}
/** Inserts a word into the trie. */
public void insert(String word) {
int idx = word.charAt(0) - 'a';
insert(word, root);
}
private void insert(String word, Node node){
int idx = word.charAt(0) - 'a';
if(node.childs[idx] == null){
node.childs[idx] = new Node();
}
if(word.length() == 1) node.childs[idx].isLeaf = true;
else insert(word.substring(1), node.childs[idx]);
}
/** Returns if the word is in the trie. */
public boolean search(String word) {
return search(word, root);
}
private boolean search(String word, Node node){
if(node == null) return false;
int idx = word.charAt(0) - 'a';
if(node.childs[idx] == null) return false;
if(word.length() == 1) return node.childs[idx].isLeaf;
return search(word.substring(1), node.childs[idx]);
}
/** Returns if there is any word in the trie that starts with the given prefix. */
public boolean startsWith(String prefix) {
return startWith(prefix, root);
}
private boolean startWith(String prefix, Node node){
if(node == null) return false;
if(prefix.length() == 0) return true;
int idx = prefix.charAt(0) - 'a';
return startWith(prefix.substring(1), node.childs[idx]);
}
}
实现一个 Trie,用来求前缀和
Input: insert("apple", 3), Output: Null
Input: sum("ap"), Output: 3
Input: insert("app", 2), Output: Null
Input: sum("ap"), Output: 5
class MapSum {
private class Trie {
int val;
Map childs;
boolean isWord;
Trie() {
childs = new HashMap<>();
}
}
private Trie root;
public MapSum() {
root = new Trie();
}
public void insert(String key, int val) {
Trie cur = root;
for(char c : key.toCharArray()) {
if(!cur.childs.containsKey(c)) {
Trie next = new Trie();
cur.childs.put(c, next);
}
cur = cur.childs.get(c);
}
cur.val = val;
cur.isWord = true;
}
public int sum(String prefix) {
Trie cur = root;
for(char c : prefix.toCharArray()) {
if(!cur.childs.containsKey(c)) return 0;
cur = cur.childs.get(c);
}
return dfs(cur);
}
private int dfs(Trie cur) {
int sum = 0;
if(cur.isWord) {
sum += cur.val;
}
for(Trie next : cur.childs.values()) {
sum += dfs(next);
}
return sum;
}
}
图
位运算
1. 基本原理
0s 表示 一串 0 ,1s 表示一串 1。
x ^ 0s = x x & 0s = 0 x | 0s = x
x ^ 1s = ~x x & 1s = x x | 1s = 1s
x ^ x = 0 x & x = x x | x = x
① 利用 x ^ 1s = ~x 的特点,可以将位级表示翻转;利用 x ^ x = 0 的特点,可以将三个数中重复的两个数去除,只留下另一个数;
② 利用 x & 0s = 0 和 x & 1s = x 的特点,可以实现掩码操作。一个数 num 与 mask :00111100 进行位与操作,只保留 num 中与 mask 的 1 部分相对应的位;
③ 利用 x | 0s = x 和 x | 1s = 1s 的特点,可以实现设置操作。一个数 num 与 mask:00111100 进行位或操作,将 num 中与 mask 的 1 部分相对应的位都设置为 1 。
>> n 为算术右移,相当于除以 2n;
>>> n 为无符号右移,左边会补上 0。
<< n 为算术左移,相当于乘以 2n。
n&(n-1) 该位运算是去除 n 的位级表示中最低的那一位。例如对于二进制表示 10110 100 ,减去 1 得到 10110011,这两个数相与得到 10110000。
n-n&(~n+1) 概运算是去除 n 的位级表示中最高的那一位。
n&(-n) 该运算得到 n 的位级表示中最低的那一位。-n 得到 n 的反码加 1,对于二进制表示 10110 100 ,-n 得到 01001100,相与得到 00000100
2. mask 计算
要获取 111111111,将 0 取反即可,~0。
要得到只有第 i 位为 1 的 mask,将 1 向左移动 i 位即可,1<
要得到 1 到 i 位为 1 的 mask,1<
要得到 1 到 i 位为 0 的 mask,只需将 1 到 i 位为 1 的 mask 取反,即 ~(1<
3. 位操作举例
① 获取第 i 位
num & 00010000 != 0
(num & (1 << i)) != 0;
② 将第 i 位设置为 1
num | 00010000
num | (1 << i);
③ 将第 i 位清除为 0
num & 11101111
num & (~(1 << i))
④ 将最高位到第 i 位清除为 0
num & 00001111
num & ((1 << i) - 1);
⑤ 将第 0 位到第 i 位清除为 0
num & 11110000
num & (~((1 << (i+1)) - 1));
⑥ 将第 i 位设置为 0 或者 1
先将第 i 位清零,然后将 v 左移 i 位,执行“位或”运算。
(num & (1 << i)) | (v << i);
4. Java 中的位操作
static int Integer.bitCount() // 统计 1 的数量
static int Integer.highestOneBit() // 获得最高位
static String toBinaryString(int i) // 转换位二进制表示的字符串
统计两个数的二进制表示有多少位不同
对两个数进行异或操作,不同的那一位结果为 1 ,统计有多少个 1 即可。
public int hammingDistance(int x, int y) {
int z = x ^ y;
int cnt = 0;
while(z != 0){
if((z & 1) == 1) cnt++;
z = z >> 1;
}
return cnt;
}
可以使用 Integer.bitcount() 来统计 1 个的个数。
public int hammingDistance(int x, int y) {
return Integer.bitCount(x ^ y);
}
翻转一个数的比特位
public int reverseBits(int n) {
int ret = 0;
for(int i = 0; i < 32; i++){
ret <<= 1;
ret |= (n & 1);
n >>>= 1;
}
return ret;
}
不用额外变量交换两个整数
a = a ^ b;
b = a ^ b;
a = a ^ b;
将 c = a ^ b,那么 b ^ c = b ^ b ^ a = a,a ^ c = a ^ a ^ b = b。
判断一个数是不是 4 的 n 次方
该数二进制表示有且只有一个奇数位为 1 ,其余的都为 0 ,例如 16 : 10000。可以每次把 1 向左移动 2 位,就能构造出这种数字,然后比较构造出来的数与要判断的数是否相同。
public boolean isPowerOfFour(int num) {
int i = 1;
while(i > 0){
if(i == num) return true;
i = i << 2;
}
return false;
}
也可以用 Java 的 Integer.toString() 方法将该数转换为 4 进制形式的字符串,然后判断字符串是否以 1 开头。
public boolean isPowerOfFour(int num) {
return Integer.toString(num, 4).matches("10*");
}
判断一个数是不是 2 的 n 次方
同样可以用 Power of Four 的方法,但是 2 的 n 次方更特殊,它的二进制表示只有一个 1 存在。
public boolean isPowerOfTwo(int n) {
return n > 0 && Integer.bitCount(n) == 1;
}
利用 1000 & 0111 == 0 这种性质,得到以下解法:
public boolean isPowerOfTwo(int n) {
return n > 0 && (n & (n - 1)) == 0;
}
数组中唯一一个不重复的元素
两个相同的数异或的结果为 0,对所有数进行异或操作,最后的结果就是单独出现的那个数。
类似的有:Leetcode : 389. Find the Difference (Easy),两个字符串仅有一个字符不相同,使用异或操作可以以 O(1) 的空间复杂度来求解,而不需要使用 HashSet。
public int singleNumber(int[] nums) {
int ret = 0;
for(int n : nums) ret = ret ^ n;
return ret;
}
数组中不重复的两个元素
两个不相等的元素在位级表示上必定会有一位存在不同。
将数组的所有元素异或得到的结果为不存在重复的两个元素异或的结果。
diff &= -diff 得到出 diff 最右侧不为 0 的位,也就是不存在重复的两个元素在位级表示上最右侧不同的那一位,利用这一位就可以将两个元素区分开来。
public int[] singleNumber(int[] nums) {
int diff = 0;
for(int num : nums) diff ^= num;
// 得到最右一位
diff &= -diff;
int[] ret = new int[2];
for(int num : nums) {
if((num & diff) == 0) ret[0] ^= num;
else ret[1] ^= num;
}
return ret;
}
判断一个数的位级表示是否不会出现连续的 0 和 1
对于 10101 这种位级表示的数,把它向右移动 1 位得到 1010 ,这两个数每个位都不同,因此异或得到的结果为 11111。
public boolean hasAlternatingBits(int n) {
int a = (n ^ (n >> 1));
return (a & (a + 1)) == 0;
}
求一个数的补码
不考虑二进制表示中的首 0 部分
对于 00000101,要求补码可以将它与 00000111 进行异或操作。那么问题就转换为求掩码 00000111。
public int findComplement(int num) {
if(num == 0) return 1;
int mask = 1 << 30;
while((num & mask) == 0) mask >>= 1;
mask = (mask << 1) - 1;
return num ^ mask;
}
可以利用 Java 的 Integer.highestOneBit() 方法来获得含有首 1 的数。
public int findComplement(int num) {
if(num == 0) return 1;
int mask = Integer.highestOneBit(num);
mask = (mask << 1) - 1;
return num ^ mask;
}
对于 10000000 这样的数要扩展成 11111111,可以利用以下方法:
mask |= mask >> 1 11000000
mask |= mask >> 2 11110000
mask |= mask >> 4 11111111
public int findComplement(int num) {
int mask = num;
mask |= mask >> 1;
mask |= mask >> 2;
mask |= mask >> 4;
mask |= mask >> 8;
mask |= mask >> 16;
return (mask ^ num);
}
实现整数的加法
a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进位。递归会终止的原因是 (a & b) << 1 最右边会多一个 0,那么继续递归,进位最右边的 0 会慢慢增多,最后进位会变为 0,递归终止。
public int getSum(int a, int b) {
return b == 0 ? a : getSum((a ^ b), (a & b) << 1);
}
字符串数组最大乘积
题目描述:字符串数组的字符串只含有小写字符。求解字符串数组中两个字符串长度的最大乘积,要求这两个字符串不能含有相同字符。
解题思路:本题主要问题是判断两个字符串是否含相同字符,由于字符串只含有小写字符,总共 26 位,因此可以用一个 32 位的整数来存储每个字符是否出现过。
public int maxProduct(String[] words) {
int n = words.length;
if (n == 0) return 0;
int[] val = new int[n];
for (int i = 0; i < n; i++) {
for (char c : words[i].toCharArray()) {
val[i] |= 1 << (c - 'a');
}
}
int ret = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if ((val[i] & val[j]) == 0) {
ret = Math.max(ret, words[i].length() * words[j].length());
}
}
}
return ret;
}
参考资料
Weiss M A, 冯舜玺. 数据结构与算法分析——C 语言描述[J]. 2004.
Sedgewick R. Algorithms[M]. Pearson Education India, 1988.
何海涛, 软件工程师. 剑指 Offer: 名企面试官精讲典型编程题[M]. 电子工业出版社, 2014.
《编程之美》小组. 编程之美[M]. 电子工业出版社, 2008.
左程云. 程序员代码面试指南[M]. 电子工业出版社, 2015.