一、引言
LeetCode 3161 块放置查询问题是区间管理与数据结构应用的经典标杆题,也是面试中考察区间维护、动态更新与高效查询能力的核心考点。这道题的核心是破解 "在数轴上动态放置障碍物,并快速查询可放置指定长度块的最左侧位置" 的问题,而线段树是解决该问题的直观、通用的进阶思路 ------ 它从区间维护的直接实现入手,在时间 / 空间复杂度与实现复杂度上取得了很好的平衡,完美对应了数据结构思维从入门到高阶的成长路径。
本文会系统拆解线段树解法:通过线段树理清区间维护与区间查询的直观逻辑(覆盖全面、易理解,适合入门),并给出可直接运行的 Java 代码,帮你从原理到实现彻底啃下这道题。

二、线段树
2.1 概念
线段树是一种专门用来高效处理「区间问题」的数据结构,能快速完成「区间更新」和「区间查询」操作,时间复杂度基本稳定在 (O(log n))。
2.2 原理
线段树的本质是一棵完全二叉树,它把数组的区间不断「对半拆分」,直到拆成单个元素,再自底向上维护每个区间的信息。
举个简单例子:数组 [1, 3, 5, 7, 9, 11],求区间和
-
根节点 :代表整个数组区间
[0, 5],值是区间和1+3+5+7+9+11=36 -
第一层子节点 :把根区间对半拆成
[0, 2](和为 9)和[3, 5](和为 27) -
第二层子节点 :再对半拆分,比如
[0, 2]拆成[0, 1](和 4)和[2, 2](和 5) -
叶子节点 :最后拆到单个元素,比如
[0,0](值 1)、[1,1](值 3)......[0,5] = 36 / \ [0,2] = 9 [3,5] = 27 / \ / \ [0,1] = 4 [2,2] =5 [3,4]=16 [5,5]=11 / \[0,0]=1 [1,1]=3
- 查询区间 :比如查询
[1,4]的和,只需要把树中完全包含在[1,4]里的节点值加起来就行,不用遍历每个元素。 - 更新节点 :比如把
[2,2]的值改成 10,只需要从叶子节点向上更新所有包含该位置的父节点的和即可。
2.3 代码实现
2.3.1 Node版
java
// 线段树节点类
class TreeNode {
int l, r; // 当前节点管的区间 [l, r]
int val; // 节点值(区间和)
TreeNode left; // 左孩子
TreeNode right; // 右孩子
public TreeNode(int l, int r) {
this.l = l;
this.r = r;
this.val = 0;
this.left = null;
this.right = null;
}
}
// 线段树(节点版)
public class SegmentTreeNode {
private TreeNode root;
private int[] nums;
// 构造函数
public SegmentTreeNode(int[] nums) {
this.nums = nums;
this.root = build(0, nums.length - 1);
}
// 1. 构建线段树(递归)
private TreeNode build(int l, int r) {
TreeNode node = new TreeNode(l, r);
// 叶子节点
if (l == r) {
node.val = nums[l];
return node;
}
// 分治
int mid = (l + r) / 2;
node.left = build(l, mid);
node.right = build(mid + 1, r);
// 合并孩子的值
node.val = node.left.val + node.right.val;
return node;
}
// 2. 单点更新
public void update(int idx, int val) {
update(root, idx, val);
}
private void update(TreeNode node, int idx, int val) {
if (node.l == node.r) {
node.val = val;
return;
}
if (idx <= node.left.r) {
update(node.left, idx, val);
} else {
update(node.right, idx, val);
}
// 更新完孩子,更新自己
node.val = node.left.val + node.right.val;
}
// 3. 区间查询 [L, R]
public int query(int L, int R) {
return query(root, L, R);
}
private int query(TreeNode node, int L, int R) {
// 完全包含
if (L <= node.l && node.r <= R) {
return node.val;
}
// 无交集
if (node.r < L || node.l > R) {
return 0;
}
// 递归查左右
return query(node.left, L, R) + query(node.right, L, R);
}
}
2.3.2 数组版
java
public class SegmentTree {
// 原数组
private int[] nums;
// 线段树数组(大小一般开 4 * 数组长度,足够用)
private int[] tree;
// 构造函数:传入原数组,自动构建线段树
public SegmentTree(int[] nums) {
this.nums = nums;
int n = nums.length;
tree = new int[4 * n];
// 构建:根节点是 1,管理区间 [0, n-1]
build(1, 0, n - 1);
}
// 1. 构建线段树
// node:当前节点编号
// l, r:当前节点管理的区间左右边界
private void build(int node, int l, int r) {
// 叶子节点:直接等于原数组值
if (l == r) {
tree[node] = nums[l];
return;
}
// 分治:拆成左右两半
int mid = (l + r) / 2;
build(2 * node, l, mid); // 左孩子
build(2 * node + 1, mid + 1, r); // 右孩子
// 父节点 = 左孩子 + 右孩子
tree[node] = tree[2 * node] + tree[2 * node + 1];
}
// 2. 单点更新:把 index 位置改成 val
public void update(int index, int val) {
update(1, 0, nums.length - 1, index, val);
}
// 递归更新
private void update(int node, int l, int r, int idx, int val) {
// 找到叶子节点,修改
if (l == r) {
nums[idx] = val;
tree[node] = val;
return;
}
int mid = (l + r) / 2;
// 要更新的点在左子树
if (idx <= mid) {
update(2 * node, l, mid, idx, val);
}
// 在右子树
else {
update(2 * node + 1, mid + 1, r, idx, val);
}
// 更新完孩子,更新父亲
tree[node] = tree[2 * node] + tree[2 * node + 1];
}
// 3. 区间查询:查询 [L, R] 的和
public int query(int L, int R) {
return query(1, 0, nums.length - 1, L, R);
}
// 递归查询
private int query(int node, int l, int r, int L, int R) {
// 当前区间完全在查询范围内,直接返回
if (L <= l && r <= R) {
return tree[node];
}
// 当前区间和查询区间无交集,返回 0
if (r < L || l > R) {
return 0;
}
// 有交集,拆分查询
int mid = (l + r) / 2;
int leftSum = query(2 * node, l, mid, L, R);
int rightSum = query(2 * node + 1, mid + 1, r, L, R);
return leftSum + rightSum;
}
}
三、线段树解法
3.1 思路
题目简化成一句话:
数轴上不断放障碍物,每次问:能不能找到一段长度为 size 的连续空白?
暴力法:
- 用一个数组
blocked[]标记哪些位置有障碍物 - 操作 1:
blocked[x] = true,O (1) 搞定 - 操作 2:从 0 遍历到 x,找最长的连续空白,看是否 ≥ sz,O (x) 搞定
线段树:
单点更新
- 每次在位置
x放障碍物,只需要在线段树里做一次单点更新,把x标记为 "已占用" - 线段树会从叶子节点向上,一路更新所有父节点维护的「最大连续空白长度」
- 时间复杂度:O(logM),和暴力的 O (1) 差距不大,但为查询做了铺垫
区间查询
- 每次查询
[0, x]内是否有长度 ≥ sz 的空白,只需要查询线段树中[0, x]区间的「最大连续空白长度」 - 线段树直接返回维护好的
maxLen,我们只需要判断maxLen >= sz即可 - 时间复杂度:O(logM),比暴力的 O (x) 快了整整一个数量级
3.2 代码实现(Node版)
java
class Solution {
public List<Boolean> getResults(int[][] queries) {
int max = 0;
// 寻找最大的数,方便构建线段树
for (int[] query : queries){
max = Math.max(max , query[1]);
}
Node root = new Node(0 , max);
List<Boolean> res = new ArrayList<>();
for (int[] query : queries){
if (query[0] == 1){
update(root , query[1]);
} else {
res.add(query(root , query[1] , query[2]) >= query[2]);
}
}
return res;
}
public class Node{
// 左右边界
int l , r;
// 最大长度
int maxLen;
// 左边,右边连续长度
int leftLen , rightLen;
// 区间长度
int len;
// 左边界,右边界是否有屏障
boolean leftFlag , rightFlag;
// 左,右节点
Node left , right;
public Node(int l , int r){
this.l = l;
this.r = r;
// 是r - l ,因为是区间不是点的数量
len = rightLen = leftLen = maxLen = r - l;
}
}
private void update(Node node , int x){
if (x > node.r || x < node.l || node.l == node.r){
// 不在区间的跳过
return;
}
// 更新左右屏障标记
node.rightFlag = node.rightFlag || node.r == x;
node.leftFlag = node.leftFlag || node.l == x;
// 只有本次标记有屏障才可以直接return,之前的不算
if (node.r == x || node.l == x){
return;
}
create(node);
Node left = node.left;
Node right = node.right;
// 递归更新,找到边界
update(left , x);
update(right , x);
// left.rightFlag = right.leftFlag , 边界都是同一个点
boolean flag = left.rightFlag;
// 如果中间有屏障,直接就是left.leftLen , 没有中间屏障并且left整个部分都没有屏障,就可以把right的部分加过来
node.leftLen = left.leftLen + (left.leftLen == left.len && !flag ? right.leftLen : 0);
node.rightLen = right.rightLen + (right.rightLen == right.len && !flag ? left.rightLen : 0);
// 要么是子节点中最长的,要么是没有中间屏障的时候把两个加起来
node.maxLen = Math.max (
Math.max(left.maxLen , right.maxLen) ,
flag ? 0 : left.rightLen + right.leftLen
);
}
private int query(Node node , int x , int sz){
if (node.l > x || sz > x){
return 0;
} else if (node.len == 0 || node.len == 1){
//这种没有递归的必要
return node.len;
} else if (node.r <= x){
// 已经完全包含在区间,可以直接返回
return node.maxLen;
}
int mid = create(node);
Node left = node.left;
Node right = node.right;
int res = query(left , x , sz);
// 符合条件的都可以直接提前返回
if (res >= sz || mid >= x){
return res;
}
res = Math.max(res , query(right , x , sz));
if (res >= sz){
return res;
}
if (!left.rightFlag){
// 注意不可以直接加right.leftLen , 还要注意边界的问题
res = Math.max(res , left.rightLen + Math.min(x - mid , right.leftLen));
}
return res;
}
private int create(Node node){
// 创造左右节点
int mid = (node.l + node.r) >>> 1;
if (node.left == null){
node.left = new Node(node.l , mid);
}
if (node.right == null){
// 此处不是mid + 1,因为是区间
node.right = new Node(mid , node.r);
}
return mid;
}
}
3.3 代码实现(数组版)
以上的这种实现需要我们手动去维护屏障,接下来我们可以使用TreeSet去存放屏障,底层也就是二分查找。
java
class Solution {
// 0 - x 内的最大空白块是多大
int[] arr;
public List<Boolean> getResults(int[][] queries) {
int max = 0;
for (int[] query : queries){
max = Math.max(max , query[1]);
}
this.arr = new int[max << 2];
// 放入两个哨兵,左右边界
TreeSet<Integer> set = new TreeSet<>(List.of(0 , max));
List<Boolean> res = new ArrayList<>();
for (int[] query : queries){
int x = query[1];
// 找到左侧的上一个障碍物(因为arr存放的是距离上一个障碍物)
int pre = set.floor(x - 1);
if (query[0] == 1){
// 找到右侧下一个屏障
int next = set.ceiling(x);
set.add(query[1]);
// 1是根节点,左右范围是0-max
update(1 , 0 , max , x , x - pre);
update(1 , 0 , max , next , next - x);
} else {
// 长度要么是0-上一次屏障,要么是当前x - 上一次屏障(这样就包含了没有初始化数组的情况)
res.add(Math.max(query(1 , 0 , max , pre) , x - pre) >= query[2]);
}
}
return res;
}
private void update(int father , int left , int right , int x , int val){
if (left == right){
arr[father] = val;
return;
}
int mid = (left + right) >>> 1;
if (x <= mid){
update(2 * father , left , mid , x , val);
} else {
update(2 * father + 1 , mid + 1 , right , x , val);
}
arr[father] = Math.max(arr[2 * father] , arr[2 * father + 1]);
}
private int query(int father , int left , int right , int x){
if (right <= x){
return arr[father];
}
int mid = (left + right) >>> 1;
if (x <= mid){
return query(2 * father , left , mid , x);
}
// 这里不需要拼接左子树的右边和右子树的左边
// 因为此处存放的是距离上一个障碍物,而不是距离子树左子树的右边界,所以这种情况已经包含了
return Math.max(arr[2 * father] , query(2 * father + 1 , mid + 1 , right , x));
}
}