一.线段树的定义
二.线段树的构造及使用
问题1. 线段树是一棵二叉树,他的每个节点包含了两个额外的属性start和end用于表示该节点所代表的区间。start和end都是整数,并按照如下的方式赋值:
- 根节点的 start 和 end 由 build 方法所给出。
- 对于节点 A 的左儿子,有 start=A.left, end=(A.left + A.right) / 2。
- 对于节点 A 的右儿子,有 start=(A.left + A.right) / 2 + 1, end=A.right。
- 如果 start 等于 end, 那么该节点是叶子节点,不再有左右儿子。
实现一个 build 方法,接受 start 和 end 作为参数, 然后构造一个代表区间 [start, end] 的线段树,返回这棵线段树的根。
Example
给出[3,2,1,4],线段树将被这样构造:
[0, 3]
/ \
[0, 1] [2, 3]
/ \ / \
[0, 0][1, 1][2, 2] [3, 3]
分析:
1. 当start > end时,应返回null;
2. 当start = end时,该线段树只有一个根节点;
3. 当start < end时,使用递归建立树。
public class SegmentTree {
/**
* Definition of SegmentTreeNode:
*/
public static class SegmentTreeNode {
public int start, end;
public SegmentTreeNode left, right;
public SegmentTreeNode(int start, int end) {
this.start = start;
this.end = end;
this.left = this.right = null;
}
}
/**
* @param start: start value.
* @param end: end value.
* @return: The root of Segment Tree.
*/
public SegmentTreeNode build(int start, int end) {
// write your code here
if (start > end) {
return null;
}
SegmentTreeNode rootNode = new SegmentTreeNode(start, end);
if (start == end) {
return rootNode;
} else {
int newStart = start;
int newEnd = (start + end) >> 1;
rootNode.left = build(newStart, newEnd);
newStart = newEnd + 1;
newEnd = end;
rootNode.right = build(newStart, newEnd);
}
return rootNode;
}}
问题2. 对于一个有n个数的整数数组,在对应的线段树中, 根节点所代表的区间为0-n-1, 每个节点有一个额外的属性max,值为该节点所代表的数组区间start到end内的最大值。
为SegmentTree设计一个 query 的方法,接受3个参数root, start和end,线段树root所代表的数组中子区间[start, end]内的最大值。
Example
对于数组 [1, 4, 2, 3], 对应的线段树为:
[0, 3, max=4]
/ \
[0,1,max=4] [2,3,max=3]
/ \ / \
[0,0,max=1] [1,1,max=4] [2,2,max=2], [3,3,max=3]
分析:
1. 首先构造线段树;
2. 然后使用递归,自低向上,找出每个区间的max值。如[0,1]的最大值,取其左子树最大值与右子树最大值进行比较,若子树最大值未知,则开始查找子树最大值。叶子节点的最大值是显见的。
public static class SegmentTreeNode {
public int start, end;
public int max;
public SegmentTreeNode left, right;
public SegmentTreeNode(int start, int end) {
this.start = start;
this.end = end;
this.left = this.right = null;
this.max = Integer.MAX_VALUE;
}
}
public void findMax(SegmentTreeNode rootNode, int[] array) {
if (rootNode.left == null) {
rootNode.max = array[rootNode.end];
} else {
if (rootNode.left.max == Integer.MAX_VALUE) {
findMax(rootNode.left, array);
}
}
if (rootNode.right != null && rootNode.right.max == Integer.MAX_VALUE) {
findMax(rootNode.right, array);
}
if (rootNode.left != null && rootNode.right != null) {
rootNode.max = rootNode.left.max > rootNode.right.max ?
rootNode.left.max : rootNode.right.max;
}
}
3. 查找指定区间的最大值,先对特殊情况进行处理:
要查找的区间不在线段树范围内
线段树只有根节点
要查找的区间等于根节点区间
4. 一般情况下,要查找的区间可能为左子树区间的子集,右子树的子集,或者跨左右子树三种情形。
public int query(SegmentTreeNode root, int start, int end) {
// write your code here
if (end < root.start || start > root.end) {
return Integer.MIN_VALUE;
}
if (root.start == root.end) {
if (start == root.start || end == root.end) {
return root.max;
} else {
return Integer.MIN_VALUE;
}
} else {
if (start == root.start && end == root.end) {
return root.max;
}
}
if (end <= root.left.end) {
/**
* 区间位于左子树
*/
SegmentTreeNode node = root.left;
return query(node, start, end);
} else if (start >= root.right.start) {
/**
* 区间位于右子树
*/
SegmentTreeNode node = root.right;
return query(node, start, end);
} else {
/**
* 区间跨子树
*/
int maxLeft = query(root.left, start, root.left.end);
int maxRight = query(root.right, root.right.start, end);
return maxLeft >= maxRight ? maxLeft : maxRight;
}
}
问题3. 给定一个整数数组 (下标由 0 到 n-1,其中 n 表示数组的规模,数值范围由 0 到 10000),以及一个 查询列表。对于每一个查询,将会给你一个整数,请你返回该数组中小于给定整数的元素的数量。
Example
对于数组 [1,2,7,8,5] ,查询 [1,8,5],返回 [0,4,2]
分析:
1. 在SegmentTreeNode中再增加min字段:
public static class SegmentTreeNode {
public int start, end;
public int max;
public int min;
public SegmentTreeNode left, right;
public SegmentTreeNode(int start, int end) {
this.start = start;
this.end = end;
this.left = this.right = null;
this.max = Integer.MAX_VALUE;
this.min = Integer.MIN_VALUE;
}
}
2. 构造完区间树后,对min和max进行初始化:
public void findMinAndMax(SegmentTreeNode node, int[] array) {
if (node.left == null) {
node.min = array[node.start];
node.max = array[node.end];
} else {
if (node.left.min == Integer.MIN_VALUE || node.left.max == Integer.MAX_VALUE) {
findMinAndMax(node.left, array);
}
}
if (node.right != null) {
if (node.right.min == Integer.MIN_VALUE || node.right.max == Integer.MAX_VALUE) {
findMinAndMax(node.right, array);
}
}
if (node.left != null && node.right != null) {
node.min = node.left.min > node.right.min ? node.right.min : node.left.min;
node.max = node.left.max > node.right.max ? node.left.max : node.right.max;
}
}
3. 先处理特殊情况,即要查询的整数超出线段树区间,和线段树只有根节点的情形;
4. 一般情况下,分左右子树分别计算,然后将左右子树的结果作和,以左子树为例,若目标整数queryInt大于左子树max,则左子树区间内所有值都符合条件,否则,queryInt落在左子树区间内,以左子树为根节点,继续向下计算。右子树同理。
public int query(SegmentTreeNode rootNode, int queryInt) {
if (queryInt > rootNode.max || queryInt < rootNode.min) {
return 0;
}
int count = 0;
if (rootNode.left == null || rootNode.right == null) {
if (queryInt > rootNode.max) {
count++;
}
} else {
SegmentTreeNode left = rootNode.left;
if (queryInt > left.max) {
count += left.end - left.start + 1;
} else if (queryInt > left.min && queryInt <= left.max) {
count += query(left, queryInt);
}
SegmentTreeNode right = rootNode.right;
if (queryInt > right.max) {
count += right.end - right.start + 1;
} else if (queryInt > right.min && queryInt <= right.max) {
count += query(right, queryInt);
}
}
return count;
}
最后,问题答案:
public List<Integer> countOfSmallerNumber(int[] A, int[] queries) {
// write your code here
List<Integer> result = new LinkedList<>();
if (A.length == 0) {
while (result.size() != queries.length) {
result.add(0);
}
return result;
}
SegmentTreeNode rootNode = build(A);
HashMap<Integer, Integer> results = new HashMap<>();
for (int i = 0; i < queries.length; i++) {
int queryInt = queries[i];
Integer integer = results.get(queryInt);
if (integer != null) {
result.add(integer);
} else {
integer = query(rootNode, queryInt);
result.add(integer);
results.put(queryInt, integer);
}
}
return result;
}
这里对结果进行了缓存,若在查询过程中,遇到相同整数,则不必再查询线段树。