最近将剑指 Offer(专项突击版)刷了一遍,总结一下,最好结合图书来看,书里有很多总结,书中解法很经典,leetcode地址:
常见的算法方法
二分查找 |
双指针 滑动窗口 |
堆 优先级队列 |
单调栈 |
前缀树 字典树 |
前缀和+哈希 |
排序 快速排序 归并排序 |
并查集 |
拓扑排序 |
回溯法 |
动态规划 |
二分查找
根据题意 分析出 目标值target 以及 左边界left 和 右边界 right,计算mid=(left+right)/2; 计算当mid时 取值函数f(mid) ,比较f(mid)和target大小关系 来决定左右边界的移动。
函数返回时 一般有 f(mid) >target && f(mid-1) < target 此时mid是大于target的最小值;
一般左边界移动 left = mid+1 右边界 right = mid – 1;
关键在于 left right 以及f(mid)计算 以及函数返回时的逻辑处理
经典题目:
class Solution { public int minEatingSpeed(int[] piles, int h) { int left = 1; int right = Arrays.stream(piles).max().getAsInt(); while (left <= right) { int mid = (left + right) >> 1; if (getTime(piles, mid) <= h) { if (mid == 1 || getTime(piles, mid-1) > h) { return mid; } right = mid - 1; } else { left = mid + 1; } } return -1; } private int getTime(int[] piles, int speed) { int result = 0; for (int pile: piles) { result += (pile - 1) / speed + 1; } return result; } } |
双指针 滑动窗
第一种 前后双指针
左指针left从起始位置出发,右指针right从结束位置出发,根据条件来移动左右指针,计算函数f(left,right),终止条件一般是left<=right。一般用来解决排序数组子数组求和,回文串判断以及极值判断
经典题目
public int maxArea(int[] h) { int left = 0; int right = h.length - 1; int maxArea = 0; // 利用双指针 初始指向索引最小值和最大值 // 矩形的面积是由双指针距离差*双指针对应的数组元素最小值决定的 // 矩形面积可能增大的情况是 指针指向元素较小值发生变化 // 尽管指针位置发生变化会使right-left减小 但是min(h[left], h[right])的增大可能会使总体面积而增大 while (left <= right) { maxArea = Math.max(maxArea, (right - left) * Math.min(h[left], h[right])); if (h[left] < h[right]) { left++; } else { right--; } } return maxArea; } |
第二种 滑动窗口
采用左右双指针 左右指针从其实位置出发,右指针不断向右 此时有区间[left,right] 定义区间函数 f(left, right) 如果此时f(left, right)已经符合题意,则已经找到可行解,此时尝试左指针右移,即此时区间缩小 计算此时f(left, right)是否符合题意,如果符合 则记录一个可行解,如果不满足,继续右指针右移,增大窗口,探索更多可行解。
class Solution { public int lengthOfLongestSubstring(String s) { int max = 0; Map<Character, Integer> map = new HashMap<>(); int left = 0; int right = 0; for ( ; right < s.length(); right++) { map.put(s.charAt(right), map.getOrDefault(s.charAt(right), 0) + 1); while (!allLessThanOne(map) && left <= right) { map.put(s.charAt(left), map.getOrDefault(s.charAt(left), 0) - 1); left++; } max = Math.max(max, right - left + 1); } return max; } private boolean allLessThanOne(Map<Character, Integer> map) { return map.values().stream().filter(num -> num > 1).count() == 0; } } |
https://leetcode-cn.com/problems/wtcaE1/
class Solution { public int minSubArrayLen(int target, int[] nums) { int left = 0; int right = 0; int sum = 0; int min = Integer.MAX_VALUE; int total = Arrays.stream(nums).sum(); if (total < target) return 0; for (right=0; right<nums.length; right++) { sum = sum + nums[right]; while (sum >= target) { min = Math.min(min, right-left+1); sum = sum - nums[left]; left++; } } return min; } } |
第三种 链表前后双指针 链表快慢双指针
https://leetcode-cn.com/problems/c32eOV/
堆 优先级队列
class Solution { public int[] topKFrequent(int[] nums, int k) { Map<Integer, Integer> map = new HashMap<>(); for (int num: nums) { map.put(num, map.getOrDefault(num, 0) + 1); } PriorityQueue<Map.Entry<Integer, Integer>> queue = new PriorityQueue<>(Comparator.comparingInt(Map.Entry::getValue)); for (Map.Entry<Integer, Integer> entry: map.entrySet()) { if (queue.size() < k) { queue.add(entry); } else { if (queue.peek().getValue() < entry.getValue()) { queue.poll(); queue.add(entry); } } } int[] result = new int[queue.size()]; int i = 0; while (!queue.isEmpty()) { result[i++] = queue.poll().getKey(); } return result; } } |
堆 一般用来求解 K值问题,最小堆以及最大堆
单调栈
根据题意 需要保持栈中数据是排序的,给定序列,当某一个元素要入栈时,需要比较当前元素与栈顶元素的大小关系,一般需要将栈顶元素出栈之后,才能将当前元素继续加入栈中。
class Solution { public int[] dailyTemperatures(int[] temperatures) { int[] result = new int[temperatures.length]; Stack<Integer> stack = new Stack<>(); for (int i=0; i<temperatures.length; i++) { while (!stack.isEmpty() && temperatures[stack.peek()] < temperatures[i]) { int index = stack.pop(); result[index] = i - index; } stack.push(i); } while (!stack.isEmpty()) { result[stack.pop()] = 0; } return result; } } |
前缀树 字典树
前缀树的构建以及前缀树的搜索,一般用于字符串的前缀搜索
public class Trie { static class TreeNode { TreeNode[] children = new TreeNode[26]; boolean isWord; } TreeNode root; /** * Initialize your data structure here. */ public Trie() { root = new TreeNode(); } /** * Inserts a word into the trie. */ public void insert(String word) { TreeNode node = root; for (char ch : word.toCharArray()) { if (node.children[ch - 'a'] == null) { node.children[ch - 'a'] = new TreeNode(); } node = node.children[ch - 'a']; } node.isWord = true; } /** * Returns if the word is in the trie. */ public boolean search(String word) { TreeNode node = root; for (char ch : word.toCharArray()) { if (node.children[ch - 'a'] == null) { return false; } node = node.children[ch - 'a']; } return node.isWord; } /** * Returns if there is any word in the trie that starts with the given prefix. */ public boolean startsWith(String prefix) { TreeNode node = root; for (char ch : prefix.toCharArray()) { if (node.children[ch - 'a'] == null) { return false; } node = node.children[ch - 'a']; } return node != null; } } |
前缀树的根节点一般不存储字符串,字符串存储在叶子节点中。
前缀和+哈希
如果序列全是正数,可以使用双指针来求解子数组和问题,如果数组不确定正负,那么求解子数组和问题可以使用前缀和和哈希。
给定序列 {x0,x1,x2,x3,x4…xn-1}
依次求解前缀和数组 Si= x0+x1+..+xs
则子数组[i,j]范围内的和可以表示为 Sj-Si-1
class Solution { public int subarraySum(int[] nums, int k) { Map<Integer, Integer> sumCnt = new HashMap<>(); int sum = 0; int result = 0; sumCnt.put(0, 1); for (int i=0; i<nums.length; i++) { sum += nums[i]; if (sumCnt.containsKey(sum - k)) { result += sumCnt.get(sum - k); } sumCnt.put(sum, sumCnt.getOrDefault(sum, 0) + 1); } return result; } } |
排序 计数排序 快速排序 归并排序
计数排序一般用于解决待排序的数据范围是有限的
快速排序
归并排序可以用于链表操作
class Solution { public int[][] merge(int[][] intervals) { Arrays.sort(intervals, Comparator.comparingInt(o -> o[0])); List<int[]> list = new ArrayList<>(); int i = 0; while (i<intervals.length) { int j = i+1; int[] temp = new int[]{intervals[i][0], intervals[i][1]}; while (j<intervals.length && intervals[j][0]<=temp[1]) { temp[1] = Math.max(temp[1], intervals[j][1]); j++; } list.add(temp); i = j; } int[][] result = new int[list.size()][2]; for (int k=0; k<list.size(); k++) { result[k] = list.get(k); } return result; } } |
并查集
class Solution { public int findCircleNum(int[][] isConnected) { int n = isConnected.length; int[] num = new int[n]; for (int i=0; i<n; i++) { num[i] = i; } for (int i=0; i<isConnected.length; i++) { for (int j=0; j<isConnected[0].length; j++) { // 初始时数量为n 如果i,j属于同一个连通分量 则链接 数量减一 if (isConnected[i][j] == 1 && union(num, i, j)) { n--; } } } return n; } private boolean union(int[] num, int i, int j) { int parentI = findParent(num, i); int parentJ = findParent(num, j); if (parentI == parentJ) { return false; } num[parentI] = parentJ; return true; } private int findParent(int[] num, int x) { while (num[x] != x) { x = num[x]; } return x; } } |
拓扑排序
class Solution { public int[] findOrder(int numCourses, int[][] prerequisites) { List<List<Integer>> graph = new ArrayList<>(); for (int i = 0; i < numCourses; i++) { graph.add(new ArrayList<>()); } int[] indegree = new int[numCourses]; for (int[] arr : prerequisites) { // arr[1]指向arr[0] arr[0]入度加一 arr[1]邻接链表增加关系 indegree[arr[0]]++; List<Integer> edge = graph.get(arr[1]); edge.add(arr[0]); } Stack<Integer> stack = new Stack<>(); for (int i=0; i<numCourses; i++) { if (indegree[i] == 0) { stack.add(i); } } List<Integer> result = new ArrayList<>(); while (!stack.isEmpty()) { int num = stack.pop(); result.add(num); List<Integer> edge = graph.get(num); for (int e : edge) { indegree[e]--; if (indegree[e] == 0) { stack.add(e); } } } if (result.size() != numCourses) { return new int[] {}; } return result.stream().mapToInt(Integer::valueOf).toArray(); } } |
回溯法
做一件事情有很多步骤,每一个步骤得有很多选择,做出某一选择后进行系列逻辑判断 看是否符合目标值 如果不符合 则回退该选择 进行下一个选择尝试,如果符合,则将结果加入到结果集中,如果需要进行下一个选择,则可以继续下一个选择尝试。
回溯法本质是一种DFS搜索算法,整个问题解决空间是一棵树,从根节点初始状态,分析根节点下一个节点选择,对于某一个节点,分析当前路径是否满足解,如果满足则加入到结果集中,如果不满足,并且已经到达搜索停止条件,则直接返回,如果不满足解,但是可以通过下一个节点来进行更多的探索,则递归进行探索。
class Solution { public List<List<Integer>> combinationSum(int[] candidates, int target) { List<List<Integer>> result = new ArrayList<>(); Arrays.sort(candidates); dfs(result, new ArrayList<>(), 0, target, 0, candidates); return result; } private void dfs(List<List<Integer>> result, List<Integer> path, int sum, int target, int index, int[] candidates) { // 如果和等于目标值 说明当前path中元素符合条件 加入结果集 if (sum == target) { result.add(new ArrayList<>(path)); return; } // 如果当前遍历索引大于等于数组长度 则说明后续已没有继续可行解 返回即可。 if (index >= candidates.length) { return; } // 如果当前和大于target 则无需继续搜索,因为数组元素为正整数,继续累加sum会一直增大 if (sum > target) { return; } // index数字不加入到集合中 dfs(result, path, sum, target, index + 1, candidates); // index数字加入到集合中 path.add(candidates[index]); sum = sum + candidates[index]; dfs(result, path, sum, target, index, candidates); path.remove(path.size() - 1); sum = sum - candidates[index]; } } |
class Solution { public List<List<Integer>> allPathsSourceTarget(int[][] graph) { List<List<Integer>> result = new ArrayList<>(); List<Integer> path = new ArrayList<>(); path.add(0); dfs(result, path, graph, 0); return result; } private void dfs(List<List<Integer>> result, List<Integer> path, int[][] graph, int index) { if (index == graph.length - 1) { result.add(new ArrayList<>(path)); return; } int[] arr = graph[index]; for (int num: arr) { path.add(num); dfs(result, path, graph, num); path.remove(path.size() - 1); } } } |
动态规划
一般用来求解极值问题,一个规模N的问题,可以由一个规模更小的问题(<N)来解决,一般使用动态规划来求解,关键在于递推关系式以及初始条件。
单序列,一般是 f(n) f(n-1) f(n-2)的关系
双序列,一般是f(n) f(n-1) g(n) g(n-1)的关系
二维动态规划 f(i,j) 与f(i-1,j) f(I,j-1) f(i-1, j-1)的关系