java算法之剑指offer题目整理3-算法篇
动态规划
1. 剑指 Offer 14- I. 剪绳子 I 动态规划来解
class Solution { public int cuttingRope(int n) { if (n == 2) return 1; if (n == 3) return 2; int dp[] = new int[n+1]; // 动态规划一定有一个起始点,下面三个就是这个问题的起始点。当n>3的时候,2 3 可以不在分割,那么最大值就是他自己。 dp[1] = 1; dp[2] = 2; dp[3] = 3; for(int i = 4; i <= n; i++){ int max_value = 0; // 记录所有可能中的最大值,然后将该值赋值给dp[i] for(int j = 1; j <= i/2; j++){ if(max_value < dp[i-j] * dp[j]){ max_value = dp[i-j] * dp[j]; } } dp[i] = max_value; } return dp[n]; } }
2. 剑指 Offer 14- II. 剪绳子 II,不能使用动态规划,因为存储在dp中的中间结果会溢出的(取模1000000007,int的溢出值为>2147483648) 。
class Solution { public int cuttingRope(int n) { if(n == 2) { return 1; } if(n == 3){ return 2; } int mod = (int)1e9 + 7; long res = 1; while(n > 4) { res *= 3; res %= mod; // 在过程中就对最终的结果取模,计算的结果会在1000000007~2147483648,不会大于2147483648的,此过程中不会存在数据溢出。 n -= 3; } return (int)(res * n % mod); } } /* 我们首先考虑对于一段长n的绳子,我们可以切出的结果包含什么? 1会包含吗? 不会,因为1 * (k - 1) < k, 只要把1和任何一个其他的片段组合在一起就有个更大的值 2可以 3可以 4可以吗? 它拆成两个2的效果和本身一样,因此也不考虑 5以上可以吗? 不可以,这些绳子必须拆,因为总有一种拆法比不拆更优,比如拆成 k / 2 和 k - k / 2 综上, 最后的结果只包含2和3(当然当总长度为2和3时单独处理), 那么很显然n >= 5时, 3*(n - 3) >= 2 * (n - 2) ,因此我们优先拆成3,最后剩余的拆成2。最后的结果一定是由若干个3和1或2个2组成. */
// 采用动态规划来求解。dp[当前骰子数][点数] = dp[当前骰子数-1][点数-1] + dp[当前骰子数-1][点数-2] + .... + dp[当前骰子数-1][点数-6] // 点数依次减1,直到减到6,减去的点数由最后一个骰子来填充 class Solution { public double[] dicesProbability(int n) { double dp[][] = new double[n+1][6*n+1]; // 初始值全部为0; for(int i = 1; i < 7; i++){ dp[1][i] = 1.0; // 初始化dp,一个骰子,点数为i,可能的出现的次数 } for(int i = 2; i <= n; i++){ // 第i个骰子 for(int j = i; j <= 6*i; j++){ // 点数为j for(int k = j-1; k >= j-6 && k != 0; k--){ // 在点数j的基础上,依次减去1~6,减去的数可以由第i个骰子补上 dp[i][j] += dp[i-1][k]; } } } double size = Math.pow(6,n); // 三个骰子,就是6*6*6种可能 int j = 0; double res[] = new double[5*n + 1]; for(int i = n; i <= 6*n; i++){ res[j++] = dp[n][i] / size; } return res; } }
请实现一个函数用来匹配包含'. '和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但与"aa.a"和"ab*a"均不匹配。
class Solution { public boolean isMatch(String s, String p) { // p代表正则表达式 int m = s.length() + 1, n = p.length() + 1; boolean dp[][] = new boolean[m][n]; // 初始值默认全为false dp[0][0] = true; // s和p均为空字符串 for(int i = 2; i < n; i = i+2){ // *如果出现在奇数位上,则其必然为false, a*bb*, 第一个b无法消除掉 dp[0][i] = dp[0][i-2] && p.charAt(i-1) == '*'; // 当前位必须为*,且前两位必须为true,相当于当s为空的时候,只有a*b*c*这样才能符合条件 } // 当字符串为0或者1的时候,会直接跳过这里 for(int i = 1; i < m; i++){ // 从s的第一个字符处开始 for(int j = 1; j < n; j++){ // 从p的第一个字符处开始 /* 将分成两种情况,一种是当前字符是否为* */ if(p.charAt(j-1) == '*'){ // 以下三种情况满足一种都可以 if(dp[i][j-2]) dp[i][j] = true; // *之前的元素出现零次,且之前的两个字符串相等 // 最关键的一步 if(dp[i-1][j] && p.charAt(j-2) == s.charAt(i-1)) dp[i][j] = true; //s当前的字符与p中*号之前的字符想等 // s = {b c b}, p = {b *},s中的第三个b等于p中的第一个b,但是s != p if(dp[i-1][j] && p.charAt(j-2) == '.') dp[i][j] = true; // // s = {a c b}, p = {a.*} *是让.出现多次,而不是c出现多次 }else{ // 当前两个字符要相等,或者j中的字符为. if(dp[i-1][j-1] && s.charAt(i-1) == p.charAt(j-1)) dp[i][j] = true; // 当前比较的两个字符之前的两个字符串要相等 if(dp[i-1][j-1] && p.charAt(j-1) == '.') dp[i][j] = true; } } } return dp[m-1][n-1]; // dp 0~m-1, 0~n-1 } } /* 动态规划 */
![]()
队列、栈操作
![]()
class MaxQueue { Queue<Integer> queue; Deque<Integer> deque; public MaxQueue() { queue = new LinkedList<Integer>(); deque = new LinkedList<Integer>(); } public int max_value() { if(deque.isEmpty()) return -1; return deque.peekFirst().intValue(); } public void push_back(int value) { queue.add(value); while(deque.peekLast() != null && deque.peekLast() < value){ deque.removeLast(); } deque.addLast(new Integer(value)); // 双端队列deque中的元素会按照从大到小排列 } public int pop_front() { if(queue.isEmpty()) return -1; int res = queue.poll().intValue(); if(deque.getFirst().intValue() == res){ deque.pollFirst(); } return res; } } /* 本算法基于问题的一个重要性质:当一个元素进入队列的时候,它前面所有比它小的元素就不会再对答案产生影响。 举个例子,如果我们向队列中插入数字序列 1 1 1 1 2,那么在第一个数字 2 被插入后,数字 2 前面的所有数字 1 将不会对结果产生影响。因为按照队列的取出顺序,数字 2 只能在所有的数字 1 被取出之后才能被取出,因此如果数字 1 如果在队列中,那么数字 2 必然也在队列中,使得数字 1 对结果没有影响。 */
其他算法
1. 剑指 Offer 20. 表示数值的字符串 有限状态机
class Solution { public boolean isNumber(String s) { Map[] states = { new HashMap<>() {{put(' ', 0); put('s', 1); put('d', 2); put('.', 4); }}, // 0 起始符号 new HashMap<>() {{put('d', 2); put('.', 4);}}, // 1 前一位是符号,那么现在这位的状态就会与集合1中的数比较。符号后可以是点 new HashMap<>() {{put('d', 2); put('.', 3); put('e', 5); put(' ', 8); }}, // 2 前一位是数字(小数点前的数字),那么现在这位的状态就会与集合2中的数比较 new HashMap<>() {{put('d', 3); put('e', 5); put(' ', 8); }}, // 3 前一位是点,或者前一位是数字(小数点后的数字)。 12.也算数字 new HashMap<>() {{put('d', 3);}}, // 4 前一位是点,小数点后必须有数字。 .3也算数字 new HashMap<>() {{put('s', 6); put('d', 7);}}, // 5 前一位是e new HashMap<>() {{put('d', 7);}}, // 6 前一位是符号位 new HashMap<>() {{put('d', 7); put(' ', 8);}}, // 7 前一位是数字。很好奇为什么指数中不能有小数??? new HashMap<>() {{put(' ', 8);}} // 8 }; char chs[] = s.toCharArray(); int p = 0; char tmp; for(char i: chs){ if(i >= '0' && i <= '9') tmp = 'd'; else if(i == '+' || i == '-') tmp = 's'; else if(i == '.') tmp = '.'; else if(i == 'e' || i == 'E') tmp = 'e'; else if(i == ' ') tmp = ' '; else tmp = '?'; if(states[p].get(tmp) == null) return false; p = (int) states[p].get(tmp); } return p == 2 || p == 3 || p == 7 || p == 8; } }
2. 剑指 Offer 41. 数据流中的中位数 利用堆排序
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
class MedianFinder {
Queue<Integer> A, B; // B在左,A在右
public MedianFinder() {
A = new PriorityQueue<>(); // 小顶堆,保存较大的一半
B = new PriorityQueue<>((x, y) -> (y - x)); // 大顶堆,保存较小的一半
}
public void addNum(int num) {
if(A.size() != B.size()) { //奇数的情况
A.add(num);
B.add(A.poll()); // 如果直接将元素压入B中,那么当元素很大的时候,会导致B堆顶的元素会大于A中的元素
} else {
B.add(num);
A.add(B.poll()); // 先给B,在从B中获取最大的元素值扔给A,由此完成排序过程
}
}
public double findMedian() {
return A.size() != B.size() ? A.peek() : (A.peek() + B.peek()) / 2.0;
}
}
/*
这题的时间复杂度可以这么理解,假设插入50000次,那么一共有50000个数组,每个数组都要排序,但是每个数组都是基本有序的,所以它就是直接插入排序时间最省为o(n),所以总的时间复杂度应该为o(n*n), 有多少个数组*每个数组排序的时间复杂度。
直接插入排序能通过,但是会时间复杂度为o(n*n),如果采用堆来求解的话,时间复杂度为o(n*logn)
*/