文章目录
- 数据结构——图
- 1 基本概念
- 2 真题
- 2.1 路径问题(动态规划)
- 2.1.1 **[64. 最小路径和](https://leetcode-cn.com/problems/minimum-path-sum/)**
- 2.2.2 **[62. 不同路径](https://leetcode-cn.com/problems/unique-paths/)**
- 2.2.3 [63. 不同路径 II](https://leetcode-cn.com/problems/unique-paths-ii/)-有障碍物
- 2.2.4 [980. 不同路径 III](https://leetcode-cn.com/problems/unique-paths-iii/)
- 2.2.5 三角形最小路径和
- 2.2.6 爬楼梯问题
- 2.2.7 圆环回原点问题
- 2.2.8 交错字符串
- 2.2.9 翻译数字
数据结构——图
1 基本概念
图:有向图、无向图、双向图
1.1 图的存储方式
- 邻接表,我把每个节点
x
的邻居都存到一个列表里,然后把x
和这个列表关联起来,可以通过一个节点x
找到它的所有相邻节点。邻接表的主要优势是节约存储空间
List<Integer>[] graph;
// graph[s]是一个列表,存储着节点s所指向的节点。
- 邻接矩阵,二维数组,我们权且成为
matrix
,如果节点x
和y
是相连的,那么就把matrix[x][y]
设为true
或者权重。如果想找节点x
的邻居,去扫一圈matrix[x][..]
就行了。邻接矩阵的主要优势是可以迅速判断两个节点是否相邻
int[][] matrix
1.2 有向加权图
如果是邻接表,我们不仅仅存储某个节点x
的所有邻居节点,还存储x
到每个邻居的权重
如果是邻接矩阵,matrix[x][y]
不再是布尔值,而是一个 int 值,0 表示没有连接,其他值表示权重
连接无向图中的节点x
和y
,把matrix[x][y]
和matrix[y][x]
都变成true
不就行了;邻接表也是类似的操作
1.3 实现
图的节点类
/* 图节点的逻辑结构 */
class Vertex {
int id;
int[] neighbors;
}
和多叉树节点几乎完全一样:
/* 基本的 N 叉树节点 */
class TreeNode {
int val;
TreeNode[] children;
}
1.4 图的遍历
图的遍历同二叉树的遍历,有两种方式:BFS、DFS,BFS递归回溯,DFS需要用到额外的对来来维持访问的顺序,类似按层遍历的思想
/* 多叉树遍历框架 */
void traverse(TreeNode root) {
if (root == null) return;
for (TreeNode child : root.children)
traverse(child);
}
图和多叉树最大的区别是,图是可能包含环的,你从图的某一个节点开始遍历,有可能走了一圈又回到这个节点。
所以,如果图包含环,遍历框架就要一个visited
数组进行辅助:
Graph graph;
boolean[] visited;
/* 图遍历框架 */
void traverse(Graph graph, int s) {
if (visited[s]) return;
// 经过节点 s
visited[s] = true;
for (TreeNode neighbor : graph.neighbors(s))
traverse(neighbor);
// 离开节点 s
visited[s] = false;
}
1.4.1 深度优先遍历图 BFS
给你一个有 n 个节点的 有向无环图(DAG),请你找出所有从节点 0 到节点 n-1 的路径并输出(不要求按特定顺序)
二维数组的第 i 个数组中的单元都表示有向图中 i 号节点所能到达的下一些节点,空就是没有下一个结点了。
输入:graph = [[1,2],[3],[3],[]]
输出:[[0,1,3],[0,2,3]]
解释:有两条路径 0 -> 1 -> 3 和 0 -> 2 -> 3
输入:graph = [[4,3,1],[3,2,4],[3],[4],[]]
输出:[[0,4],[0,3,4],[0,1,3,4],[0,1,2,3,4],[0,1,4]]
// 记录所有路径
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> allPathsSourceTarget(int[][] graph) {
LinkedList<Integer> path = new LinkedList<>();
traverse(graph, 0, path);
return res;
}
public void traverse(int[][] graph,int node,LinkedList<Integer> path){
// 1.选择节点
path.add(node);
// 2.判断该节点是否是最后的节点
int n = graph.length;
if(node==n-1){
// 为什么创建新路径,直接用原来的不行吗?
res.add(new LinkedList<>(path));
path.removeLast();
return;
}
// 3.遍历当前节点所有与它相连的节点
for (int vertex: graph[node]) {
traverse(graph,vertex,path);
}
// 4.遍历完毕,回退上一个选择
path.removeLast();
}
1.4.2 广度优先遍历图 BFS
分为有环和无环图的两种遍历
//模板
void bfs(){
将起始点放入队列中
标记起点访问//如果是有环图的话,需要另外维护一个访问数组,将访问过的节点进行标记
while(如果队列不为空){
访问队首元素
删除队首元素
for(x所有相邻的点){
if(该点未被访问过且合法){
将该点加入队列末尾
}
}
}
队列为空,广搜结束
}
public List<List<Integer>> allPathsSourceTarget(int[][] graph) { List<List<Integer>> ans = new LinkedList<>(); int n = graph.length; Queue<Node> queue = new LinkedList<>(); queue.add(new Node(0)); while(queue.size() > 0){ Node node = queue.poll(); if(node.index==n-1){ res.add(node.path); continue; } for (int vertex : graph[node.index]) { queue.offer(new Node(vertex,node.path)); } } return ans; }class Node{ int index; List<Integer> path; public Node(int index) { this.index = index; this.path = new LinkedList<>(); path.add(index); } public Node(int index, List<Integer> path) { this.index = index; this.path = new LinkedList<>(path); path.add(index); }}
1.5 拓扑排序
1.5.1 判断有向图是否存在环
看到依赖问题,首先想到的就是把问题转化成「有向图」这种数据结构,只要图中存在环,那就说明存在循环依赖。
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
输入:numCourses = 2, prerequisites = [[1,0]]输出:true解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。输入:numCourses = 2, prerequisites = [[1,0],[0,1]]输出:false解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
public class BuildGraph {
boolean[] visited;//用来标记节点是否访问过
boolean[] onPath;//用来标记当前遍历的路径上是否已经有该节点了,有就代表这条路径上有环
boolean flag=false;//是否有环的标志位
public boolean canFinish(int numCourses, int[][] prerequisites){
visited = new boolean[numCourses];
onPath = new boolean[numCourses];
// 1.根据输入构造图的线性表结构
List<Integer>[] graph = buildGraph(numCourses,prerequisites);
// 2.遍历图结构,因为可能存在课程是孤立的所有采用for循环去遍历所有节点
for (int i = 0; i < numCourses; i++) {
traverse(graph,i);
}
return !flag;
}
public List<Integer>[] buildGraph(int numCourses, int[][] prerequisites) {
List<Integer>[] graph = new LinkedList[numCourses];
// 1.初始化!非常重要,不然会报 NullPointException;
for (int i = 0; i < numCourses; i++) {
graph[i] = new LinkedList<>();
}
for (int[] edge : prerequisites) {
int from = edge[1];
int to = edge[0];
graph[from].add(to);
}
return graph;
}
public void traverse(List<Integer>[] graph,int node){//遍历当前节点,如果已经访问过就直接返回
// 1.先判断当前节点是否已经在路径上
if (onPath[node]) {
flag=true;
}
// 2.判断当前节点是否已经遍历过
if(visited[node]){
return ;
}
// 3.如果没有,就将当前节点的状态改为已遍历,并加入当前遍历路径上
visited[node] = true;
onPath[node] =true;
// 4.遍历当前节点的所有相关的节点
for (Integer t : graph[node]) {
traverse(graph,t);
}
// 5.遍历完一样需要回退节点
onPath[node] = false;
}
}
1.5.2 输出拓扑顺序:深度遍历法
——将后序遍历的结果进行反转,就是拓扑排序的结果
直观地说就是,让你把一幅图「拉平」,而且这个「拉平」的图里面,所有箭头方向都是一致的
不仅要判断是否能修完所有课程,并且如果能的话还要输出上课的顺序结果
输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
输出:[0,2,1,3]
解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。
ArrayList<Integer> list = new ArrayList<>();
public int[] findOrder(int numCourses, int[][] prerequisites){
// 1.如果输入成环的话,直接就没有
if(!canFinish(numCourses,prerequisites)){
return new int[]{};
}
// 2.建图
List<Integer>[] graph = buildGraph(numCourses, prerequisites);
// 3.深度遍历图
visited = new boolean[numCourses];
for (int i = 0; i < numCourses; i++) {
traverseOrder(graph,i);
}
// 4.反转遍历结果
Collections.reverse(list);
int[] ans = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
ans[i] = list.get(i);
}
return ans;
}
// 后续遍历
public void traverseOrder(List<Integer>[] graph,int node){
// 1.判断当前节点是否已经遍历过
if(visited[node]){
return ;
}
// 2.如果没有,就将当前节点的状态改为已遍历
visited[node] = true;
// 3.遍历当前节点的所有相关的节点
for (Integer t : graph[node]) {
traverseOrder(graph,t);
}
list.add(node);
}
public boolean canFinish(int numCourses, int[][] prerequisites){}
1.5.3 拓扑排序:广度优先遍历法
不仅要判断是否能修完所有课程,并且如果能的话还要输出上课的顺序结果
输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
输出:[0,2,1,3]
解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。
拓扑排序的作用:
1、得到一个「拓扑序」,「拓扑序」不唯一;
2、检测「有向图」是否有环。
算法流程:
1、在开始排序前,扫描对应的存储空间(使用邻接表),将入度为 0 的结点放入队列。
2、只要队列非空,就从队首取出入度为 0 的结点,将这个结点输出到结果集中,并且将这个结点的所有邻接结点的入度减 1,在减 1 以后,如果这个被减 1 的结点的入度为 0 ,就继续入队。
3、当队列为空的时候,检查结果集中的顶点个数是否和课程数相等。
public int[] findOrder(int numCourses, int[][] prerequisites) {
// 1.创建节点的线性表,防止重复,用HashSet
HashSet<Integer>[] graph = new HashSet[numCourses];
for (int i = 0; i < numCourses; i++) {
graph[i] = new HashSet<>();
}
// 2.将节点信息添加到set中
// 3.维护所有节点的入度
int[] deep = new int[numCourses];
for (int[] edge : prerequisites) {
int from = edge[1];
int to = edge[0];
graph[from].add(to);
deep[to]++;
}
// 4.把所有度为0的节点入队列
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; i++) {
if(deep[i]==0){
queue.offer(i);
}
}
// 5.创建结果集
int[] res = new int[numCourses];
int count=0;
// 6.不断删除度为0的节点,将节点入结果集并更新其后继的度
while(!queue.isEmpty()){
Integer node = queue.poll();
res[count++] = node;
for (Integer next : graph[node]) {
deep[next]--;
if(deep[next]==0){
queue.offer(next);
}
}
}
if(count!=numCourses){
return new int[]{};
}else{
return res;
}
}
2 真题
2.1 路径问题(动态规划)
推荐好文章,代码非常合我心意:https://www.jianshu.com/p/524979bde668
2.1.1 64. 最小路径和
给定一个包含非负整数的
m x n
网格grid
,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。输入:grid = [[1,3,1],[1,5,1],[4,2,1]] 输出:7 解释:因为路径 1→3→1→1→1 的总和最小
动态规划,dp[i] [j]表示走到这个位置的最小代价
public int minPathSum(int[][] grid) {
if(grid==null || grid.length==0){
return 0;
}
int m = grid.length;
int n = grid[0].length;
int[][] dp = new int[m][n];
dp[0][0] = grid[0][0];
// 1.对于第1列的同志,只能从上面下来
for (int i = 1; i < n; i++) {
dp[0][i] = dp[0][i-1]+grid[0][i];
}
// 2.对于第一行的同志,只能从第一行过去
for (int i = 1; i < m; i++) {
dp[i][0] = dp[i-1][0]+grid[i][0];
}
// 3.其余的同志,就看左边过来的小还是右边过来的小
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = Math.min(dp[i-1][j],dp[i][j-1])+grid[i][j];
}
}
return dp[m-1][n-1];
}
滚动数组:因为i行依赖i行和i - 1行,所以我们计算完i行覆盖i - 1行的值,不会对最终结果造成影响。
public int minPathSum2(int[][] grid) {
if(grid==null || grid.length==0){
return 0;
}
// 滚动数组优化
int m = grid.length;
int n = grid[0].length;
int[] dp = new int[n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if(j==0){
dp[j] = dp[j];
}else if(i==0){
dp[j] = dp[j-1];
}else{
dp[j] = Math.min(dp[j],dp[j-1]);
}
dp[j] = dp[j]+grid[i][j];
}
}
return dp[n-1];
}
2.2.2 62. 不同路径
一个机器人位于一个
m x n
网格的左上角 ,机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角问总共有多少条不同的路径?输入:m = 3, n = 2 输出:3 解释: 从左上角开始,总共有 3 条路径可以到达右下角。 1. 向右 -> 向下 -> 向下 2. 向下 -> 向下 -> 向右 3. 向下 -> 向右 -> 向下
动态规划,dp[i] [j] 表示到达这个位置的路径数
public int countPaths(int m, int n) {
if(m==0||n==0){
return 0;
}
int[][] dp = new int[m][n];
dp[0][0]=1;
// 1.第一列的同志只有一条路
for (int i = 0; i < m; i++) {
dp[i][0]=1;
}
// 2.第一行的同志只有一条路
for (int i = 0; i < n; i++) {
dp[0][i] = 1;
}
// 3.其他同志可以从左边过来,也可以从上面下来
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
// 类似青蛙跳台阶问题 dp[i] = dp[i-1]+ dp[i-2]
// 转移方程:dp[i][j] = dp[i-1][j]+ dp[i][j-1]
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
滚动数组优化
public int countPaths(int m, int n) {
int less = Math.min(m, n), more = Math.max(m, n);
int[] dp = new int[less];
Arrays.fill(dp, 1);
for (int i = 1; i < more; ++i) {
for (int j = 1; j < less; ++j) {
dp[j] = dp[j] + dp[j - 1];
}
}
return dp[less - 1];
}
2.2.3 63. 不同路径 II-有障碍物
一个机器人位于一个
m x n
网格的左上角 ,机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角问总共有多少条不同的路径?此时网格中有障碍物,网格中的障碍物和空位置分别用1
和0
来表示。输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]] 输出:2 解释: 3x3 网格的正中间有一个障碍物。 从左上角到右下角一共有 2 条不同的路径: 1. 向右 -> 向右 -> 向下 -> 向下 2. 向下 -> 向下 -> 向右 -> 向右
public int countPathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int[][] dp = new int[m][n];
if(obstacleGrid[0][0]==1){
return 0;
}
// 1.第1列的同志,只有一条路
// 如果有一个为障碍物,以后的 值都要是 0
for (int i = 0; i < m; ++i) {
if(obstacleGrid[i][0]==1) break;
else dp[i][0] = 1;
}
// 2.第1行的同志,只有一条路
for (int i = 0; i < n; ++i) {
if(obstacleGrid[0][i]==1) break;
else dp[0][i] = 1;
}
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
if(obstacleGrid[i][j]==1){
continue;
}
// 如果有障碍物,那么就直接是0,否则就是两部分相加
dp[i][j] = dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
2.2.4 980. 不同路径 III
更好的解法是:回溯,//todo
在二维网格
grid
上,有 4 种类型的方格:
1
表示起始方格。且只有一个起始方格。2
表示结束方格,且只有一个结束方格。0
表示我们可以走过的空方格。-1
表示我们无法跨越的障碍。返回在四个方向(上、下、左、右)上行走时,从起始方格到结束方格的不同路径的数目。每一个无障碍方格都要通过一次,但是一条路径中不能重复通过同一个方格。
输入:[[1,0,0,0],[0,0,0,0],[0,0,2,-1]] 输出:2 解释:我们有以下两条路径: 1. (0,0),(0,1),(0,2),(0,3),(1,3),(1,2),(1,1),(1,0),(2,0),(2,1),(2,2) 2. (0,0),(1,0),(2,0),(2,1),(1,1),(0,1),(0,2),(0,3),(1,3),(1,2),(2,2)
2.2.5 三角形最小路径和
给定一个三角形
triangle
,找出自顶向下的最小路径和。输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]] 输出:11 解释:如下面简图所示: 2 3 4 6 5 7 4 1 8 3 自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。 可以理解为: [ [2], [3,4], [6,5,7], [4,1,8,3] ] 相邻结点:与(i, j)选择自 (i - 1, j) 和 (i - 1, j - 1)。
同样使用滚动数组,从下往上,但是这里的遍历,只遍历下三角
public int minimumTotal(List<List<Integer>> triangle) {
int n = triangle.size();
int[] dp = new int[n];
// 1.先把最下面一层作为dp的初始值
for (int i = 0; i < n; i++) {
dp[i] = triangle.get(n-1).get(i);
}
// 下三角遍历
for (int i = n-2; i >=0; i--) {
for (int j = 0; j <= i; j++) {
// 2.当前位置[i, j]选择只与 上一层的[i-1,j]和[i-1,j-1]相关
// 在滚动之后,只与 j 和 j-1相关
dp[j] = Math.min(dp[j],dp[j+1])+triangle.get(i).get(j);
}
}
return dp[0];
}
2.2.6 爬楼梯问题
/**
* 70.爬楼梯
* @param n
* @return
*/
public int climbStairs(int n) {
if(n<3){
return n;
}
int[] dp = new int[n+1];
dp[1] = 1;
dp[2] = 2;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i-1]+dp[i-2];
}
return dp[n];
}
2.2.7 圆环回原点问题
题目描述:圆环上有10个点,编号为0~9。从0点出发,**每次可以逆时针和顺时针走一步,问走n步回到0点共有多少种走法。**举例:
- 如果n=1,则从0出发只能到1或者9,不可能回到0,共0种走法
- 如果n=2,则从0出发有4条路径:0->1->2, 0->1->0, 0->9->8, 0->9->0,其中有两条回到了0点,故一共有2种走法
类似与爬楼梯缩小规模的思路,爬上n阶楼梯 = 爬上n-1阶楼梯数+爬上n-2阶楼梯数
走
n
步到0
的方案数 = 走n-1
步到1
的数 + 走n-1
步到9
的数
dp[i][j] = 走i步到j的方案数, = 走i-1步到j-1处的数 + 走i-1到j+1处的数
但是因为是○,所以通过取余来避免越界,(j-1+len)%len,(j+1)%len====>
防止越界的思想可以好好学习
dp[i][j] = dp[i-1][(j-1+len)%len]+dp[i-1][(j-1)%len]

public int circlePath(int step,int len){
// 特殊情况的排除
if(len==0){
return 1;
}
if(len==2){
return step%2==0?1:0;
}
// 1.dp[i][j]表示走i步到达j的方法数,所以i<step+1,j<len
int[][] dp = new int[step+1][len];
// 2.对于特殊情况的处理
dp[0][0] = 1;
for (int i = 1; i <= step; i++) {
for (int j = 0; j < len; j++) {
dp[i][j] = dp[i-1][(j-1+len)%len] + dp[i-1][(j+1)%len];
}
}
return dp[step][0];
}
2.2.8 交错字符串
s = s1 + s2 + … + sn
t = t1 + t2 + … + tm
|n - m| <= 1
交错 是s1 + t1 + s2 + t2 + s3 + t3 + ...
或者t1 + s1 + t2 + s2 + t3 + s3 + ...
问题转化为:每次只能向右或者向下选择字符,问是否存在target路径。
dp[i][j]
代表 s1 前 i 个字符与 s2 前 j 个字符拼接成 s3 的 i+j 字符,也就是存在目标路径能够到达 i ,j ;- 状态转移方程:到达(i, j)可能由上边位置过来或者左边位置过来
target
的每个字符都是从s1
(向下)或者s2
(向右)拿到的,所以只要判断是否存在这条target
路径即可;定义
boolean[][] dp ,dp[i][j]
代表s1
前i
个字符与s2
前j
个字符拼接成s3
的i+j
字符,存在目标路径能够到达i ,j
;
- 边界 1:
dp[0][0] = true;
- 边界 2:
if i=0 : dp[0]dp[j] = s2[0-j) equals s3[0,j)
遇到false
后面可以直接省略- 边界 3:
if j=0 : dp[i]dp[0] = s1[0-i) equals s3[0,i)
遇到false
后面可以直接省略其他情况,到达
(i,j)
可能由(i-1,j)
点向下一步,选择s1[i-1]
到达;也可能由(i,j-1)
点向右一步,选择s2[j-1]
到达;dp[i,j] = (dp[i-1][j] &&s3[i+j-1] == s1[i-1]) || (dp[i][j-1] && s3[i+j-1] == s2[j-1])
,交错字符串,很像路径总数这个问题,可以从左边来,也可以从上边来,不同于路径总数相加的是,直接或表示存在

public boolean isInterleave(String s1, String s2, String s3) {
int m = s1.length();
int n = s2.length();
// 1.特殊情况处理
if(s3.length()!=m+n){
return false;
}
boolean[][] dp = new boolean[m+1][n+1];
dp[0][0] = true;
// 1.当s2为空时如何处理
for (int i = 1; i <= m && s1.charAt(i-1)==s3.charAt(i-1); i++) {
dp[i][0] = true;
}
// 3.当s1为空时如何处理
for (int j = 1; j <= n && s2.charAt(j-1)==s3.charAt(j-1); j++) {
dp[0][j] = true;
}
// 4.转移方程
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
dp[i][j] = (dp[i-1][j]&&(s1.charAt(i-1)==s3.charAt(i+j-1))
||(dp[i][j-1]&&(s2.charAt(j-1)==s3.charAt(i+j-1))));
}
}
return dp[m][n];
}
2.2.9 翻译数字
给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
输入: 12258 输出: 5 解释: 12258有5种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi"和"mzi"
//String.compareTo()方法,如果第一个字符和参数的第一个字符不等,结束比较,返回第一个字符的ASCII码差值。如果第一个字符和参数的第一个字符相等,则以第二个字符和参数的第二个字符做比较,以此类推,直至不等为止,返回该字符的ASCII码差值。 如果两个字符串不一样长,可对应字符又完全一样,则返回两个字符串的长度差值。
@Test
public void test(){
System.out.println("15".compareTo("10"));//输出 5,第一个字符相同,返回第二个字符差值的ASCII码值 5
System.out.println("35".compareTo("25"));//输出 1,第一个字符不同,返回第一个字符差值的ASCII码值 1
System.out.println("5".compareTo("10"));// 输出 4,第一个字符不同,返回第一个字符差值的ASCII码值 4
System.out.println("15".compareTo("25"));// 输出 4,第一个字符不同,返回第一个字符差值的ASCII码值 -1
}
/**
* 剑指 Offer 46. 把数字翻译成字符串
* 就类似于爬楼梯,将数字规模缩小
* dp[i]表示把第i个字符翻译成字符串的数量
* 分情况讨论,dp[i] = dp[i-1]+判断(dp[i-2])
* @param num
* @return
*/
public int translateNum(int num) {
String s = num+"";
int len = s.length();
int[] dp = new int[len+1];
dp[0] = 1;
dp[1] = 1;
// 1.遍历所有数位
for (int i = 2; i <= len; i++) {
// 2.如果当前的数字可以和前一个数位组合,就相加
String temp = s.substring(i-2,i);
if(temp.compareTo("10")>=0 && temp.compareTo("25")<=0){
dp[i] = dp[i-1]+dp[i-2];
}else{
dp[i] = dp[i-1];
}
}
return dp[len];
}
/**
* 滚动数组优化
* @param num
* @return
*/
public int translateNum2(int num) {
String s = num+"";
int len = s.length();
// 1.遍历所有数位
int a = 1,b=1;
for (int i = 2; i < len; i++) {
// 2.如果当前的数字可以和前一个数位组合,就相加
String temp = s.substring(i-2,i);
int c = temp.compareTo("10")>=0 && temp.compareTo("25")<=0?a+b:a;
b = a;
a = c;
}
return a;
}