背景:
线段树(Segment Tree)是一种树形结构,具有二叉搜索树的性质。想要学好某种数据结构,首先要明白它是为了解决什么问题或那类问题而产生的。这里首先抛出一个非常典型的问题:
给定一个数组nums,现在要你实现两个操作,
- 查询:查询数组nums的下标在[left,right]范围内的和sum,即sum=nums[left]+…+nums[right]
- 更新:将数组nums的下标为position的值改为value,即nums[position]=value
问题十分简单,也很容易理解,两个操作的时间复杂度也很明显,查询O(n),更新O(1),但是在操作的次数很庞大时,总查询的时间复杂度变为O(n*m),m为操作次数,有人可能会利用前缀和的思想对查询这个操作优化下,即再引入一个数组,用于存前缀和,那么一次查询操作的时间复杂度降为O(1),不过,因为存在更新操作,为了维护前缀和数组,一次更新操作的时间复杂度升为O(n)。
简介:
还有更好的办法么?这时线段树就出来了,它使查询或更新操作的时间复杂度都为O(logn),先来张图,有个大致印象
这是种树结构,其节点可以存在很多信息,视情况而定,看你构造的节点类型,其中必不可少的就是“线段”啦,就是一头一尾的下标。而我们一般使用数组来存储树结构,学过堆结构的同学应该很快会想到这种方法。下面将介绍基于本文章开头提出的问题,分别用递归与非递归的方式来实现线段树。
递归实现:
这里规定根节点在数组的下标为0,即从0开始,对任意节点的下标index,其左孩子的下标为2 * index+1,右孩子为2 * index+2,树节点的值存的是对应“线段”的和。
首先是构建树,
public void build(int index, int start, int end) {
if (start == end) {
tree[index] = nums[start];
return;
}
int mid = start + (end - start) / 2;
build(2 * index + 1, start, mid);
build(2 * index + 2, mid + 1, end);
tree[index] = tree[2 * index + 1] + tree[2 * index + 2];
}
查询,
public int search(int index, int start, int end, int left, int right) {
if (start > right || end < left) {
return 0;
}
if (start >= left && end <= right) {
return tree[index];
}
int mid = start + (end - start) / 2;
return search(2 * index + 1, start, mid, left, right)
+ search(2 * index + 2, mid + 1, end, left, right);
}
更新,
public void update(int index, int start, int end, int position, int value) {
if (start == end) {
tree[index] = value;
return;
}
int mid = start + (end - start) / 2;
if (position <= mid) {
update(2 * index + 1, start, mid, position, value);
} else {
update(2 * index + 2, mid + 1, end, position, value);
}
tree[index] = tree[2 * index + 1] + tree[2 * index + 2];
}
完整代码如下:
public class SegmentTree {
int[] nums;
int[] tree;
int n;
public SegmentTree(int[] nums) {
n = nums.length;
this.nums = nums;
tree = new int[4 * n];
build(0, 0, n - 1);
}
public void build(int index, int start, int end) {
if (start == end) {
tree[index] = nums[start];
return;
}
int mid = start + (end - start) / 2;
build(2 * index + 1, start, mid);
build(2 * index + 2, mid + 1, end);
tree[index] = tree[2 * index + 1] + tree[2 * index + 2];
}
public int search(int index, int start, int end, int left, int right) {
if (start > right || end < left) {
return 0;
}
if (start >= left && end <= right) {
return tree[index];
}
int mid = start + (end - start) / 2;
return search(2 * index + 1, start, mid, left, right)
+ search(2 * index + 2, mid + 1, end, left, right);
}
public void update(int index, int start, int end, int position, int value) {
if (start == end) {
tree[index] = value;
return;
}
int mid = start + (end - start) / 2;
if (position <= mid) {
update(2 * index + 1, start, mid, position, value);
} else {
update(2 * index + 2, mid + 1, end, position, value);
}
tree[index] = tree[2 * index + 1] + tree[2 * index + 2];
}
}
非递归实现:
非递归方式的实现就是从底向上依次构建树,查询与更新操作也是从底向上进行,对与递归方式的实现,则是从上往下,并且递归占用的空间也比较大,为防止越界,数组要开4倍n的大小。而非递归只需2倍n的大小即可。这里规定树的根节点下标从1开始,则任意节点index的左孩子可以表示为index << 1,右孩子表示为index << 1 | 1
建树,
public void build() {
System.arraycopy(nums, 0, tree, n, n);
for (int i = n - 1; i > 0; i--) {
tree[i] = tree[i << 1] + tree[i << 1 | 1];
}
}
查询,
public int search(int left, int right) {
int sum = 0;
left += n;
right += n;
while (left <= right) {
if ((left & 1) == 1) {
sum += tree[left];
left++;
}
if ((right & 1) == 0) {
sum += tree[right];
right--;
}
left >>= 1;
right >>= 1;
}
return sum;
}
更新,
public void update(int position, int value) {
position += n;
tree[position] = value;
while (position > 0) {
int left = position;
int right = position;
if ((left & 1) == 0) {
right++;
} else {
left--;
}
tree[position >> 1] = tree[left] + tree[right];
position >>= 1;
}
}
完整代码为,
class SegmentTree {
int[] nums;
int[] tree;
int n;
public SegmentTree(int[] nums) {
n = nums.length;
this.nums = nums;
tree = new int[2 * n];
build();
}
public void build() {
System.arraycopy(nums, 0, tree, n, n);
for (int i = n - 1; i > 0; i--) {
tree[i] = tree[i << 1] + tree[i << 1 | 1];
}
}
public int search(int left, int right) {
int sum = 0;
left += n;
right += n;
while (left <= right) {
if ((left & 1) == 1) {
sum += tree[left];
left++;
}
if ((right & 1) == 0) {
sum += tree[right];
right--;
}
left >>= 1;
right >>= 1;
}
return sum;
}
public void update(int position, int value) {
position += n;
tree[position] = value;
while (position > 0) {
int left = position;
int right = position;
if ((left & 1) == 0) {
right++;
} else {
left--;
}
tree[position >> 1] = tree[left] + tree[right];
position >>= 1;
}
}
}