字符串搜索
主串A: a, b, c, d, e, f, g
模式串B: c, d, e
判断B是否在A中, 存在返回在A中的下标, 不存在返回-1
再如: A: ABCABCAABCABCD; B: ABCABCD; 返回值7.
BF: 暴力破解
复杂度O( (n-m)*m )
1//BF:暴力破解
2 int BF(String A, String B){
3 int aLength = A.length();
4 int bLength = B.length();
5 //这里aLength - bLength稍微优化了一下
6 for (int i = 0; i <= aLength - bLength; i++) {
7 int j;
8 for (j = 0; j < bLength; j++) {
9 if (A.charAt(i+j) != B.charAt(j)){
10 break;
11 }
12 }
13 //需要判断上面的子循环什么时候将B完整遍历了一遍
14 //当j=B.length时, 刚好执行了最后一次j++
15 if(j == B.length()){
16 return i;
17 }
18 }
19 return -1;
20 }
RK算法: hash算法
基于BF进行优化, 将A中的字符串按照顺序截取B字符串的长度: abc, bcd, cde, def, efg. 进行hash运算然后与B的hash值进行比较. 时间复杂度: O(m*n), hash算法参与字符串位数, 主串长度相关.
优化: hash算法: 按26进制取和, abc=1+2+3=6, 进行量化. 每个子串的hash值是前一个子串的hash值-前串最小下标对应字母的值+本串最大下标字母对应的值.
此时间复杂度为O(n), 只与主串长度相关, 但hash冲突极端情况下退化为BF
1//RK:hash算法
2 static int RK(String A, String B){
3 int aLength = A.length();
4 int bLength = B.length();
5 int bCode = B.hashCode();
6 for (int i = 0; i <= aLength - bLength; i++) {
7 String aSub = A.substring(i, i + bLength);
8 if(aSub.hashCode() == bCode){
9 //防止hash碰撞
10 int j;
11 for (j = 0; j < bLength; j++) {
12 if (aSub.charAt(j) != B.charAt(j)){
13 return -1;
14 }
15 }
16 if (j == bLength){
17 return i;
18 }
19 }
20 }
21 return -1;
22 }
23
24 //RK: hash算法 优化hash
25 static int hashCode(String string){
26 int hashCode = 0;
27 //初始化字母表
28 HashMap<Character, Integer> map = new HashMap<>();
29 int base = (int)'A'-1;
30 for (char c='A'; c<='Z'; c++){
31 map.put(c, (int)c-base);
32 }
33 for (char c='a'; c<='z'; c++){
34 map.put(c, (int)c-base);
35 }
36
37 for (int i = 0; i < string.length(); i++) {
38 hashCode += map.get(string.charAt(i));
39 }
40 return hashCode;
41 }
BM算法
1.坏字符规则: 从右往左匹配, 找到A中第一个不匹配的字符(坏字符), 将B串右移, 直到出现与A串坏字符对齐的字符, 再从右往左寻找坏字符. 如果B串中没有该坏字符, 则直接移到该坏字符的下一位即可.
例如A串:ABCABCAABCABCD; B串ABCDBC. A串前6个为ABCABC, 初始从右往左匹配, 这时B串的C与A串该位置的C对应, 继续匹配B, 然后B串中的D与A串中的对应位置A不匹配, 出现换字符.
1ABCABCAABCABCD
2ABCDBC
于是右移B串, 第一个与A坏字符对应的B串位置就是第一个字母, 现在变成了
1ABCABCAABCABCD
2 ABCDBC
B串往右移动了两格. 又开始从右往左寻找坏字符, 这时第一个B与C就冲突了, 于是又开始右移, 对齐这个坏字符:
1ABCABCAABCABCD
2 ABCDBC
B串往右移动一格后坏字符对齐, 又开始从右往左寻找坏字符, A与D出现冲突, 右移:
1ABCABCAABCABCD
2 ABCDBC
B串右移三格. 这一次再移动一格就到头了, 不行.
坏字符规则是从右往左比较的, 并且这个移动规则保证了跳过多余的比较同时又不会遗留.
2.好后缀规则: 从右往左匹配, 找到坏字符(坏字符后面的串是匹配的, 是好串), 往左寻找B中是否还有该好后缀, 如果有, 将B右移到该位置与A好后缀进行对齐. 重复该规则. 如果B串往右没有该好后缀, 则右移到好后缀的往右错一位的位置, 重复该规则. 避免B串的前缀与好后缀的后缀匹配
还是以上面为例:
1ABCABCAABCABCD
2ABCDBC
这时好串是BC, 往前寻找发现还有这个好后缀, 于是进行对齐:
1ABCABCAABCABCD
2 ABCDBC
这时坏字符是第一个, 没有好串, 只有先对齐了:
1ABCABCAABCABCD
2 ABCDBC
有好串了, 继续
1ABCABCAABCABCD
2 ABCDBC
在移动就超过长度了, 于是没有匹配的.
再举有匹配的一例:
1ABCABCAABCABCD
2ABCABCD
3开始位置0; 坏字符下标:6; 需要移位:3;
4
5ABCABCAABCABCD
6 ABCABCD
7开始位置3; 坏字符下标:6; 需要移位:1;
8
9ABCABCAABCABCD
10 ABCABCD
11开始位置4; 坏字符下标:6; 需要移位:3
12
13ABCABCAABCABCD
14 ABCABCD
15开始位置7; 无坏字符.
两种规则综合使用, 哪种移动的位数多使用哪种
时间复杂度: O(n/m), 最坏O(m*n)
1//BM
2 static int BM(String A, String B){
3 int aLength = A.length();
4 int bLength = B.length();
5 //每次操作结束后A串对齐B串的标志位
6 int index = 0;
7 while (true) {
8 for (int j = bLength-1; j > 0; j--) {
9 //出现坏字符
10 if (A.charAt(index + j) != B.charAt(j)){
11 Boolean ifHaveBad = false;
12 for (int i = j; i > 0; i--) {
13 //B串中有坏字符
14 if (A.charAt(index + j) == B.charAt(i)){
15 ifHaveBad = true;
16 index += j-i;
17 break;
18 }
19 }
20
21 if (!ifHaveBad){
22 //B串中没有坏字符, 应移位到坏字符下一位
23 index += j;
24 }
25
26 //如果已经不能再移位了, 说明没有匹配的.
27 if (aLength - index < bLength) return -1;
28
29 //移位, 进行下一次规则循环比较.
30 break;
31 }
32 //在B串一次循环中没有一次进入if分支, 说明全部匹配
33 return index;
34 }
35 }
36 }
KMP算法
前缀: 字符串A=B+S, S非空, 那么B就是A的前缀
后缀: 字符串A=S+B, S非空, 那么B是A的后缀
PMT值: 前缀集合和后缀集合的交集中, 最长元素的长度
部分匹配表: PMT值集合, 字符串的所有前缀的PMT值
perfix数组: 每一个下标位置对应一个PMT值, 组成的数组
next数组: perfix向右移一个下标位置, 组成next数组
这个PMT值就是我们每次找到坏字符后, pattern字符串需要移动的距离.
求next数组:
1//KMP: next数组生成
2 static void getNext(char[] pattern, int[]next){
3 //next数组其余位置默认是0.
4 next[0] = -1;
5 int i = 0, j = -1;
6
7 while (i < pattern.length){
8 //每当j=-1时, 就是说明当前子串已经找完了.
9 if(j == -1){
10 i++;
11 j++;
12 }else if(pattern[i] == pattern[j]){
13 i++;
14 j++;
15 //这时i位置上的值赋值的是i-1位置上的KMP值, 错位了.
16 next[i] = j;
17 }else {
18 //当字母不匹配后回到这里, j慢慢缩小直到-1.
19 j = next[j];
20 }
21 }
22 }
以ABCABCD串解释如何求出next数组.
1.默认情况下i=0, j=-1, 这时刚好寻找下标为0子串的KMP值, 这是肯定为0的, 所以我们进行i++,j++.
1ABCABCD
2ABCABCD
2.现在变成了i=1, j=0; 由于此时B != A, 说明子串AB的KMP值为0, 于是将j赋值为-1, 这样i就会++. 表明这个位置没有匹配的, 仍需要移动
1ABCABCD
2 ABCABCD
3.现在i=1, j=-1, 又变成i=2, j=0, C!=A, 第一个仍然不匹配, 继续
1ABCABCD
2 ABCABCD
4.现在i=3, j=0, 匹配了, 将next[4] = 1, i++, j++后继续匹配, next[5] = 2, i++, j++后继续匹配, next[6] = 3, 当i=6, j=3时, 不匹配了, 又回到j=0了, 然后j=-1, 最后i = 7 = pattern的长度了.
1ABCABCD
2 ABCABCD
最后一个问题, j = next[j];是否一定会让j变成-1, 有没有例外情况? 我们举例来看看:
1如果串是: ABCABCDABCDA
2
3ABCABCDABCDA
4 ABCABCDABCDA
5这时i=10 j=3时不匹配, 那么j=next[3]=0
初步估计这个next数组是越来越大的, 因为子串的长度在不断增大, KMP值越来越大, 所以它总会收敛于-1的.
算法本体:
1//KMP
2 static int search(char[] str, char[] pattern, int[] next){
3 int i = 0;
4 int j = 0;
5
6 while (i < str.length && j < pattern.length){
7 if (j == -1 || str[i] == pattern[j]){
8 i++;
9 j++;
10 }else {
11 //不匹配了 , 就往后移动.
12 //所以下标0的-1也是移动的手段,
13 j = next[j];
14 }
15 }
16
17 //如果j最后一直到了最后都和i匹配, 说明找到了.
18 if (j == pattern.length){
19 return i - j;
20 }else {
21 return -1;
22 }
23 }
测试:
1public static void main(String[] args) {
2 String A = "ABCABCAABCABCD";
3 String B = "ABCABCD";
4 System.out.println(BF(A, B));
5 System.out.println(RK(A, B));
6 //System.out.println(hashCode(B));
7 System.out.println(BM(A,B));
8
9 int[] next = new int[B.length()];
10 getNext(B.toCharArray(), next);
11 int i = search(A.toCharArray(), B.toCharArray(), next);
12 System.out.println(Arrays.toString(next));
13 System.out.println(i);
14 System.out.println(A.indexOf(B));
15 }
打家劫舍
动态规划三要素:
- 最优子结构: 每一个问题的最优解都包含子问题的最优解(n的最优依赖的是n-1的最优)
- 递推公式(状态转移方程): 找问题的规律, 解和前面解的关系
- 重叠子问题
初始问题
问题: 小偷偷钱, 不能偷相邻的房间. 给定一个代表每个房屋存放金额的非负整数数组, 计算你不触动警报装置的情况下, 一夜能偷窃到的最高金额
输入: [1,2,3,1] 输出:4
输入:[2,7,9,3,1] 输出12
1public class Rob {
2
3 public static void main(String[] args) {
4 int[] num = new int[]{100, 2, 1, 100};
5 int index = num.length -1;
6 System.out.println(maxMoney(num, index));
7 System.out.println(maxMoney2(num));
8 }
9
10
11 static int maxMoney(int[] num, int index){
12 if (num == null || index < 0){
13 return 0;
14 }
15 if (index == 0){
16 return num[index];
17 }
18 return Math.max(maxMoney(num, index-1), num[index]+maxMoney(num, index-2));
19 }
20
21 static int maxMoney2(int[] num){
22 //健壮性
23 int length = num.length;
24 if (num == null || length == 0){
25 return 0;
26 }
27 if (length == 1){
28 return num[length-1];
29 }
30
31 //dp数组, 存放以及计算的值, 优化计算
32 int[] dp = new int[num.length];
33 dp[0] = num[0];
34 dp[1] = Math.max(num[0], num[1]);
35 for (int i = 2; i <length; i++) {
36 dp[i] = Math.max(dp[i-1], dp[i-2]+num[i]);
37 }
38
39 return dp[length-1];
40 }
41
42 //优化空间复杂度, 只放两个位置.
43 static int maxMoney3(int[] num){
44 //健壮性
45 int length = num.length;
46 if (num == null || length == 0){
47 return 0;
48 }
49 if (length == 1){
50 return num[length-1];
51 }
52
53 //只需要两个变量, 优化空间复杂度从O(n)到O(1)
54 int first = num[0];
55 int second = Math.max(num[0], num[1]);
56 for (int i = 2; i <length; i++) {
57 int temp = second;
58 second = Math.max(second, first+num[i]);
59 first = temp;
60 }
61
62 return second;
63 }
64}
65
首尾相连
街道房间是圆形的. 第一个和最后一个也算是相邻的.
由于第一个和最后一个是互斥的, 于是分解为两个子问题: 第一个房子到倒数第二个房子的最优解, 与第二个房子到最后一个房子的最优解. 分别求出来比较大小.
1public static void main(String[] args) {
2 int[] num = new int[]{100, 2, 1, 100};
3 System.out.println(Math.max(
4 maxMoney4(num, 0, num.length-2),
5 maxMoney4(num, 1, num.length-1)
6 ));
7 }
8
9
10//将下标作为变量, 分别进行最优解运算.
11 static int maxMoney4(int[] num, int start, int end){
12 //健壮性
13 int length = num.length;
14 if (num == null || length == 0){
15 return 0;
16 }
17 if (length == 1){
18 return num[length-1];
19 }
20
21 //只需要两个变量, 优化空间复杂度从O(n)到O(1)
22 int first = num[start];
23 int second = Math.max(num[start], num[start+1]);
24 for (int i = start+2; i <= end; i++) {
25 int temp = second;
26 second = Math.max(second, first+num[i]);
27 first = temp;
28 }
29
30 return second;
31 }
二叉树
父子关系就是相邻的情况.
1public static void main(String[] args) {
2 TreeNode node5 = new TreeNode(1, null, null);
3 TreeNode node4 = new TreeNode(3, null, null);
4 TreeNode node3 = new TreeNode(3, null, node5);
5 TreeNode node2 = new TreeNode(2, null, node4);
6 TreeNode node1 = new TreeNode(3, node2, node3);
7 //传入根节点
8 int[] dfs = dfs(node1);
9 System.out.println(Math.max(dfs[0], dfs[1]));
10 }
11
12
13//深度优先算法
14 public static int[] dfs(TreeNode node){
15 //int[]两个值, 一是选了这个节点select的最优解,第二个是没选这个节点not select的最优解
16 if (node == null){
17 //如果是null节点, 选与不选结果都是1.
18 return new int[]{0,0};
19 }
20 int[] l = dfs(node.left);
21 int[] r = dfs(node.right);
22 //选这个节点意味着不能选下面的节点
23 int select = node.val + l[1] + r[1];
24 //不选这个节点意味着选子节点, 选子节点最大的情况.
25 int notSelect = Math.max(l[0], l[1]) + Math.max(r[0], r[1]);
26 return new int[]{select, notSelect};
27 }
反转链表
1.迭代
用变量保存当前节点和下一个节点的信息, 以此赋值.
1public class ReverseList {
2
3 public static void main(String[] args) {
4 ListNode node5 = new ListNode(5, null);
5 ListNode node4 = new ListNode(4, node5);
6 ListNode node3 = new ListNode(3, node4);
7 ListNode node2 = new ListNode(2, node3);
8 ListNode node1 = new ListNode(1, node2);
9 ListNode iterate = iterate(node1);
10 }
11
12 static class ListNode{
13 int val;
14 ListNode next;
15
16 public ListNode(int val, ListNode next){
17 this.val = val;
18 this.next = next;
19 }
20 }
21
22 public static ListNode iterate(ListNode head){
23 //保存前一个节点
24 ListNode pre = null;
25 //保存当前节点
26 ListNode curr = head;
27 ListNode next;
28 while (curr != null){
29 //先保存下一个节点的信息, 避免被覆盖
30 next = curr.next;
31 //在pre还未指向新的pre之前给自己赋值
32 curr.next = pre;
33 pre = curr;
34 curr = next;
35 }
36 return pre;
37 }
38
39
40}
41
2.递归
以相似的方式重复, 类似于树结构, 从最里面开始遍历.
1public static ListNode recursion(ListNode head){
2 if (head == null || head.next == null){
3 return head;
4 }
5 //从最后往前修改
6 ListNode new_head = recursion(head.next);
7 head.next.next = head; // 后一个指向前一个
8 head.next = null; //断原来的链
9 return new_head;
10 }
素数个数统计
1.暴力算法
对每个数字x继续筛选, 看x是否会被小于根号x的数字整除. 可以用i * i < x 表示根号.
1public class PrimeSearch {
2
3
4 public static int bf(int x){
5 int count = 0;
6 for (int i = 2; i <= x; i++) {
7 count += isPrime(i) ? 1 : 0;
8 }
9 return count;
10 }
11
12 private static boolean isPrime(int i) {
13 for (int j = 2; j * j <= i; j++) {
14 if (i % j == 0){
15 return false;
16 }
17 }
18 return true;
19 }
20}
21
2.埃氏筛选
埃氏筛选: 找到一个质数, 立刻将这个质数所有的倍数都淘汰为合数.
1 public static int eratosthenes(int n){
2 int count = 0;
3 //由于数组下标的因素, 设置为n+1
4 Boolean[] isPrime = new Boolean[n+1]; //初始化为false
5 Arrays.fill(isPrime, true); //所有默认为true
6 for (int i = 2; i <= n; i++) {
7 if (isPrime[i]){
8 count++;
9 //排除所有这个质数的倍数的合数.
10 for (int j = i*i; j <= n; j+=i) {
11 isPrime[j] = false;
12 }
13 }
14 }
15 return count;
16 }
删除排序数组中的重复项
一个有序数组nums, 原地删除重复出现的元素, 使每个元素只出现一次, 返回删除后数组的新长度
不能使用额外的数组空间, 必须在原地修改数组并在使用O(1)额外空间的条件下完成.
输入: [0,1,2,2,3,3,4]
输出: 5
Java中没有数组元素删除操作, 删除了只能置为null. 或者只能新建数组拷贝.
双指针算法: i(慢指针)和j(快指针)依次指向第一个和第二个, 如果他们对应元素不相等, 则都+1; 如果对应元素相等, 那么j+1; 如果是相等之后出现了不相等, 那么这时将j所在位置的元素赋值给i+1所在位置的元素(nums[i+1]=nums[j]); 如果j到了最后一个, 则返回i.
1public class SortedArrayDuplicates {
2
3 public static void main(String[] args) {
4 System.out.println(
5 removeDuplicates(new int[]{0,1,2,2,2,3,3,4})
6 );
7 }
8
9 public static int removeDuplicates(int[] nums){
10 if (nums.length == 0){
11 return 0;
12 }
13
14 int i = 0;
15 for (int j = 1; j < nums.length; j++) {
16 //当不相等时, 如果是挨着的, 那么nums[i] = nums[j];无影响
17 //如果不是挨着的, 那么会将重复的元素赋值为下一个不重复的元素
18 if (nums[i] != nums[j]){
19 i++;
20 nums[i] = nums[j];
21 }
22 }
23
24 return i + 1;
25 }
26}
27
寻找数组的中心下标
给定一个整数数组nums, 返回数组"中心下标"的方法, 不存在返回-1, 有多个则返回最靠近左边的那个.
中心下标是数组的一个下标, 其左侧的所有元素相加的和等于右侧所有元素相加的和. 中心下标可能出现在数组的两端.
这道题的关键是理解题意, 左边等于右边, 这是不包括当前位置的值的. 于是思路有
-
轮到下标为i元素时, 判断0+1+..+i-1+ i 是否等于 sum - 0 - 1 - (i-1). 双方都加一个当前元素等于没有加.
1public static int pivotIndex(int[] nums){ 2 int sumMinusPrev = Arrays.stream(nums).sum(); 3 int leftPlusCurr = 0; 4 for (int i = 0; i < nums.length; i++) { 5 leftPlusCurr += nums[i]; 6 if (leftPlusCurr == sumMinusPrev){ 7 return i; 8 } 9 sumMinusPrev -= nums[i]; 10 } 11 return -1; 12 }
-
既然左边右边是相等的, 那么2*(0+1+2+…+i-1) + i = sum总和.
1public static int pivotIndex2(int[] nums){ 2 int sum = Arrays.stream(nums).sum(); 3 int ima = 0; 4 for (int i = 0; i < nums.length; i++){ 5 if (ima * 2 + nums[i] == sum){ 6 return i; 7 } 8 ima += nums[i]; 9 } 10 return -1; 11 }
验证:
1public static void main(String[] args) {
2 System.out.println(pivotIndex(new int[]{1,7,3,6,5,6}));
3 System.out.println(pivotIndex2(new int[]{1,7,3,6,5,6}));
4 }
X的平方根
不使用sqrt函数, 得到x的平方根的整数部分.
二分法
寻找i * i < x 但 (i+1)*(i+1) > x的值, 使用二分法缩减寻找次数.
1public static int binarySearch(int x){
2 int index = -1;
3 int lP = 0, rP = x;
4 while (lP <= rP){
5 //计算新的左右指针的中间值
6 int mid = (lP + rP) / 2;
7 if (mid * mid <= x){
8 lP = mid + 1;
9 //取小值
10 index = mid;
11 }else {
12 rP = mid - 1;
13 }
14
15 }
16 return index;
17 }
牛顿迭代
原理: x/n 与 n的均值比他们原来更趋近于根号x.
原本的牛顿迭代是迭代选取的点对应切线与x轴交点的值(x1 = x0 - f(x0)/df(x0)),
这里相当于是求x^2-x0=0方程的根, 化简之后就是(i + x/i)/2, 和牛顿迭代一致.
1public static int newton(int x){
2 //第一个参数值无所谓, 越接近根号x, 则迭代次数越少.
3 return (int)sqrt(x, x);
4 }
5
6 public static double sqrt(double i, int x){
7 double res = (i + x/i)/2;
8 if (res == i){
9 return res;
10 }else {
11 sqrt(res, x);
12 }
13 }
数组中三个数的最大乘积
整形数组nums, 在数组中找出由三个数字组成的最大乘积, 并输出.
当数组全为整数时, 最大乘积为前三个最大的数的乘积;
当数组有一个负数时, 最大乘积为前三个最大的数的乘积;
当数组由两个及以上的负数时, 最大乘积为前三个最大的数的乘积 和 最大的数与两个最小的数的乘积之大者.
先排序后计算
算法复杂度取决于排序算法.
1public static int sortAndCalc(int[] nums){
2 int length = nums.length;
3 Arrays.sort(nums);
4 return Math.max(
5 nums[0]*nums[1]*nums[length-1],
6 nums[length-1]*nums[length-2]*nums[length-3]
7 );
8 }
线性扫描: 只找出前三最大和前二最小
1public static int getMax3AndMin2(int[] nums){
2 int max1 = Integer.MIN_VALUE, max2 = Integer.MIN_VALUE, max3 = Integer.MIN_VALUE;
3 int min1 = Integer.MAX_VALUE, min2 = Integer.MAX_VALUE;
4 for (int num : nums){
5 if (num > max1){
6 max3 = max2;
7 max2 = max1;
8 max1 = num;
9 }else if (num > max2){
10 max3 = max2;
11 max2 = num;
12 }else if (num > max3){
13 max3 = num;
14 }
15
16 if (num < min1){
17 min2 = min1;
18 min1 = num;
19 }else if (num < min2){
20 min2 = num;
21 }
22 }
23
24 return Math.max(
25 max1*max2*max3,
26 max1*min1*min2
27 );
28 }
两数之和–无序数组
给定整数数组nums, 从中寻找两个数满足他们之和为给定目标target.
不可重复使用元素, 返回两数的下标值, 以数组形式返回.
可以使用暴力算法.
或者利用map记录扫描过的数及下标. 这样空间复杂度会稍高.
1import java.util.Arrays;
2import java.util.HashMap;
3import java.util.Map;
4
5public class TwoNumberForSum {
6
7 public static void main(String[] args) {
8 System.out.println(
9 Arrays.toString(solution(new int[]{1, 2, 3, 4, 5, 6}, 10))
10 );
11 }
12
13
14 public static int[] solution(int[] nums, int target){
15 Map<Integer, Integer> map = new HashMap<>();
16 for (int i = 0; i < nums.length; i++) {
17 if (map.containsKey(target - nums[i])){
18 return new int[]{map.get(target - nums[i]), i};
19 }
20 map.put(nums[i], i);
21 }
22 return new int[0];
23 }
24}
25
两数之和–有序数组
二分查找
遍历左边的界限, 在界限之中应用二分查找target-nums[i]的数.
1public static int[] twoSearch(int[] nums, int target){
2 for (int i = 0; i < nums.length; i++) {
3 int low = nums[i], high = nums.length - 1;
4 while (low <= high){
5 int mid = (low + high) / 2;
6 if (nums[mid] == target - nums[i]){
7 return new int[]{i, mid};
8 }else if (nums[mid] > target - nums[i]){
9 high = mid - 1;
10 }else {
11 low = mid + 1;
12 }
13 }
14 }
15 return new int[0];
16 }
双指针
前提是有序.
1public static int[] twoPointer(int[] nums, int target){
2 int low = 0, high = nums.length - 1;
3 while (low < high){
4 int sum = nums[low] + nums[high];
5 if (sum == target){
6 return new int[]{low, high};
7 }else if (sum > target){
8 high--;
9 }else {
10 low++;
11 }
12 }
13 return new int[0];
14 }
计算斐波那契数列
0, 1, 1, 2, 3, 5, 8, …. 后一项等于前面两项的和.
暴力递归
直接递归, 这样有些项存在严重的重复计算.
1public static int BFRecursion(int num){
2 if (num == 0) return 0;
3 if (num == 1) return 1;
4 return BFRecursion(num - 1) + BFRecursion(num - 2);
5 }
去重递归
用空间保存计算过的项. 时间O(n) 空间O(n)
1public static int SavedRecursion(int num){
2 //由于第一个是0, 所以需要多一个空间.
3 int[] saveArr = new int[num + 1];
4 return Recursion(saveArr, num);
5 }
6
7 private static int Recursion(int[] saveArr, int num) {
8 if (num == 0) return 0;
9 if (num == 1) return 1;
10 //如果之前这个位置已经被运算, 直接返回.
11 if (saveArr[num] != 0) return saveArr[num];
12 saveArr[num] = Recursion(saveArr, num - 1) + Recursion(saveArr, num - 2);
13 return saveArr[num];
14 }
双指针迭代
只需要两个位置保存, 节省了空间至O(1)
1public static int TwoPointer(int num){
2 if (num == 0) return 0;
3 if (num == 1) return 1;
4 int first = 1, second = 0;
5 for (int i = 2; i <= num; i++) {
6 int temp = first;
7 first = first + second;
8 second = temp;
9 }
10 return first;
11 }
排列硬币
n枚硬币, 将他们排列为阶梯形状, 第k行有k枚硬币.
给定一个数字n, 找出可形成完整阶梯行的总行数. 就是可以剩, 不需要全部用完.
暴力迭代
啥也不管, 一行一行地排
1public static int arrangeCoinBF(int n){
2 for (int i = 1; i < n; i++) {
3 n -= i;
4 if (n < i + 1){
5 return i;
6 }
7 }
8 return -1;
9 }
二分查找
行数必定在0到n之间, 查找这之中满足求和=n的值, 找不到就返回最大的行使求和<n.
1public static int arrangeCoinSearch(int n){
2 int low = 0, high = n - 1;
3 while (low < high){
4 int mid = (low + high) / 2;
5 int predict = ((mid + 1) * mid) / 2;
6 if (n == predict){
7 return mid;
8 }else if (n < predict){
9 high = mid - 1;
10 }else {
11 low = mid + 1;
12 }
13 }
14 return low;
15 }
牛顿迭代
给定总数n求行数x, 由于 (x^2 + x) /2= n, 想求对应的x, 实际上是函数f(x) = (x^2 + x) /2 - n的根, 于是可以采用牛顿迭代, 每次迭代x1 = x0 - f(x0)/df(x0) = (x0^2 + 2n)/(2x0+1), 如果从n开始迭代, 则第一次为
1public static double newtonRecursion(int n, int res){
2 res = (res * res + 2 * n) / (res * 2 + 1);
3 if (res * res + res == 2 * n){
4 return res;
5 }else {
6 return newtonRecursion(n, res);
7 }
8 }
环形链表
给定额链表, 判断链表中是否有环(如果链表有某个节点可以通过连续跟踪next指针再次到该节点, 就存在环). 存在环返回true, 否则返回false.
内部类:
1//内部类
2 static class ListNode{
3 int val;
4 ListNode next;
5
6 public ListNode(int val, ListNode next){
7 this.val = val;
8 this.next = next;
9 }
10 }
11
12 public static void main(String[] args) {
13 ListNode node5 = new ListNode(5, null);
14 ListNode node4 = new ListNode(4, node5);
15 ListNode node3 = new ListNode(3, node4);
16 ListNode node2 = new ListNode(2, node3);
17 ListNode node1 = new ListNode(1, node2);
18 //node5.next = node3;
19
20 System.out.println(isCycle(node1));
21 System.out.println(isCycleTwoPointer(node1));
22 }
普通循环
时间O(n), 空间O(n)
1public static Boolean isCycle(ListNode head){
2 HashSet<ListNode> set = new HashSet<>();
3 while (head != null){
4 if (!set.add(head)){
5 return true;
6 }
7 head = head.next;
8 }
9 return false;
10 }
双指针
如果快指针和慢指针可以重叠, 说明存在环; 如果快指针到达了null, 说明没有环.
时间O(n), 空间O(1)
1public static Boolean isCycleTwoPointer(ListNode head){
2 if (head == null || head.next == null) return false;
3 ListNode slow = head;
4 ListNode fast = head.next;
5 while (fast.next != null && fast.next.next != null){
6 if (slow == fast) return true;
7 slow = slow.next;
8 fast = fast.next.next;
9 }
10 return false;
11 }
合并两个有序数组
两个有序整数数组nums1和nums2, 将nums2合并到nums1中, 使nums1成为一个有序数组. nums1和nums2元素个数分别为m和n, 假设nums1的空间大小等于m+n.
合并后排序
取决于排序的时间复杂度O(N*logN), 不消化额外空间
1//m为nums1实际元素个数, n为nums2实际元素个数
2 public static int[] mergeAndSort(int[] nums1, int m, int[] nums2, int n){
3 //将数组2拷贝至数组1的后面.
4 System.arraycopy(nums2, 0, nums1, m, n);
5 //排序, 复杂度取决于排序的复杂度, 为N*logN
6 Arrays.sort(nums1);
7 return nums1;
8 }
双指针
需要消耗O(n)的空间
1public static int[] compareAndInsert(int[] nums1, int m, int[] nums2, int n){
2 //要求返回nums1, 所以我们需要处理一下.
3 int[] num1Copy = new int[m];
4 System.arraycopy(nums1, 0, num1Copy, 0, m);
5 int pointerNum1 = 0, pointerNum2 = 0;
6 int i = 0;
7// int[] res = new int[m+n];
8
9 while (pointerNum1 < m && pointerNum2 < n){
10 //被选中的才会++, 且是先用后++.
11 nums1[i++] = num1Copy[pointerNum1] < nums2[pointerNum2] ? num1Copy[pointerNum1++] : nums2[pointerNum2++];
12// if (nums1[pointerNum1] > nums2[pointerNum2]){
13// res[i] = nums2[pointerNum2];
14// pointerNum2++;
15// }else {
16// res[i] = nums1[pointerNum1];
17// pointerNum1++;
18// }
19// i++;
20 }
21 if (pointerNum2 < n){
22 System.arraycopy(nums2, pointerNum2, nums1, pointerNum1 + pointerNum2, m + n - pointerNum2 -pointerNum1);
23 }
24 if (pointerNum1 < m){
25 System.arraycopy(num1Copy, pointerNum1, nums1, pointerNum1 + pointerNum2, m + n - pointerNum2 -pointerNum1);
26 }
27 return nums1;
28 }
倒着排
1 public static int[] reverseMerge(int[] nums1, int m, int[] nums2, int n) {
2 int p1 = m - 1, p2 = n-1;
3 int i = m+n-1;
4
5 while (p1 >= 0 && p2 >= 0) {
6 nums1[i--] = nums1[p1] > nums2[p2] ? nums1[p1--] : nums2[p2--];
7 }
8 //有两种情况, 1是nums2下标先走完至-1, 这时无序任何操作
9 //2是nums1下标先走完至-1, 这是nums2还有额外的元素需要复制
10 System.arraycopy(nums2, 0, nums1, 0, p2 + 1);
11 return nums1;
12 }
子数组最大平均数
给一个整数数组, 找出平均数最大且长度为k的下标连续的子数组, 并输出该最大平均数.
输入: [1,12,-5,-6,50,3], k=4
输出: 12.75
最大平均数 (12-5-6+50)/4=12.75
滑动窗口
是双指针的特例:
1public static double twoPointer(int[] nums, int k){
2 int pl = 0, pr = k-1;
3 int maxSum = 0;
4 for (int i = pl; i <= pr; i++) {
5 maxSum += nums[i];
6 }
7
8 while (pr < nums.length - 1){
9 int tempSum = maxSum -nums[pl] + nums[pr+1];
10 if (tempSum > maxSum) maxSum = tempSum;
11 pl++;
12 pr++;
13 }
14
15 return 1.0 * maxSum/k;
16 }
由于长度固定, 故只需要一个指针:
1public static double slideWindow(int[] nums, int k){
2 int sum = 0;
3 for (int i = 0; i < k; i++) {
4 sum += nums[i];
5 }
6 int max = sum;
7
8 for (int i = k; i < nums.length; i++) {
9 sum = sum - nums[i-k] + nums[i];
10 max = Math.max(sum, max);
11 }
12
13 return 1.0 * max/4;
14 }
二叉树的最小深度
最小深度是从根节点到最近叶子节点的最短路径上的节点数量.
深度优先
先找到叶子节点, 然后从叶子节点往上找, 计算每个节点的最小深度(取左右叶子节点较小的那个+1.
空间复杂度O(logN) 取决于树的深度, 时间复杂度O(N)
1 public static int minDepth(TreeNode root) {
2 //空节点
3 if (root == null) return 0;
4 //叶子节点
5 if (root.left == null && root.right == null) return 1;
6
7 int min = Integer.MAX_VALUE;
8 //寻找左右子节点中最小的深度
9 if (root.left != null) min = Math.min(min, minDepth(root.left));
10 if (root.right != null) min = Math.min(min, minDepth(root.right));
11
12 //最后加上本节点的1返回.
13 return min + 1;
14 }
广度优先
一层一层遍历, 遍历到的第一个叶子节点就是最小深度.
空间复杂度取决于队列O(N), 时间复杂度O(N)
1public static int minWidth(TreeNode root){
2 if (root == null) return 0;
3
4 //由于队列的性质, 它是广度遍历. 遍历完同一层才会遍历下一层
5 Queue<TreeNode> queue = new LinkedList<TreeNode>();
6 root.setDeep(1);
7 //入队
8 queue.offer(root);
9
10 while (!queue.isEmpty()){
11 //出队
12 TreeNode node = queue.poll();
13
14 //遍历到根节点直接返回
15 if (node.left == null && node.right == null){
16 return node.deep;
17 }
18
19 if (node.left != null) {
20 node.left.setDeep(node.getDeep() + 1);
21 queue.offer(node.left);
22 }
23 if (node.right != null){
24 node.right.setDeep(node.getDeep() + 1);
25 queue.offer(node.right);
26 }
27 }
28
29 //非正常退出
30 return -1;
31 }
最长连续递增序列
给定一个未排序的整数数组, 找到最长且连续递增的子序列, 并返回该序列的长度.
方式一: 记录当前序列的开始位置, 如果后面的数字小于这个序列最后一个数, 则更新开始下标.
方式二: 直接记录最大序列的长度和当前序列的长度.
1public class MaxSeq {
2
3 public static void main(String[] args) {
4 System.out.println(findLength(new int[]{1,2,3,2,3,4,3,4,5,6,7}));
5 }
6
7 public static int findLength(int[] nums){
8 int start = 0;
9 int max = 0;
10 for (int i = 1; i < nums.length; i++) {
11 if (nums[i] < nums[i-1]) start = i;
12 max = Math.max(max, i - start + 1);
13 }
14 return max;
15 }
16}
17
柠檬水找零
每杯柠檬水5美元, 顾客排队买, 顾客只会付5, 10, 20美元, 必须给顾客正确找零. 一开始手里面没有任何零钱, 如果能正确找零返回true, 否则返回false.
分析: 对于顾客付的5美元, 直接收下; 对于顾客付的10美元, 只能找5美元; 对于20美元, 可以三个5, 也可以10+5, 但是为了局部最优, 最好10+5. 因为这里5是万能的, 尽量减少使用.
1public class LemonWater {
2
3 public static void main(String[] args) {
4 System.out.println(isCanGiveChange(new int[]{5,5,5,20}));
5
6 }
7
8 public static boolean isCanGiveChange(int[] nums){
9 //5和10的票子数量
10 int fiveNum = 0, tenNum = 0;
11 for (int num: nums) {
12 switch (num){
13 case 5:
14 fiveNum++;
15 break;
16 case 10:
17 if (fiveNum > 0){
18 fiveNum--;
19 tenNum++;
20 }else
21 return false;
22 break;
23 case 20:
24 if (fiveNum > 0 && tenNum > 0){
25 fiveNum--;
26 tenNum--;
27 }else if (fiveNum >= 3){
28 fiveNum -= 3;
29 }else
30 return false;
31 }
32 }
33
34 return true;
35 }
36}
37
三角形的最大周长
给一个正数数组arr, 返回由其3个数组成的, 面积不为0的三角形的周长可能的最大值.
直接找最大和的三个数, 看看是否能形成三角形, 不行就减少最大数继续. 因为既然最大的三个数不行, 那么前两最大的数和任何一个数都不行了, 所以只能往下找.
1import java.util.Arrays;
2
3public class Triangle {
4
5 public static void main(String[] args) {
6 System.out.println(maxPerimeter(new int[]{3,6,3,2}));
7 }
8
9 public static int maxPerimeter(int[] nums){
10 Arrays.sort(nums);
11 for (int i = nums.length - 1; i >= 2 ; i--) {
12 if (nums[i-1] + nums[i-2] > nums[i]){
13 return nums[i] + nums[i-1] + nums[i-2];
14 }
15 }
16 return 0;
17 }
18}
19
二叉树遍历
前序
递归实现
1//前序:根左右
2 public static void preorder(TreeNode root){
3 if (root == null) return;
4
5 System.out.println(root.val);
6 preorder(root.left);
7 preorder(root.right);
8 }
中序
递归实现
1 //中序
2 public static void midorder(TreeNode root){
3 if (root == null) return;
4
5 if (root.left != null){
6 preorder(root.left);
7 }
8
9 System.out.println(root.val);
10
11 if (root.right != null){
12 preorder(root.right);
13 }
14
15 }
后序
递归实现
1//后序
2 public static void postorder(TreeNode root){
3 if (root == null) return;
4
5 if (root.left != null){
6 preorder(root.left);
7 }
8
9 if (root.right != null){
10 preorder(root.right);
11 }
12
13 System.out.println(root.val);
14 }
层序
递归实现
1//层序遍历
2 //这个参数i代表的第几层, 由于树不是完全的, 所以一定之间会有null值
3 public static void levelorder(TreeNode root, int i, ArrayList list){
4 if (root == null) return;
5
6 int length = list.size();
7 //为防止数组越界, 这里提前填充至i.
8 if (length <= i){
9 for (int j = 0; j <= i - length; j++) {
10 list.add(length+j, null);
11 }
12 }
13
14 list.set(i, root.val);
15 levelorder(root.left, 2*i, list);
16 levelorder(root.right, 2*i+1, list);
17 }