动态规划
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)
*/