目录
动态规划
1 翻译数字
剑指 Offer 46. 把数字翻译成字符串https://leetcode-cn.com/problems/ba-shu-zi-fan-yi-cheng-zi-fu-chuan-lcof/
最初的思路是借助栈和求余将数字转换成一个数组,再进行动态规划。
记数字 num 第 i 位数字为 xi,数字 num 的位数为 n ;
例如: num = 12258 的 n = 5 , x1 = 1
动态规划解析:
- 状态定义: 设动态规划列表 dp ,dp[i] 代表以 xi 为结尾的数字的翻译方案数量
-
转移方程: 若 xi 和 xi−1 组成的两位数字可以被翻译,则 dp[i]=dp[i−1]+dp[i−2] ;否则 dp[i]=dp[i−1] 。
这里划分的比我实现的要更细致!我是在0到25之间 再限制十位不是0。
-
初始状态: dp[0] = dp[1] = 1 ,即 “无数字” 和 “第 1 位数字” 的翻译方法数量均为 1 ;
-
返回值: dp[n],即此数字的翻译方案数量。
Q: 无数字情况 dp[0] = 1 从何而来?
A: 当 num 第 1, 2 位的组成的数字 ∈[10,25] 时,显然应有 2 种翻译方法,即 dp[2]=dp[1]+dp[0]=2 ,而显然 dp[1] = 1dp[1]=1 ,因此推出 dp[0] = 1dp[0]=1 。
动态规划思想通透了~ 下一步将是把数字转换出每一位来走!俩思想
- 一个是转成字符串切片,(我是用的转成字符串后charAt定位)
- 另一个是求余
1. 动态规划+求余
package jzof.Day10;
import org.w3c.dom.stylesheets.LinkStyle;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
/**
* @author ahan
* @create_time 2021-11-11-12:29 下午
* 给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1
* 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
*
*/
public class _46 {
public static void main(String[] args) {
System.out.println(new _46().translateNum_0(12258));
System.out.println(new _46().translateNum(220));
System.out.println(new _46().translateNum(506));
}
public int translateNum_0(int num) {
Deque<Integer> nums = new LinkedList<>();
int i = 0;
System.out.print(num%10+ " ");
nums.push(num%10);
while(num/10!=0){
num /= 10;
nums.push(num%10);
System.out.print(num%10+ " ");
}
System.out.println("\n---");
int[] numsList = new int[nums.size()];
int[] dp = new int[nums.size()+1];
while (!nums.isEmpty()){
int temp = nums.pop();
numsList[i++] = temp;
}
dp[0] = 1;
dp[1] = 1;
for (int j = 2; j < numsList.length+1; j++) {
int t = numsList[j-1]+numsList[j-2]*10;
if(numsList[j-2] != 0 && t <= 25 && t >= 0)
dp[j] = dp[j-1] + dp[j-2];
else
dp[j] = dp[j-1];
}
for (int j = 0; j < dp.length; j++) {
System.out.print(dp[j]+" ");
}
System.out.println("\n---");
return dp[numsList.length];
}
public int translateNum(int num) {
Deque<Integer> nums = new LinkedList<>();
int i = 0;
nums.push(num%10);
while(num/10!=0){
num /= 10;
nums.push(num%10);
}
int[] numsList = new int[nums.size()];
int[] dp = new int[nums.size()+1];
while (!nums.isEmpty()){
int temp = nums.pop();
numsList[i++] = temp;
}
dp[0] = 1;
dp[1] = 1;
for (int j = 2; j < numsList.length+1; j++) {
int t = numsList[j-1]+numsList[j-2]*10;
if(numsList[j-2] != 0 && t <= 25 && t >= 0)
dp[j] = dp[j-1] + dp[j-2];
else
dp[j] = dp[j-1];
}
return dp[numsList.length];
}
}
1.2 空间优化
class Solution {
public int translateNum(int num) {
Deque<Integer> nums = new LinkedList<>();
int i = 0;
nums.push(num%10);
while(num/10!=0){
num /= 10;
nums.push(num%10);
}
int[] numsList = new int[nums.size()];
while (!nums.isEmpty()){
int temp = nums.pop();
numsList[i++] = temp;
}
int pre_1 = 1;
int pre_2 = 1;
int cur = 1;
for (int j = 2; j < numsList.length+1; j++) {
int t = numsList[j-1]+numsList[j-2]*10;
if(numsList[j-2] != 0 && t <= 25 && t >= 0)
cur = pre_1 + pre_2;
else
cur = pre_2;
pre_1 = pre_2;
pre_2 = cur;
}
return cur;
}
}
2. 字符串string定位前俩数字
class Solution {
public int translateNum(int num) {
String s = Integer.toString(num);
int pre_1 = 1;
int pre_2 = 1;
int cur = 1;
for (int j = 2; j < s.length()+1; j++) {
int t = (int)s.charAt(j-1)-48+((int)s.charAt(j-2)-48)*10;
if(((int)s.charAt(j-2)-48) != 0 && t <= 25 && t >= 0)
cur = pre_1 + pre_2;
else
cur = pre_2;
pre_1 = pre_2;
pre_2 = cur;
}
return cur;
}
}
3. (大佬实现)细节满满
class Solution {
public int translateNum(int num) {
String s = String.valueOf(num);
int a = 1, b = 1;
for(int i = 2; i <= s.length(); i++) {
String tmp = s.substring(i - 2, i);
int c = tmp.compareTo("10") >= 0 && tmp.compareTo("25") <= 0 ? a + b : a;
b = a;
a = c;
}
return a;
}
}
此题的动态规划计算是 对称的 ,即 从左向右 遍历(从第 dp[2] 计算至 dp[n] )和 从右向左 遍历(从第 dp[n - 2]计算至 dp[0] )所得方案数一致。从右向左遍历的代码如下所示。
class Solution {
public int translateNum(int num) {
String s = String.valueOf(num);
int a = 1, b = 1;
for(int i = s.length() - 2; i > -1; i--) {
String tmp = s.substring(i, i + 2);
int c = tmp.compareTo("10") >= 0 && tmp.compareTo("25") <= 0 ? a + b : a;
b = a;
a = c;
}
return a;
}
}
- 上述方法虽然已经节省了 dp 列表的空间占用,但字符串 s 仍使用了 O(N) 大小的额外空间。
空间复杂度优化:
- 利用求余运算 num%10 和求整运算 num//10 ,可获取数字 num 的各位数字(获取顺序为个位、十位、百位…)。
- 因此,可通过 求余 和 求整 运算实现 从右向左 的遍历计算。而根据上述动态规划 “对称性” ,可知从右向左的计算是正确的。
- 自此,字符串 s 的空间占用也被省去,空间复杂度从 O(N) 降至 O(1) 。
复杂度分析:
- 时间复杂度O(N) : NN 为字符串 ss 的长度(即数字 num 的位数 log(num) ),其决定了循环次数。
- 空间复杂度 O(1) : 几个变量使用常数大小的额外空间。
既然从右向左也是可以的求余就更好写了。
class Solution {
public int translateNum(int num) {
int a = 1, b = 1, x, y = num % 10;
while(num != 0) {
num /= 10;
x = num % 10;
int tmp = 10 * x + y;
int c = (tmp >= 10 && tmp <= 25) ? a + b : a;
b = a;
a = c;
y = x;
}
return a;
}
}
思路有想到。在转化数字的时候卡顿了~
2 最长不含重复字符的子字符串
只想到了滑动窗口,效果不理想~
1. 滑动窗口
提交前忘记考虑如果abba这种情况,在start和end都移动到2,2,再滑动,start反而会朝前移动,要在移动后判断map里有的信息置为无效(-1)。
package jzof.Day10;
import java.util.*;
/**
* @author ahan
* @create_time 2021-11-11-8:29 下午
* 请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
*/
public class _48 {
public static void main(String[] args) {
// String s = new String("abcabcbb");
// String s = new String("bbbbb");
// String s = new String("pwwkew");
// String s = new String("abcabcbb");
String s = new String("abba");
System.out.println(new _48().lengthOfLongestSubstring(s));
}
// 存节点index 对 遇到已存在的 start移动到之前的后一个 然后更新start end继续滑动
public int lengthOfLongestSubstring(String s) {
HashMap<Character, Integer> map = new HashMap<>();
int start = 0;
int max = 0;
for (int i = 0; i < s.length(); i++) {
if(map.get(s.charAt(i)) != null && map.get(s.charAt(i)) != -1){
start = map.get(s.charAt(i)) + 1;
for (Map.Entry<Character, Integer> entry: map.entrySet()) {
if(entry.getValue() < start){
map.put(entry.getKey(), -1);
}
}
}
map.put(s.charAt(i), i);
if(max < (i - start + 1)) max = (i -start + 1);
}
return max;
}
}
长度为 N 的字符串共有 (1+N)N /2个子字符串(复杂度为 O(N^2) ),判断长度为 N 的字符串是否有重复字符的复杂度为 O(N) ,因此本题使用暴力法解决的复杂度为 O(N^3) 。考虑使用动态规划降低时间复杂度。
动态规划解析:
-
状态定义: 设动态规划列表 dp ,dp[j] 代表以字符 s[j] 为结尾的 “最长不重复子字符串” 的长度。
-
转移方程:还是固定右边界 ,设字符 s[j] 左边距离最近的相同字符为 s[i],即 s[i] = s[j]。
-
当 i < 0 ,即 s[j] 左边无相同字符,则 dp[j] = dp[j-1] + 1;
- 当 dp[j - 1] < j - i,说明字符 s[i] 在子字符串 dp[j−1] 区间之外 ,则 dp[j] = dp[j - 1] + 1;
- 当 dp[j−1]≥j−i ,说明字符 s[i] 在子字符串 dp[j-1] 区间之中 ,则 dp[j]的左边界由 s[i] 决定,即 dp[j] = j - i ;
-
当 i < 0 时,由于 dp[j−1] ≤ j 恒成立,因而 dp[j - 1] < j - i恒成立,因此分支
1.
和2.
可被合并。
-
返回值:max(dp) ,即全局的 “最长不重复子字符串” 的长度。
空间复杂度优化:
由于返回值是取 dp 列表最大值,因此可借助变量 tmptmp 存储 dp[j],变量 res 每轮更新最大值即可。
2. 动态规划+hash表
- 哈希表统计:遍历字符串 s 时,使用哈希表(记为 dic )统计各字符最后一次出现的索引位置 。
- 左边界 i 获取方式:遍历到 s[j] 时,可通过访问哈希表 dic[s[j]] 获取最近的相同字符的索引 ii
复杂度分析:
- 时间复杂度 O(N): 其中 NN 为字符串长度,动态规划需遍历计算dp 列表。
- 空间复杂度 O(1) : 字符的 ASCII 码范围为 0 ~ 127 ,哈希表 dicdic 最多使用 O(128) = O(1)大小的额外空间。
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> dic = new HashMap<>();
int res = 0, tmp = 0;
for(int j = 0; j < s.length(); j++) {
int i = dic.getOrDefault(s.charAt(j), -1); // 获取索引 i
dic.put(s.charAt(j), j); // 更新哈希表
tmp = tmp < j - i ? tmp + 1 : j - i; // dp[j - 1] -> dp[j]
res = Math.max(res, tmp); // max(dp[j - 1], dp[j])
}
return res;
}
}
3. 动态规划+线性遍历
- 左边界 i 获取方式: 遍历到 s[j]s[j] 时,初始化索引 i = j - 1 ,向左遍历搜索第一个满足 s[i] = s[j] 的字符即可 。
复杂度分析:
- 时间复杂度 O(N^2) : 其中 N 为字符串长度,动态规划需遍历计算 dp 列表,占用 O(N) ;每轮计算 dp[j] 时搜索 i 需要遍历 j 个字符,占用 O(N) 。
- 空间复杂度 O(1) : 几个变量使用常数大小的额外空间。
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> dic = new HashMap<>();
int res = 0, tmp = 0;
for(int j = 0; j < s.length(); j++) {
int i = j - 1;
while(i >= 0 && s.charAt(i) != s.charAt(j)) i--; // 线性查找 i
tmp = tmp < j - i ? tmp + 1 : j - i; // dp[j - 1] -> dp[j]
res = Math.max(res, tmp); // max(dp[j - 1], dp[j])
}
return res;
}
}
4. 双指针+hashmap(题解写法)
本质上与方法一类似,不同点在于左边界 ii 的定义。
- 哈希表 dic 统计: 指针 j 遍历字符 s ,哈希表统计字符 s[j] 最后一次出现的索引 。
- 更新左指针 i : 根据上轮左指针 i 和 dic[s[j]] ,每轮更新左边界 i ,保证区间 [i + 1, j]内无重复字符且最大。
复杂度分析:
- 更新结果 res: 取上轮 resres 和本轮双指针区间 [i + 1,j][i+1,j] 的宽度(即 j - ij−i )中的最大值。
- 时间复杂度 O(N) : 其中 NN 为字符串长度,动态规划需遍历计算 dp 列表。
- 空间复杂度 O(1) : 字符的 ASCII 码范围为 0 ~ 127 ,哈希表 dicdic 最多使用 O(128) = O(1)大小的额外空间。
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> dic = new HashMap<>();
int i = -1, res = 0;
for(int j = 0; j < s.length(); j++) {
if(dic.containsKey(s.charAt(j)))
i = Math.max(i, dic.get(s.charAt(j))); // 更新左指针 i
dic.put(s.charAt(j), j); // 哈希表记录
res = Math.max(res, j - i); // 更新结果
}
return res;
}
}
这里更新左指针,会遇到abba这种情况,我解决的策略是如果之前有过 就置为-1,在更新前判断是不是不等于-1,存在且不等于-1就更新,但是这样多了O(N^2)的遍历,
题解巧妙的根据,前面有过的肯定比新的索引小这一条件,在更新左指针时,判断map内的和当前的索引哪个大,存大的索引。