【算法训练营 · 补充】LeetCode Hot100(上)

文章目录

二叉树部分

二叉树展开为链表

题目链接:114. 二叉树展开为链表

解题逻辑:

先说原地展开的方法。注意到前序遍历是中左右,我们可以找到当前节点的前驱节点A(左子树的最后一个节点),将当前节点的右孩子接在A的右边,然后将当前节点的左孩子接到右孩子的位置上。依次对链表中的元素进行处理,即可实现。

解题代码:

java 复制代码
class Solution {
    public void flatten(TreeNode root) {      
        TreeNode cur = root;
        while(cur != null) {
            //找到前缀节点
            if(cur.left != null) {
                TreeNode preNode = cur.left;
                while(preNode.right != null) preNode = preNode.right;
                preNode.right = cur.right;
                cur.right = cur.left;
                cur.left = null;
            }
            cur = cur.right;
        }
    }
}

当然也可以借助队列把中序遍历的元素放到队列中,然后一个个弹出来拼在root的右孩子处:

java 复制代码
class Solution {
    Deque<TreeNode> que = new ArrayDeque<>();
    public void flatten(TreeNode root) {    
        preWalk(root);
        TreeNode pointer = root;
        if(que.size() == 0 || que.size() == 1) return;
        que.removeFirst();
        while(!que.isEmpty()) {
            pointer.left = null;
            pointer.right = que.removeFirst();
            pointer = pointer.right;
        }
    }
    
    public void preWalk(TreeNode node){
        if(node == null) return;

        que.addLast(new TreeNode(node.val));
        preWalk(node.left);
        preWalk(node.right);
    }
}

从前序与中序遍历序列构造二叉树

题目链接:105. 从前序与中序遍历序列构造二叉树

解题思路:

用当前二叉树的前序定根,因为根左右,第一个元素肯定是根,将这个元素带入到中序遍历中,可以确定左右子树的元素。

接下来根据元素个数,通过数组切分得到左子树的前序与中序。重复上述逻辑依次递归,就可以构造出二叉树。

解题代码:

java 复制代码
class Solution {
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        
        if(preorder.length == 0) return null;
        if(preorder.length == 1) return new TreeNode(preorder[0]);

        int num = preorder[0];
        TreeNode root = new TreeNode(num);
        int divide = findItem(inorder,num);
        int[] leftInorder = Arrays.copyOfRange(inorder,0,divide);
        int[] leftPreorder = Arrays.copyOfRange(preorder,1,1 + leftInorder.length);
        TreeNode left = buildTree(leftPreorder,leftInorder);

        int[] rightInorder = null;
        int[] rightPreorder = null;
        if(divide == preorder.length - 1) {
            rightInorder = new int[0];
            rightPreorder = new int[0];
        }else {
            rightInorder = Arrays.copyOfRange(inorder,divide + 1,inorder.length);
            rightPreorder = Arrays.copyOfRange(preorder,preorder.length - rightInorder.length,preorder.length);
        }
        
        
        TreeNode right = buildTree(rightPreorder,rightInorder);

        root.left = left;
        root.right = right;
        
        return root;
    }

    public int findItem(int[] nums,int target) {
        for(int i = 0;i < nums.length;i++) {
            if(nums[i] == target) return i;
        }
        return -1;
    }
}

注意:

  • 因为当数组大小为1或0的时候直接返回,所以左子树的前序与中序是一定存在的
  • 但是右子树的前序和中序就不一定了,当divide在中序的尾部时,说明右子树为空,那么此时右子树的前序和中序数组直接返回空数组即可

二叉树的直径

解题逻辑:543. 二叉树的直径

坑点在于最长路径不一定经过根节点。所以需要把每个节点的左最大深度 + 右最大深度比较,取最大的。

这里涉及到两个递归:

  • postWalk递归用来获得该节点的最大深度
  • search递归遍历树
  • 在search中调用postWalk,从递归遍历每个树节点,同时拿到经过该节点的最长路径,遍历完树节点之后取最长的就行。
java 复制代码
class Solution {
    int max = 0;
    public int diameterOfBinaryTree(TreeNode root) {
        search(root);
        return max;
    }
    public void search(TreeNode node){
        if(node == null) return;

        search(node.left);
        search(node.right);
        int path = postWalk(node.left) + postWalk(node.right);
        if(path > max) max = path;
    }

    public int postWalk(TreeNode node){
        if(node == null) return 0;

        int left = postWalk(node.left);
        int right = postWalk(node.right);

        return Math.max(left,right) + 1;
    }
}

二叉搜索树中第 K 小的元素

题目链接:230. 二叉搜索树中第 K 小的元素

解题逻辑:

直接用二叉搜索树的特性中序遍历是一个递增序列!

解题代码:

java 复制代码
class Solution {
    List<Integer> list = new ArrayList<>();
    
    public int kthSmallest(TreeNode root, int k) {
        midWalk(root);
        return list.get(k - 1);
    }

    public void midWalk(TreeNode node){
        if(node == null) return;

        midWalk(node.left);
        list.add(node.val);
        midWalk(node.right);
    }
}

路径总和 III

题目链接:437. 路径总和 III

解题思路:

可以使用两个递归:

  • 一个递归用来获得以当前节点为起点,符合targetSum的路径的条数
  • 另一个递归用来遍历整棵树的节点

本题的思想其实和二叉树的直径那一题有点像,不经过跟结点的情况,那么我们可以通过变换参考系来解决。

解题代码:

java 复制代码
class Solution {
    public int pathSum(TreeNode root, int targetSum) {
        preWalk(root,targetSum);
        return count;
    }
    int count = 0;
    long curSum = 0;
    public void searchPath(TreeNode root, int targetSum){
        if(root == null) return;
        curSum += root.val;
        if(curSum == targetSum) count++;
        searchPath(root.left,targetSum);
        searchPath(root.right,targetSum);
        curSum -= root.val;
    }

    public void preWalk(TreeNode root,int targetSum){
        if(root == null) return;
        searchPath(root,targetSum);
        preWalk(root.left,targetSum);
        preWalk(root.right,targetSum);
    }

}

二叉树中的最大路径和

题目链接:124. 二叉树中的最大路径和

解题思想:

通过递归遍历每个节点,同时考虑"穿过该节点的双分支路径"和"从该节点延伸的单分支路径",从而找到整体最大路径和

解题代码:

java 复制代码
class Solution {
    public int maxPathSum(TreeNode root) {
        max = root.val;
        searchPath(root);
        return max;
    }
    int max = 0;
    public int searchPath(TreeNode root){
        if(root == null) return 0;
        int left = searchPath(root.left);
        int right = searchPath(root.right);
        int sum = root.val;   
        int choose = Math.max(left,right);
        if(choose > 0) sum += choose;
        if(sum > max) max = sum;
        int maybe = Math.min(left,right) + sum;
        if(maybe > max) max = maybe;
        return sum;
    }
}

这段代码的核心思路是:

  1. 用递归函数 searchPath 计算以当前节点为"起点",向下延伸(只能选左或右一条分支)的最大路径和(sum),并实时更新全局最大路径和 max
  2. 对于每个节点,除了考虑"单分支路径"(左或右子树选一个),还额外判断了"双分支路径"(左+根+右)的和(maybe),确保不遗漏这种可能的最大路径。
  3. 递归返回值是"单分支路径和"(供父节点继续向上拼接),而全局变量 max 则记录所有可能路径中的最大值。

回溯部分

括号生成

题目链接:22. 括号生成

解题思路:

涉及到回溯算法那么能把树状图画出来,就做对了80%:

通过递归三部曲来理解:

  • 递归参数与返回值:返回值为void,参数需要str记录当前拼接的字符串,left记录左括号个数,right记录右括号个数,n记录需要的对数
  • 递归出口:left == n && right == n 说明满足题意进行收集,然后右括号不能大于左括号,括号总数不能超过n的两倍
  • 单层逻辑:本层要么拼接左括号要么拼接右括号,递归完成之后要记得回溯把尾部括号去掉

解题代码:

java 复制代码
class Solution {
    public List<String> generateParenthesis(int n) {
        backtracking("",0,0,n);
        return result;
    }

    List<String> result = new ArrayList<>();

    public void backtracking(String str,int left,int right,int n){
        if(left == n && right == n) {
            result.add(str);
            return;
        }

        if(right > left || str.length() > 2 * n) return;
        
        for(int i = 0;i < 2;i++) {
            if(i == 0) {
                str += "(";
                backtracking(str,left + 1,right,n);
            }else {
                str += ")";
                backtracking(str,left,right + 1,n);
            }
            str = str.substring(0,str.length() - 1);
        }
    }
}

单词搜索

题目链接:79. 单词搜索

解题思路:

遍历board的每个位置,符合开头的则进行递归查找。

本题的注意点在于:

  • 需要一个record二维数组记录走过的路径
  • 再进行回溯以及遍历查找头遍历节点的时候的时候都要同步更新record数组
  • 每一层递归的x,y不要拿着直接用,而是复制一份为x1,y1再用,因为x,y为该层递归的基坐标,是以该点为起点进行上下左右的移动,所以不能随便改变。

解题代码:

java 复制代码
class Solution {
    public boolean exist(char[][] board, String word) {
        int[][] record = new int[board.length][board[0].length];
        for(int i = 0;i < board.length;i++) {
            for(int j = 0;j < board[0].length;j++) {
                if(board[i][j] != word.charAt(0)) continue;
                record[i][j] = 1;
                boolean result = backtracking(board,word,"" + board[i][j],i,j,record);
                record[i][j] = 0;
                if(result) return true;
            }
        }
        return false;
    }

    int[][] dire = {{1,0},{-1,0},{0,-1},{0,1}};

    public boolean backtracking(char[][] board, String word,String current,int x,int y,int[][] record){
        if(current.equals(word)) return true;

        if(current.length() > word.length()) return false;

        for(int i = 0;i < 4;i++) {
            int x1 = x + dire[i][0];
            int y1 = y + dire[i][1];
            if(x1 < 0 || x1 >= board.length || y1 < 0 || y1 >= board[0].length || record[x1][y1] == 1) continue;
            if(board[x1][y1] != word.charAt(current.length())) continue;
            current += board[x1][y1];
            record[x1][y1] = 1;
            boolean result = backtracking(board,word,current,x1,y1,record);
            if(result) return true;
            current = current.substring(0,current.length() - 1);
            record[x1][y1] = 0;
        }

        return false;

    }
}

二分查找部分

与快排的辨析

二分查找和快速排序虽然都运用了分治思想,但它们的核心目标和具体实现方式有显著区别,不能说思想完全一样。

具体来看:

  • 二分查找

    • 用于在有序集合中高效查找目标元素。
    • 核心思路是每次通过与中间元素比较,将查找范围减半(要么在左半部分,要么在右半部分),不断缩小范围直到找到目标或确定不存在。
    • 查找算法,不改变原集合的结构,时间复杂度为 O(log n)。
  • 快速排序

    • 用于将无序集合排序。
    • 核心思路是选择一个基准元素,将集合分为小于基准大于基准的两部分(分区操作),然后对这两部分递归排序,最终使整个集合有序。
    • 排序算法,会改变原集合的顺序,平均时间复杂度为 O(n log n)。

总结来说,两者都通过"分而治之"减少问题规模,但二分查找是单向缩小查找范围 ,快速排序是双向递归处理分区,应用场景和目标截然不同。

本章二分查找全部使用闭区间处理

搜索插入位置

题目链接:35. 搜索插入位置

解题逻辑:

本题使用二分查找,如果能找到元素直接返回下标,找不到则寻找插入位置,可以转化为查找第一个大于目标的下标,那么其实这个下标就是left所指的地方【可以当作结论记下来】

为什么?

当指针碰撞之后有三种情况:

  • 找到了元素直接返回
  • 即使找不到元素,也是在距离该元素最近的相邻位置
    • 如果target小于双指针所指元素,那么left指向的就是第一个大于目标的值
    • 如果target大于双指针所指元素,那么left会右移一个,指向的也是第一个大于目标的值

解题代码:

java 复制代码
class Solution {
    public int searchInsert(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;  
        while(right >= left) {
            int middle = (left + right) / 2;
            if(nums[middle] == target) return middle;
            else if(nums[middle] > target) right = middle - 1;
            else left = middle + 1;
        }
        return left;
    }
}

搜索二维矩阵

题目链接:74. 搜索二维矩阵

解题逻辑:

使用每行的最后一个元素(也就是每行的最大元素),建立一个索引数组,通过二分查找索引数组可以知道应该在matrix的哪一行进行搜索。得到索引之后,我们再去matrix的对应行中再进行二分查找即可。

代码:

java 复制代码
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        int[] index = new int[matrix.length];
        for(int i = 0;i < matrix.length;i++) index[i] = matrix[i][matrix[0].length - 1];
        //二分查找索引
        int left = 0;
        int right = index.length - 1;
        int indexNum = -1;
        while(left <= right) {
            int middle = (left + right) / 2;
            if(index[middle] == target) {
                indexNum = middle;
                break;
            }
            else if(index[middle] < target) left = middle + 1;
            else right = middle - 1; 
        }
        indexNum = indexNum == -1 ? (left == matrix.length ? left - 1 : left) : indexNum;
        //二分查找具体元素
        int[] nums = matrix[indexNum];
        left = 0;
        right = nums.length - 1;
        while(left <= right) {
            int middle = (left + right) / 2;
            if(nums[middle] == target) return true;
            else if(nums[middle] < target) left = middle + 1;
            else right = middle - 1; 
        }
        return false;
    }
}

查找元素的第一个和最后一个位置

题目链接:34. 在排序数组中查找元素的第一个和最后一个位置

解题逻辑:

使用二分减小范围的大方向没有变,只是在中间元素为target的时候,两个指针同时向内部逼近,从而得到符合条件的范围。

解题代码:

java 复制代码
class Solution {
    public int[] searchRange(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        int[] result = {-1,-1};
        while(left <= right) {
            int middle = (left + right) / 2;
            if(nums[middle] == target) {
                while(nums[left] != target) left++;
                while(nums[right] != target) right--;
                result[0] = left;
                result[1] = right;
                return result;
            }else if(nums[middle] < target) left = middle + 1;
            else right = middle - 1;
        }

        return result;
    }
}

搜索旋转排序数组

题目链接:33. 搜索旋转排序数组
本题就是对应数组在局部有序的情况下二分法怎么用。宗旨不会变:缩小范围,直到找到目标。

解题逻辑:

本题的特点在于:将数组一分为二,其中一定有一个是有序的,另一个可能是有序,也能是部分有序。此时有序部分用二分法查找。无序部分再一分为二,其中一个一定有序,另一个可能有序,可能无序。就这样循环,一步步减少范围.

解题代码:

java 复制代码
class Solution {
    public int search(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        while(left <= right) {
            int middle = (left + right) / 2;
            if(nums[middle] == target) return middle;
            if(nums[left] <= nums[middle]) {
                //左边有序
                if(target >= nums[left] && target < nums[middle]) right = middle - 1;
                else left = middle + 1;
            }else {
                //右边有序
                if(target > nums[middle] && target <= nums[right]) left = middle + 1;
                else right = middle - 1;
            }
        }
        return -1;
    }
}

注意:
nums[left] <= nums[middle]都属于左边有序,即使相等也只能感知到左边,不能判定右边相等。

动态规划部分

杨辉三角

题目链接:118. 杨辉三角

解题逻辑:

从DP四部曲分析:

  • dp数组以及下标的含义:dp[i][j]表示对应杨辉三角(i,j)处的值
  • 递推公式:dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1]
  • 初始化:dp[0][0] = 1
  • 遍历顺序:从上到下,从左到右

解题代码:

sql 复制代码
class Solution {
    public List<List<Integer>> generate(int numRows) {
        List<List<Integer>> result = new ArrayList<>();
        Integer[] arr = {1};
        result.add(Arrays.asList(arr));
        Integer[][] dp = new Integer[numRows][numRows];
        dp[0][0] = 1;
        for(int i = 1;i < numRows;i++) {
            for(int j = 0;j <= i;j++) {
                if(j - 1 < 0 || j > i - 1) dp[i][j] = 1;
                else dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1];
            }
            result.add(Arrays.asList(Arrays.copyOfRange(dp[i],0,i + 1)));
        }
        return result;
    }
}

乘积最大子数组

题目链接:152. 乘积最大子数组

解题思路:

维护一个二维的dp数组:

  • 第一行维护正数最大值
  • 第二行维护负数最小值

解题代码:

sql 复制代码
class Solution {
    public int maxProduct(int[] nums) {
        Integer[][] dp = new Integer[2][nums.length];
        dp[0][0] = nums[0] >= 0 ? nums[0] : null;
        dp[1][0] = nums[0] < 0 ? nums[0] : null;
        int max = nums[0];
        for(int j = 1;j < nums.length;j++) {
            if(nums[j] >= 0) {
                //当前为正数
                if(dp[0][j - 1] != null) dp[0][j] = Math.max(nums[j] * dp[0][j - 1],nums[j]);
                else dp[0][j] = nums[j];
                if(dp[1][j - 1] != null) dp[1][j] = dp[1][j - 1] * nums[j];
            }else {
                //当前为负数
                if(dp[1][j - 1] != null) dp[0][j] = nums[j] * dp[1][j - 1];
                if(dp[0][j - 1] != null) dp[1][j] = Math.min(dp[0][j - 1] * nums[j],nums[j]);
                else dp[1][j] = nums[j];
            }
            if(dp[0][j] != null && dp[0][j] > max) max = dp[0][j];     
        }
        return max;
    }
}

最长有效括号

题目链接:32. 最长有效括号

方法1:

使用栈结构,在栈底维护一个边界索引,用于计算有效括号的长度。遇到左括号直接添加。遇到右括号,分为两种情况处理:

  • 弹出一个栈顶元素,如果此时栈还没为空,使用当前右括号的索引减去栈顶元素,获得当前有效长度
  • 弹出一个栈顶元素,如果此时栈为空(说明此时右括号多了,没有匹配的左括号,所以要重新维护一个边界索引,用于重新计算最长有效括号),把当前右括号入栈重新维护一个边界

解题逻辑:

java 复制代码
class Solution {
    
    public int longestValidParentheses(String s) {
        Deque<Integer> stack = new ArrayDeque<>();
        stack.push(-1);
        int result = 0;
        for(int i = 0;i < s.length();i++) {
            if(s.charAt(i) == '(') stack.push(i);
            else {
                int num = stack.pop();
                if(stack.isEmpty()) {
                    stack.push(i);
                }else {
                    int cur = i - stack.peek();
                    if(cur > result) result = cur;
                }
            }
        }
        return result;
    }
}

最小路径和

题目链接:64. 最小路径和

方法1:回溯算法(时间会超出限制)

java 复制代码
class Solution {
    public int minPathSum(int[][] grid) {
        backtracking(grid,0,0);
        return min;
    }

    int[][] dire = {{1,0},{0,1}};

    int min = Integer.MAX_VALUE;

    int cur = 0;

    public void backtracking(int[][] grid,int x,int y){
        if(x >= grid.length || y >= grid[0].length) return;

        if(x == grid.length - 1 && y == grid[0].length - 1) {
            cur += grid[x][y];
            if(cur < min) min = cur;
            cur -= grid[x][y];
            return;
        }
        
        for(int i = 0;i < 2;i++) {
            cur += grid[x][y];
            int x1 = x + dire[i][0];
            int y1 = y + dire[i][1];
            backtracking(grid,x1,y1);
            cur -= grid[x][y];
        }
    }
}

方法2:dp

从DP四部曲分析:

  • dp数组以及下标的含义:dp[i][j]表示到达i,j的最小路径和
  • 递推公式:dp[i][j] = min(dp[i][j - 1],dp[i - 1][j]) + grid[i][j]
  • 初始化:dp[0][0] = 1,第一列以及第一行
  • 遍历顺序:从上到下,从左到右
java 复制代码
class Solution {
    public int minPathSum(int[][] grid) {
        int[][] dp = new int[grid.length][grid[0].length];
        dp[0][0] = grid[0][0];
        for(int i = 1;i < grid[0].length;i++) dp[0][i] = dp[0][i - 1] + grid[0][i];
        for(int i = 1;i < grid.length;i++) dp[i][0] = dp[i - 1][0] + grid[i][0];
        //dp[i][j] = min(dp[i][j - 1],dp[i - 1][j]) + grid[i][j]
        for(int i = 1;i < grid.length;i++) {
            for(int j = 1;j < grid[0].length;j++) {
                dp[i][j] = Math.min(dp[i][j - 1],dp[i - 1][j]) + grid[i][j];
            }
        }
        return dp[grid.length - 1][grid[0].length - 1];
    }
}
相关推荐
一条星星鱼4 小时前
从0到1:如何用统计学“看透”不同睡眠PSG数据集的差异(域偏差分析实战)
人工智能·深度学习·算法·概率论·归一化·睡眠psg
浮灯Foden4 小时前
算法-每日一题(DAY18)多数元素
开发语言·数据结构·c++·算法·leetcode·面试
小欣加油5 小时前
leetcode 844 比较含退格的字符串
算法·leetcode·职场和发展
小龙报5 小时前
《算法每日一题(1)--- 第31场蓝桥算法挑战赛》
c语言·开发语言·c++·git·算法·学习方法
llz_1125 小时前
五子棋小游戏
开发语言·c++·算法
如竟没有火炬5 小时前
全排列——交换的思想
开发语言·数据结构·python·算法·leetcode·深度优先
寂静山林6 小时前
UVa 12526 Cellphone Typing
算法
kyle~7 小时前
C++---嵌套类型(Nested Types)封装与泛型的基石
开发语言·c++·算法
sali-tec7 小时前
C# 基于halcon的视觉工作流-章48-短路断路
开发语言·图像处理·人工智能·算法·计算机视觉