1382.将二叉搜索树变平衡

目标

给你一棵二叉搜索树,请你返回一棵 平衡后 的二叉搜索树,新生成的树应该与原来的树有着相同的节点值。如果有多种构造方法,请你返回任意一种。

如果一棵二叉搜索树中,每个节点的两棵子树高度差不超过 1 ,我们就称这棵二叉搜索树是 平衡的 。

示例 1:

输入:root = [1,null,2,null,3,null,4,null,null]
输出:[2,1,3,null,null,null,4]
解释:这不是唯一的正确答案,[3,1,4,null,2,null,null] 也是一个可行的构造方案。

示例 2:

输入: root = [2,1,3]
输出: [2,1,3]

说明:

  • 树节点的数目在 [1, 10^4] 范围内。
  • 1 <= Node.val <= 10^5

思路

将给定的二叉搜索树变平衡。

中序遍历二叉搜索树,结果列表有序,从中间重新构建 BST。

代码


/**
 * @date 2026-02-09 8:56
 */
public class BalanceBST1382 {

    public TreeNode balanceBST(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        dfs(list, root);
        int l = 0, r = list.size() - 1;
        return buildTree(list, l, r);
    }

    public void dfs(List<Integer> nums, TreeNode node) {
        if (node == null) {
            return;
        }
        dfs(nums, node.left);
        nums.add(node.val);
        dfs(nums, node.right);
    }

    public TreeNode buildTree(List<Integer> nums, int l, int r) {
        if (l == r) {
            return new TreeNode(nums.get(l));
        } else if (l > r) {
            return null;
        }
        int m = l + (r - l) / 2;
        return new TreeNode(nums.get(m), buildTree(nums, l, m - 1), buildTree(nums, m + 1, r));
    }

}

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 * int val;
 * TreeNode left;
 * TreeNode right;
 * TreeNode() {}
 * TreeNode(int val) { this.val = val; }
 * TreeNode(int val, TreeNode left, TreeNode right) {
 * this.val = val;
 * this.left = left;
 * this.right = right;
 * }
 * }
 */

性能

110.平衡二叉树

目标

给定一个二叉树,判断它是否是 平衡二叉树 。平衡二叉树 是指该树所有节点的左右子树的高度相差不超过 1。

示例 1:

输入:root = [3,9,20,null,null,15,7]
输出:true

示例 2:

输入:root = [1,2,2,3,3,null,null,4,4]
输出:false

示例 3:

输入:root = []
输出:true

说明:

  • 树中的节点数在范围 [0, 5000] 内
  • -10^4 <= Node.val <= 10^4

思路

判断给定二叉树是否是平衡二叉树。

dfs 返回子树高度,判断高度差是否超过 1。

代码


/**
 * @date 2026-02-09 11:35
 */
public class IsBalanced110 {

    boolean res = true;

    public boolean isBalanced(TreeNode root) {
        dfs(root);
        return res;
    }

    public int dfs(TreeNode node) {
        if (node == null) {
            return 0;
        }
        int lh = dfs(node.left);
        int rh = dfs(node.right);
        if (Math.abs(lh - rh) > 1) {
            res = false;
        }
        return Math.max(lh, rh) + 1;
    }

}

性能

1653.使字符串平衡的最少删除次数

目标

给你一个字符串 s ,它仅包含字符 'a' 和 'b' 。

你可以删除 s 中任意数目的字符,使得 s 平衡 。当不存在下标对 (i,j) 满足 i < j ,且 s[i] = 'b' 的同时 s[j]= 'a' ,此时认为 s 是 平衡 的。

请你返回使 s 平衡 的 最少 删除次数。

示例 1:

输入:s = "aababbab"
输出:2
解释:你可以选择以下任意一种方案:
下标从 0 开始,删除第 2 和第 6 个字符("aababbab" -> "aaabbb"),
下标从 0 开始,删除第 3 和第 6 个字符("aababbab" -> "aabbbb")。

示例 2:

输入:s = "bbaaaaabb"
输出:2
解释:唯一的最优解是删除最前面两个字符。

说明:

  • 1 <= s.length <= 10^5
  • s[i] 要么是 'a' 要么是 'b'​ 。​

思路

有一个仅包含 a, b 两个字符的字符串 s,如果 a 的左边没有 bb 的右边没有 a 则认为 s 是平衡的。求使得 s 平衡需要删除的最少次数。

可以枚举分割线,分割线左侧的 b 都要删除,右侧的 a 也要删除,取最小值即可。可以使用前后缀分解来解决。

也可也使用动态规划,定义 dp[i] 表示前 i 个字符平衡需要删除的最小次数。

  • ib 时,dp[i] = dp[i - 1]
  • ia 时,dp[i] = min(dp[i - 1] + 1, cntB),其中 cntB 表示 i 前面的 b 的个数

代码


/**
 * @date 2026-02-09 11:31
 */
public class MinimumDeletions1653 {

    public int minimumDeletions(String s) {
        int n = s.length();
        int[] prefixB = new int[n + 1];
        char[] chars = s.toCharArray();
        for (int i = 0; i < n; i++) {
            int c = chars[i] - 'a';
            prefixB[i + 1] = prefixB[i] + c;
        }
        int deletedA = 0;
        int res = Integer.MAX_VALUE;
        for (int i = n - 1; i > 0; i--) {
            int c = chars[i] - 'a';
            if (c == 0) {
                res = Math.min(res, prefixB[i] + deletedA);
                deletedA++;
            }
        }
        res = Math.min(res, deletedA);
        return res;
    }

}

性能

3634.使数组平衡的最少移除数目

目标

给你一个整数数组 nums 和一个整数 k。

如果一个数组的 最大 元素的值 至多 是其 最小 元素的 k 倍,则该数组被称为是 平衡 的。

你可以从 nums 中移除 任意 数量的元素,但不能使其变为 空 数组。

返回为了使剩余数组平衡,需要移除的元素的 最小 数量。

注意:大小为 1 的数组被认为是平衡的,因为其最大值和最小值相等,且条件总是成立。

示例 1:

输入:nums = [2,1,5], k = 2
输出:1
解释:
移除 nums[2] = 5 得到 nums = [2, 1]。
现在 max = 2, min = 1,且 max <= min * k,因为 2 <= 1 * 2。因此,答案是 1。

示例 2:

输入:nums = [1,6,2,9], k = 3
输出:2
解释:
移除 nums[0] = 1 和 nums[3] = 9 得到 nums = [6, 2]。
现在 max = 6, min = 2,且 max <= min * k,因为 6 <= 2 * 3。因此,答案是 2。

示例 3:

输入:nums = [4,6], k = 2
输出:0
解释:
由于 nums 已经平衡,因为 6 <= 4 * 2,所以不需要移除任何元素。

说明:

  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^9
  • 1 <= k <= 10^5

思路

定义平衡数组是 元素最大值 <= k * 元素最小值 的数组。有一个数组 nums,每次操作可以移除一个元素,求使得数组变成平衡数组的最少操作次数。

直接二分查找大于 k * nums[i] 的最小下标 index,删掉的元素是前面 i 个元素(下标 i 其实是第 i + 1 个元素,前面有 i 个),加上 n - 1 - index + 1 个大于 k * nums[i] 的元素。

可以使用滑动窗口,删掉的元素就是总长度减去窗口内的元素,针对每一个 left 将其扩展到最右端,然后移出 left 继续判断。

代码


/**
 * @date 2026-02-06 8:54
 */
public class MinRemoval3634 {

    public int minRemoval(int[] nums, int k) {
        int n = nums.length;
        Arrays.sort(nums);
        int res = n - 1;
        int r = 0;
        for (int l = 0; l < n; l++) {
            while (r < n && (long) nums[l] * k >= nums[r]) {
                r++;
            }
            res = Math.min(res, n - (r - l));
        }
        return res;
    }

}

性能

3379.转换数组

目标

给你一个整数数组 nums,它表示一个循环数组。请你遵循以下规则创建一个大小 相同 的新数组 result :

对于每个下标 i(其中 0 <= i < nums.length),独立执行以下操作:

  • 如果 nums[i] > 0:从下标 i 开始,向 右 移动 nums[i] 步,在循环数组中落脚的下标对应的值赋给 result[i]。
  • 如果 nums[i] < 0:从下标 i 开始,向 左 移动 abs(nums[i]) 步,在循环数组中落脚的下标对应的值赋给 result[i]。
  • 如果 nums[i] == 0:将 nums[i] 的值赋给 result[i]。

返回新数组 result。

注意:由于 nums 是循环数组,向右移动超过最后一个元素时将回到开头,向左移动超过第一个元素时将回到末尾。

示例 1:

输入: nums = [3,-2,1,1]
输出: [1,1,1,3]
解释:
对于 nums[0] 等于 3,向右移动 3 步到 nums[3],因此 result[0] 为 1。
对于 nums[1] 等于 -2,向左移动 2 步到 nums[3],因此 result[1] 为 1。
对于 nums[2] 等于 1,向右移动 1 步到 nums[3],因此 result[2] 为 1。
对于 nums[3] 等于 1,向右移动 1 步到 nums[0],因此 result[3] 为 3。

示例 2:

输入: nums = [-1,4,-1]
输出: [-1,-1,4]
解释:
对于 nums[0] 等于 -1,向左移动 1 步到 nums[2],因此 result[0] 为 -1。
对于 nums[1] 等于 4,向右移动 4 步到 nums[2],因此 result[1] 为 -1。
对于 nums[2] 等于 -1,向左移动 1 步到 nums[1],因此 result[2] 为 4。

说明:

  • 1 <= nums.length <= 100
  • -100 <= nums[i] <= 100

思路

将循环数组 nums 根据规则转换成一个新数组,新数组下标 i 的值是 nums[offset],当 nums[i] > 0 时,offseti 右边 nums[i] 个位置上的值,当 nums[i] < 0 时,取 i 左边 nums[i] 个位置上的值,如果为 0,取 nums[i]

代码


/**
 * @date 2026-02-05 8:43
 */
public class ConstructTransformedArray3379 {

    public int[] constructTransformedArray_v1(int[] nums) {
        int n = nums.length;
        int[] res = new int[n];
        for (int i = 0; i < n; i++) {
            res[i] = nums[((i + nums[i]) % n + n) % n];
        }
        return res;
    }

}

性能

3640.三段式数组II

目标

给你一个长度为 n 的整数数组 nums。

三段式子数组 是一个连续子数组 nums[l...r](满足 0 <= l < r < n),并且存在下标 l < p < q < r,使得:

  • nums[l...p] 严格 递增,
  • nums[p...q] 严格 递减,
  • nums[q...r] 严格 递增。

请你从数组 nums 的所有三段式子数组中找出和最大的那个,并返回其 最大 和。

示例 1:

输入:nums = [0,-2,-1,-3,0,2,-1]
输出:-4
解释:
选择 l = 1, p = 2, q = 3, r = 5:
nums[l...p] = nums[1...2] = [-2, -1] 严格递增 (-2 < -1)。
nums[p...q] = nums[2...3] = [-1, -3] 严格递减 (-1 > -3)。
nums[q...r] = nums[3...5] = [-3, 0, 2] 严格递增 (-3 < 0 < 2)。
和 = (-2) + (-1) + (-3) + 0 + 2 = -4。

示例 2:

输入: nums = [1,4,2,7]
输出: 14
解释:
选择 l = 0, p = 1, q = 2, r = 3:
nums[l...p] = nums[0...1] = [1, 4] 严格递增 (1 < 4)。
nums[p...q] = nums[1...2] = [4, 2] 严格递减 (4 > 2)。
nums[q...r] = nums[2...3] = [2, 7] 严格递增 (2 < 7)。
和 = 1 + 4 + 2 + 7 = 14。

说明:

  • 4 <= n = nums.length <= 10^5
  • -10^9 <= nums[i] <= 10^9
  • 保证至少存在一个三段式子数组。

思路

3637.三段式数组I 判断是否是三段式数组,本题则是计算数组的三段式子数组的最大和。

定义 dp[k][i] 表示第 k 段以 i 结尾的前 k 段子数组的最大和,其中 k ∈ [0.2]。由于每一段至少有两个元素,如果 i - 1 是该段的起始,根据前面的定义,应该属于 k - 1 段。因此有 dp[k][i] = Math.max(dp[k - 1][i - 1], dp[k][i - 1]) + nums[i],当 k = 0 时,将 dp[k - 1][i - 1] 替换为 nums[i - 1] 即可。如果处于严格递增段,可以是第一段或第三段,如果处于严格递减段,则只能是第二段。

代码


/**
 * @date 2026-02-04 8:57
 */
public class MaxSumTrionic3640 {

    public long maxSumTrionic(int[] nums) {
        int n = nums.length;
        long[][] dp = new long[3][n];
        for (int k = 0; k < 3; k++) {
            Arrays.fill(dp[k], Long.MIN_VALUE / 2);
        }
        long res = Long.MIN_VALUE / 2;
        for (int i = 1; i < n; i++) {
            if (nums[i] > nums[i - 1]) {
                dp[0][i] = Math.max(nums[i - 1], dp[0][i - 1]) + nums[i];
                dp[2][i] = Math.max(dp[1][i - 1], dp[2][i - 1]) + nums[i];
            } else if (nums[i] < nums[i - 1]) {
                dp[1][i] = Math.max(dp[0][i - 1], dp[1][i - 1]) + nums[i];
            }
            res = Math.max(res, dp[2][i]);
        }
        return res;
    }

}

性能

3637.三段式数组I

目标

给你一个长度为 n 的整数数组 nums。

如果存在索引 0 < p < q < n − 1,使得数组满足以下条件,则称其为 三段式数组(trionic):

  • nums[0...p] 严格 递增,
  • nums[p...q] 严格 递减,
  • nums[q...n − 1] 严格 递增。

如果 nums 是三段式数组,返回 true;否则,返回 false。

示例 1:

输入: nums = [1,3,5,4,2,6]
输出: true
解释:
选择 p = 2, q = 4:
nums[0...2] = [1, 3, 5] 严格递增 (1 < 3 < 5)。
nums[2...4] = [5, 4, 2] 严格递减 (5 > 4 > 2)。
nums[4...5] = [2, 6] 严格递增 (2 < 6)。

示例 2:

输入: nums = [2,1,3]
输出: false
解释:
无法选出能使数组满足三段式要求的 p 和 q 。

说明:

  • 3 <= n <= 100
  • -1000 <= nums[i] <= 1000

思路

判断数组是否是三段式数组,所谓三段式数组指第一段严格递增,第二段严格递减,第三段严格递增。

根据题意模拟,判断数组拐弯的次数是否等于 2,注意第一段应该是严格递增的。

代码


/**
 * @date 2026-02-03 9:11
 */
public class IsTrionic3637 {

    public boolean isTrionic(int[] nums) {
        if (nums[0] >= nums[1]) {
            return false;
        }
        int n = nums.length;
        int cnt = 0;
        for (int i = 1; i < n - 1; i++) {
            if (nums[i] == nums[i + 1]) {
                return false;
            } else if (nums[i - 1] > nums[i] != nums[i] > nums[i + 1]) {
                cnt++;
            }
        }
        return cnt == 2;
    }

}

性能

3013.将数组分成最小总代价的子数组II

目标

给你一个下标从 0 开始长度为 n 的整数数组 nums 和两个 正 整数 k 和 dist 。

一个数组的 代价 是数组中的 第一个 元素。比方说,[1,2,3] 的代价为 1 ,[3,4,1] 的代价为 3 。

你需要将 nums 分割成 k 个 连续且互不相交 的子数组,满足 第二 个子数组与第 k 个子数组中第一个元素的下标距离 不超过 dist 。换句话说,如果你将 nums 分割成子数组 nums[0..(i1 - 1)], nums[i1..(i2 - 1)], ..., nums[ik-1..(n - 1)] ,那么它需要满足 ik-1 - i1 <= dist 。

请你返回这些子数组的 最小 总代价。

示例 1:

输入:nums = [1,3,2,6,4,2], k = 3, dist = 3
输出:5
解释:将数组分割成 3 个子数组的最优方案是:[1,3] ,[2,6,4] 和 [2] 。这是一个合法分割,因为 ik-1 - i1 等于 5 - 2 = 3 ,等于 dist 。总代价为 nums[0] + nums[2] + nums[5] ,也就是 1 + 2 + 2 = 5 。
5 是分割成 3 个子数组的最小总代价。

示例 2:

输入:nums = [10,1,2,2,2,1], k = 4, dist = 3
输出:15
解释:将数组分割成 4 个子数组的最优方案是:[10] ,[1] ,[2] 和 [2,2,1] 。这是一个合法分割,因为 ik-1 - i1 等于 3 - 1 = 2 ,小于 dist 。总代价为 nums[0] + nums[1] + nums[2] + nums[3] ,也就是 10 + 1 + 2 + 2 = 15 。
分割 [10] ,[1] ,[2,2,2] 和 [1] 不是一个合法分割,因为 ik-1 和 i1 的差为 5 - 1 = 4 ,大于 dist 。
15 是分割成 4 个子数组的最小总代价。

示例 3:

输入:nums = [10,8,18,9], k = 3, dist = 1
输出:36
解释:将数组分割成 4 个子数组的最优方案是:[10] ,[8] 和 [18,9] 。这是一个合法分割,因为 ik-1 - i1 等于 2 - 1 = 1 ,等于 dist 。总代价为 nums[0] + nums[1] + nums[2] ,也就是 10 + 8 + 18 = 36 。
分割 [10] ,[8,18] 和 [9] 不是一个合法分割,因为 ik-1 和 i1 的差为 3 - 1 = 2 ,大于 dist 。
36 是分割成 3 个子数组的最小总代价。

说明:

  • 3 <= n <= 10^5
  • 1 <= nums[i] <= 10^9
  • 3 <= k <= n
  • k - 2 <= dist <= n - 2

思路

定义数组的代价是其第一个元素值,有一个数组 nums,将其分割成 k 个连续且不相交的子数组,并且要求第 2 个子数组 与 第 k 个子数组的第一个元素的下标的距离不超过 dist,求子数组的最小总代价。

//todo

代码

性能

3010.将数组分成最小总代价的子数组I

目标

给你一个长度为 n 的整数数组 nums 。

一个数组的 代价 是它的 第一个 元素。比方说,[1,2,3] 的代价是 1 ,[3,4,1] 的代价是 3 。

你需要将 nums 分成 3 个 连续且没有交集 的子数组。

请你返回这些子数组的 最小 代价 总和 。

示例 1:

输入:nums = [1,2,3,12]
输出:6
解释:最佳分割成 3 个子数组的方案是:[1] ,[2] 和 [3,12] ,总代价为 1 + 2 + 3 = 6 。
其他得到 3 个子数组的方案是:
- [1] ,[2,3] 和 [12] ,总代价是 1 + 2 + 12 = 15 。
- [1,2] ,[3] 和 [12] ,总代价是 1 + 3 + 12 = 16 。

示例 2:

输入:nums = [5,4,3]
输出:12
解释:最佳分割成 3 个子数组的方案是:[5] ,[4] 和 [3] ,总代价为 5 + 4 + 3 = 12 。
12 是所有分割方案里的最小总代价。

示例 3:

输入:nums = [10,3,1,1]
输出:12
解释:最佳分割成 3 个子数组的方案是:[10,3] ,[1] 和 [1] ,总代价为 10 + 1 + 1 = 12 。
12 是所有分割方案里的最小总代价。

说明:

  • 3 <= n <= 50
  • 1 <= nums[i] <= 50

思路

定义数组的代价是其第一个元素值,有一个数组 nums,将其分割成 3 个连续且不相交子数组,求子数组的最小总代价。

第一个数组的代价是固定的,问题变成从 1 ~ n -1 选两个最小的元素值。可以排序后取前三个元素的和,或者使用双指针记录最小与次小元素。

代码


/**
 * @date 2026-02-02 9:49
 */
public class MinimumCost3010 {

    public int minimumCost(int[] nums) {
        int n = nums.length;
        int min1 = Integer.MAX_VALUE;
        int min2 = Integer.MAX_VALUE;
        for (int i = 1; i < n; i++) {
            if (nums[i] < min1) {
                min2 = min1;
                min1 = nums[i];
            } else if (nums[i] < min2) {
                min2 = nums[i];
            }
        }
        return nums[0] + min1 + min2;
    }
}

性能

744.寻找比目标字母大的最小字母

目标

给你一个字符数组 letters,该数组按非递减顺序排序,以及一个字符 target。letters 里至少有两个不同的字符。

返回 letters 中大于 target 的最小的字符。如果不存在这样的字符,则返回 letters 的第一个字符。

示例 1:

输入: letters = ['c', 'f', 'j'],target = 'a'
输出: 'c'
解释:letters 中字典上比 'a' 大的最小字符是 'c'。

示例 2:

输入: letters = ['c','f','j'], target = 'c'
输出: 'f'
解释:letters 中字典顺序上大于 'c' 的最小字符是 'f'。

示例 3:

输入: letters = ['x','x','y','y'], target = 'z'
输出: 'x'
解释:letters 中没有一个字符在字典上大于 'z',所以我们返回 letters[0]。

说明:

  • 2 <= letters.length <= 10^4
  • letters[i] 是一个小写字母
  • letters 按非递减顺序排序
  • letters 最少包含两个不同的字母
  • target 是一个小写字母

思路

有一个升序排列的字符数组 letters,返回大于 target 的最小字符,如不存在返回第一个字符。

使用二分,查找第一个大于 target 的字符。

代码


/**
 * @date 2026-02-02 9:42
 */
public class NextGreatestLetter744 {

    public char nextGreatestLetter(char[] letters, char target) {
        int n = letters.length;
        int r = n - 1;
        int l = 0;
        int m = l + (r - l) / 2;
        while (l <= r) {
            if (letters[m] <= target) {
                l = m + 1;
            } else {
                r = m - 1;
            }
            m = l + (r - l) / 2;
        }
        return l < n ? letters[l] : letters[0];
    }

}

性能