算法思想
贪心思想
贪心思想保证每次操作都是局部最优的,并且最后得到的结果是全局最优的。
分配饼干
1 | Input: [1,2], [1,2,3] |
题目描述:每个孩子都有一个满足度,每个饼干都有一个大小,只有饼干的大小大于等于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。
因为最小的孩子最容易得到满足,因此先满足最小孩子。给一个孩子的饼干应当尽量小又能满足该孩子,这样大饼干就能拿来给满足度比较大的孩子。因此贪心策略
证明:假设在某次选择中,贪心策略选择给当前满足度最小的孩子分配第 m 个饼干,第 m 个饼干为可以满足该孩子的最小饼干。假设存在一种最优策略,给该孩子分配第 n 个饼干,并且 m < n。我们可以发现,经过这一轮分配,贪心策略分配后剩下的饼干一定有一个比最优策略来得大。因此在后续的分配中,贪心策略一定能满足更多的孩子。也就是说不存在比贪心策略更优的策略,即贪心策略就是最优策略。
1 | public int findContentChildren(int[] g, int[] s) { |
不重叠的区间个数
435. Non-overlapping Intervals (Medium)
1 | Input: [ [1,2], [1,2], [1,2] ] |
1 | Input: [ [1,2], [2,3] ] |
题目描述:计算让一组区间不重叠所需要移除的区间个数。
计算最多能组成的不重叠区间个数,然后用区间总个数减去不重叠区间的个数。
在每次选择中,区间的结尾最为重要,选择的区间结尾越小,留给后面的区间的空间越大,那么后面能够选择的区间个数也就越大。
按区间的结尾进行排序,每次选择结尾最小,并且和前一个区间不重叠的区间。
1 | public int eraseOverlapIntervals(Interval[] intervals) { |
使用 lambda 表示式创建 Comparator 会导致算法运行时间过长,如果注重运行时间,可以修改为普通创建 Comparator 语句:
1 | Arrays.sort(intervals, new Comparator<Interval>() { |
投飞镖刺破气球
452. Minimum Number of Arrows to Burst Balloons (Medium)
1 | Input: |
题目描述:气球在一个水平数轴上摆放,可以重叠,飞镖垂直投向坐标轴,使得路径上的气球都会刺破。求解最小的投飞镖次数使所有气球都被刺破。
也是计算不重叠的区间个数,不过和 Non-overlapping Intervals 的区别在于,[1, 2] 和 [2, 3] 在本题中算是重叠区间。
1 | public int findMinArrowShots(int[][] points) { |
根据身高和序号重组队列
406. Queue Reconstruction by Height(Medium)
1 | Input: |
题目描述:一个学生用两个分量 (h, k) 描述,h 表示身高,k 表示排在前面的有 k 个学生的身高比他高或者和他一样高。
为了在每次插入操作时不影响后续的操作,身高较高的学生应该先做插入操作,否则身高较小的学生原先正确插入第 k 个位置可能会变成第 k+1 个位置。
身高降序、k 值升序,然后按排好序的顺序插入队列的第 k 个位置中。
1 | public int[][] reconstructQueue(int[][] people) { |
分隔字符串使同种字符出现在一起
763. Partition Labels (Medium)
1 | Input: S = "ababcbacadefegdehijhklij" |
1 | public List<Integer> partitionLabels(String S) { |
种植花朵
1 | Input: flowerbed = [1,0,0,0,1], n = 1 |
题目描述:花朵之间至少需要一个单位的间隔,求解是否能种下 n 朵花。
1 | public boolean canPlaceFlowers(int[] flowerbed, int n) { |
判断是否为子序列
1 | s = "abc", t = "ahbgdc" |
1 | public boolean isSubsequence(String s, String t) { |
修改一个数成为非递减数组
665. Non-decreasing Array (Easy)
1 | Input: [4,2,3] |
题目描述:判断一个数组能不能只修改一个数就成为非递减数组。
在出现 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]。
1 | public boolean checkPossibility(int[] nums) { |
股票的最大收益
122. Best Time to Buy and Sell Stock II (Easy)
题目描述:一次股票交易包含买入和卖出,多个交易之间不能交叉进行。
对于 [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] 添加到收益中,从而在局部最优的情况下也保证全局最优。
1 | public int maxProfit(int[] prices) { |
双指针
双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。
有序数组的 Two Sum
Leetcode :167. Two Sum II - Input array is sorted (Easy)
1 | Input: numbers={2, 7, 11, 15}, target=9 |
题目描述:在有序数组中找出两个数,使它们的和为 target。
使用双指针,一个指针指向值较小的元素,一个指针指向值较大的元素。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。
如果两个指针指向元素的和 sum == target,那么得到要求的结果;如果 sum > target,移动较大的元素,使 sum 变小一些;如果 sum < target,移动较小的元素,使 sum 变大一些。
1 | public int[] twoSum(int[] numbers, int target) { |
两数平方和
633. Sum of Square Numbers (Easy)
1 | Input: 5 |
题目描述:判断一个数是否为两个数的平方和,例如 5 = 12 + 22。
1 | public boolean judgeSquareSum(int c) { |
反转字符串中的元音字符
345. Reverse Vowels of a String (Easy)
1 | Given s = "leetcode", return "leotcede". |
使用双指针,指向待反转的两个元音字符,一个指针从头向尾遍历,一个指针从尾到头遍历。
1 | private final static HashSet<Character> vowels = new HashSet<>(Arrays.asList('a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U')); |
回文字符串
680. Valid Palindrome II (Easy)
1 | Input: "abca" |
题目描述:可以删除一个字符,判断是否能构成回文字符串。
1 | public boolean validPalindrome(String s) { |
归并两个有序数组
1 | Input: |
题目描述:把归并结果存到第一个数组上。
需要从尾开始遍历,否则在 nums1 上归并得到的值会覆盖还未进行归并比较的值
1 | public void merge(int[] nums1, int m, int[] nums2, int n) { |
判断链表是否存在环
使用双指针,一个指针每次移动一个节点,一个指针每次移动两个节点,如果存在环,那么这两个指针一定会相遇。
1 | public boolean hasCycle(ListNode head) { |
最长子序列
524. Longest Word in Dictionary through Deleting (Medium)
1 | Input: |
题目描述:删除 s 中的一些字符,使得它构成字符串列表 d 中的一个字符串,找出能构成的最长字符串。如果有多个相同长度的结果,返回按字典序排序的最大字符串。
1 | public String findLongestWord(String s, List<String> d) { |
排序
快速选择
一般用于求解 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
215. Kth Largest Element in an Array (Medium)
排序 :时间复杂度 O(NlogN),空间复杂度 O(1)
1 | public int findKthLargest(int[] nums, int k) { |
堆排序 :时间复杂度 O(NlogK),空间复杂度 O(K)。
1 | public int findKthLargest(int[] nums, int k) { |
快速选择 :时间复杂度 O(N),空间复杂度 O(1)
1 | public int findKthLargest(int[] nums, int k) { |
桶排序
出现频率最多的 k 个数
347. Top K Frequent Elements (Medium)
1 | Given [1,1,1,2,2,3] and k = 2, return [1,2]. |
设置若干个桶,每个桶存储出现频率相同的数,并且桶的下标代表桶中数出现的频率,即第 i 个桶中存储的数出现的频率为 i。把数都放到桶之后,从后向前遍历桶,最先得到的 k 个数就是出现频率最多的的 k 个数。
1 | public List<Integer> topKFrequent(int[] nums, int k) { |
按照字符出现次数对字符串排序
451. Sort Characters By Frequency (Medium)
1 | Input: |
1 | public String frequencySort(String s) { |
荷兰国旗问题
荷兰国旗包含三种颜色:红、白、蓝。有这三种颜色的球,算法的目标是将这三种球按颜色顺序正确地排列。
它其实是三向切分快速排序的一种变种,在三向切分快速排序中,每次切分都将数组分成三个区间:小于切分元素、等于切分元素、大于切分元素,而该算法是将数组分成三个区间:等于红色、等于白色、等于蓝色。
按颜色进行排序
1 | Input: [2,0,2,1,1,0] |
题目描述:只有 0/1/2 三种颜色。
1 | public void sortColors(int[] nums) { |
二分查找
正常实现
1 | public int binarySearch(int[] nums, int key) { |
时间复杂度
二分查找也称为折半查找,每次都能将查找区间减半,这种折半特性的算法时间复杂度都为 O(logN)。
m 计算
有两种计算中值 m 的方式:
- m = (l + h) / 2
- m = l + (h - l) / 2
l + h 可能出现加法溢出,最好使用第二种方式。
返回值
循环退出时如果仍然没有查找到 key,那么表示查找失败。可以有两种返回值:
- -1:以一个错误码表示没有查找到 key
- l:将 key 插入到 nums 中的正确位置
变种
二分查找可以有很多变种,变种实现要注意边界值的判断。例如在一个有重复元素的数组中查找 key 的最左位置的实现如下:
1 | public int binarySearch(int[] nums, int key) { |
该实现和正常实现有以下不同:
- 循环条件为 l < h
- h 的赋值表达式为 h = m
- 最后返回 l 而不是 -1
在 nums[m] >= key 的情况下,可以推导出最左 key 位于 [l, m] 区间中,这是一个闭区间。h 的赋值表达式为 h = m,因为 m 位置也可能是解。
在 h 的赋值表达式为 h = mid 的情况下,如果循环条件为 l <= h,那么会出现循环无法退出的情况,因此循环条件只能是 l < h。以下演示了循环条件为 l <= h 时循环无法退出的情况:
1 | nums = {0, 1, 2}, key = 1 |
当循环体退出时,不表示没有查找到 key,因此最后返回的结果不应该为 -1。为了验证有没有查找到,需要在调用端判断一下返回位置上的值和 key 是否相等。
求开方
1 | Input: 4 |
一个数 x 的开方 sqrt 一定在 0 \ x 之间,并且满足 sqrt == x / sqrt。可以利用二分查找在 0 \ x 之间查找 sqrt。
对于 x = 8,它的开方是 2.82842…,最后应该返回 2 而不是 3。在循环条件为 l <= h 并且循环退出时,h 总是比 l 小 1,也就是说 h = 2,l = 3,因此最后的返回值应该为 h 而不是 l。
1 | public int mySqrt(int x) { |
大于给定元素的最小元素
744. Find Smallest Letter Greater Than Target (Easy)
1 | Input: |
题目描述:给定一个有序的字符数组 letters 和一个字符 target,要求找出 letters 中大于 target 的最小字符,如果找不到就返回第 1 个字符。
1 | public char nextGreatestLetter(char[] letters, char target) { |
有序数组的 Single Element
540. Single Element in a Sorted Array (Medium)
1 | Input: [1,1,2,3,3,4,4,8,8] |
题目描述:一个有序数组只有一个数不出现两次,找出这个数。要求以 O(logN) 时间复杂度进行求解。
令 index 为 Single Element 在数组中的位置。如果 m 为偶数,并且 m + 1 < index,那么 nums[m] == nums[m + 1];m + 1 >= index,那么 nums[m] != nums[m + 1]。
从上面的规律可以知道,如果 nums[m] == nums[m + 1],那么 index 所在的数组位置为 [m + 2, h],此时令 l = m + 2;如果 nums[m] != nums[m + 1],那么 index 所在的数组位置为 [l, m],此时令 h = m。
因为 h 的赋值表达式为 h = m,那么循环条件也就只能使用 l < h 这种形式。
1 | public int singleNonDuplicate(int[] nums) { |
第一个错误的版本
题目描述:给定一个元素 n 代表有 [1, 2, …, n] 版本,可以调用 isBadVersion(int x) 知道某个版本是否错误,要求找到第一个错误的版本。
如果第 m 个版本出错,则表示第一个错误的版本在 [l, m] 之间,令 h = m;否则第一个错误的版本在 [m + 1, h] 之间,令 l = m + 1。
因为 h 的赋值表达式为 h = m,因此循环条件为 l < h。
1 | public int firstBadVersion(int n) { |
旋转数组的最小数字
153. Find Minimum in Rotated Sorted Array (Medium)
1 | Input: [3,4,5,1,2], |
1 | public int findMin(int[] nums) { |
查找区间
34. Search for a Range (Medium)
1 | Input: nums = [5,7,7,8,8,10], target = 8 |
1 | public int[] searchRange(int[] nums, int target) { |
搜索
深度优先搜索和广度优先搜索广泛运用于树和图中,但是它们的应用远远不止如此。
BFS
广度优先搜索的搜索过程有点像一层一层地进行遍历,每层遍历都以上一层遍历的结果作为起点,遍历一个距离能访问到的所有节点。需要注意的是,遍历过的节点不能再次被遍历。
第一层:
- 0 -> {6,2,1,5};
第二层:
- 6 -> {4}
- 2 -> {}
- 1 -> {}
- 5 -> {3}
第三层:
- 4 -> {}
- 3 -> {}
可以看到,每一层遍历的节点都与根节点距离相同。设 di 表示第 i 个节点与根节点的距离,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 di<=dj。利用这个结论,可以求解最短路径等 最优解 问题:第一次遍历到目的节点,其所经过的路径为最短路径。应该注意的是,使用 BFS 只能求解无权图的最短路径。
在程序实现 BFS 时需要考虑以下问题:
- 队列:用来存储每一轮遍历得到的节点;
- 标记:对于遍历过的节点,应该将它标记,防止重复遍历。
计算在网格中从原点到特定点的最短路径长度
1 | [[1,1,0,1], |
1 表示可以经过某个位置,求解从 (0, 0) 位置到 (tr, tc) 位置的最短路径长度。
1 | public int minPathLength(int[][] grids, int tr, int tc) { |
组成整数的最小平方数数量
1 | For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9. |
可以将每个整数看成图中的一个节点,如果两个整数之差为一个平方数,那么这两个整数所在的节点就有一条边。
要求解最小的平方数数量,就是求解从节点 n 到节点 0 的最短路径。
本题也可以用动态规划求解,在之后动态规划部分中会再次出现。
1 | public int numSquares(int n) { |
最短单词路径
1 | Input: |
1 | Input: |
找出一条从 beginWord 到 endWord 的最短路径,每次移动规定为改变一个字符,并且改变之后的字符串必须在 wordList 中。
1 | public int ladderLength(String beginWord, String endWord, List<String> wordList) { |
DFS
广度优先搜索一层一层遍历,每一层得到的所有新节点,要用队列存储起来以备下一层遍历的时候再遍历。
而深度优先搜索在得到一个新节点时立马对新节点进行遍历:从节点 0 出发开始遍历,得到到新节点 6 时,立马对新节点 6 进行遍历,得到新节点 4;如此反复以这种方式遍历新节点,直到没有新节点了,此时返回。返回到根节点 0 的情况是,继续对根节点 0 进行遍历,得到新节点 2,然后继续以上步骤。
从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 可达性 问题。
在程序实现 DFS 时需要考虑以下问题:
- 栈:用栈来保存当前节点信息,当遍历新节点返回时能够继续遍历当前节点。可以使用递归栈。
- 标记:和 BFS 一样同样需要对已经遍历过的节点进行标记。
查找最大的连通面积
695. Max Area of Island (Easy)
1 | [[0,0,1,0,0,0,0,1,0,0,0,0,0], |
1 | private int m, n; |
矩阵中的连通分量数目
200. Number of Islands (Medium)
1 | Input: |
可以将矩阵表示看成一张有向图。
1 | private int m, n; |
好友关系的连通分量数目
1 | Input: |
好友关系可以看成是一个无向图,例如第 0 个人与第 1 个人是好友,那么 M[0][1] 和 M[1][0] 的值都为 1。
1 | private int n; |
填充封闭区域
130. Surrounded Regions (Medium)
1 | For example, |
使被 ‘X’ 包围的 ‘O’ 转换为 ‘X’。
先填充最外侧,剩下的就是里侧了。
1 | private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; |
能到达的太平洋和大西洋的区域
417. Pacific Atlantic Water Flow (Medium)
1 | Given the following 5x5 matrix: |
左边和上边是太平洋,右边和下边是大西洋,内部的数字代表海拔,海拔高的地方的水能够流到低的地方,求解水能够流到太平洋和大西洋的所有位置。
1 |
|
Backtracking
Backtracking(回溯)属于 DFS。
- 普通 DFS 主要用在 可达性问题 ,这种问题只需要执行到特点的位置然后返回即可。
- 而 Backtracking 主要用于求解 排列组合 问题,例如有 { ‘a’,’b’,’c’ } 三个字符,求解所有由这三个字符排列得到的字符串,这种问题在执行到特定的位置返回之后还会继续执行求解过程。
因为 Backtracking 不是立即就返回,而要继续求解,因此在程序实现时,需要注意对元素的标记问题:
- 在访问一个新元素进入新的递归调用时,需要将新元素标记为已经访问,这样才能在继续递归调用时不用重复访问该元素;
- 但是在递归返回时,需要将元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,可以访问已经访问过但是不在当前递归链中的元素。
数字键盘组合
17. Letter Combinations of a Phone Number (Medium)
1 | Input:Digit string "23" |
1 |
|
IP 地址划分
93. Restore IP Addresses(Medium)
1 | Given "25525511135", |
1 | public List<String> restoreIpAddresses(String s) { |
在矩阵中寻找字符串
1 | For example, |
1 | private final static int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; |
输出二叉树中所有从根到叶子的路径
1 | 1 |
1 | ["1->2->5", "1->3"] |
1 |
|
排列
1 | [1,2,3] have the following permutations: |
1 | public List<List<Integer>> permute(int[] nums) { |
含有相同元素求排列
1 | [1,1,2] have the following unique permutations: |
数组元素可能含有相同的元素,进行排列时就有可能出现重复的排列,要求重复的排列只返回一个。
在实现上,和 Permutations 不同的是要先排序,然后在添加一个元素时,判断这个元素是否等于前一个元素,如果等于,并且前一个元素还未访问,那么就跳过这个元素。
1 | public List<List<Integer>> permuteUnique(int[] nums) { |
组合
1 | If n = 4 and k = 2, a solution is: |
1 | public List<List<Integer>> combine(int n, int k) { |
组合求和
1 | given candidate set [2, 3, 6, 7] and target 7, |
1 | public List<List<Integer>> combinationSum(int[] candidates, int target) { |
含有相同元素的求组合求和
40. Combination Sum II (Medium)
1 | For example, given candidate set [10, 1, 2, 7, 6, 1, 5] and target 8, |
1 | public List<List<Integer>> combinationSum2(int[] candidates, int target) { |
1-9 数字的组合求和
216. Combination Sum III (Medium)
1 | Input: k = 3, n = 9 |
从 1-9 数字中选出 k 个数不重复的数,使得它们的和为 n。
1 | public List<List<Integer>> combinationSum3(int k, int n) { |
子集
找出集合的所有子集,子集不能重复,[1, 2] 和 [2, 1] 这种子集算重复
1 | public List<List<Integer>> subsets(int[] nums) { |
含有相同元素求子集
1 | For example, |
1 | public List<List<Integer>> subsetsWithDup(int[] nums) { |
分割字符串使得每个部分都是回文数
131. Palindrome Partitioning (Medium)
1 | For example, given s = "aab", |
1 | public List<List<String>> partition(String s) { |
数独
1 | private boolean[][] rowsUsed = new boolean[9][10]; |
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)。
1 | private List<List<String>> solutions; |
分治
给表达式加括号
241. Different Ways to Add Parentheses (Medium)
1 | Input: "2-1-1". |
1 | public List<Integer> diffWaysToCompute(String input) { |
动态规划
递归和动态规划都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存了子问题的解,避免重复计算。
斐波那契数列
爬楼梯
题目描述:有 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) 复杂度。
1 | public int climbStairs(int n) { |
强盗抢劫
题目描述:抢劫一排住户,但是不能抢邻近的住户,求最大抢劫量。
定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。由于不能抢劫邻近住户,因此如果抢劫了第 i 个住户那么只能抢劫 i - 2 或者 i - 3 的住户,所以
1 | public int rob(int[] nums) { |
强盗在环形街区抢劫
1 | public int rob(int[] nums) { |
母牛生产
题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。
第 i 年成熟的牛的数量为:
信件错排
题目描述:有 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 种取值,因此共有 (i-1)*dp[i-1] 种错误装信方式。
综上所述,错误装信数量方式数量为:
dp[N] 即为所求。
矩阵路径
矩阵的最小路径和
1 | [[1,3,1], |
题目描述:求从矩阵的左上角到右下角的最小路径和,每次只能向右和向下移动。
1 | public int minPathSum(int[][] grid) { |
矩阵的总路径数
题目描述:统计从矩阵左上角到右下角的路径总数,每次只能向右或者向下移动。
1 | public int uniquePaths(int m, int n) { |
也可以直接用数学公式求解,这是一个组合问题。机器人总共移动的次数 S=m+n-2,向下移动的次数 D=m-1,那么问题可以看成从 S 从取出 D 个位置的组合数量,这个问题的解为 C(S, D)。
1 | public int uniquePaths(int m, int n) { |
数组区间
数组区间和
303. Range Sum Query - Immutable (Easy)
1 | Given nums = [-2, 0, 3, -5, 2, -1] |
求区间 i \ j 的和,可以转换为 sum[j] - sum[i-1],其中 sum[i] 为 0 \ i 的和。
1 | class NumArray { |
子数组最大的和
1 | For example, given the array [-2,1,-3,4,-1,2,1,-5,4], |
1 | public int maxSubArray(int[] nums) { |
数组中等差递增子区间的个数
413. Arithmetic Slices (Medium)
1 | A = [1, 2, 3, 4] |
dp[i] 表示以 A[i] 为结尾的等差递增子区间的个数。
如果 A[i] - A[i - 1] == A[i - 1] - A[i - 2],表示 [A[i - 2], A[i - 1], A[i]] 是一个等差递增子区间。如果 [A[i - 3], A[i - 2], A[i - 1]] 是一个等差递增子区间,那么 [A[i - 3], A[i - 2], A[i - 1], A[i]] 也是。因此在这个条件下,dp[i] = dp[i-1] + 1。
1 | public int numberOfArithmeticSlices(int[] A) { |
分割整数
分割整数的最大乘积
题目描述:For example, given n = 2, return 1 (2 = 1 + 1); given n = 10, return 36 (10 = 3 + 3 + 4).
1 | public int integerBreak(int n) { |
按平方数来分割整数
题目描述:For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9.
1 | public int numSquares(int n) { |
分割整数构成字母字符串
题目描述:Given encoded message “12”, it could be decoded as “AB” (1 2) or “L” (12).
1 | public int numDecodings(String s) { |
最长递增子序列
已知一个序列 {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} 即为所求。
最长递增子序列
300. Longest Increasing Subsequence (Medium)
1 | public int lengthOfLIS(int[] nums) { |
使用 Stream 求最大值会导致运行时间过长,可以改成以下形式:
1 | int ret = 0; |
以上解法的时间复杂度为 O(N2) ,可以使用二分查找将时间复杂度降低为 O(NlogN)。
定义一个 tails 数组,其中 tails[i] 存储长度为 i + 1 的最长递增子序列的最后一个元素。对于一个元素 x,
- 如果它大于 tails 数组所有的值,那么把它添加到 tails 后面,表示最长递增子序列长度加 1;
- 如果 tails[i-1] < x <= tails[i],那么更新 tails[i-1] = x。
例如对于数组 [4,3,6,5],有:
1 | tails len num |
可以看出 tails 数组保持有序,因此在查找 Si 位于 tails 数组的位置时就可以使用二分查找。
1 | public int lengthOfLIS(int[] nums) { |
一组整数对能够构成的最长链
646. Maximum Length of Pair Chain (Medium)
1 | Input: [[1,2], [2,3], [3,4]] |
题目描述:对于 (a, b) 和 (c, d) ,如果 b < c,则它们可以构成一条链。
1 | public int findLongestChain(int[][] pairs) { |
最长摆动子序列
376. Wiggle Subsequence (Medium)
1 | Input: [1,7,4,9,2,5] |
要求:使用 O(N) 时间复杂度求解。
1 | public int wiggleMaxLength(int[] nums) { |
最长公共子序列
对于两个子序列 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 。
- 在求最终解时,最长公共子序列中 dp[N][M] 就是最终解,而最长递增子序列中 dp[N] 不是最终解,因为以 SN 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。
1 | public int lengthOfLCS(int[] nums1, int[] nums2) { |
0-1 背包
有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。
定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。设第 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 背包的状态转移方程为:
1 | public int knapsack(int W, int N, int[] weights, int[] values) { |
空间优化
在程序实现时可以对 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],在程序实现时需要按倒序来循环求解。
1 | public int knapsack(int W, int N, int[] weights, int[] values) { |
无法使用贪心算法的解释
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 |
变种
完全背包:物品数量为无限个
多重背包:物品数量有限制
多维费用背包:物品不仅有重量,还有体积,同时考虑这两种限制
其它:物品之间相互约束或者依赖
划分数组为和相等的两部分
416. Partition Equal Subset Sum (Medium)
1 | Input: [1, 5, 11, 5] |
可以看成一个背包大小为 sum/2 的 0-1 背包问题。
1 | public boolean canPartition(int[] nums) { |
改变一组数的正负号使得它们的和为一给定数
1 | Input: nums is [1, 1, 1, 1, 1], S is 3. |
该问题可以转换为 Subset Sum 问题,从而使用 0-1 背包的方法来求解。
可以将这组数看成两部分,P 和 N,其中 P 使用正号,N 使用负号,有以下推导:
1 | sum(P) - sum(N) = target |
因此只要找到一个子集,令它们都取正号,并且和等于 (target + sum(nums))/2,就证明存在解。
1 | public int findTargetSumWays(int[] nums, int S) { |
DFS 解法:
1 | public int findTargetSumWays(int[] nums, int S) { |
字符串按单词列表分割
1 | s = "leetcode", |
dict 中的单词没有使用次数的限制,因此这是一个完全背包问题。
0-1 背包和完全背包在实现上的不同之处是,0-1 背包对物品的迭代是在最外层,而完全背包对物品的迭代是在最里层。
1 | public boolean wordBreak(String s, List<String> wordDict) { |
01 字符构成最多的字符串
1 | Input: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3 |
这是一个多维费用的 0-1 背包问题,有两个背包大小,0 的数量和 1 的数量。
1 | public int findMaxForm(String[] strs, int m, int n) { |
找零钱的方法数
1 | Example 1: |
题目描述:给一些面额的硬币,要求用这些硬币来组成给定面额的钱数,并且使得硬币数量最少。硬币可以重复使用。
- 物品:硬币
- 物品大小:面额
- 物品价值:数量
因为硬币可以重复使用,因此这是一个完全背包问题。
1 | public int coinChange(int[] coins, int amount) { |
组合总和
377. Combination Sum IV (Medium)
1 | nums = [1, 2, 3] |
完全背包。
1 | public int combinationSum4(int[] nums, int target) { |
只能进行 k 次的股票交易
188. Best Time to Buy and Sell Stock IV (Hard)
1 | public int maxProfit(int k, int[] prices) { |
只能进行两次的股票交易
123. Best Time to Buy and Sell Stock III (Hard)
1 | public int maxProfit(int[] prices) { |
股票交易
需要冷却期的股票交易
309. Best Time to Buy and Sell Stock with Cooldown(Medium)
题目描述:交易之后需要有一天的冷却时间。
1 | public int maxProfit(int[] prices) { |
需要交易费用的股票交易
714. Best Time to Buy and Sell Stock with Transaction Fee (Medium)
1 | Input: prices = [1, 3, 2, 8, 4, 9], fee = 2 |
题目描述:每交易一次,都要支付一定的费用。
1 | public int maxProfit(int[] prices, int fee) { |
买入和售出股票最大的收益
121. Best Time to Buy and Sell Stock (Easy)
题目描述:只进行一次交易。
只要记录前面的最小价格,将这个最小价格作为买入价格,然后将当前的价格作为售出价格,查看当前收益是不是最大收益。
1 | public int maxProfit(int[] prices) { |
字符串编辑
删除两个字符串的字符使它们相等
583. Delete Operation for Two Strings (Medium)
1 | Input: "sea", "eat" |
可以转换为求两个字符串的最长公共子序列问题。
1 | public int minDistance(String word1, String word2) { |
编辑距离
1 | Example 1: |
题目描述:修改一个字符串成为另一个字符串,使得修改次数最少。一次修改操作包括:插入一个字符、删除一个字符、替换一个字符。
1 | public int minDistance(String word1, String word2) { |
复制粘贴字符
题目描述:最开始只有一个字符 A,问需要多少次操作能够得到 n 个字符 A,每次操作可以复制当前所有的字符,或者粘贴。
1 | Input: 3 |
1 | public int minSteps(int n) { |
1 | public int minSteps(int 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) * …
生成素数序列
埃拉托斯特尼筛法在每次找到一个素数时,将能被素数整除的数排除掉。
1 | public int countPrimes(int n) { |
最大公约数
1 | int gcd(int a, int b) { |
最小公倍数为两数的乘积除以最大公约数。
1 | int lcm(int a, int b) { |
使用位操作和减法求解最大公约数
对于 a 和 b 的最大公约数 f(a, b),有:
- 如果 a 和 b 均为偶数,f(a, b) = 2*f(a/2, b/2);
- 如果 a 是偶数 b 是奇数,f(a, b) = f(a/2, b);
- 如果 b 是偶数 a 是奇数,f(a, b) = f(a, b/2);
- 如果 a 和 b 均为奇数,f(a, b) = f(b, a-b);
乘 2 和除 2 都可以转换为移位操作。
1 | public int gcd(int a, int b) { |
进制转换
7 进制
1 | public String convertToBase7(int num) { |
Java 中 static String toString(int num, int radix) 可以将一个整数转换为 redix 进制表示的字符串。
1 | public String convertToBase7(int num) { |
16 进制
405. Convert a Number to Hexadecimal (Easy)
1 | Input: |
负数要用它的补码形式。
1 | public String toHex(int num) { |
26 进制
168. Excel Sheet Column Title (Easy)
1 | 1 -> A |
因为是从 1 开始计算的,而不是从 0 开始,因此需要对 n 执行 -1 操作。
1 | public String convertToTitle(int n) { |
阶乘
统计阶乘尾部有多少个 0
172. Factorial Trailing Zeroes (Easy)
尾部的 0 由 2 * 5 得来,2 的数量明显多于 5 的数量,因此只要统计有多少个 5 即可。
对于一个数 N,它所包含 5 的个数为:N/5 + N/52 + N/53 + …,其中 N/5 表示不大于 N 的数中 5 的倍数贡献一个 5,N/52 表示不大于 N 的数中 52 的倍数再贡献一个 5 …。
1 | public int trailingZeroes(int n) { |
如果统计的是 N! 的二进制表示中最低位 1 的位置,只要统计有多少个 2 即可,该题目出自 编程之美:2.2 。和求解有多少个 5 一样,2 的个数为 N/2 + N/22 + N/23 + …
字符串加法减法
二进制加法
1 | a = "11" |
1 | public String addBinary(String a, String b) { |
字符串加法
字符串的值为非负整数。
1 | public String addStrings(String num1, String num2) { |
相遇问题
改变数组元素使所有的数组元素都相等
462. Minimum Moves to Equal Array Elements II (Medium)
1 | Input: |
每次可以对一个数组元素加一或者减一,求最小的改变次数。
这是个典型的相遇问题,移动距离最小的方式是所有元素都移动到中位数。理由如下:
设 m 为中位数。a 和 b 是 m 两边的两个元素,且 b > a。要使 a 和 b 相等,它们总共移动的次数为 b - a,这个值等于 (b - m) + (m - a),也就是把这两个数移动到中位数的移动次数。
设数组长度为 N,则可以找到 N/2 对 a 和 b 的组合,使它们都移动到 m 的位置。
解法 1
先排序,时间复杂度:O(NlogN)
1 | public int minMoves2(int[] nums) { |
解法 2
使用快速选择找到中位数,时间复杂度 O(N)
1 | public int minMoves2(int[] nums) { |
多数投票问题
数组中出现次数多于 n / 2 的元素
先对数组排序,最中间那个数出现次数一定多于 n / 2。
1 | public int majorityElement(int[] nums) { |
可以利用 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。
1 | public int majorityElement(int[] nums) { |
其它
平方数
367. Valid Perfect Square (Easy)
1 | Input: 16 |
平方序列:1,4,9,16,..
间隔:3,5,7,…
间隔为等差数列,使用这个特性可以得到从 1 开始的平方序列。
1 | public boolean isPerfectSquare(int num) { |
3 的 n 次方
1 | public boolean isPowerOfThree(int n) { |
乘积数组
238. Product of Array Except Self (Medium)
1 | For example, given [1,2,3,4], return [24,12,8,6]. |
给定一个数组,创建一个新数组,新数组的每个元素为原始数组中除了该位置上的元素之外所有元素的乘积。
要求时间复杂度为 O(N),并且不能使用除法。
1 | public int[] productExceptSelf(int[] nums) { |
找出数组中的乘积最大的三个数
628. Maximum Product of Three Numbers (Easy)
1 | Input: [1,2,3,4] |
1 | public int maximumProduct(int[] nums) { |
数据结构相关
栈和队列
用栈实现队列
232. Implement Queue using Stacks (Easy)
1 | class MyQueue { |
用队列实现栈
225. Implement Stack using Queues (Easy)
在将一个元素 x 插入队列时,需要让除了 x 之外的所有元素出队列,再入队列。此时 x 在队首,第一个出队列,实现了后进先出顺序。
1 | class MyStack { |
最小值栈
1 | class MinStack { |
对于实现最小值队列问题,可以先将队列使用栈来实现,然后就将问题转换为最小值栈,这个问题出现在 编程之美:3.7。
用栈实现括号匹配
1 | "()[]{}" |
1 | public boolean isValid(String s) { |
数组中元素与下一个比它大的元素之间的距离
739. Daily Temperatures (Medium)
1 | Input: [73, 74, 75, 71, 69, 72, 76, 73] |
在遍历数组时用 Stack 把数组中的数存起来,如果当前遍历的数比栈顶元素来的大,说明栈顶元素的下一个比它大的数就是当前元素。
1 | public int[] dailyTemperatures(int[] temperatures) { |
循环数组中比当前元素大的下一个元素
503. Next Greater Element II (Medium)
1 | Input: [1,2,1] |
与 739. Daily Temperatures (Medium) 不同的是,数组是循环数组,并且最后要求的不是距离而是下一个元素。
1 | public int[] nextGreaterElements(int[] nums) { |
哈希表
哈希表使用 O(N) 空间复杂度存储数据,从而能够以 O(1) 时间复杂度求解问题。
Java 中的 HashSet 用于存储一个集合,可以查找元素是否在集合中。
如果元素有穷,并且范围不大,那么可以用一个布尔数组来存储一个元素是否存在。例如对于只有小写字符的元素,就可以用一个长度为 26 的布尔数组来存储一个字符集合,使得空间复杂度降低为 O(1)。
Java 中的 HashMap 主要用于映射关系,从而把两个元素联系起来。
在对一个内容进行压缩或者其它转换时,利用 HashMap 可以把原始内容和转换后的内容联系起来。例如在一个简化 url 的系统中Leetcdoe : 535. Encode and Decode TinyURL (Medium),利用 HashMap 就可以存储精简后的 url 到原始 url 的映射,使得不仅可以显示简化的 url,也可以根据简化的 url 得到原始 url 从而定位到正确的资源。
HashMap 也可以用来对元素进行计数统计,此时键为元素,值为计数。和 HashSet 类似,如果元素有穷并且范围不大,可以用整型数组来进行统计。
数组中的两个数和为给定值
可以先对数组进行排序,然后使用双指针方法或者二分查找方法。这样做的时间复杂度为 O(NlogN),空间复杂度为 O(1)。
用 HashMap 存储数组元素和索引的映射,在访问到 nums[i] 时,判断 HashMap 中是否存在 target - nums[i],如果存在说明 target - nums[i] 所在的索引和 i 就是要找的两个数。该方法的时间复杂度为 O(N),空间复杂度为 O(N),使用空间来换取时间。
1 | public int[] twoSum(int[] nums, int target) { |
判断数组是否含有重复元素
217. Contains Duplicate (Easy)
1 | public boolean containsDuplicate(int[] nums) { |
最长和谐序列
594. Longest Harmonious Subsequence (Easy)
1 | Input: [1,3,2,2,5,2,3,7] |
和谐序列中最大数和最小数只差正好为 1,应该注意的是序列的元素不一定是数组的连续元素。
1 | public int findLHS(int[] nums) { |
最长连续序列
128. Longest Consecutive Sequence (Hard)
1 | Given [100, 4, 200, 1, 3, 2], |
要求以 O(N) 的时间复杂度求解。
1 | public int longestConsecutive(int[] nums) { |
字符串
两个字符串包含的字符是否完全相同
1 | s = "anagram", t = "nagaram", return true. |
字符串只包含小写字符,总共有 26 个小写字符。可以用 HashMap 来映射字符与出现次数。因为键的范围很小,因此可以使用长度为 26 的整型数组对字符串出现的字符进行统计,然后比较两个字符串出现的字符数量是否相同。
1 | public boolean isAnagram(String s, String t) { |
计算一组字符集合可以组成的回文字符串的最大长度
409. Longest Palindrome (Easy)
1 | Input : "abccccdd" |
使用长度为 256 的整型数组来统计每个字符出现的个数,每个字符有偶数个可以用来构成回文字符串。
因为回文字符串最中间的那个字符可以单独出现,所以如果有单独的字符就把它放到最中间。
1 | public int longestPalindrome(String s) { |
字符串同构
205. Isomorphic Strings (Easy)
1 | Given "egg", "add", return true. |
记录一个字符上次出现的位置,如果两个字符串中的字符上次出现的位置一样,那么就属于同构。
1 | public boolean isIsomorphic(String s, String t) { |
回文子字符串
647. Palindromic Substrings (Medium)
1 | Input: "aaa" |
从字符串的某一位开始,尝试着去扩展子字符串。
1 | private int cnt = 0; |
判断一个整数是否是回文数
要求不能使用额外空间,也就不能将整数转换为字符串进行判断。
将整数分成左右两部分,右边那部分需要转置,然后判断这两部分是否相等。
1 | public boolean isPalindrome(int x) { |
统计二进制字符串中连续 1 和连续 0 数量相同的子字符串个数
696. Count Binary Substrings (Easy)
1 | Input: "00110011" |
1 | public int countBinarySubstrings(String s) { |
字符串循环移位包含
1 | s1 = AABCD, s2 = CDAA |
给定两个字符串 s1 和 s2,要求判定 s2 是否能够被 s1 做循环移位得到的字符串包含。
s1 进行循环移位的结果是 s1s1 的子字符串,因此只要判断 s2 是否是 s1s1 的子字符串即可。
字符串循环移位
1 | s = "abcd123" k = 3 |
将字符串向右循环移动 k 位。
将 abcd123 中的 abcd 和 123 单独逆序,得到 dcba321,然后对整个字符串进行逆序,得到 123abcd。
字符串中单词的翻转
1 | s = "I am a student" |
将每个单词逆序,然后将整个字符串逆序。
数组与矩阵
把数组中的 0 移到末尾
1 | For example, given nums = [0, 1, 0, 3, 12], after calling your function, nums should be [1, 3, 12, 0, 0]. |
1 | public void moveZeroes(int[] nums) { |
改变矩阵维度
566. Reshape the Matrix (Easy)
1 | Input: |
1 | public int[][] matrixReshape(int[][] nums, int r, int c) { |
找出数组中最长的连续 1
485. Max Consecutive Ones (Easy)
1 | public int findMaxConsecutiveOnes(int[] nums) { |
一个数组元素在 [1, n] 之间,其中一个数被替换为另一个数,找出重复的数和丢失的数
1 | Input: nums = [1,2,2,4] |
1 | Input: nums = [1,2,2,4] |
最直接的方法是先对数组进行排序,这种方法时间复杂度为 O(NlogN)。本题可以以 O(N) 的时间复杂度、O(1) 空间复杂度来求解。
主要思想是通过交换数组元素,使得数组上的元素在正确的位置上。遍历数组,如果第 i 位上的元素不是 i + 1,那么一直交换第 i 位和 nums[i] - 1 位置上的元素。
1 | public int[] findErrorNums(int[] nums) { |
类似题目:
- 448. Find All Numbers Disappeared in an Array (Easy),寻找所有丢失的元素
- 442. Find All Duplicates in an Array (Medium),寻找所有重复的元素。
找出数组中重复的数,数组值在 [1, n] 之间
287. Find the Duplicate Number (Medium)
要求不能修改数组,也不能使用额外的空间。
二分查找解法:
1 | public int findDuplicate(int[] nums) { |
双指针解法,类似于有环链表中找出环的入口:
1 | public int findDuplicate(int[] nums) { |
有序矩阵查找
240. Search a 2D Matrix II (Medium)
1 | [ |
1 | public boolean searchMatrix(int[][] matrix, int target) { |
有序矩阵的 Kth Element
378. Kth Smallest Element in a Sorted Matrix ((Medium))
1 | matrix = [ |
解题参考:Share my thoughts and Clean Java Code
二分查找解法:
1 | public int kthSmallest(int[][] matrix, int k) { |
堆解法:
1 | public int kthSmallest(int[][] matrix, int k) { |
数组相邻差值的个数
667. Beautiful Arrangement II (Medium)
1 | Input: n = 3, k = 2 |
题目描述:数组元素为 1~n 的整数,要求构建数组,使得相邻元素的差值不相同的个数为 k。
让前 k+1 个元素构建出 k 个不相同的差值,序列为:1 k+1 2 k 3 k-1 … k/2 k/2+1.
1 | public int[] constructArray(int n, int k) { |
数组的度
697. Degree of an Array (Easy)
1 | Input: [1,2,2,3,1,4,2] |
题目描述:数组的度定义为元素出现的最高频率,例如上面的数组度为 3。要求找到一个最小的子数组,这个子数组的度和原数组一样。
1 | public int findShortestSubArray(int[] nums) { |
对角元素相等的矩阵
1 | 1234 |
1 | public boolean isToeplitzMatrix(int[][] matrix) { |
嵌套数组
1 | Input: A = [5,4,0,3,1,6,2] |
题目描述:S[i] 表示一个集合,集合的第一个元素是 A[i],第二个元素是 A[A[i]],如此嵌套下去。求最大的 S[i]。
1 | public int arrayNesting(int[] nums) { |
分隔数组
769. Max Chunks To Make Sorted (Medium)
1 | Input: arr = [1,0,2,3,4] |
题目描述:分隔数组,使得对每部分排序后数组就为有序。
1 | public int maxChunksToSorted(int[] arr) { |
链表
链表是空节点,或者有一个值和一个指向下一个链表的指针,因此很多链表问题可以用递归来处理。
找出两个链表的交点
160. Intersection of Two Linked Lists (Easy)
1 | A: a1 → a2 |
要求:时间复杂度为 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 两个链表的指针能同时访问到交点。
1 | public ListNode getIntersectionNode(ListNode headA, ListNode headB) { |
如果只是判断是否存在交点,那么就是另一个问题,即 编程之美:3.6 的问题。有两种解法:把第一个链表的结尾连接到第二个链表的开头,看第二个链表是否存在环;或者直接比较两个链表的最后一个节点是否相同。
链表反转
206. Reverse Linked List (Easy)
递归
1 | public ListNode reverseList(ListNode head) { |
头插法
1 | public ListNode reverseList(ListNode head) { |
归并两个有序的链表
21. Merge Two Sorted Lists (Easy)
1 | public ListNode mergeTwoLists(ListNode l1, ListNode l2) { |
从有序链表中删除重复节点
83. Remove Duplicates from Sorted List (Easy)
1 | Given 1->1->2, return 1->2. |
1 | public ListNode deleteDuplicates(ListNode head) { |
删除链表的倒数第 n 个节点
19. Remove Nth Node From End of List (Medium)
1 | Given linked list: 1->2->3->4->5, and n = 2. |
1 | public ListNode removeNthFromEnd(ListNode head, int n) { |
交换链表中的相邻结点
24. Swap Nodes in Pairs (Medium)
1 | Given 1->2->3->4, you should return the list as 2->1->4->3. |
题目要求:不能修改结点的 val 值,O(1) 空间复杂度。
1 | public ListNode swapPairs(ListNode head) { |
链表求和
445. Add Two Numbers II (Medium)
1 | Input: (7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4) |
题目要求:不能修改原始链表。
1 | public ListNode addTwoNumbers(ListNode l1, ListNode l2) { |
回文链表
234. Palindrome Linked List (Easy)
题目要求:以 O(1) 的空间复杂度来求解。
切成两半,把后半段反转,然后比较两半是否相等。
1 | public boolean isPalindrome(ListNode head) { |
分隔链表
725. Split Linked List in Parts(Medium)
1 | Input: |
题目描述:把链表分隔成 k 部分,每部分的长度都应该尽可能相同,排在前面的长度应该大于等于后面的。
1 | public ListNode[] splitListToParts(ListNode root, int k) { |
链表元素按奇偶聚集
328. Odd Even Linked List (Medium)
1 | Example: |
1 | public ListNode oddEvenList(ListNode head) { |
树
递归
一棵树要么是空树,要么有两个指针,每个指针指向一棵树。树是一种递归结构,很多树的问题可以使用递归来处理。
树的高度
104. Maximum Depth of Binary Tree (Easy)
1 | public int maxDepth(TreeNode root) { |
平衡树
110. Balanced Binary Tree (Easy)
1 | 3 |
平衡树左右子树高度差都小于等于 1
1 | private boolean result = true; |
两节点的最长路径
543. Diameter of Binary Tree (Easy)
1 | Input: |
1 | private int max = 0; |
翻转树
226. Invert Binary Tree (Easy)
1 | public TreeNode invertTree(TreeNode root) { |
归并两棵树
617. Merge Two Binary Trees (Easy)
1 | Input: |
1 | public TreeNode mergeTrees(TreeNode t1, TreeNode t2) { |
判断路径和是否等于一个数
Leetcdoe : 112. Path Sum (Easy)
1 | Given the below binary tree and sum = 22, |
路径和定义为从 root 到 leaf 的所有节点的和
1 | public boolean hasPathSum(TreeNode root, int sum) { |
统计路径和等于一个数的路径数量
1 | root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8 |
路径不一定以 root 开头,也不一定以 leaf 结尾,但是必须连续。
1 | public int pathSum(TreeNode root, int sum) { |
子树
572. Subtree of Another Tree (Easy)
1 | Given tree s: |
1 | public boolean isSubtree(TreeNode s, TreeNode t) { |
树的对称
1 | 1 |
1 | public boolean isSymmetric(TreeNode root) { |
最小路径
111. Minimum Depth of Binary Tree (Easy)
树的根节点到叶子节点的最小路径长度
1 | public int minDepth(TreeNode root) { |
统计左叶子节点的和
404. Sum of Left Leaves (Easy)
1 | 3 |
1 | public int sumOfLeftLeaves(TreeNode root) { |
相同节点值的最大路径长度
687. Longest Univalue Path (Easy)
1 | 1 |
1 | private int path = 0; |
间隔遍历
337. House Robber III (Medium)
1 | 3 |
1 | public int rob(TreeNode root) { |
找出二叉树中第二小的节点
671. Second Minimum Node In a Binary Tree (Easy)
1 | Input: |
一个节点要么具有 0 个或 2 个子节点,如果有子节点,那么根节点是最小的节点。
1 | public int findSecondMinimumValue(TreeNode root) { |
层次遍历
使用 BFS 进行层次遍历。不需要使用两个队列来分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。
一棵树每层节点的平均数
637. Average of Levels in Binary Tree (Easy)
1 | public List<Double> averageOfLevels(TreeNode root) { |
得到左下角的节点
513. Find Bottom Left Tree Value (Easy)
1 | Input: |
1 | public int findBottomLeftValue(TreeNode root) { |
前中后序遍历
1 | 1 |
- 层次遍历顺序:[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 实现。
前序、中序、后序遍只是在对节点访问的顺序有一点不同,其它都相同。
① 前序
1 | void dfs(TreeNode root) { |
② 中序
1 | void dfs(TreeNode root) { |
③ 后序
1 | void dfs(TreeNode root) { |
非递归实现二叉树的前序遍历
144. Binary Tree Preorder Traversal (Medium)
1 | public List<Integer> preorderTraversal(TreeNode root) { |
非递归实现二叉树的后序遍历
145. Binary Tree Postorder Traversal (Medium)
前序遍历为 root -> left -> right,后序遍历为 left -> right -> root,可以修改前序遍历成为 root -> right -> left,那么这个顺序就和后序遍历正好相反。
1 | public List<Integer> postorderTraversal(TreeNode root) { |
非递归实现二叉树的中序遍历
94. Binary Tree Inorder Traversal (Medium)
1 | public List<Integer> inorderTraversal(TreeNode root) { |
BST
二叉查找树(BST):根节点大于等于左子树所有节点,小于等于右子树所有节点。
二叉查找树中序遍历有序。
修剪二叉查找树
669. Trim a Binary Search Tree (Easy)
1 | Input: |
题目描述:只保留值在 L ~ R 之间的节点
1 | public TreeNode trimBST(TreeNode root, int L, int R) { |
寻找二叉查找树的第 k 个元素
230. Kth Smallest Element in a BST (Medium)
中序遍历解法:
1 | private int cnt = 0; |
递归解法:
1 | public int kthSmallest(TreeNode root, int k) { |
把二叉查找树每个节点的值都加上比它大的节点的值
Convert BST to Greater Tree (Easy)
1 | Input: The root of a Binary Search Tree like this: |
先遍历右子树。
1 | private int sum = 0; |
二叉查找树的最近公共祖先
235. Lowest Common Ancestor of a Binary Search Tree (Easy)
1 | _______6______ |
1 | public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { |
二叉树的最近公共祖先
236. Lowest Common Ancestor of a Binary Tree (Medium)
1 | _______3______ |
1 | public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { |
从有序数组中构造二叉查找树
108. Convert Sorted Array to Binary Search Tree (Easy)
1 | public TreeNode sortedArrayToBST(int[] nums) { |
根据有序链表构造平衡的二叉查找树
109. Convert Sorted List to Binary Search Tree (Medium)
1 | Given the sorted linked list: [-10,-3,0,5,9], |
1 | public TreeNode sortedListToBST(ListNode head) { |
在二叉查找树中寻找两个节点,使它们的和为一个给定值
653. Two Sum IV - Input is a BST (Easy)
1 | Input: |
使用中序遍历得到有序数组之后,再利用双指针对数组进行查找。
应该注意到,这一题不能用分别在左右子树两部分来处理这种思想,因为两个待求的节点可能分别在左右子树中。
1 | public boolean findTarget(TreeNode root, int k) { |
在二叉查找树中查找两个节点之差的最小绝对值
530. Minimum Absolute Difference in BST (Easy)
1 | Input: |
利用二叉查找树的中序遍历为有序的性质,计算中序遍历中临近的两个节点之差的绝对值,取最小值。
1 | private int minDiff = Integer.MAX_VALUE; |
寻找二叉查找树中出现次数最多的值
501. Find Mode in Binary Search Tree (Easy)
1 | 1 |
答案可能不止一个,也就是有多个值出现的次数一样多,并且是最大的。
1 | private int curCnt = 1; |
Trie
Trie,又称前缀树或字典树,用于判断字符串是否存在或者是否具有某种字符串前缀。
实现一个 Trie
208. Implement Trie (Prefix Tree) (Medium)
1 | class Trie { |
实现一个 Trie,用来求前缀和
1 | Input: insert("apple", 3), Output: Null |
1 | class MapSum { |
图
二分图
如果可以用两种颜色对图中的节点进行着色,并且保证相邻的节点颜色不同,那么这个图就是二分图。
判断是否为二分图
785. Is Graph Bipartite? (Medium)
1 | Input: [[1,3], [0,2], [1,3], [0,2]] |
1 | Example 2: |
1 | public boolean isBipartite(int[][] graph) { |
拓扑排序
常用于在具有先序关系的任务规划中。
课程安排的合法性
1 | 2, [[1,0]] |
1 | 2, [[1,0],[0,1]] |
题目描述:一个课程可能会先修课程,判断给定的先修课程规定是否合法。
本题不需要使用拓扑排序,只需要检测有向图是否存在环即可。
1 | public boolean canFinish(int numCourses, int[][] prerequisites) { |
课程安排的顺序
210. Course Schedule II (Medium)
1 | 4, [[1,0],[2,0],[3,1],[3,2]] |
使用 DFS 来实现拓扑排序,使用一个栈存储后序遍历结果,这个栈的逆序结果就是拓扑排序结果。
证明:对于任何先序关系:v->w,后序遍历结果可以保证 w 先进入栈中,因此栈的逆序结果中 v 会在 w 之前。
1 | public int[] findOrder(int numCourses, int[][] prerequisites) { |
并查集
并查集可以动态地连通两个点,并且可以非常快速地判断两个点是否连通。
冗余连接
684. Redundant Connection (Medium)
1 | Input: [[1,2], [1,3], [2,3]] |
题目描述:有一系列的边连成的图,找出一条边,移除它之后该图能够成为一棵树。
1 | public int[] findRedundantConnection(int[][] edges) { |
位运算
1. 基本原理
0s 表示一串 0,1s 表示一串 1。
1 | x ^ 0s = x x & 0s = 0 x | 0s = 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&(n-1) 去除 n 的位级表示中最低的那一位。例如对于二进制表示 10110 100 ,减去 1 得到 10110011,这两个数相与得到 10110000。
- n&(-n) 得到 n 的位级表示中最低的那一位。-n 得到 n 的反码加 1,对于二进制表示 10110 100 ,-n 得到 01001100,相与得到 00000100。
- n-n&(~n+1) 去除 n 的位级表示中最高的那一位。
移位运算:
- >> n 为算术右移,相当于除以 2n;
- >>> n 为无符号右移,左边会补上 0。
- << n 为算术左移,相当于乘以 2n。
2. mask 计算
要获取 111111111,将 0 取反即可,~0。
要得到只有第 i 位为 1 的 mask,将 1 向左移动 i-1 位即可,1<<(i-1) 。例如 1<<4 得到只有第 5 位为 1 的 mask :00010000。
要得到 1 到 i 位为 1 的 mask,1<<(i+1)-1 即可,例如将 1<<(4+1)-1 = 00010000-1 = 00001111。
要得到 1 到 i 位为 0 的 mask,只需将 1 到 i 位为 1 的 mask 取反,即 ~(1<<(i+1)-1)。
3. Java 中的位操作
1 | static int Integer.bitCount(); // 统计 1 的数量 |
统计两个数的二进制表示有多少位不同
1 | Input: x = 1, y = 4 |
对两个数进行异或操作,位级表示不同的那一位为 1,统计有多少个 1 即可。
1 | public int hammingDistance(int x, int y) { |
使用 z&(z-1) 去除 z 位级表示最低的那一位。
1 | public int hammingDistance(int x, int y) { |
可以使用 Integer.bitcount() 来统计 1 个的个数。
1 | public int hammingDistance(int x, int y) { |
数组中唯一一个不重复的元素
1 | Input: [4,1,2,1,2] |
两个相同的数异或的结果为 0,对所有数进行异或操作,最后的结果就是单独出现的那个数。
1 | public int singleNumber(int[] nums) { |
找出数组中缺失的那个数
1 | Input: [3,0,1] |
题目描述:数组元素在 0-n 之间,但是有一个数是缺失的,要求找到这个缺失的数。
1 | public int missingNumber(int[] nums) { |
数组中不重复的两个元素
260. Single Number III (Medium)
两个不相等的元素在位级表示上必定会有一位存在不同。
将数组的所有元素异或得到的结果为不存在重复的两个元素异或的结果。
diff &= -diff 得到出 diff 最右侧不为 0 的位,也就是不存在重复的两个元素在位级表示上最右侧不同的那一位,利用这一位就可以将两个元素区分开来。
1 | public int[] singleNumber(int[] nums) { |
翻转一个数的比特位
1 | public int reverseBits(int n) { |
如果该函数需要被调用很多次,可以将 int 拆成 4 个 byte,然后缓存 byte 对应的比特位翻转,最后再拼接起来。
1 | private static Map<Byte, Integer> cache = new HashMap<>(); |
不用额外变量交换两个整数
1 | a = a ^ b; |
判断一个数是不是 2 的 n 次方
二进制表示只有一个 1 存在。
1 | public boolean isPowerOfTwo(int n) { |
利用 1000 & 0111 == 0 这种性质,得到以下解法:
1 | public boolean isPowerOfTwo(int n) { |
判断一个数是不是 4 的 n 次方
这种数在二进制表示中有且只有一个奇数位为 1,例如 16(10000)。
1 | public boolean isPowerOfFour(int num) { |
也可以使用正则表达式进行匹配。
1 | public boolean isPowerOfFour(int num) { |
判断一个数的位级表示是否不会出现连续的 0 和 1
693. Binary Number with Alternating Bits (Easy)
1 | Input: 10 |
对于 1010 这种位级表示的数,把它向右移动 1 位得到 101,这两个数每个位都不同,因此异或得到的结果为 1111。
1 | public boolean hasAlternatingBits(int n) { |
求一个数的补码
1 | Input: 5 |
题目描述:不考虑二进制表示中的首 0 部分。
对于 00000101,要求补码可以将它与 00000111 进行异或操作。那么问题就转换为求掩码 00000111。
1 | public int findComplement(int num) { |
可以利用 Java 的 Integer.highestOneBit() 方法来获得含有首 1 的数。
1 | public int findComplement(int num) { |
对于 10000000 这样的数要扩展成 11111111,可以利用以下方法:
1 | mask |= mask >> 1 11000000 |
1 | public int findComplement(int num) { |
实现整数的加法
371. Sum of Two Integers (Easy)
a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进位。
递归会终止的原因是 (a & b) << 1 最右边会多一个 0,那么继续递归,进位最右边的 0 会慢慢增多,最后进位会变为 0,递归终止。
1 | public int getSum(int a, int b) { |
字符串数组最大乘积
318. Maximum Product of Word Lengths (Medium)
1 | Given ["abcw", "baz", "foo", "bar", "xtfn", "abcdef"] |
题目描述:字符串数组的字符串只含有小写字符。求解字符串数组中两个字符串长度的最大乘积,要求这两个字符串不能含有相同字符。
本题主要问题是判断两个字符串是否含相同字符,由于字符串只含有小写字符,总共 26 位,因此可以用一个 32 位的整数来存储每个字符是否出现过。
1 | public int maxProduct(String[] words) { |
统计从 0 ~ n 每个数的二进制表示中 1 的个数
对于数字 6(110),它可以看成是 4(100) 再加一个 2(10),因此 dp[i] = dp[i&(i-1)] + 1;
1 | public int[] countBits(int num) { |
参考资料
- Leetcode
- Weiss M A, 冯舜玺. 数据结构与算法分析——C 语言描述[J]. 2004.
- Sedgewick R. Algorithms[M]. Pearson Education India, 1988.
- 何海涛, 软件工程师. 剑指 Offer: 名企面试官精讲典型编程题[M]. 电子工业出版社, 2014.
- 《编程之美》小组. 编程之美[M]. 电子工业出版社, 2008.
- 左程云. 程序员代码面试指南[M]. 电子工业出版社, 2015.