问题导入
如果有一个数组
int [] arr = {1, 3, 5, 7, 9, 11};
要进行一系列操作:
1、arr[i]加上一个数
2、求区间L-R的和
容易看出,第一个操作复杂度是O(1),第二个是O(n)
有没有更快的呢,答案是有的,利用线段树来实现复杂度为O(logn)
注:这个问题树状数组也可以完成,而且实现更加容易
线段树的构成
当需要查询区间2 - 5的和时,过程如图
当要修改arr[4]的值时,从下往上修改
构造线段树
为了方便用数组构造,我们将树当成是完全二叉树
可以看的出来,
当前节点的左儿子为 2 * node + 1,
右儿子为2 * node + 2
代码实现
在代码中,
凡是带node的区间均是树的区间
凡是带node的区间均是树的区间
凡是带node的区间均是树的区间
数组则用start、end、L、R表示
建树
arr是原本的数组,tree是我们要建的线段树,node是指代表当前函数所求区间[start, end]中树的结点的位置,一开始传入build_tree(arr, tree, 0, 0 arr.length - 1)代表整个区间在树中的结点是0,剩余解析看注释
public static void build_tree(int [] arr, int [] tree, int node, int start, int end) {
if(start == end) {
tree[node] = arr[start];
}else{
//先求左右结点对应的区间
int mid = (start + end) / 2;
int left_node = 2 * node + 1;
int right_node = 2 * node + 2;
//分治的思想,先将左右结点建好,按照递归的性质,下面两行代码会一个递归到叶子结点
//也就是第上面的if条件语句,然后开始由下往上赋值
build_tree(arr, tree, left_node, start, mid);
build_tree(arr, tree, right_node, mid + 1, end);
tree[node] = tree[left_node] + tree[right_node];
}
}
更新
可以看到其实参数跟建树类似,只是多了idx,val,分别是在原始数组中将idx位置的值改为val
public static void update_tree(int [] arr, int [] tree, int node, int start, int end, int idx, int val) {
if(start == end) {
arr[idx] = val;
tree[node] = val;
}else {
//同样的先求左右结点对应的区间
int mid = (start + end) / 2;
int left_node = 2 * node + 1;
int right_node = 2 * node + 2;
//此时如果idx不在我们当前函数所求的区间中,就不需要递归了,其他步骤类似建树
if(idx >= start && idx <= mid) {
update_tree(arr, tree, left_node, start, mid, idx, val);
}else {
update_tree(arr, tree, right_node, mid + 1, end, idx, val);
}
tree[node] = tree[left_node] + tree[right_node];
}
}
查询
查询稍微复杂一点,参数多了我们所查询的区间L、R
public static int query_tree(int [] arr, int [] tree,int node, int start, int end, int L, int R) {
//如果当前函数所查询的区间完全偏离了要求的区间,返回0
if(R < start || L > end) {
return 0;
}
//类似的,如果包含,则直接放回,当前对应结点的值
else if(L <= start && end <= R) {
return tree[node];
}
else if(start == end) {
return tree[node];
}else {
//与上面两个函数类似,不加累述
int mid = (start + end) / 2;
int left_node = 2 * node + 1;
int right_node = 2 * node + 2;
int sum_left = query_tree(arr, tree, left_node, start, mid, L, R);
int sum_right = query_tree(arr, tree, right_node, mid + 1, end, L, R);
return sum_left + sum_right;
}
}
所有代码
public class RMQ {
static int MAX_LEN = 1000; //树的节点数,可以开大点
public static void main(String[] args) {
int arr[] = {1, 3, 5, 7, 9, 11}; //原始数组
int size = arr.length;
int [] tree = new int [MAX_LEN];
build_tree(arr, tree, 0, 0, size - 1);
}
public static int query_tree(int [] arr, int [] tree,int node, int start, int end, int L, int R) {
if(R < start || L > end) {
return 0;
}
else if(L <= start && end <= R) {
return tree[node];
}
else if(start == end) {
return tree[node];
}else {
int mid = (start + end) / 2;
int left_node = 2 * node + 1;
int right_node = 2 * node + 2;
int sum_left = query_tree(arr, tree, left_node, start, mid, L, R);
int sum_right = query_tree(arr, tree, right_node, mid + 1, end, L, R);
return sum_left + sum_right;
}
}
public static void update_tree(int [] arr, int [] tree, int node, int start, int end, int idx, int val) {
if(start == end) {
arr[idx] = val;
tree[node] = val;
}else {
int mid = (start + end) / 2;
int left_node = 2 * node + 1;
int right_node = 2 * node + 2;
if(idx >= start && idx <= mid) {
update_tree(arr, tree, left_node, start, mid, idx, val);
}else {
update_tree(arr, tree, right_node, mid + 1, end, idx, val);
}
tree[node] = tree[left_node] + tree[right_node];
}
}
public static void build_tree(int [] arr, int [] tree, int node, int start, int end) {
if(start == end) {
tree[node] = arr[start];
}else{
int mid = (start + end) / 2;
int left_node = 2 * node + 1;
int right_node = 2 * node + 2;
build_tree(arr, tree, left_node, start, mid);
build_tree(arr, tree, right_node, mid + 1, end);
tree[node] = tree[left_node] + tree[right_node];
}
}
}
可以看的出来,无论是构造、更新或者是查询,第一步都是先写
int mid = (start + end) / 2;
int left_node = 2 * node + 1;
int right_node = 2 * node + 2;
之后都是分治,注意出口条件即可,很好记忆
注:以上均整理自B站正月点灯笼的视频