每周 Leetcode : 221、208、720、207、389

331 阅读9分钟

2021 06 03

本周的 5 道 Leetcode 分别是:

  • 221 最大正方形
  • 208 实现前缀树
  • 720 词典中最长的单词
  • 207 课程表
  • 389 找不同

221 最大正方形

题目描述

在一个由 '0''1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,并返回其 面积

  • 示例 1

image.png

输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
输出:4

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/ma…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一 动态规划

dp[i][j] 表示以第 i 行,第 j 列为右下角的所能构成的正方形的最大边长。

那么如果 matrix[i][j]'0',此时不能构成任何正方形,dp[i][j] = 0

如果 matrix[i][j]'1',此时最小的正方形就是它本身,边长为 1,如果正方形要扩大,那么 matrix[i][j-1],matrix[i-1][j],matrix[i-1][j-1] 这三个位置都必须能构成正方形,而且能够增加的边长必须是这三个位置分别能够构成的边长的最小值(只有取最小的边长时,才能保证垂直和水平方向增加的边长相等)。所以此时,我们有;

dp[i][j]=1+min(dp[i1][j],dp[i][j1],dp[i1][j1])dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])

实际上,我们只需要对 '1' 的位置进行操作即可。还要注意边界判断,防止越界,第 0 行和第 0 列时,dp[i][j] 最大也不会超过 1

// 动态规划
public int maximalSquare(char[][] matrix) {
    int[][] dp = new int[matrix.length][matrix[0].length];
    // 最大边长
    int maxSideLength = 0;
    for(int i = 0; i < matrix.length; ++i){
        for(int j = 0; j < matrix[0].length; ++j){
            if(matrix[i][j] == '1'){
                dp[i][j] = 1;
                // 边界时,边长无法增加
                dp[i][j] += (i == 0 || j == 0) ? 0 : min(dp[i-1][j-1],dp[i][j-1],dp[i-1][j]);
                // 更新最大边长
                maxSideLength = Math.max(maxSideLength,dp[i][j]);
            }

        }
    }
    // 返回面积
    return maxSideLength*maxSideLength;
}

// 三个数的最小值
public int min(int a, int b, int c){
    return Math.min(Math.min(a,b),c);
}

208 实现前缀树

题目描述

Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于 高效地存储和检索字符串数据集中的键 。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。

请你实现 Trie 类:

Trie() 初始化前缀树对象。
void insert(String word) 向前缀树中插入字符串 word 。
boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
  • 示例:
输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]
解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple");   // 返回 True
trie.search("app");     // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app");     // 返回 True

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/im…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

实现

在一个前缀树节点中,可以包含以下几个属性:

  • 当前节点存储的字符集合:用 HashMap 或者数组来实现
  • 当前节点是否是字符串的终止节点:用 boolean isEnd 表示
  • 以当前节点为终止的字符串的值:用 value 表示

前缀树节点的定义如下:

// 前缀树节点
public class TrieNode {
    public HashMap<Character,TrieNode> children;
    public boolean isEnd;
    public String value;

    public TrieNode(){
        this.children = new HashMap<>();
        this.isEnd = false;
        this.value = null;
    }
}

在前缀树中,插入字符串 insert 时,查看当前节点的字符集合中是否包含了该字符,

  • 如果包含,则当前节点指向该字符串的下一个节点
  • 如果不包含,则在当前节点的字符集合中插入该节点,并指向下一个节点

这个逻辑同样适用于 searchstartsWith

// 前缀树实现
public class Trie {
    private TrieNode root;

    public Trie() {
        this.root = new TrieNode();
    }

    public void insert(String word) {
        TrieNode cur = this.root;
        for(char c : word.toCharArray()){
            if (!cur.children.containsKey(c)) {
                cur.children.put(c,new TrieNode());
            }
            cur = cur.children.get(c);
        }
        cur.isEnd = true;
        cur.value = word;
    }

    public boolean search(String word) {
        TrieNode cur = this.root;
        for(char c : word.toCharArray()){
            if(!cur.children.containsKey(c)){
                return false;
            }
            cur = cur.children.get(c);
        }
        return cur.isEnd;
    }

    public boolean startsWith(String prefix) {
        TrieNode cur = this.root;
        for(char c : prefix.toCharArray()){
            if(!cur.children.containsKey(c)){
                return false;
            }
            cur = cur.children.get(c);
        }
        return true;
    }
}

注意在 startsWith 中直接返回 true ,而 search 中返回 isEnd 是因为,一个字符串的前缀存在,并不能算作该前缀存在于前缀树中。只有该前缀使用 insert 方法插入后,才能认为存在于前缀树中。


720 词典中最长的单词

题目描述

给出一个字符串数组 words 组成的一本英语词典。从中找出最长的一个单词,该单词是由words词典中其他单词逐步添加一个字母组成。若其中有多个可行的答案,则返回答案中 字典序最小 的单词。

若无答案,则返回空字符串。

  • 示例 1:
输入:
words = ["w","wo","wor","worl", "world"]
输出:"world"
解释: 
单词"world"可由"w", "wo", "wor", 和 "worl"添加一个字母组成。
  • 示例 2:
输入:
words = ["a", "banana", "app", "appl", "ap", "apply", "apple"]
输出:"apple"
解释:
"apply"和"apple"都能由词典中的单词组成。但是"apple"的字典序小于"apply"。

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/lo…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一 字典树(前缀树)

首先用给定的字符串数组构建一棵前缀树,

for(String word : words){
    this.insert(word);
}

然后,我们遍历 words 数组,在字典树中判断当前单词是否满足要求。

题目的要求是如果一个单词(例如 apple)满足条件,那么该单词的所有前缀(a,ap,app,appl,apple)都必须存在于字典树中,最后返回满足条件的最长单词,如果长度相等,则返回字母顺序小的。

初始化结果字符串 result = "",对于一个字符串,如果可能成为最长单词,就遍历字典树,否则直接遍历下一个单词,判断是否可能的条件封装如下函数:

/**
* @param word 当前遍历的字符串
* @param result 当前的结果字符串
*/
private boolean potentialLongestWord(String word,String result){
    return word.length() > result.length() || word.length() == result.length() && word.compareTo(result) < 0;
}

遍历字典树的过程中,如果遇到当前字符对应的 isEndfalse ,说明以该字符结尾的前缀不存在,直接遍历下一个字符串。如果 isEndtrue,则继续遍历一个节点。

完整的代码如下:

public class Trie{
    // ... 省略其他方法
    
    // 寻找最长的单词
    public String longestWord(String[] words){
        if(words == null || words.length == 0){
            return null;
        }
        // 构建字典树
        for(String word : words){
            this.insert(word);
        }
        String result = "";
        for(String word : words){
            TrieNode cur = this.root;
            // 如果可能成为最长单词
            if(potentialLongestWord(word,result)){
                boolean isWord = true;
                for(char ch : word.toCharArray()){
                    cur = cur.children.get(ch);
                    if(!cur.isEnd){
                        isWord = false;
                        break;
                    }
                }
                if(isWord){
                    result = word;
                }
            }
        }
        return result;
    }

    private boolean potentialLongestWord(String word,String result){
        return word.length() > result.length() || word.length() == result.length() && word.compareTo(result) < 0;
    }
}

207 课程表

题目描述

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai必须 先学习课程  bi

例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false

  • 示例 1:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
  • 示例 2:
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/co…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一 判断是否有环

将给定的课程和先学课程构建为一个图,用邻接矩阵来表示。图中节点的个数就是要学习的课程数。

如果图中存在环,则说明课程之间存在循环依赖的关系,此时无法完成所有课程的学习。

我们可以使用 DFS 深度优先搜索来判断一个图中是否有环。具体如下:

定义一个访问数组 visited 表示某个节点的状态,0 表示未访问,1 表示从该节点往后存在环,2 表示从该节点往后不存在环。

对于某一个节点 node,先将 visited[node] 设为 1,表示该节点被访问并假设存在环,然后获取它的后继节点,如果后继节点递归过程中都不存在环,则说明从 node 遍历不存在环。

递归函数 isLoopFrom 如下定义:

// 递归函数,从节点 node 开始遍历,是否存在环
public boolean isLoopFrom(int node, int[] visited, List<List<Integer>> adjacency){
    if(visited[node] == 1){
        return true;
    }
    if(visited[node] == 2){
        return false;
    }
    visited[node] = 1; // 被访问过,且假设有环
    for(int suc : adjacency.get(node)){
        // 递归搜索每个后继,看是否存在环
        if(isLoopFrom(suc,visited,adjacency)){
            return true;
        }
    }
    // 没有后继或者后继都不存在环
    visited[node] = 2;
    return false;
}

对每个课程都调用 isLoopFrom,如果有一个课程存在环,则无法完成所有课程。

// 方法一 DFS 深度优先搜索
public boolean canFinish(int numCourses, int[][] prerequisites) {
    List<List<Integer>> adjacency = buildGraph(numCourses,prerequisites);
    int[] visited = new int[numCourses];
    for(int i = 0; i < numCourses; ++i){
        if(isLoopFrom(i,visited,adjacency)){
            return false;
        }
    }
    return true;
}

// 构建图的邻接矩阵
public List<List<Integer>> buildGraph(int numCourses,int[][] prerequisites){
    List<List<Integer>> adjacency = new ArrayList<>();
    for(int i = 0; i < numCourses; ++i){
        adjacency.add(new ArrayList<>());
    }
    // initialize adjacency list
    for(int[] arr : prerequisites){
        adjacency.get(arr[1]).add(arr[0]);
    }
    return adjacency;
}

解法二 拓扑排序

拓扑排序 的思想是,每次从图中取出一个 入度为 0 的节点,直到图中的所有节点都被取出。如果有多个入度为 0 的节点,它们的顺序无关紧要。

定义一个 inDegrees[i] 数组表示节点 i 的入度。使用队列存储当前入度为 0 的所有节点。

遍历队列,取出入度为 0 的节点,将课程数量减 1。将该节点的后继节点的入度减 1,如果后继的入度减 1 后变成入度 0,则加入队列。

如果一个图中有环,那么说明该环构成的子图永远不存在入度为 0 的节点,所以最终队列为空时,课程数不为 0,此时说明无法完成课程的学习。如果队列为空时,课程数也为 0,说明能够完成学习。

// 方法二 拓扑排序
public boolean canFinish(int numCourses, int[][] prerequisites) {
    List<List<Integer>> adjacency = new ArrayList<>();
    int[] inDegrees = new int[numCourses];
    Queue<Integer> queue = new LinkedList<>();
    for(int i = 0; i < numCourses; ++i){
        adjacency.add(new ArrayList<>());
    }
    for(int[] arr : prerequisites){
        inDegrees[arr[0]]++;
        adjacency.get(arr[1]).add(arr[0]);
    }

    // 将所有入度为 0 的节点加入队列
    for(int i = 0; i < inDegrees.length; i++){
        if(inDegrees[i] == 0){
            queue.offer(i);
        }
    }
    while(!queue.isEmpty()){
        // 入度为 0 节点出队
        int zeroDegree = queue.poll();
        numCourses--;
        // 后继节点的入度减 1,如果变成 0 ,加入队列
        for(int cur : adjacency.get(zeroDegree)){
            inDegrees[cur]--;
            if(inDegrees[cur] == 0){
                queue.add(cur);
            }
        }
    }
    return numCourses == 0;
}

389 找不同

题目描述

给定两个字符串 st,它们只包含小写字母。

字符串 t 由字符串 s 随机重排,然后在随机位置添加一个字母。

请找出在 t 中被添加的字母。

  • 示例 1:
输入:s = "abcd", t = "abcde"
输出:"e"
解释:'e' 是那个被添加的字母。
  • 示例 2:
输入:s = "", t = "y"
输出:"y"
  • 示例 3:
输入:s = "a", t = "aa"
输出:"a"
  • 示例 4:
输入:s = "ae", t = "aea"
输出:"a"

来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/fi…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法一 计数法

由于只包含小写字母,那么可以用一个长度为 26 的数组,下标与字符对应,数组值表示字符出现的次数。

遍历 s ,将字符出现的次数 +1 ,遍历 t ,将字符出现的次数 -1 。那么最后数组中一定有且仅有一个位置的次数为 -1 ,这个位置代表的就是我们寻找的字符。

字符 ch 和索引 index 的对应关系为 index = ch - 'a'

// 解法一 计数法
public char findTheDifference(String s, String t) {
    int[] table = new int[26];
    for(char ch : s.toCharArray()){
        table[ch - 'a']++;
    }
    for(char ch : t.toCharArray()){
        table[ch - 'a']--;
    }
    // O(1)
    for(int i = 0; i < 26; i++){
        if(table[i] < 0){
            return (char) (i + 'a');
        }
    }
    return 'a';
}

需要注意的是 ,这种方法的空间消耗是常量级的(无论 s,t 如何变化,都只需要长度 26 的数组空间),空间复杂度为 O(1) 。同样,第三个循环的复杂度也是 O(1)

解法二 加减法

思路和计数法类似,同样利用 ch - 'a' 将字符转换为整数,进行加减,先对 t 中字符做加法,再对 s 中字符做减法,这样同时出现在 s,t 的字符会抵消,最后剩下的整数就是要求的字符所对应的。

需要注意的是 ,先对 t 遍历做加法,再对 s 做减法是为了保证最后的整数一定是 正数 。如果先对 s 做加法,再对 t 做减法,最后的整数是 负数 ,需要先取绝对值再转换为字符。

// 解法二 加减法
public char findTheDifference(String s, String t) {
    int sum = 0;
    for(char ch : t.toCharArray()){
        sum += ch - 'a';
    }
    for(char ch : s.toCharArray()){
        sum -= ch - 'a';
    }
    return (char) ('a' + sum);
}