算法
双指针
双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。
1、两数之和(Easy)
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
暴力法
两层for循环,检测两个数之和是否为目标值,如果是,则返回它们的下标。当然,这个算法性能太差了,O(n^2),需要优化。
class Solution {
public int[] twoSum(int[] nums, int target) {
int [] indexs = new int[2];
for(int i=0;i<nums.length;i++){
for(int j=i+1;j<nums.length;j++){
if(nums[i]+nums[j]==target){
indexs[0]=i;
indexs[1]=j;
}
}
}
return indexs;
}
}
哈希法
hashMap可以存放值与索引的关联,在这道题中,也是讨论值与索引,而哈希可以很快的根据索引找到值,不过需要存放所有数据,是一种空间换时间的策略。时间复杂度O(n),空间复杂度O(n)。
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer,Integer> map = new HashMap<>();
for(int i=0;i<nums.length;i++){
map.put(nums[i],i);
}
for(int i=0;i<nums.length;i++){
int complement = target-nums[i];
if(map.containsKey(complement)&&map.get(complement)!=i){
return new int[]{i,map.get(complement)};
}
}
throw new IllegalArgumentException("No two sum solution");
}
}
一次哈希法
当将数组元素添加到哈希表中,可以反过来再检查哈希表中是否有当前元素对应的元素,如果有的话,则返回它们对应的索引。
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer,Integer> map = new HashMap<>();
for(int i=0;i<nums.length;i++){
int complement = target-nums[i];
if(map.containsKey(complement)){
return new int[]{map.get(complement),i};
}
map.put(nums[i],i);
}
throw new IllegalArgumentException("No two sum solution");
}
}
双指针法
用low指针指向数组中小的元素,用high指针指向数组中大的元素
sum = num[low] + num[high]
- sum == target: 输出low,high
- sum < target: low++,小的元素增大一点
- sum>target: high–,大的元素减小一点
时间复杂度O(n), 空间复杂度O(1)
class Solution {
public int[] twoSum(int[] numbers, int target) {
int low = 0,high = numbers.length - 1;
while(low < high){
int sum = numbers[low] + numbers[high];
if(sum == target){
return new int[] {low + 1,high + 1};
}else if(sum < target){
low++;
}else{
high--;
}
}
return new int[]{-1, -1};
}
}
2、平方数之和(Easy)
给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a^2 + b^2 = c。
示例1:
输入: 5
输出: True
解释: 1 * 1 + 2 * 2 = 5示例2:
输入: 3
输出: False
双指针法
给定一个非负整数c,假设有a^2 + b^2 == c,则a和b的大小不会超过c的平方根。
让a指向较小的数,b指向较大的数。sum = a^2 + b^2
- sum == c,即为答案,返回true
- sum < c,则要将sum调大一点,a++
- sum>c,则要将sum调小一点,b–
重复上面操作,a>b时停止循环,返回false。
class Solution {
public boolean judgeSquareSum(int c) {
int a = 0;
int b = (int)Math.sqrt(c);
while(a <= b){
int sum = a * a + b * b;
if(sum == c){
return true;
}else if(sum < c){
a++;
}else{
b--;
}
}
return false;
}
}
3、反转字符串中的元音字母(Easy)
编写一个函数,以字符串作为输入,反转该字符串中的元音字母。
示例 1:
输入: “hello”
输出: “holle”
示例 2:输入: “leetcode”
输出: “leotcede”
说明:
元音字母不包含字母"y"
双指针法
思路一
用low指针指向左边的元音元素,用high指针指向右边的元音元素。然后将两者进行交换。
用两个循环分别找到左边的元音元素、右边的元音元素。
class Solution {
public String reverseVowels(String s) {
int low = 0, high = s.length() - 1;
char[] str = s.toCharArray();
while(low < high){
while(low < high && !isVowels(str[low])) low++;
while(low < high && !isVowels(str[high])) high--;
if(low > high)
break;
char t = str[low];
str[low] = str[high];
str[high] = t;
low++;
high--;
}
return new String(str);
}
public boolean isVowels(char c){
if(c!='a' && c!='e' && c!='i' && c!='o' && c!='u' && c!='A' && c!='E' && c!='I' && c!='O' && c!='U'){
return false;
}
return true;
}
}
思路二
使用两个指针,一个指针从头到尾遍历,另一个指针从尾到头遍历,如果两个都指向元音字母的时候则进行交换,否则指向辅音字母的指针继续移动,不进行交换。
class Solution {
public final static HashSet<Character>isVowels= new HashSet<>(
Arrays.asList('a','e','i','o','u','A','E','I','O','U')
);
public boolean isVowels(char c){
if(c!='a' && c!='e' && c!='i' && c!='o' && c!='u' && c!='A' && c!='E' && c!='I' && c!='O' && c!='U'){
return false;
}
return true;
}
public String reverseVowels(String s) {
int low = 0, high = s.length() - 1;
char[] str = s.toCharArray();
while(low < high){
char left = s.charAt(low);
char right = s.charAt(high);
if(!isVowels(left)){
low++;
}else if(!isVowels(right)){
high--;
}else{
char t = str[low];
str[low] = str[high];
str[high] = t;
low++;
high--;
}
}
return new String(str);
}
}
4、验证回文字符串 Ⅱ(Easy)
给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。
示例 1:
输入: “aba”
输出: True
示例 2:输入: “abca”
输出: True
解释: 你可以删除c字符。
双指针法
判断回文的方法:使用左右指针,左指针从前往后,右指针从后往前。左指针指向的元素和右指针指向的元素相等时,则继续移动指针。当两者不相等时,则不是回文。
本题可以最多删除一个字符,那么可以删除左边的,也可以删除右边的,如果有其一是回文则整体也是回文。要注意的是,只需要判断子字符串即可,因为已经判断了的字符是相等的了,两端就不需要再判断了。
class Solution {
public boolean validPalindrome(String s) {
for(int i = 0,j = s.length() - 1;i < j;i++, j--){
if(s.charAt(i) != s.charAt(j)){
return isPalindrome(s,i+1, j) || isPalindrome(s,i,j - 1);
}
}
return true;
}
public boolean isPalindrome(String s, int i ,int j){
while(i < j){
if(s.charAt(i++) != s.charAt(j--))
return false;
}
return true;
}
}
5、合并两个有序数组[Easy]
输入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3输出: [1,2,2,3,5,6]
双指针法
用一个指针指向数组一,用另一个指针指向数组二。
第一种思路就是用归并排序中merge操作,使用额外数组,将两个数组中的元素从小到大,填入额外数组中。
第二种思路就是不使用额外数组,而是将指针从尾开始,将指针指向元素较大的填入大数组的末尾。这样可以防止从头开始填入导致大数组中的元素丢失。
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int p1 = 0,p2 = 0;
int[] nums3 = new int[nums1.length];
int i = 0;
while(p1<m && p2<n){
if(nums1[p1] <= nums2[p2]){
nums3[i++] = nums1[p1++];
}else if(nums1[p1] > nums2[p2]){
nums3[i++] = nums2[p2++];
}
}
while(p1<m){
nums3[i++] = nums1[p1++];
}
while(p2<n){
nums3[i++] = nums2[p2++];
}
for( i = 0;i < nums3.length;i++){
nums1[i] = nums3[i];
}
}
}
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int p1 = m-1,p2 = n-1;
int indexMerge = m + n -1;
while(p1 >=0 || p2>=0){
if(p1 < 0){
nums1[indexMerge--] = nums2[p2--];
}else if(p2 < 0){
nums1[indexMerge--] = nums1[p1--];
}else if(nums1[p1] > nums2[p2]){
nums1[indexMerge--] = nums1[p1--];
}else{
nums1[indexMerge--] = nums2[p2--];
}
}
}
}
6、环形链表(Easy)
题目
使用快慢指针,慢指针每次走一个,快指针每次走两个,如果有环的话,那么快指针和满指针一定会相遇的。
以慢指针为参考系,满指针就相等于原地不动,快指针就每次走一个,这样在环里就一定会相遇的。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
if(head == null)
return false;
ListNode p1 = head,p2 = head.next;
while(p1 !=null && p2 != null && p2.next !=null){
if(p1 == p2){
return true;
}
p1 = p1.next;
p2 = p2.next.next;
}
return false;
}
}
7、通过删除字母匹配到字典里最长单词(Easy)
给定一个字符串和一个字符串字典,找到字典里面最长的字符串,该字符串可以通过删除给定字符串的某些字符来得到。如果答案不止一个,返回长度最长且字典顺序最小的字符串。如果答案不存在,则返回空字符串。 示例 1: 输入: s = "abpcplea", d = ["ale","apple","monkey","plea"] 输出: "apple" 示例 2: 输入: s = "abpcplea", d = ["a","b","c"] 输出: "a"
双指针法:一个指针指向一个字符串s,另一个指针指向d中的某个字符串target。然后检查target是否是s的子串。
class Solution {
public String findLongestWord(String s, List<String> d) {
String longestWord = "";
for(String target:d){
int l1 = longestWord.length(),l2 = target.length();
if(l1 > l2 ||(l1 == l2 && longestWord.compareTo(target)<0))
continue;
else if(isSubString(target,s)){
longestWord = target;
}
}
return longestWord;
}
public boolean isSubString(String target,String s){
int i = 0,j = 0;
while(i < s.length() && j< target.length()){
if(target.charAt(j) == s.charAt(i))
j++;
i++;
}
return j == target.length();
}
}
排序
堆
确定第k个最大的元素,可以利用小根堆,堆顶元素就是。
快速选择
利用快速排序可以确定一个元素的位置的特性,partition的功能就是确定一个元素的位置,还有数组序列基本上有序。
在选择基准元素时,要随机选择。否则会退化成O(n^2).
1、数组中的第K个最大元素(Medium)
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
堆
class Solution {
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>();
for(int t:nums){
heap.add(t);
if(heap.size() > k){
heap.poll();
}
}
return heap.peek();
}
}
快速选择
class Solution {
public int findKthLargest(int[] nums, int k) {
int L = 0,R = nums.length - 1;
k = nums.length - k;
while(true){
int pos = partition(nums,L ,R);
if(pos == k){
return nums[pos];
}else if(pos < k){
L = pos + 1;
}else{
R = pos - 1;
}
}
}
public int partition(int[] nums,int L ,int R){
swap(nums,L + (int)Math.random()*(R - L + 1),R);
int num = nums[R];
int less = L - 1;
int more = R + 1;
int cur = L;
while(cur < more){
if(nums[cur] < num){
swap(nums,++less,cur++);
}else if(nums[cur]>num){
swap(nums,--more,cur);
}else{
cur++;
}
}
return more-1;
}
public void swap(int[] nums,int i ,int j){
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
}
桶排序
1、前 K 个高频元素(Medium)
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2 输出: [1,2]
示例 2:输入: nums = [1], k = 1 输出: [1]
设置若干个桶,每个桶存放同一频率的数。桶的数量最多是输入数据的数量。
桶的顺序是频率由小到大的,那么从后往前,输出非空桶中的数即可。
首先这题需要使用hashmap来获取每个数的频率。接着使用List 做桶,存放同一频率的数。
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);
}
List<Integer>[] frequecyList = new ArrayList[nums.length + 1];
for(int key: map.keySet()){
int frequecy = map.get(key);
if(frequecyList[frequecy] == null){
frequecyList[frequecy] = new ArrayList<>();
}
frequecyList[frequecy].add(key);
}
List<Integer> topKey = new ArrayList<>();
for(int i = frequecyList.length - 1;topKey.size() < k;i--){
if(frequecyList[i] == null){
continue;
}
if(frequecyList[i].size() <= k - topKey.size()){
topKey.addAll(frequecyList[i]);
}
}
int[] topKeyInt = new int[k];
int index = 0;
for(Integer i: topKey){
topKeyInt[index++] = i;
}
return topKeyInt;
}
}
2、根据字符出现频率排序
同样的设置若干个桶,字符出现的频率不同,放进不同的桶,同一个桶可能会有多个字符。
频率依次从小到大,从后往前依次从桶里取数(如果桶非空的话)。
注意:桶的数量应该设置所有数的数量加1。比如有5个字符的字符串“eeeee”,那么桶的数量应该设置为6,这样桶的下标可达5。
class Solution {
public String frequencySort(String s) {
Map<Character,Integer> map = new HashMap<>();
for(int i = 0;i<s.length();i++){
map.put(s.charAt(i),map.getOrDefault(s.charAt(i),0) + 1);
}
char[] resChar = new char[s.length()];
List<Character>[] frequency = new ArrayList[s.length() + 1];
for(Character key:map.keySet()){
int frequ = map.get(key);
if(frequency[frequ] == null){
frequency[frequ] = new ArrayList<>();
}
frequency[frequ].add(key);
}
int index = 0;
for(int i = frequency.length - 1;i>=0;i--){
if(frequency[i] == null){
continue;
}
for(Character ch:frequency[i]){
int k = i;
while(k-- != 0){
resChar[index++] = ch;
}
}
}
return new String(resChar);
}
}
下面代码使用了java的StringBuilder类,它是可变字符对象。
StringBuilder strBd = new StringBuilder();
for(int i = frequency.length - 1;i>=0;i--){
if(frequency[i] == null){
continue;
}
for(Character ch:frequency[i]){
for(int j = i;j>0;j--){
strBd.append(ch);
}
}
}
return strBd.toString();
荷兰国旗问题
荷兰国旗包含三种颜色:红、白、蓝。
有三种颜色的球,算法的目标是将这三种球按颜色顺序正确地排列。它其实是三向切分快速排序的一种变种,在三向切分快速排序中,每次切分都将数组分成三个区间:小于切分元素、等于切分元素、大于切分元素,而该算法是将数组分成三个区间:等于红色、等于白色、等于蓝色。
1、按颜色进行排序
给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
注意: 不能使用代码库中的排序函数来解决这道题。
示例:
输入: [2,0,2,1,1,0] 输出: [0,0,1,1,2,2]
进阶:一个直观的解决方案是使用计数排序的两趟扫描算法。 首先,迭代计算出0、1 和 2 元素的个数,然后按照0、1、2的排序,重写当前数组。
你能想出一个仅使用常数空间的一趟扫描算法吗?
思路:(0元素组,1元素组,2元素组)。用less、more表示【0…less, less+1…more-1, more…length-1】。使用cur表示当前数组下标的位置。一次遍历,检测到1元素 ,cur++,不进行交换;检测到0元素,和0元素组的下一个位置进行交换(swap(nums,cur++,++less);),当前数组下标加一;检测到2元素,swap(nums,cur,–more);
class Solution {
public void sortColors(int[] nums) {
if(nums == null || nums.length < 2){
return;
}
quickSort(nums, 0 ,nums.length - 1);
}
public static int[] quickSort(int[] nums ,int L ,int R){
int less = L - 1;
int more = R + 1;
int cur = L;
while(cur < more){
if(nums[cur] == 1){
cur++;
}else if(nums[cur] == 0){
swap(nums,cur++,++less);
}else{
swap(nums,cur,--more);
}
}
return new int[]{less + 1, more - 1};
}
public static void swap(int[] nums,int i, int j){
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
}
贪心
1 分发饼干(Easy)
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i ,都有一个胃口值 gi
,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j ,都有一个尺寸 sj 。如果 sj >= gi ,我们可以将这个饼干 j
分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。注意:
你可以假设胃口值为正。 一个小朋友最多只能拥有一块饼干。
示例 1:
输入: [1,2,3], [1,1]
输出: 1
解释: 你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。 所以你应该输出1。 示例 2:输入: [1,2], [1,2,3]
输出: 2
解释: 你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。 你拥有的饼干数量和尺寸都足以让所有孩子满足。 所以你应该输出2.
**思路:**每次分配时都给每一个孩子分配一个当前看来最优的一个值,然后全局就会最优。算法的时间复杂度是:O(m + n)。
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int gi,si;
gi = si = 0;
while(gi < g.length && si < s.length){
if(g[gi] <= s[si]){
gi++;
}
si++;
}
return gi;
}
}
2 不重叠区间的数目(Medium)
problem
Non-overlapping Intervals
计算一组区间不重叠需要移除的区间的个数.
我们可以先得到组成不重叠区间的个数,然后再将总区间的个数减去它就得到需要移除区间的个数.
区间的末尾很重要: 因为区间的末尾小, 则可以给后面留下更多的空间
先按照区间的末尾进行从小到大的排序. 然后每次选择结尾最小, 还要跟前面区间不重叠的区间.
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
if(intervals.length == 0){
return 0;
}
Arrays.sort(intervals, Comparator.comparingInt(o->o[1]));
int cnt = 1;
int end = intervals[0][1];
for(int i = 1; i < intervals.length; i++){
//当前区间 与上一个区间没有重叠
if(intervals[i][0] < end){
continue;
}
cnt++;
end = intervals[i][1];
}
return intervals.length - cnt;
}
}
上面排序使用的是Lambda表达式, 创建comparator会需要更多的时间. 可以使用普通的创建Comparator方法:
Arrays.sort(intervals, new Comparator<int[]>(){
public int compare(int[] o1, int[] o2){
return o1[1] - o2[1];
}
}
);
3 投飞镖刺破气球(Medium)
题目的含义在于计算有多少个重叠的区间, 与Non-overlapping intervals的区别在于: [1, 2] 和 [2 , 3]也是重叠区间.
class Solution {
public int findMinArrowShots(int[][] points) {
if(points == null || points.length ==0){
return 0;
}
Arrays.sort(points, new Comparator<int[]>(){
public int compare(int[] o1,int [] o2){
return o1[1] - o2[1];
}
});
int end = points[0][1];
int cnt = 1;
for(int i = 1;i < points.length;i++){
if(points[i][0] <= end){
continue;
}
cnt++;
end = points[i][1];
}
return cnt;
}
}
4 根据身高和序号重组队列(Medium)
Queue Reconstruction by Height
思路:
-
先按身高降序排序,相同身高按k升序排序,经过此次排序后高的人一定在矮的人的前面并且相同高度的人的相对顺序就是最终结果的相对顺序。请记住这两点,敲黑板
-
创建一个集合,这个集合的每个元素是一个一维数组,也就是我们二维数组的一行。
-
以行为单位遍历排好序的people[][]数组,假设每行数据是p[], 把每行元素插入到集合的索引为p[1]的位置,
-
把集合中的数据转换为一个二维数组,返回即是正确结果
下面解释为什么经过第三步后就达到了我们题目要求的输出结果:
经过第一步排序后高的人一定在矮的人的前面并且相同高度的人的相对顺序就是最终结果的相对顺序,所以 在进行第三步的过程中,高的人的数据肯定是先被存入集合的,所以每当我们取出一行数据,集合中已有的元素的身高肯定都是大于等于当前元素的身高的,所以当我们取出p[]数组后,发现前面应该有p[1]个人比自己高或者高度和自己相同,那么当前元素就应该排在集合的p[1]下标的位置(仔细想想是不是),好比说目前有一个队列的人,这些人要么比你高,要么和你一样高,现在要你插入入队中,保证你前面有p[1]个人的身高大于等于你,你是不是应该排在索引为p[1]的位置
所以经过第三步的插入操作后,把每个人都插入到了正确的位置,所以根据这个集合转换的二维数组当然就是正确结果了。
下面是完整代码:
class Solution {
public int[][] reconstructQueue(int[][] people) {
// 先按身高降序排序,相同身高按k升序排序
Arrays.sort(people, new Comparator<int[]>(){ // 排序的每个元素是一个一维数组,也就是原二维数组的一行数据
public int compare(int[] o1, int[] o2){ // 二维数组排序还能这么排,真是学到了,学到了
return o1[0] == o2[0] ? o1[1] - o2[1] : o2[0] - o1[0];
}
});
// 遍历数组,把每行元素添加到一个集合的p[1]位置
List<int[]> list = new LinkedList<int[]>(); // 把二维数组一行元素作为一个对象
for(int[] p : people){
list.add(p[1], p);
}
int n = list.size();
// 返回集合转换为数组的数组
return list.toArray(new int[n][2]); // 将集合转换成二维数组
//return list.toArray(people); 也可写成这样.
}
}
5 买卖股票最大的收益(Easy)
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 =6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
思路: 记录前面最小的价格,并将当前价格与最小价格的差值作为最大值, 也就是局部最大值 . 这样中途一定会出现整体的最大值, 结果就会得到整体的最大值.
class Solution {
public int maxProfit(int[] prices) {
if(prices == null || prices.length == 0){
return 0;
}
int soFarMin = 0,max = 0;
for(int i = 1; i < prices.length;i++){
if(prices[soFarMin] > prices[i]){
soFarMin = i;
}else{
if(max < prices[i] - prices[soFarMin]){
max = prices[i] - prices[soFarMin];
}
}
}
return max;
}
}
6 买卖股票的最大收益 II(medium)
a <= b <= c <= d, 这种情况应该是d - a 为最大收益了.
可是 d - a = (d -c ) + (c - b) + (b - a).
“我” 是一个很贪心的人,每次当前的股票比前一支股票高就卖出.
可以找到峰谷和峰顶, 也可以只要有上升段就买入卖出.结果是一样的.
class Solution {
public int maxProfit(int[] prices) {
if(prices == null || prices.length == 0){
return 0;
}
int maxProfit = 0;
for(int i = 1;i < prices.length;i++){
if(prices[i] > prices[i - 1]){
maxProfit += prices[i] - prices[i - 1];
}
}
return maxProfit;
}
}
7 种花问题(Easy)
problem
如果两边都没有种花, 那么此地可以种花, 不管最后能不能获得最多的花, 只管局部能种的花,最后获得就是最多的.
class Solution {
public boolean canPlaceFlowers(int[] flowerbed, int n) {
int cnt = 0;
for(int i = 0; i < flowerbed.length && cnt < n;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;
}
}
8 判断子序列(Easy)
示例 1: s = “abc”, t = “ahbgdc”
返回 true.
示例 2: s = “axc”, t = “ahbgdc”
返回 false.
在这道题中,使用到String类的方法:
public int indexOf(int ch, int fromIndex): 返回从 fromIndex 位置开始查找指定字符在字符串中第一次出现处的索引,如果此字符串中没有这样的字符,则返回 -1。
class Solution {
public boolean isSubsequence(String s, String t) {
int index = -1;
for(char c : s.toCharArray()){
index = t.indexOf(c, index + 1);
if(index == -1){
return false;
}
}
return true;
}
}
9 修改一个数成为非递减数组(Easy)
贪心算法, 使得局部最优, 从而全局最优. 每步只考虑局部当前的情况, 在这题中. 要想使得整个数组成为一个非递减数组, 我们可以先得到局部的非递减数组, 其中有很多种情况, 我们只取其中一种, 那就是不会影响到后续的操作. 是为最优.比如数组 2 4 3 3 当i = 2时 , 要想获得非递减数组, 有两种 2 3 3 , 2 4 4 .如果是第二种, 那么就会影响到后面的操作, 出现4 > 3的情况, 而第一种就是非递减的情况. 所以说第一种情况是局部的最优解.
每次获得 i 之前的非递减数组 , 而且还是最优的情况, 不会影响到后续操作的非递减数组.
具体的操作有两种情况, 首先一般的情况就是 当nums[i - 1] > nums[i] 时, 为了让i 之前成为非递减数组, 改成nums[i-1] = nums[i], 而不是nums[i] = nums[i-1], 虽然都是非递减数组,但是第二种情况可能会影响后续的操作, nums[i]的值变大了, 可能比nums[i+1]还大.
注意特殊的情况的, nums[i-2] > nums[i] 例如 数组3 4 2 3 当i = 2时, 如果改成nums[i-1] = nums[i] 就会出现3 2 2 , 就不是局部非递减数组了.此时的操作应该是: nums[i] = nums[i - 1] , 即 3 4 4 这样就是局部非递减数组了.
class Solution {
public boolean checkPossibility(int[] nums) {
int cnt = 0;
for(int i = 1; i < nums.length; i++){
if(nums[i] >= nums[i - 1]){
continue;
}
cnt++;
if(i - 2 >=0 && nums[i-2] > nums[i]){
nums[i] = nums[i - 1];
}else{
nums[i - 1] = nums[i];
}
}
return cnt<=1;
}
}
10 最大子序和(Easy)
problem
思路: 若当前指针所指元素之前的和小于0, 则丢弃当前元素之前的数列.
class Solution {
public int maxSubArray(int[] nums) {
if(nums == null || nums.length == 0){
return 0;
}
int preSum = nums[0];
int maxSum = nums[0];
for( int i = 1;i < nums.length ;i++){
preSum = preSum > 0 ? preSum + nums[i] : nums[i];
maxSum = Math.max(maxSum, preSum);
}
return maxSum;
}
}
11 划分字母区间(Medium)
思路
策略就是不断地选择从最左边起最小的区间。可以从第一个字母开始分析,假设第一个字母是 ‘a’,那么第一个区间一定包含最后一次出现的 ‘a’。但第一个出现的 ‘a’ 和最后一个出现的 ‘a’ 之间可能还有其他字母,这些字母会让区间变大。举个例子,在 “abccaddbeffe” 字符串中,第一个最小的区间是 “abccaddb”。
通过以上的分析,我们可以得出一个算法:对于遇到的每一个字母,去找这个字母最后一次出现的位置,用来更新当前的最小区间。
算法
定义数组 last[char] 来表示字符 char 最后一次出现的下标。定义 anchor 和 j 来表示当前区间的首尾。如果遇到的字符最后一次出现的位置下标大于 j, 就让 j=last[c] 来拓展当前的区间。当遍历到了当前区间的末尾时(即 i==j ),把当前区间加入答案,同时将 start 设为 i+1 去找下一个区间。
class Solution {
public List<Integer> partitionLabels(String S) {
if(S == null || S.length() == 0){
return null;
}
int[] postion = new int[26];
char[] chs = S.toCharArray();
for(int i = 0;i < chs.length;i++){
postion[chs[i] - 'a'] = i;
}
int anchor = 0, pos = 0, end = 0;
List<Integer> resLs = new ArrayList<>();
while(pos < chs.length){
end = Math.max(end, postion[chs[pos] - 'a']);
if(end == pos){
resLs.add(end - anchor + 1);
anchor = end + 1;
}
pos++;
}
return resLs;
}
}
二分查找
从一个有序数组中找一个元素, 查找的方法有很多种, 比如顺序查找, 二分查找,而二分查找算法是比较优秀的算法. 每次查找都可以砍掉一半, 时间复杂度为O(logn).
中值的计算
mid = ( low + high) / 2 算出来的中值可能会大于整型, 造成溢出.最佳的写法应该是: mid = low + (high - low ) /2;
public int binarySearch(int[] nums, int key) {
int l = 0, h = nums.length - 1;
while (l <= h) {
int m = l + (h - l) / 2;
if (nums[m] == key) {
return m;
} else if (nums[m] > key) {
h = m - 1;
} else {
l = m + 1;
}
}
return -1;
}
1 求开方
题目
一个数x的开方 0~x之间,通过二分查找来求x的开方, low =1 ,high = x,得到中值mid后, 再求sqrt = x / mid, 如果mid == sqrt, 则sqrt就是x的开方, 直接返回. while循环结束的条件是 high < low, 而开方又是取整数, 所以应该返回high而不是low
class Solution {
public int mySqrt(int x) {
int low = 1, high = x;
while(low <= high){
int mid = low + (high - low ) /2 ;
int sqrt = x / mid;
if(mid == sqrt){
return mid;
}else if(mid < sqrt){
low = mid + 1;
}else{
high = mid - 1;
}
}
return high;
}
}
2 寻找比目标字母大的最小字母
给你一个排序后的字符列表 letters ,列表中只包含小写英文字母。另给出一个目标字母
target,请你寻找在这一有序列表里比目标字母大的最小字母。在比较时,字母是依序循环出现的。举个例子:
如果目标字母 target = ‘z’ 并且字符列表为 letters = [‘a’, ‘b’],则答案返回 ‘a’
思路: 题目是要寻找比目标字母大的最小字母,使用二分查找算法, 当前查找元素小于或者等于目标字母的情况都将low = mid + 1 , 反之则将high = mid + 1. 需要注意的是, 循环退出的结果是 low > high. 而题目要找比目标字母大的, 所以应该选择下标low的元素.
class Solution {
public char nextGreatestLetter(char[] letters, char target) {
int low = 0, high = letters.length - 1;
while(low <= high){
int mid = low +(high - low ) / 2;
if(letters[mid]<= target){
low = mid + 1;
}else{
high = mid - 1;
}
}
return low == letters.length ? letters[0] : letters[low];
}
}
有序数组中的单一元素(Medium)
思路: 与原数组一样,包含单个元素的子数组元素个数必为奇数,不包含单个元素的子数组必为偶数。 因此,当原数组移除一对元素后,然后计算出哪一侧的子数组元素个数是奇数,这样我们就能够知道下一步应该在哪一侧进行搜索。
令 index 为 Single Element 在数组中的位置。在 index 之后,数组中原来存在的成对状态被改变。如果 mid 为偶数,并且 mid + 1 < index,那么 nums[mid] == nums[mid + 1];mid + 1 >= index,那么 nums[mid] != nums[mid + 1]。
从上面的规律可以知道,如果 nums[mid] == nums[mid + 1],那么 index 所在的数组位置为 [mid + 2, h],此时令 l = mid + 2;如果 nums[mid] != nums[mid + 1],那么 index 所在的数组位置为 [l, mid],此时令 h = mid。
因为 h 的赋值表达式为 h = m,那么循环条件也就只能使用 l < h 这种形式。
class Solution {
public int singleNonDuplicate(int[] nums) {
int low = 0, high = nums.length - 1;
while(low < high){
int mid = low + (high - low) / 2;
if(mid % 2 == 1){
mid--;//保证 l/h/m 都在偶数位,使得查找区间大小一直都是奇数
}
if(nums[mid] == nums[mid + 1]){
low = mid + 2;
}else{
high = mid;
}
}
return nums[low];
}
}
第一个错误的版本
题目
题目描述:给定一个元素 n 代表有 [1, 2, …, n] 版本,在第 x 位置开始出现错误版本,导致后面的版本都错误。可以调用 isBadVersion(int x) 知道某个版本是否错误,要求找到第一个错误的版本。
m表示中间判断的位置, 如果m是错误版本, 则第一个错误的版本范围是[l, m], 令h = m, 反之, 第一个错误的版本范围是[m+1, h], 令l = m + 1
因为 h 的赋值表达式为 h = m,因此循环条件为 l < h。
public class Solution extends VersionControl {
public int firstBadVersion(int n) {
int low = 1, high = n;
while(low < high){
int mid = low + (high - low ) / 2;
if(isBadVersion(mid)){
high = mid;
}else{
low = mid + 1;
}
}
return low;
}
}
旋转数组的最小数字
题目
思路:
分界点左边的数均大于分界点右边的数.
mid表示中间的数,如果nums[mid] <nums[h] ,则说明应该往左侧查找
反之,则应该往右侧查找.
![]](https://img-blog.csdnimg.cn/20200710084528403.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3ppbW9qaWFuZw==,size_16,color_FFFFFF,t_70)
class Solution {
public int findMin(int[] nums) {
int l = 0,h = nums.length - 1;
while(l < h){
int m = l + (h - l) / 2;
if(nums[m] < nums[h]){
h = m ;
}else{
l = m + 1;
}
}
return nums[l];
}
}
查找区间
题目描述:给定一个有序数组 nums 和一个目标 target,要求找到 target 在 nums 中的第一个位置和最后一个位置。
思路:
可以用二分查找找出第一个位置和最后一个位置,但是寻找的方法有所不同,需要实现两个二分查找。我们将寻找 target 最后一个位置,转换成寻找 target+1 第一个位置,再往前移动一个位置。这样我们只需要实现一个二分查找代码即可。
在寻找第一个位置的二分查找代码中,需要注意 h 的取值为 nums.length,而不是 nums.length - 1。先看以下示例:
nums = [2,2], target = 2
如果 h 的取值为 nums.length - 1,那么 last = findFirst(nums, target + 1) - 1 = 1 - 1 = 0。这是因为 findLeft 只会返回 [0, nums.length - 1] 范围的值,对于 findFirst([2,2], 3) ,我们希望返回 3 插入 nums 中的位置,也就是数组最后一个位置再往后一个位置,即 nums.length。所以我们需要将 h 取值为 nums.length,从而使得 findFirst返回的区间更大,能够覆盖 target 大于 nums 最后一个元素的情况。
class Solution {
public int findFirst(int [] nums, int target){
int l = 0, h = nums.length;
while(l < h){
int m = l + (h - l)/ 2;
if(nums[m]>=target){
h = m;
}else{
l = m + 1;
}
}
return l;
}
public int[] searchRange(int[] nums, int target) {
int left = findFirst(nums, target);
int right = findFirst(nums, target+ 1) - 1;
if(left == nums.length || nums[left] !=target){
return new int[]{-1,-1};
} else{
return new int[]{left, right};
}
}
}
分治
给表达式加括号(Medium)
解题思路
首先明确一点:不管多长的多项式运算,最后一步一定是两个明确数值的运算。
如 8*(4-7)-6*2 最后一步是 (-24)-12, 其中-24和6是明确的
再分解一下左边:上例中的得到 -24 ,也是同样有最后一步是两个明确的数值运算
8 * (-3),同样的右边是 6 * 2
再分解一下左边,容易得到结束点是:剩下一个数值或者两个(理论上也可以用 一个 统一解释)
完整的分解过程(宽度):
输入:23-45(已转换)
第一步的分解:a:2和3-45;b:23和45;c:23-4和5;
第二部的细分:a部分:左边:直接返回2 右边:在分解,aa:3和45,ab:3-4和5
补充:同一个符号的左右各会产生至少一个,至多多个数值。左多个数和右多个数进行运算(两个循环搞定所有结果) 如a:2和3-45 ,左边产生2,右边产生-5和-17,那么a这一级最终有-10,-34两个数值。
主要就是遍历符号,分成左右两部分不断分,最后在合。
因为每一层都有贮存开销,在空间复杂度还需要优化。(时间效果2ms)
下面代码的两个内层for循环 中列表 left\ right中只会有一个数
class Solution {
public List<Integer> diffWaysToCompute(String input) {
List<Integer> way = new ArrayList<>();
for(int i = 0; i < input.length();i++){
char op = input.charAt(i);
if(op == '+' || op == '*' || op == '-' ){
List<Integer> left = diffWaysToCompute(input.substring(0,i));
List<Integer> right = diffWaysToCompute(input.substring(i+1));
for(Integer l :left){
for(Integer r:right){
switch(op){
case '+':way.add(l + r);break;
case '-':way.add(l - r);break;
case '*':way.add(l * r);break;
}
}
}
}
}
if(way.size() == 0){
way.add(Integer.valueOf(input));
}
return way;
}
}
不同的二叉搜索树 II(Medium)
题目
给定一个数字 n,要求生成所有值为 1…n 的二叉搜索树。
思路:
要求整棵树, 可以分治求它的子树,整棵树的范围是[1, n], 而1到n,每个数都可以作为根, 假设i为根, 则左子树的范围是[1,i-1], 而右子树的范围则是[i+1, n].
所以对对于任意一棵树根 i 的范围可以是 [s,e], 而它的左子树范围则是[s, i - 1], 右子树范围则是[i + 1 , e]
需要注意的是, 当s > e时, 这说明没有比这个数小,或者比这个数大的数了 所以应该返回null的list.
class Solution {
public List<TreeNode> generateTrees(int n) {
if(n < 1){
return new LinkedList<>();
}
return generateSubTrees(1, n);
}
public List<TreeNode> generateSubTrees(int s,int e){
List<TreeNode> res = new LinkedList<>();
if(s > e){
res.add(null);
return res;
}
for(int i = s ;i <=e;i++){
List<TreeNode> left = generateSubTrees(s, i-1);
List<TreeNode> right = generateSubTrees(i+1, e);
for(TreeNode l :left){
for(TreeNode r:right){
TreeNode root = new TreeNode(i);
root.left = l;
root.right = r;
res.add(root);
}
}
}
return res;
}
}
搜索
BFS
广度优先搜索一层一层地进行遍历,每层遍历都是以上一层遍历的结果作为起点,遍历一个距离能访问到的所有节点。需要注意的是,遍历过的节点不能再次被遍历。
第一层:
0 -> {6,2,1,5}
第二层:
6 -> {4}
2 -> {}
1 -> {}
5 -> {3}
第三层:
4 -> {}
3 -> {}
每一层遍历的节点都与根节点距离相同。设 di 表示第 i 个节点与根节点的距离,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 di <= dj。利用这个结论,可以求解最短路径等 最优解 问题:第一次遍历到目的节点,其所经过的路径为最短路径。应该注意的是,使用 BFS 只能求解无权图的最短路径,无权图是指从一个节点到另一个节点的代价都记为 1。
在程序实现 BFS 时需要考虑以下问题:
- 队列:用来存储每一轮遍历得到的节点;
- 标记:对于遍历过的节点,应该将它标记,防止重复遍历。
1 二进制矩阵中的最短路径(Medium)
计算在网格中从原点到特定点的最短路径长度
题目
理解bfs ,哔哩哔哩
leetcode1091详解,哔哩哔哩
简洁版
visit和grid结合在一起.
class Solution {
public int shortestPathBinaryMatrix(int[][] grid) {
Queue<Pair<Integer,Integer>> queue = new LinkedList<>();
int n = grid[0].length;
if(grid[0][0] ==1 || grid[n-1][n-1] == 1) return -1;
grid[0][0] = 1;
queue.add(new Pair<>(0,0));
while(!queue.isEmpty()){
Pair<Integer,Integer> pair = queue.poll();
int r = pair.getKey(),c = pair.getValue();
for(int i = -1;i<=1;i++){
for(int j =-1;j<=1;j++){
int ra = r + i, ca = c + j;
if(ra <0 || ca < 0 || ra>=n || ca >=n || grid[ra][ca]!=0 ) continue;
grid[ra][ca] = grid[r][c] + 1;
queue.add(new Pair<>(ra, ca));
}
}
}
return grid[n-1][n-1] == 0 ? -1 : grid[n-1][n-1];
}
}
容易理解版
public int shortestPathBinaryMatrix(int[][] grids) {
if (grids == null || grids.length == 0 || grids[0].length == 0) {
return -1;
}
int[][] direction = {{1, -1}, {1, 0}, {1, 1}, {0, -1}, {0, 1}, {-1, -1}, {-1, 0}, {-1, 1}};
int m = grids.length, n = grids[0].length;
Queue<Pair<Integer, Integer>> queue = new LinkedList<>();
queue.add(new Pair<>(0, 0));
int pathLength = 0;
while (!queue.isEmpty()) {
int size = queue.size();
pathLength++;
while (size-- > 0) {
Pair<Integer, Integer> cur = queue.poll();
int cr = cur.getKey(), cc = cur.getValue();
if (grids[cr][cc] == 1) {
continue;
}
if (cr == m - 1 && cc == n - 1) {
return pathLength;
}
grids[cr][cc] = 1; // 标记
for (int[] d : direction) {
int nr = cr + d[0], nc = cc + d[1];
if (nr < 0 || nr >= m || nc < 0 || nc >= n) {
continue;
}
queue.add(new Pair<>(nr, nc));
}
}
}
return -1;
}
补充:
- Queue: 队列是一种特殊的线性表,它只允许在表的前端进行删除操作,而在表的后端进行插入操作。LinkedList类实现了Queue接口,因此我们可以把LinkedList当成Queue来用。
- Pair: 当一个函数返回两个值并且两个值都有重要意义时我们一般会用Map的key和value来表达,但是这样的话就需要两个键值对,用Map映射去做处理时,此时的key相当于value的一个描述或者引用,而具体的信息都保存在value中,我们可以通过key去获取对应的value。但是当key和value都保存具体信息时,我们就需要用到Pair对了。Pair对也是键值对的形式。
具体的实现:
1.在javax.util包下,有一个简单Pair类可以直接调用,用法是直接通过构造函数将所吸引类型的Key和value存入,这个key和value没有任何的对应关系类型也是任意定的。
用法:Pair<String, String> pair = new Pair<>("aku", "female"); pair.getKey(); pair.getValue();
2 完全平方数(Medium)
题目链接
思路:
可以将每个整数看成图中的一个节点,如果两个整数之差为一个平方数,那么这两个整数所在的节点就有一条边。
要求解最小的平方数数量,就是求解从节点 n 到节点 0 的最短路径。
class Solution {
public int numSquares(int n) {
List<Integer> squares = generateSquares(n);
Queue<Integer> queue = new LinkedList<>();
boolean[] visit = new boolean[n + 1];
queue.add(n);
visit[n] = true;
int level = 0;
while(!queue.isEmpty()){
int size = queue.size();
level++;
while(size-- !=0 ){
int cur = queue.poll();
for(int square: squares){
int next = cur - square;
if(next < 0){
break;
}
if(next == 0){
return level;
}
if(visit[next]){
continue;
}
visit[next] = true;
queue.add(next);
}
}
}
return n;
}
public static List<Integer> generateSquares(int n){
List<Integer> squares = new ArrayList<>();
for(int j = 1,i = 1;i<=n;){
squares.add(i);
j++;
i = j*j;
}
return squares;
}
}
补充:
LinkedList和ArrayList的区别:
- ArrayList实现了List接口,它是以数组的方式来实现的,数组的特性是可以使用索引的方式来快速定位对象的位置,因此对于快速的随机取得对象的需求,使用ArrayList实现执行效率上会比较好.
- LinkedList是采用链表的方式来实现List接口的,它本身有自己特定的方法,如: addFirst(),addLast(),getFirst(),removeFirst()等. 由于是采用链表实现的,因此在进行insert和remove动作时在效率上要比ArrayList要好得多!适合用来实现Stack(堆栈)与Queue(队列),前者先进后出,后者是先进先出.
- 因为ArrayList是使用数组实现的,若要从数组中删除或插入某一个对象,需要移动后段的数组元素,从而会重新调整索引顺序,调整索引顺序会消耗一定的时间,所以速度上就会比LinkedList要慢许多. 相反,LinkedList是使用链表实现的,若要从链表中删除或插入某一个对象,只需要改变前后对象的引用即可!
3 单词接龙
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
wordList.add(beginWord);
int N = wordList.size();
int start = N - 1;
int end = 0;
while(end < N && !wordList.get(end).equals(endWord)){
end++;
}
if(end == N){
return 0;
}
List<Integer>[] graphic = buildGraphic(wordList);
return shortestPath(graphic, start, end);
}
public int shortestPath(List<Integer>[] graphic, int start, int end){
Queue<Integer> queue = new LinkedList<>();
int n = graphic.length;
boolean[] marked = new boolean[n];
marked[start] = true;
int level = 1;
queue.add(start);
while(!queue.isEmpty()){
int size = queue.size();
level++;
while(size--!=0){
int cur = queue.poll();
for(int i:graphic[cur]){
if(marked[i]){
continue;
}
if(i == end){
return level;
}
queue.add(i);
marked[i] = true;
}
}
}
return 0;
}
public static boolean diffOneChar(String str1, String str2){
int cnt = 0;
for(int i = 0;i<str1.length();i++){
if(str1.charAt(i) !=str2.charAt(i)){
cnt++;
}
}
return cnt==1;
}
public static List<Integer>[] buildGraphic(List<String> list){
int N = list.size();
List<Integer>[] graphic = new List[N];
for(int i = 0;i< N;i++){
graphic[i] = new ArrayList<>();
for(int j = 0;j < N;j++){
if(isConnect(list.get(i), list.get(j))){
graphic[i].add(j);
}
}
}
return graphic;
}
public static boolean isConnect(String s1, String s2){
int cnt = 0;
char[] ch1 = s1.toCharArray();
char[] ch2 = s2.toCharArray();
for(int i = 0;i < ch1.length; i++){
if(ch1[i] != ch2[i]){
cnt++;
}
}
return cnt == 1;
}
}
DFS
广度优先搜索一层一层遍历,每一层得到的所有新节点,要用队列存储起来以备下一层遍历的时候再遍历。
而深度优先搜索在得到一个新节点时立即对新节点进行遍历:从节点 0 出发开始遍历,得到到新节点 6 时,立马对新节点 6 进行遍历,得到新节点 4;如此反复以这种方式遍历新节点,直到没有新节点了,此时返回。返回到根节点 0 的情况是,继续对根节点 0 进行遍历,得到新节点 2,然后继续以上步骤。
从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 可达性 问题。
在程序实现 DFS 时需要考虑以下问题:
- 栈:用栈来保存当前节点信息,当遍历新节点返回时能够继续遍历当前节点。可以使用递归栈。
- 标记:和 BFS 一样同样需要对已经遍历过的节点进行标记。
4 岛屿的最大面积
题目链接
思路:
我们可以将二维网格看成一个无向图,竖直或水平相邻的 1 之间有边相连。
为了求出岛屿的数量,我们可以扫描整个二维网格。如果一个位置为 1,则以其为起始节点开始进行深度优先搜索。在深度优先搜索的过程中,每个搜索到的 1 都会被重新标记为 0。
最终岛屿的数量就是我们进行深度优先搜索的次数。
class Solution {
private int[][] direction = {{1,0},{-1,0},{0,1},{0,-1}};
private int r,c;
public int maxAreaOfIsland(int[][] grid) {
if(grid.length ==0 || grid == null){
return 0;
}
int maxArea = 0;
r = grid.length;c = grid[0].length;
for(int i = 0;i < r;i++ ){
for(int j = 0;j< c;j++){
maxArea = Math.max(maxArea,dfs(grid,i ,j ));
}
}
return maxArea;
}
public int dfs(int[][]grid,int i,int j){
if(i < 0 || i>=r || j < 0 || j>=c || grid[i][j] == 0){
return 0;
}
int area = 1;
grid[i][j] = 0;
for(int[] d:direction){
area +=dfs(grid,i + d[0],j+d[1]);
}
return area;
}
}
矩阵中的连通分量数目
- Number of Islands (Medium)
题目链接
思路: 有了DFS的思想就不难, 将网格中出现1的位置进行DFS, 将访问过的进行标记, 接着计数加一即可.如果先前是一个岛屿上’1’ 就都会变成’0’.
class Solution {
private int[][] direction = {{1,0}, {-1,0},{0,1},{0,-1}};
private int r,c;
public int numIslands(char[][] grid) {
if(grid.length == 0 || grid == null) return 0;
r = grid.length;c = grid[0].length;
int numOfLands = 0;
for(int i = 0; i < r; i++){
for(int j =0;j < c;j++){
if(grid[i][j] == '1'){
dfs(grid,i ,j);
numOfLands++;
}
}
}
return numOfLands;
}
public void dfs(char[][] grid, int i ,int j){
if(i < 0 || i >= r || j < 0 || j >=c || grid[i][j] == '0') return ;
grid[i][j] = '0';
for(int [] d: direction){
dfs(grid,i + d[0],j + d[1]);
}
}
}
好友关系的连通分量数目
- Friend Circles (Medium)
题目链接
思路: 从第0个学生DFS,对遍历到的学生进行标记,第一个DFS结束后,在继续遍历下一个没有发现过的学生,每一次DFS结束就代表发现了一个朋友圈 hasVisited代表学生是否被访问过.具体操作是 对每个学生进行DFS
class Solution {
private int n;
public int findCircleNum(int[][] M) {
n = M.length;
boolean [] hasVisited = new boolean[n];
int circleFriend = 0;
for(int i = 0;i < n;i++){
if(!hasVisited[i]){
dfs(M,i,hasVisited);
circleFriend++;
}
}
return circleFriend;
}
public void dfs(int[][] M, int i, boolean[] hasVisited){
hasVisited[i] = true;
for(int j = 0;j < n;j++){
if(M[i][j] == 1 && !hasVisited[j]){
dfs(M, j, hasVisited);
}
}
}
}
填充封闭区域
- Surrounded Regions (Medium)
题目链接
参考题解大意 洪水 哔哩哔哩
思路: 从网格的四周开始, 遇到’O’就进行DFS, 然后进行标记,将与四周直接或间接相邻的’O’标记成’T’,DFS结束之后, 再将’T’还原成’O’,同时网格里面封闭的’O’全部改成’X’.(非’T’即"X")
class Solution {
private int r,c;
private int[][] direction = {{1,0}, {-1,0},{0,1},{0,-1}};
public void solve(char[][] board) {
if(board == null || board.length ==0) return;
r = board.length;
c = board[0].length;
for(int i = 0;i < r;i++){
dfs(board, i,0);
dfs(board,i,c - 1);
}
for(int j = 0;j < c;j++){
dfs(board,0,j);
dfs(board,r - 1,j);
}
for(int i = 0;i < r;i++){
for(int j = 0;j < c;j++){
if(board[i][j] == 'T'){
board[i][j] = 'O';
}else{
board[i][j] = 'X';
}
}
}
}
public void dfs(char[][] board, int i ,int j){
if(i < 0 || j < 0 || i >= r || j >= c || board[i][j] !='O') return;
board[i][j] = 'T';
for(int [] d:direction){
dfs(board, i + d[0],j + d[1]);
}
}
}
能到达的太平洋和大西洋的区域
- Pacific Atlantic Water Flow (Medium)
题目链接
思路: 水从低处流,那么也可以反向思考.从四周开始,首先上边界和左边界都能够达到太平洋,里面的点如果比边界的点要高,那么也可以到达太平洋,依照这个思路, 对边界的点采用DFS. 同样的, 下边界和右边界都能够达到大西洋,里面的点如果比边界的点要高,那么也可以到达大西洋,依照这个思路,对边界的点采用DFS.
class Solution {
private int[][] matrix;
private int m,n;
private int[][] direction = {{1,0},{-1,0},{0,1},{0,-1}};
public List<List<Integer>> pacificAtlantic(int[][] matrix) {
List<List<Integer>> list = new ArrayList<>();
if(matrix == null || matrix.length == 0) return list;
m = matrix.length;
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 j = 0;j < n;j++){
dfs(0,j,canReachP);
dfs(m - 1,j,canReachA);
}
for(int i = 0;i<m;i++){
for(int j =0;j<n;j++){
if(canReachA[i][j] && canReachP[i][j]){
list.add(Arrays.asList(i,j));
}
}
}
return list;
}
public void dfs(int i, int j ,boolean [][] canReach){
if(canReach[i][j]) {
return;
}
canReach[i][j] = true;
for(int [] d:direction){
int nexti = i + d[0];
int nextj = j + d[1];
if(nexti < 0 || nexti >= m || nextj < 0 || nextj >= n || matrix[i][j] > matrix[nexti][nextj]){
continue;
}
dfs(nexti,nextj, canReach);
}
}
}
Backtracking(回溯)
Backtracking(回溯)属于 DFS。
-
普通 DFS 主要用在 可达性问题 ,这种问题只需要执行到特点的位置然后返回即可。
-
而 Backtracking 主要用于求解 排列组合 问题,例如有 { ‘a’,‘b’,‘c’ } 三个字符,求解所有由这三个字符排列得到的字符串,这种问题在执行到特定的位置返回之后还会继续执行求解过程。
因为 Backtracking 不是立即返回,而要继续求解,因此在程序实现时,需要注意对元素的标记问题: -
在访问一个新元素进入新的递归调用时,需要将新元素标记为已经访问,这样才能在继续递归调用时不用重复访问该元素;
-
但是在递归返回时,需要将元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,可以访问已经访问过但是不在当前递归链中的元素。
1 数字键盘组合
- Letter Combinations of a Phone Number (Medium)
题目链接
思路: 采用回溯算法, 将一个字符添加到StringBuilder之后, 递归到底之后回溯把先前添加的字符剔除,再继续递归.
class Solution {
private static final char[][] KEYS = {{},{},{'a','b','c'},{'d','e','f'},{'g','h','i'},{'j','k','l'},
{'m','n','o'},{'p','q','r','s'},{'t','u','v'},{'w','x','y','z'}};
public List<String> letterCombinations(String digits) {
List<String> combination = new ArrayList<>();
if(digits.length() == 0 || digits == null) return combination;
doCombination(new StringBuilder(),combination,digits);
return combination;
}
public void doCombination(StringBuilder prefix,List<String> combination,String digits){
if(prefix.length() == digits.length()){
combination.add(prefix.toString());
return;
}
char[] letters = KEYS[digits.charAt(prefix.length()) - '0'];
for(char letter: letters){
prefix.append(letter);
doCombination(prefix,combination,digits);
prefix.deleteCharAt(prefix.length() - 1);
}
}
}
2 复原ip地址
题目链接
本题使用回溯和递归的思想复原 ip 地址
首先创建 ans 来接收复原后的所有 ip 地址,然后通过创建回溯方法进行筛选,最终返回 ans。
创建回溯方法体需要传入四个参数进行把控:1.给定的数字字符串 s,2.回溯过程中遍历到的位置 pos,3.当前确定好的 ip 段的数量,4.收集结果的 ans
考虑方法体出口:如果确定好 4 段并且遍历完整个 s,就将 cur 之间的段以 . 分隔开来放入 ans
接下来对 s 进行筛选,其中注意每段的长度最大为 3,拆箱为 int 后的长度不超过 255,起始位置不能为 0
控制好这些边界条件后就可以就可以正常的利用递归和回溯遍历字符串,具体可参考代码注释。
class Solution {
public List<String> restoreIpAddresses(String s) {
List<String> ans = new ArrayList<>();
if (s == null || s.length() == 0) {
return ans;
}
// 回溯
back(s, 0, new ArrayList<>(), ans);
return ans;
}
// 中间两个参数解释:pos-当前遍历到 s 字符串中的位置,cur-当前存放已经确定好的 ip 段的数量
private void back(String s, int pos, List<String> cur, List<String> ans) {
if (cur.size() == 4) {
// 如果此时 pos 也刚好遍历完整个 s
if (pos == s.length()) {
// join 用法:例如 [[255],[255],[111],[35]] -> 255.255.111.35
ans.add(String.join(".", cur));
}
return;
}
// ip 地址每段最多有三个数字
for (int i = 1; i <= 3; i++) {
// 如果当前位置距离 s 末尾小于 3 就不用再分段了,直接跳出循环即可。
if (pos + i > s.length()) {
break;
}
// 将 s 的子串开始分段
String segment = s.substring(pos, pos + i);
// 剪枝条件:段的起始位置不能为 0,段拆箱成 int 类型的长度不能大于 255
if (segment.startsWith("0") && segment.length() > 1 || (i == 3 && Integer.parseInt(segment) > 255)) {
continue;
}
// 符合要求就加入到 cur 数组中
cur.add(segment);
// 继续递归遍历下一个位置
back(s, pos + i, cur, ans);
// 回退到上一个元素,即回溯
cur.remove(cur.size() - 1);
}
}
}
补充:
String.join()方法的学习
java startsWith
3 单词搜索
private final static int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
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;
boolean[][] hasVisited = new boolean[m][n];
for (int r = 0; r < m; r++) {
for (int c = 0; c < n; c++) {
if (backtracking(0, r, c, hasVisited, board, word)) {
return true;
}
}
}
return false;
}
private boolean backtracking(int curLen, int r, int c, boolean[][] visited, final char[][] board, final String word) {
if (curLen == word.length()) {
return true;
}
if (r < 0 || r >= m || c < 0 || c >= n
|| board[r][c] != word.charAt(curLen) || visited[r][c]) {
return false;
}
visited[r][c] = true;
for (int[] d : direction) {
if (backtracking(curLen + 1, r + d[0], c + d[1], visited, board, word)) {
return true;
}
}
visited[r][c] = false;
return false;
}
4 输出二叉树中所有从根到叶子的路径
- 二叉树的所有路径
题目链接
class Solution {
public List<String> binaryTreePaths(TreeNode root) {
List<String> res = new ArrayList<>();
if(root == null){
return res;
}
backTracking(root,res,new ArrayList<>());
return res;
}
public void backTracking(TreeNode node,List<String> res, List<String> cur){
if(node == null){
return;
}
cur.add(String.valueOf(node.val));
if(node.left == null && node.right == null){
res.add(String.join("->",cur));
}else{
backTracking(node.left,res,cur);
backTracking(node.right,res,cur);
}
//不管是遍历完叶子节点,还是中间节点遍历完左右孩子节点,都需要将此节点数据移出cur
cur.remove(cur.size() - 1);
}
}
5 排列
- Permutations (Medium)
题目链接
思路: 像这题的关键就是: 要对访问数组进行遍历.而不是对原数组进行遍历,像之前的数字键盘, 是在原来的数组的基础上进行遍历,然后回溯. 这题也是用回溯方法,但是遍历是针对访问数组的,回溯要将已经标记的元素进行标记未访问.这样每一次递归的时候都是将未访问的元素进行添加到list中.
注意: 在将list添加到结果list中的时候,需要新生成一个list,因为list引用的存在.会导致每个list的结果是一样的,而是由于最后移除元素, 所以全部 会为空.
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
if(nums == null || nums.length == 0) return res;
boolean [] hasVisited = new boolean[nums.length];
backTracking(nums,res,hasVisited,new ArrayList<>());
return res;
}
public void backTracking(int[] nums,List<List<Integer>> res, boolean[] hasVisited,List<Integer> cur){
if(cur.size() == nums.length){
res.add(new ArrayList<>(cur));// 重新构造一个 List
return;
}
for(int i = 0;i < hasVisited.length;i++){
if(hasVisited[i]){
continue;
}
hasVisited[i] = true;
cur.add(nums[i]);
backTracking(nums,res,hasVisited,cur);
cur.remove(cur.size() - 1);
hasVisited[i] = false;
}
}
}
6 含有相同元素求排列
- Permutations II (Medium)
数组元素可能含有相同的元素,进行排列时就有可能出现重复的排列,要求重复的排列只返回一个。
在实现上,和 Permutations 不同的是要先排序,然后在添加一个元素时,判断这个元素是否等于前一个元素,如果等于,并且前一个元素还未访问,那么就跳过这个元素。
class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
if(nums == null || nums.length ==0) return res;
boolean[] visited = new boolean[nums.length];
Arrays.sort(nums);
backTracking(res,nums,new ArrayList<>(),visited);
return res;
}
public void backTracking(List<List<Integer>> res,int[] nums,List<Integer> cur,boolean[] visited){
if(nums.length == cur.size()){
res.add(new ArrayList<>(cur));
return;
}
for(int i = 0;i< visited.length;i++){
if(visited[i]){
continue;
}
if(i!=0 && nums[i] == nums[i - 1] && !visited[i - 1]){ //防止重复
continue;
}
visited[i] = true;
cur.add(nums[i]);
backTracking(res,nums,cur,visited);
cur.remove(cur.size() - 1);
visited[i] =false;
}
}
}
7 组合
- Combinations (Medium)
按照这个规律: [1,2] ,[1,3],[1,4],[2,3],[2,4],[3,4]
先添加i, 然后添加i+1->n
如添加1,再添加2->4.
再添加2,然后添加3->4
接着添加3,添加4.
class Solution {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> combinations = new ArrayList<>();
List<Integer> combineList = new ArrayList<>();
backtracking(combineList, combinations,
1, k, n);
return combinations;
}
private void backtracking(List<Integer> combineList, List<List<Integer>> combinations, int start, int k, final int n) {
if (k == 0) {
combinations.add(new ArrayList<>(combineList));
return;
}
for (int i = start; i <= n; i++) { // 剪枝
combineList.add(i);
backtracking(combineList, combinations, i + 1, k - 1, n);
combineList.remove(combineList.size() - 1);
}
}
}
8组合求和
题目链接
思路: 利用回溯的方法,每次循环遍历数组,将当前元素添加, 然后更新target = target-当前元素.
注意: 为了
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
if(candidates == null || candidates.length == 0) {
return res;
}
Arrays.sort(candidates);
backTracking(candidates,target,new ArrayList<>(),res,0);
return res;
}
public void backTracking(int[] candidates,int target,List<Integer> cur,List<List<Integer>> res,int start){
if(target == 0){
res.add(new ArrayList<>(cur));
return;
}
for(int i = start;i < candidates.length;i++){
if(candidates[i] <= target){
cur.add(candidates[i]);
backTracking(candidates,target - candidates[i],cur,res,i);
cur.remove(cur.size() - 1);
}
}
}
}
9 含相同元素的组合求和
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(candidates);
boolean[] visited = new boolean[candidates.length];
backTracking(res,new ArrayList<>(),target,visited,0,candidates);
return res;
}
public void backTracking(List<List<Integer>> res, List<Integer> cur,int target,boolean[] visited,int start,int [] candidates){
if(target == 0){
res.add(new ArrayList<>(cur));
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){
visited[i] = true;
cur.add(candidates[i]);
backTracking(res,cur,target - candidates[i], visited,i + 1,candidates);
cur.remove(cur.size() - 1);
visited[i] = false;
}
}
}
}
10 1-9 数字的组合求和
class Solution {
public List<List<Integer>> combinationSum3(int k, int n) {
List<List<Integer>> combinations = new ArrayList<>();
List<Integer> path = new ArrayList<>();
backtracking(k, n, 1, path, combinations);
return combinations;
}
private void backtracking(int k, int n, int start,
List<Integer> tempCombination, List<List<Integer>> combinations) {
if (k == 0 && n == 0) {
combinations.add(new ArrayList<>(tempCombination));
return;
}
if (k == 0 || n == 0) {
return;
}
for (int i = start; i <= 9; i++) {
tempCombination.add(i);
backtracking(k - 1, n - i, i + 1, tempCombination, combinations);
tempCombination.remove(tempCombination.size() - 1);
}
}
}
11 子集
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
for(int i = 0;i <= nums.length;i++){
backTracking(nums,res,new ArrayList<>(),0,i);
}
return res;
}
public void backTracking(int[] nums,List<List<Integer>> res, List<Integer> cur, int start,int k){
if(k == cur.size()){
res.add(new ArrayList<>(cur));
return;
}
for(int i = start;i < nums.length;i++){
cur.add(nums[i]);
backTracking(nums,res,cur,i + 1,k);
cur.remove(cur.size() - 1);
}
}
}
12 含有相同元素求子集
class Solution {
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
boolean[] visited = new boolean[nums.length];
Arrays.sort(nums);
for(int i = 0;i <= nums.length;i++){
backTracking(res,new ArrayList<>(),i,0,nums,visited);
}
return res;
}
public void backTracking(List<List<Integer>> res,List<Integer> cur,int k,int start,int [] nums,boolean[] visited){
if(cur.size() == k){
res.add(new ArrayList<>(cur));
return;
}
for(int i = start;i < nums.length;i++){
if(i !=0 && nums[i] == nums[i - 1] && !visited[i - 1]){
continue;
}
visited[i] = true;
cur.add(nums[i]);
backTracking(res,cur,k,i + 1,nums,visited);
cur.remove(cur.size() - 1);
visited[i] = false;
}
}
}
13 分割字符串使得每个部分都是回文数
class Solution {
public List<List<String>> partition(String s) {
List<List<String>> res = new ArrayList<>();
backTracking(res,new ArrayList<>(),s);
return res;
}
public void backTracking(List<List<String>> res,List<String>cur, String s){
if(0 == s.length()){
res.add(new ArrayList<>(cur));
return;
}
for(int i = 0;i < s.length();i++){
if(isPalindrome(s,0,i)){
cur.add(s.substring(0,i+1));
backTracking(res,cur,s.substring(i + 1));
cur.remove(cur.size() - 1);
}
}
}
public boolean isPalindrome(String str,int begin, int end){
while(begin <= end){
if(str.charAt(begin) == str.charAt(end)){
begin++;
end--;
}else{
return false;
}
}
return true;
}
}
数独
方法 1:回溯法
使用的概念
了解两个编程概念会对接下来的分析有帮助。
第一个叫做 约束编程。
基本的意思是在放置每个数字时都设置约束。在数独上放置一个数字后立即
排除当前 行, 列 和 子方块 对该数字的使用。这会传播 约束条件 并有利于减少需要考虑组合的个数。
第二个叫做 回溯。
让我们想象一下已经成功放置了几个数字
在数独上。
但是该组合不是最优的并且不能继续放置数字了。该怎么办? 回溯。
意思是回退,来改变之前放置的数字并且继续尝试。如果还是不行,再次 回溯。
如何枚举子方块
一种枚举子方块的提示是:
使用 方块索引= (行 / 3) * 3 + 列 / 3
其中 / 表示整数除法。
算法
现在准备好写回溯函数了
backtrack(row = 0, col = 0)。
从最左上角的方格开始 row = 0, col = 0。直到到达一个空方格。
从1 到 9 迭代循环数组,尝试放置数字 d 进入 (row, col) 的格子。
如果数字 d 还没有出现在当前行,列和子方块中:
将 d 放入 (row, col) 格子中。
记录下 d 已经出现在当前行,列和子方块中。
如果这是最后一个格子row == 8, col == 8 :
意味着已经找出了数独的解。
否则
放置接下来的数字。
如果数独的解还没找到:
将最后的数从 (row, col) 移除。
class Solution {
boolean[][] rowUsed = new boolean[9][10];
boolean[][] colUsed = new boolean[9][10];
boolean[][] cubeUsed = new boolean[9][10];
char[][] board;
public void solveSudoku(char[][] board) {
this.board = board;
buildNumused();
backTracking(0,0);
}
public boolean backTracking(int r, int c){
while(r < 9 && board[r][c] !='.'){
r = c == 8 ? r + 1:r;
c = c == 8 ? 0 : c + 1;
}
if(r == 9){
return true;
}
for(int i = 1;i<=9;i++){
if(rowUsed[r][i] || colUsed[c][i] || cubeUsed[cubeNum(r,c)][i]){
continue;
}
board[r][c] = (char)(i + '0');
rowUsed[r][i] = colUsed[c][i] = cubeUsed[cubeNum(r,c)][i] = true;
if(backTracking(r,c))
return true;
board[r][c] = '.';
rowUsed[r][i] = colUsed[c][i] = cubeUsed[cubeNum(r,c)][i] = false;
}
return false;
}
public void buildNumused(){
for(int i = 0;i < 9;i++){
for(int j = 0;j < 9;j++){
if(board[i][j] != '.'){
int num = board[i][j] - '0';
rowUsed[i][num] = colUsed[j][num] = cubeUsed[cubeNum(i,j)][num] = true;
}
}
}
}
public int cubeNum(int i, int j){
int r = i / 3;
int c = j / 3;
return r * 3 + c;
}
}
动态规划
递归和动态规划都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存了子问题的解,避免重复计算。
斐波那契数列
1. 爬楼梯
- Climbing Stairs (Easy)
题目链接
题目描述:有 N 阶楼梯,每次可以上一阶或者两阶,求有多少种上楼梯的方法。
定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始),dp[i] 表示走到第 i 个楼梯的方法数目。
第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。
2.打家劫舍
考虑到 dp[i] 只与 dp[i - 1] 和 dp[i - 2] 有关,因此可以只用两个变量来存储 dp[i - 1] 和 dp[i - 2],使得原来的 O(N) 空间复杂度优化为 O(1) 复杂度。
题目链接
class Solution {
public int climbStairs(int n) {
if(n <=2 ){
return n;
}
int pre1 = 1,pre2 = 2,cur = 3;
for(int i = 3; i <= n;i++){
cur = pre1 + pre2;
pre1 = pre2;
pre2 = cur;
}
return cur;
}
}
要想获得i间的最大金额, 可以考虑两种情况, 偷或者不偷第i间房,用f[i]表示i间房间的最大金额,则有
f[i] = max(nums[i] + f[i - 2], f[i - 1])
偷第 i 间房: nums[i] + f[ i - 2]
不偷第 i 间房: f[i - 1]
class Solution {
public int rob(int[] nums) {
int pre1 = 0,pre2 = 0,cur = 0;
for(int i = 0;i < nums.length;i++){
cur = Math.max(nums[i] + pre2, pre1);
pre2 = pre1;
pre1 = cur;
}
return cur;
}
}
3 强盗在环形街区抢劫
- House Robber II (Medium)
class Solution {
public int rob(int[] nums) {
if(nums.length == 1){
return nums[0];
}
int n = nums.length;
return Math.max(rob(nums,0,n - 2), rob(nums,1, n - 1));
}
public int rob(int [] nums, int first ,int end){
int pre1 = 0, pre2 = 0, cur = 0;
for(int i = first; i <= end;i++){
cur = Math.max(nums[i] + pre2 , pre1);
pre2 = pre1;
pre1 = cur;
}
return cur;
}
}
4 信件错排
题目描述:有 N 个 信 和 信封,它们被打乱,求错误装信方式的数量。
定义一个数组 dp 存储错误方式数量,dp[i] 表示前 i 个信和信封的错误方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据 i 和 k 是否相等,有两种情况:
i==k,交换 i 和 j 的信后,它们的信和信封在正确的位置,但是其余 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] 种错误装信方式。
综上所述,错误装信数量方式数量为:
矩阵路径
1 矩阵的最小路径和
class Solution {
public int minPathSum(int[][] grid) {
for(int i = 0;i < grid.length;i ++){
for( int j = 0; j < grid[0].length; j ++){
if(i != 0 && j != 0){
grid[i][j] = Math.min(grid[i-1][j],grid[i][j-1]) + grid[i][j];
}else if(i == 0 && j!= 0){
grid[i][j] = grid[i][j - 1] + grid[i][j];
}else if(i !=0 && j== 0){
grid[i][j] = grid[i - 1][j] + grid[i][j];
}
}
}
return grid[grid.length - 1][grid[0].length - 1];
}
}
矩阵的总路径数
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for(int i = 0;i < m;i ++){
for(int j = 0;j < n;j ++){
if(i != 0 && j !=0){
dp[i][j] = dp[i][j - 1] + dp[i - 1][j];
}else if(i ==0 && j!= 0){
dp[i][j] = dp[i][j - 1];
}else if(i !=0 && j ==0){
dp[i][j] = dp[i -1][j];
}else{
dp[i][j] = 1;
}
}
}
return dp[m - 1][n - 1];
}
}
数组区间
1. 数组区间和
303. Range Sum Query - Immutable (Easy)
状态:sum[i]表示0~i-1的和
转移方程: sum[i ] = sum[i - 1] + nums[i - 1]
返回: sum[j+1] - sum[i]
class NumArray {
private int[] sum;
public NumArray(int[] nums) {
sum = new int[nums.length + 1];
for(int i = 1;i <= nums.length;i ++){
sum[i] = sum[i - 1] + nums[i - 1];
}
}
public int sumRange(int i, int j) {
return sum[j + 1] - sum[i];
}
}
2. 数组中等差递增子区间的个数
413. Arithmetic Slices (Medium)
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-1] 为结尾的递增子区间的后面再加上一个 A[i],一样可以构成新的递增子区间。
dp[2] = 1
[0, 1, 2]
dp[3] = dp[2] + 1 = 2
[0, 1, 2, 3], // [0, 1, 2] 之后加一个 3
[1, 2, 3] // 新的递增子区间
dp[4] = dp[3] + 1 = 3
[0, 1, 2, 3, 4], // [0, 1, 2, 3] 之后加一个 4
[1, 2, 3, 4], // [1, 2, 3] 之后加一个 4
[2, 3, 4] // 新的递增子区间
综上,在 A[i] - A[i-1] == A[i-1] - A[i-2] 时,dp[i] = dp[i-1] + 1。
因为递增子区间不一定以最后一个元素为结尾,可以是任意一个元素结尾,因此需要返回 dp 数组累加的结果。
class Solution {
public int numberOfArithmeticSlices(int[] A) {
int sum = 0;
int [] dp = new int[A.length];
for(int i = 2; i < A.length;i ++){
if(A[i] - A[i - 1] == A[i - 1] - A[i - 2]){
dp[i] = dp[i - 1] + 1;
sum += dp[i];
}
}
return sum;
}
}