一文通数据结构与算法之——图+常见题型与解题策略+Leetcode经典题

数据结构——图

1 基本概念

图片

图:有向图、无向图、双向图

1.1 图的存储方式

  • 邻接表,我把每个节点x的邻居都存到一个列表里,然后把x和这个列表关联起来,可以通过一个节点x找到它的所有相邻节点。邻接表的主要优势是节约存储空间
List<Integer>[] graph;
// graph[s]是一个列表,存储着节点s所指向的节点。
  • 邻接矩阵,二维数组,我们权且成为matrix,如果节点xy是相连的,那么就把matrix[x][y]设为true或者权重。如果想找节点x的邻居,去扫一圈matrix[x][..]就行了。邻接矩阵的主要优势是可以迅速判断两个节点是否相邻
int[][] matrix

image-20210927145825769

image-20210927145757334

1.2 有向加权图

如果是邻接表,我们不仅仅存储某个节点x的所有邻居节点,还存储x到每个邻居的权重

如果是邻接矩阵,matrix[x][y]不再是布尔值,而是一个 int 值,0 表示没有连接,其他值表示权重

连接无向图中的节点xy,把matrix[x][y]matrix[y][x]都变成true不就行了;邻接表也是类似的操作

image-20210925175352337

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

797. 所有可能的路径

给你一个有 n 个节点的 有向无环图(DAG),请你找出所有从节点 0 到节点 n-1 的路径并输出(不要求按特定顺序)

二维数组的第 i 个数组中的单元都表示有向图中 i 号节点所能到达的下一些节点,空就是没有下一个结点了。

输入:graph = [[1,2],[3],[3],[]]
输出:[[0,1,3],[0,2,3]]
解释:有两条路径 0 -> 1 -> 3 和 0 -> 2 -> 3

img

输入: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]]

img

// 记录所有路径
    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 判断有向图是否存在环

207. 课程表

看到依赖问题,首先想到的就是把问题转化成「有向图」这种数据结构,只要图中存在环,那就说明存在循环依赖

你这个学期必须选修 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 输出拓扑顺序:深度遍历法

——将后序遍历的结果进行反转,就是拓扑排序的结果

直观地说就是,让你把一幅图「拉平」,而且这个「拉平」的图里面,所有箭头方向都是一致的

image-20210927110637049

210. 课程表 II

不仅要判断是否能修完所有课程,并且如果能的话还要输出上课的顺序结果

输入: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 拓扑排序:广度优先遍历法

210. 课程表 II

不仅要判断是否能修完所有课程,并且如果能的话还要输出上课的顺序结果

输入: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、当队列为空的时候,检查结果集中的顶点个数是否和课程数相等。

image-20210927120308362

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
解释:因为路径 13111 的总和最小

动态规划,dp[i] [j]表示走到这个位置的最小代价

image-20210927153942393

	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] 表示到达这个位置的路径数

image-20210927155723753

	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 网格的左上角 ,机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角问总共有多少条不同的路径?此时网格中有障碍物,网格中的障碍物和空位置分别用 10 来表示。

输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右

image-20210927161847482

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 三角形最小路径和

120. 三角形最小路径和

给定一个三角形 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. 爬楼梯

	/**
     * 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]

image-20210928155248548
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 交错字符串

97. 交错字符串

s = s1 + s2 + … + sn
t = t1 + t2 + … + tm
|n - m| <= 1
交错 是 s1 + t1 + s2 + t2 + s3 + t3 + ...或者t1 + s1 + t2 + s2 + t3 + s3 + ...

img

问题转化为:每次只能向右或者向下选择字符,问是否存在target路径。

  • dp[i][j]代表 s1 前 i 个字符与 s2 前 j 个字符拼接成 s3 的 i+j 字符,也就是存在目标路径能够到达 i ,j ;
  • 状态转移方程:到达(i, j)可能由上边位置过来或者左边位置过来

target 的每个字符都是从 s1(向下)或者 s2(向右)拿到的,所以只要判断是否存在这条 target 路径即可;

定义boolean[][] dp ,dp[i][j]代表 s1i个字符与 s2j 个字符拼接成 s3i+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]),

交错字符串,很像路径总数这个问题,可以从左边来,也可以从上边来,不同于路径总数相加的是,直接或表示存在

image.png
	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 翻译数字

剑指 Offer 46. 把数字翻译成字符串

给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。

输入: 12258
输出: 5
解释: 122585种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi""mzi"

image-20210928172951246

//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;
    }
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值