线段树
目标:
- 认识线段树
- 创建线段树
- 线段树的操作
- 区间查询
- 更新操作
1.1 认识线段树
-
什么是线段树
- 使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。不考虑添加或者删除元素,大多数情况下,解决的问题都是区间固定的。
- 对于线段树来说,每一个节点表示的都是一个区间内的信息。
-
基本结构
-
线段树是建立在线段的基础上,每个结点都代表了一条线段[a,b]。
-
长度为1的线段称为元线段。非元线段都有两个子结点,左结点代表的线段为[a,(a + b) / 2],右结点代表的线段为[((a + b) / 2)+1,b]。
- 线段树不是完全二叉树(奇数个元素),但是平衡二叉树(任意节点的子树的高度差都小于等于1)。
-
满二叉树
接下来我们思考一个问题,如果区间有n个元素,用数组表示的话,需要多少节点?
- h层,一共有2^h^-1个节点,大约是2^h^
- 最后一层(h-1)层,有2^(h-1)^个节点,最后一层的节点数大致等于前面所有节点数之和
- 如果区间有n个元素,n=2^k^,那么需要2n的空间,最坏情况,如果n=2^k^+1,那么需要4n的空间。
那么如果我们的线段树不考虑添加元素,即固定区间的话,使用4n的静态空间即可。
-
-
解决的问题
-
区间染色
- 对于给定区间,更新其中一个元素或者一个区间的值
-
区间查询
- 对于给定区间查询一个区间[i,j]的最大值,最小值,或者区间数字和
- 实质:基于区间的统计查询
-
对于更新和查询来说
- 链表:O(n)
- 线段树:O(logn)
-
1.2 创建线段树
-
创建线段树
代码如下:
// 返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引 public int leftChild(int index) { return 2 * index + 1; } // 返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引 public int rightChild(int index) { return 2 * index + 2; } public SegmentTree(E[] arr, Merger<E> merger) { this.merger = merger; data = (E[]) new Object[arr.length]; for (int i = 0; i < data.length; i++) { data[i] = arr[i]; } tree = (E[]) new Object[4 * arr.length]; buildSegmentTree(0, 0, data.length - 1); } // 在treeIndex的位置创建表示区间[l...r]的线段树 private void buildSegmentTree(int treeIndex, int l, int r) { // 当l==r时,只有一个元素 if (l == r) { tree[treeIndex] = data[l]; return; } // 当l!=r时,l<r int leftTreeIndex = leftChild(treeIndex); int rightTreeIndex = rightChild(treeIndex); // 计算中间位置,当l和r很大的时候,可能会有溢出 int mid = l + (r - l) / 2; buildSegmentTree(leftTreeIndex, l, mid); buildSegmentTree(rightTreeIndex, mid + 1, r); tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]); } -
求和线段树
如上图所示,根节点为所有元素的和,左子节点为左半部分元素的和,右子节点为右半部分元素的和,然后继续递归,直至最后节点不能再划分了,节点只有一个元素。
当l==r时,只有一个元素,由于treeIndex节点放置的就是[l,r]区间的值,所以此时直接让treeIndex节点的值为data[r]即可。
当l!=r时,计算中间节点位置mid,然后调用递归函数即可。由于我们创建求和线段树,所以需要考虑当不是整型Interger时的情况,所以创建merge函数,进行合并操作。
-
查询操作
如下图:
假如我们查询[2,5]区间,由上图可知,从根节点出发,[2,5]内元素是[0,7]的子集,由于左子树和右子树的区分是中间节点位置,所以[2,5]在左子树和右子树都有。所以我们从左子树查询[2,3],从右子树查询[4,5],然后合并即可。
实现代码如下:
// 返回区间[queryL, queryR]的值 public E query(int queryL, int queryR) { if (queryL < 0 || queryL >= data.length || queryR < 0 || queryR >= data.length || queryL > queryR) { throw new IllegalArgumentException("Index is illegal."); } return query(0, 0, data.length - 1, queryL, queryR); } // 在以treeID为根的线段树中[l...r]的范围里,查询区间[queryL...queryR]的值 private E query(int treeIndex, int l, int r, int queryL, int queryR) { if (l == queryL && r == queryR) { return tree[treeIndex]; } int mid = l + (r - l) / 2; int leftTreeIndex = leftChild(treeIndex); int rightTreeIndex = rightChild(treeIndex); // 分三种情况,在左边,在右半边,在中间 if (queryL >= mid + 1) { return query(rightTreeIndex, mid + 1, r, queryL, queryR); } else if (queryR <= mid) { return query(leftTreeIndex, l, mid, queryL, queryR); } E leftResult = query(leftTreeIndex, l, mid, queryL, mid); E rightResult = query(rightTreeIndex, mid + 1, r, mid + 1, queryR); return merger.merge(leftResult, rightResult); } -
更新操作
代码如下:
// 更新操作:将index位置的值,更新为e public void set(int index, E e) { if (index < 0 || index >= data.length) { throw new IllegalArgumentException("Index is illegal."); } data[index] = e; set(0, 0, data.length - 1, index, e); } // 在以treeIndex为根的线段树中更新Index的值为e private void set(int treeIndex, int l, int r, int index, E e) { if (l == r) { tree[treeIndex] = e; return; } int mid = l + (r - l) / 2; int leftTreeIndex = leftChild(treeIndex); int rightTreeIndex = rightChild(treeIndex); if (index >= mid + 1) { set(rightTreeIndex, mid + 1, r, index, e); } else { set(leftTreeIndex, l, mid, index, e); } tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]); }代码逻辑是这样的,当我们更新index位置的元素为e时,同样先计算mid的值,然后判断index和mid的大小,也就是确定index是在左子树还是右子树中。
需要注意的是,当我们确定index的位置,并且更新完成之后,是不够的,只是因为在返回的过程中,其父节点等等都会受到影响,都是区间内的统计结果,所以index节点的值发生变化,其父节点也要发生变化。
额……好开心,今天把线段树的知识点汇总了一遍。总体来说收获不小,对于树结构的操作和递归思想的理解更加深刻。
当然,只是简单的总结肯定还是不够的,后续需要多加练习,这样才能掌握的更好。
唉,刷题一时难,一直刷题一直难。
OK,今天就到这里,下班走起。
拜了个拜~