LeetCode面试热题七

124. 二叉树中的最大路径和

路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中至多出现一次 。该路径至少包含一个节点,且不一定经过根节点。路径和是路径中各节点值的总和。
给你一个二叉树的根节点 root ,返回其最大路径和 。
示例:
请添加图片描述

输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42

解题思路:递归,在树中的一条路径就是从叶子节点往上走到根节点,然后再往下。从叶子节点向上出发,叶子节点告诉父节点自己的值,父节点根据左右两个子节点返回的值,选择一个最大值的叶子节点(叶子节点都不为负,负则不选择叶子节点作为路径中的节点),父节点将选择的叶子节点的值与自身的值相加,值的和向上返回,在这个过程中使用 一个全局变量保存最大的路径值。

class Solution {
	// 路径的最大值初始为最小值。
    Integer max = Integer.MIN_VALUE;
    public int maxPathSum(TreeNode root) {
    	// 求解节点的最大值
        maxSum(root);
        // 返回路径的最大值
        return max;
    }
    public int maxSum(TreeNode root){
        if(root == null){
            return 0;
        }
        // 求解左右节点能返回的最大值,小于0,则舍弃
        int maxLeft = Math.max(maxSum(root.left),0);
        int maxRight = Math.max(maxSum(root.right),0);
      	// 当前节点所能返回的最大路径值
        int maxRoot = Math.max(maxLeft,maxRight)+root.val;
        // 以当前节点为树根的路径最大值与历史最大值,选择最大的作为当前最大值。
        max = Math.max(maxLeft+maxRight+root.val,max);
        // 返回给父节点,以该子节点的最大路径值
        return maxRoot;
    }
}

125. 验证回文串

给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
说明:本题中,我们将空字符串定义为有效的回文串。

示例 :

输入: "A man, a plan, a canal: Panama"
输出: true
解释:"amanaplanacanalpanama" 是回文串

解题思路:直接写就行,注意忽略非字母和非数字的其它字符

public boolean isPalindrome(String s) {
    // 忽略大小写,只考虑字母和数字
    // 大写字母 65-90 小写字母 97 - 122 数字 48-57
    int left = 0;
    int right = s.length() - 1;
    char[] chars = s.toCharArray();
    while(left < right){
    	// 非字母,非数字,left++,如果left不小于right,说明字符串判断完毕,返回true
        while(!(65<=chars[left] && chars[left]<=90) &&
                !(97<=chars[left]&&chars[left]<=122)&&
                !(48<=chars[left] && chars[left] <= 57)){
            if(left < right){
                left++;
            }else {
                return true;
            }
        }
        // 非字母,非数字,right++,如果right不大于left,说明字符串判断完毕,返回true
        while(!(65<=chars[right] && chars[right]<=90) &&
                !(97<=chars[right]&&chars[right]<=122)&&
                !(48<=chars[right] && chars[right] <= 57)){
            if(left < right) {
                right--;
            }else{
                return true;
            }
        }
        if(48<=chars[left] && chars[left] <= 57){
            // 数字比较
            if(chars[left] != chars[right]){
                return false;
            }
        }else{
            // 字母比较,先都转换为小写
            chars[left] = (char) ((int)(chars[left])|32);
            chars[right] = (char) ((int)(chars[right])|32);
            if(chars[left] != chars[right]){
                return false;
            }
        }
        left++;
        right--;
    }
    return true;
}

127. 单词接龙

字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列:
1.序列中第一个单词是 beginWord 。
2.序列中最后一个单词是 endWord 。
3.每次转换只能改变一个字母。
4.转换过程中的中间单词必须是字典 wordList 中的单词。
给你两个单词 beginWord 和 endWord 和一个字典 wordList,找到从 beginWord 到 endWord 的 最短转换序列中的单词数目 。如果不存在这样的转换序列,返回 0。

输入:beginWord = "hit", endWord = "cog", 
wordList = ["hot","dot","dog","lot","log","cog"]
输出:5
解释:一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5

解题思路:求最短的转换序列,所以使用BFS广度优先。从开始单词判断,改变了单词中的某一位,查看字典中是否有这个单词,如果有则加入下一层,等到本层判断完成后,进入下一层的判断,直到遇到结束单词或者没有下层结束。
将字典中的单词加入哈希表,方便查找字典。使用队列,存放下一层的单词。使用一个哈希表,记录已经访问过的单词。

public int ladderLength(String beginWord, String endWord, List<String> wordList) {
    // 哈希容器 set 将字典中所有的单词放入set集合
    HashSet<String> set = new HashSet<>(wordList);
    if(!set.contains(endWord)){
        return 0;
    }
    // 哈希容器 visited,将已经访问的单词加入容器,表示已经访问
    HashSet<String> visited = new HashSet<>();
    visited.add(beginWord);
    // 用于存放要遍历的单词
    Deque<String> deque = new LinkedList<>();
    deque.add(beginWord);
    int step = 1;
    while(!deque.isEmpty()){
    	// 记录本层的单词个数。
        int size = deque.size();
        for(int i = 0; i < size; i++){
            String s = deque.poll();
            // 将本层每个单词的每个字母进行修改为 a-z,然后判断新单词是否在字典中存在。
            for(int j = 0;j<s.length();j++){
                char[] chars = s.toCharArray();
                for(char k = 'a';k <= 'z';k++){
                    chars[j] = k;
                    String c = String.valueOf(chars);
                    // 新单词在字典中存在且未被访问过,访问并加入下一层
                    if(set.contains(c) && !visited.contains(c)){
                    	// 如果这个单词是结束单词,返回层数,即为转换的长度
                        if(c.equals(endWord)){
                            return step + 1;
                        }
                        deque.add(c);
                        visited.add(c);
                    }
                }
            }
        }
        step++;
    }
    return 0;
}

以上是单向BFS的写法,对于本题,已知起点和终点,则可以使用双向BFS,从两端同时搜索。
如图所示

在这里插入图片描述

public int ladderLength(String beginWord, String endWord, List<String> wordList) {
    Set<String> set = new HashSet<>(wordList);
    if (!set.contains(endWord)) {
        return 0;
    }
    Set<String> visited = new HashSet<>();
    // 分别用左边和右边扩散的哈希表代替单向 BFS 里的队列,它们在双向 BFS 的过程中交替使用
    Set<String> beginVisited = new HashSet<>();
    beginVisited.add(beginWord);
    Set<String> endVisited = new HashSet<>();
    endVisited.add(endWord);
    int step = 1;
    // beginVisited和endVisited都不为空,因为只要为空,就不能存在beginWord到endWord的转换
    while (!beginVisited.isEmpty() && !endVisited.isEmpty()) {
        // 优先选择小的哈希表进行扩散
        if (beginVisited.size() > endVisited.size()) {
            Set<String> temp = beginVisited;
            beginVisited = endVisited;
            endVisited = temp;
        }
        // 保证 beginVisited 是相对较小的集合
        Set<String> nextLevel = new HashSet<>();
        // beginVisited存放的是当前层的所有单词。
        for (String word : beginVisited) {
        	// 对于当前层每一个单词的每一个位置都改变为 a-z,再判断有没有词典中有没有这个单词
            for (int i = 0; i < word.length(); i++) {
                char[] charArray = word.toCharArray();
                for (char c = 'a'; c <= 'z'; c++) {
                    charArray[i] = c;
                    String nextWord = String.valueOf(charArray);
                    if (set.contains(nextWord)) {
                    	// 如果这个新单词,已经在endVisited中说明,可以完成转换,返回层数
                        if (endVisited.contains(nextWord)) {
                            return step + 1;
                        }
                        // 进行访问,并加入下一层。
                        if (!visited.contains(nextWord)) {
                            nextLevel.add(nextWord);
                            visited.add(nextWord);
                        }
                    }
                }
            }
        }
        beginVisited = nextLevel;
        step++;
    }
    return 0;
}

128. 最长连续序列

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
解题思路:使用哈希表。将数组存放到hashset,进行去重,然后在数组中只从最开始的值开始技术寻找最长连续序列。如果一个值为 x,如果x-1存在于数组中,那么以x开头的序列一定不是最长连续序列。

public int longestConsecutive(int[] nums) {
    int maxnum = 0;
    int num;
    HashSet<Integer> hashSet = new HashSet<>();
    // 数组存放入hash表
    for(int i = 0;i < nums.length; i++){
        hashSet.add(nums[i]);
    }
    Iterator<Integer> iterator = hashSet.iterator();
    // 遍历hash表
    while(iterator.hasNext()){
        Integer next = iterator.next();
        // 只有在值为开头的序列才进行寻找最长子序列
        if(!hashSet.contains(next-1)) {
            next++;
            num = 1;
            while (hashSet.contains(next)) {
                next++;
                num++;
            }
            if (num > maxnum)
                maxnum = num;
        }
    }
    return maxnum;
}

130. 被围绕的区域

给你一个 m x n 的矩阵 board ,由若干字符 ‘X’ 和 ‘O’ ,找到所有被 ‘X’ 围绕的区域,并将这些区域里所有的 ‘O’ 用 ‘X’ 填充。
示例:请添加图片描述

输入:board = [["X","X","X","X"],
			  ["X","O","O","X"],
			  ["X","X","O","X"],
			  ["X","O","X","X"]]
输出:	[["X","X","X","X"],
		 ["X","X","X","X"],
		 ["X","X","X","X"],
		 ["X","O","X","X"]]
解释:被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 
任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。
如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。

解题思路:遍历边界上的 字母 O,并进行递归将所有与边界相连的 字母 O改变为 字母 A,然后再将数组中的字母 O变成 字母X,最后将字母A变为字母O

public void solve(char[][] board) {
    int m,n;
    m = board.length;
    n = board[0].length;
    //遍历四个边,将与边上相连的字母O变为字母A
    for(int i = 0;i<n; i++){
        if(board[0][i] == 'O'){
            dfs(board,0,i,m,n);
        }
    }
    for(int i = 0;i<n; i++){
        if(board[m-1][i] == 'O'){
            dfs(board,m-1,i,m,n);
        }
    }
    for(int i = 0;i<m; i++){
        if(board[i][0] == 'O'){
            dfs(board,i,0,m,n);
        }
    }
    for(int i = 0;i<m; i++){
        if(board[i][n-1] == 'O'){
            dfs(board,i,n-1,m,n);
        }
    }
    // 将字母O变为 X
    for(int i=1;i<m-1;i++){
        for(int j=1;j<n-1;j++){
            if(board[i][j] == 'O'){
                board[i][j] = 'X';
            }
        }
    }
    //将所有的字母A再变回字母O
    for(int i=0;i<m;i++){
        for(int j=0;j<n;j++){
            if(board[i][j] == 'A'){
                board[i][j] = 'O';
            }
        }
    }
}
// dfs,将所以边界相邻的 O 替换为 A
public void dfs(char[][] board,int i,int j,int m,int n){
    board[i][j] = 'A';
    // 上
    if(0 <= i-1 && board[i-1][j] == 'O'){
        dfs(board,i-1,j,m,n);
    }
    // 下
    if(m > i + 1 && board[i+1][j] == 'O'){
        dfs(board,i+1,j,m,n);
    }
    // 左
    if(0 <= j-1 && board[i][j-1] == 'O'){
        dfs(board,i,j-1,m,n);
    }
    // 右
    if(n > j+1 && board[i][j+1] == 'O'){
        dfs(board,i,j+1,m,n);
    }
}

131. 分割回文串

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。回文串 是正着读和反着读都一样的字符串。

示例:
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]

解题思路:动态规划对字符串预处理+回溯
动态规划:dp[i][j]表示 字符 i 到字符 j 是否为回文串。
状态转移方程为

				true i >= j
			/ 
dp[i][j] = 
			\
			  s[i]==s[j] && dp[i+1][i-1]			

在回溯时,直接使用dp[i][j]进行判断。

public List<List<String>> partition(String s) {
    List<List<String>> result = new ArrayList<List<String>>();
    List<String> list = new ArrayList<String>();
    int n = s.length();
    // dp数组记录是否为回文串
    boolean[][] dp = new boolean[n][n];
    for (int i = 0; i < s.length(); ++i) {
        Arrays.fill(dp[i], true);
    }
    // 初始化dp数组。
    for (int i = s.length() - 1; i >= 0; --i) {
        for (int j = i + 1; j < s.length(); ++j) {
            dp[i][j] = (s.charAt(i) == s.charAt(j)) && dp[i + 1][j - 1];
        }
    }
    // 回溯
    dfs(s,result,new ArrayList<String>(),dp,0);
    return result;
}
// 参数说明,n为当前要分割的元素下标
public void dfs(String s,List<List<String>> result,List<String> list,boolean[][] dp,int n) {
	// 等于 s长度,将当前结果加入最终结果集
    if (s.length() == n) {
        result.add(new ArrayList<String>(list));
        return;
    }
    for (int i = n;i< s.length(); ++i) {
    	// 判断是否为回文串,是就继续分割。
        if (dp[n][i]) {
            list.add(s.substring(n, i + 1));
            dfs(s,result,list,dp,i + 1);
            list.remove(list.size() - 1);
        }
    }
}

无动态规划代码

public List<List<String>> partition(String s) {
    List<List<String>> result = new ArrayList<>();
    partition(result,new ArrayList<String>(),s,0);
    return result;
}
public void partition(List<List<String>> result,List<String> list,String s,int n){
    if(n == s.length()){
        result.add(new ArrayList<>(list));
        return;
    }
    for(int j = 1;j + n - 1 < s.length();j++){
        // 从 n开始,j个字符是不是回文串
        int l = n;
        int r = n + j - 1;
        while(l <= r && s.charAt(l) == s.charAt(r)){
            l++;r--;
        }
        if(l > r){
            // 是回文串
            list.add(s.substring(n,n + j));
            partition(result,list,s,n + j);
            list.remove(list.size()-1);
        }
    }
}

134. 加油站

在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i + 1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。

说明: 
如果题目有解,该答案即为唯一答案。
输入数组均为非空数组,且长度相同。
输入数组中的元素均为非负数。

示例:

输入: 
gas  = [1,2,3,4,5]
cost = [3,4,5,1,2]
输出: 3
解释:3 号加油站(索引为 3)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。

解题思路:如果全部加油站的油小于全部的花费,则必无解,否则有解。如果从第 i 点出发,到第 j点无解,则从第 i 点到 第 j 点的任意点都不存在解。下次直接从j+1点开始求解即可。

public int canCompleteCircuit(int[] gas, int[] cost) {
    int gass = 0,costs = 0;
    int num = 0,start = 0;
    // 判断是否有解
    for(int i = 0;i<gas.length;i++){
        gass += gas[i];
        costs += cost[i];
    }
    if(costs > gass){
        return -1;
    }
    // 若有解,从第0点开始求解
    for(int i = 0;i < gas.length;i++){
        num += gas[i]-cost[i];
        // 若无解,从第i+1开始求解
        if(num < 0){
            num = 0;
            start = i + 1;
        }
    }
    return start;
}

136. 只出现一次的数字

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

示例:

输入: [4,1,2,1,2]
输出: 4

解题思路:使用异或运算,因为a^a = 0,所以最后得到的结果一定是只出现一次的元素。

public int singleNumber(int[] nums) {
    int num = 0;
    for(int i = 0;i < nums.length;i++){
        num = num^nums[i];
    }
    return num;
}

138. 复制带随机指针的链表

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。
构造这个链表深拷贝。 深拷贝应该正好由 n 个 全新节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。

例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。
返回复制链表的头节点。

用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:
val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。
你的代码 只 接受原链表的头节点 head 作为传入参数。

示例 :

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

请添加图片描述
方法一
解题思路:使用集合将旧的节点和新创建的节点进行映射,然后在根据旧节点的值对新节点进行赋值。

public Node copyRandomList(Node head) {
    if(head == null){
        return null;
    }
    HashMap<Node,Node> map = new HashMap<>();
    Node current = head;
    // 创建新节点,新节点与旧节点进行映射
    while(current != null){
        map.put(current,new Node(current.val));
        current = current.next;
    }
    current = head;
    while(current != null){
    	// 根据旧节点的值,为新节点赋值
        map.get(current).next = map.get(current.next);
        map.get(current).random = map.get(current.random);
        current = current.next;
    }
    return map.get(head);
}

递归写法

HashMap<Node,Node> map = new HashMap<>();
public Node copyRandomList(Node head) {
    if(head == null){
        return null;
    }
    if(!map.containsKey(head)){
    	// 创建新的节点,旧的节点与新的节点进行映射
        Node key = new Node(head.val);
        map.put(head,key);
        // 根据旧的节点为新的节点赋值
        key.next = copyRandomList(head.next);
        key.random = copyRandomList(head.random);
    }
    return map.get(head);
}

方法二
解题思路:迭代 + 节点拆分,在原始链表中创建新的节点,并插入到原始节点的后面。如有链表 A->B->C,复制原始节点并插入到原始节点的后面。链表为 A->A’->B->B’->C->C’。然后对新节点的 random赋值,然后将链表拆分为 A->B->C与A’->B’->C’两个链表。返回新链表的头。

public Node copyRandomList(Node head) {
    if(head == null){
        return null;
    }
    Node newNode;
    // 赋值旧节点,插入原始链表
    for(Node node = head;node!=null;node = node.next.next){
        newNode = new Node(node.val);
        newNode.next = node.next;
        node.next = newNode;
    }
    // 为新节点的random赋值
    for(Node node = head;node!=null;node = node.next.next){
        newNode = node.next;
        if(node.random != null){
            newNode.random = node.random.next;
        }
    }
    // 链表分离
    Node newHead = head.next;
    for(Node node = head;node != null; node = node.next) {
        newNode = node.next;
        node.next = node.next.next;
        if(newNode.next != null){
            newNode.next = newNode.next.next;
        } 
    }
    return newHead;
}

139. 单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典,判定 s 是否可以由空格拆分为一个或多个在字典中出现的单词。
说明:拆分时可以重复使用字典中的单词。

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"

方法一:回溯
超时了

public boolean wordBreak(String s, List<String> wordDict) {
    // 回溯法
    HashSet<String> hashSet = new HashSet<>(wordDict);
    return wordBreak(s,hashSet,0,0);
}
public boolean wordBreak(String s,HashSet<String> wordDict,int left,int right){
    if(right == s.length()){
        if(left == right){
            return true;
        }
        return left != 0 && wordDict.contains(s.substring(left,right));
    }
    if(wordDict.contains(s.substring(left,right + 1))){
        // 拆分
        if(wordBreak(s,wordDict,right+1,right+1)){
            return true;
        }
        // 不拆分
        if(wordBreak(s,wordDict,left,right + 1)){
            return true;
        }
    }else{
        // 不可以拆分
        if(wordBreak(s,wordDict,left,right + 1)){
            return true;
        }
    }
    return false;
}

方法二:动态规划
定义dp[i] 表示字符串 s 前 i 个字符组成的字符串 s[0…i-1] 是否能被空格拆分成若干个字典中出现的单词。
考虑状态转移方程,dp[i] 的取值,看s[0…i-1]中的分割点 j,s[0…j−1] 组成的字符串 s1 和 s[j…i-1] 组成的字符串 s2是否都合法。s1 是否合法可以直接由 dp[j]直接得知,剩下的只需要看s2是否合法。
状态转移方程为:
dp[i]=dp[j] && check(s[j…i−1])
其中 check(s[j…i-1]) 表示子串 s[j…i-1] 是否出现在字典中。

public boolean wordBreak(String s, List<String> wordDict) {
    HashSet<String> hashSet = new HashSet<>(wordDict);
    boolean[] dp = new boolean[s.length()+1];
    // 0表示空串,空串为true
    dp[0] = true;
    for(int i = 1;i <= s.length();i++){
    	// 对于前 i 个字符,将 0 - i-1中的每一个中间字符尝试分割。
        for(int j = 0;j < i;j++){
            if (dp[j] && hashSet.contains(s.substring(j, i))) {
            	// dp[j] 为true,且j - i-1在字典中,可以分割。
                dp[i] = true;
                break;
            }
        }
    }
    return dp[s.length()];
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值