線段樹
線段樹是一種二叉搜索樹,與區間樹相似,它將一個區間劃分成一些單元區間,每個單元區間對應線段樹中的一個葉結點。
使用線段樹可以快速的查找某一個節點在若干條線段中出現的次數,時間複雜度爲O(logN)。而未優化的空間複雜度爲2N,實際應用時一般還要開4N的數組以免越界,因此有時需要離散化讓空間壓縮。
這不是一棵完全二叉樹,也不是滿二叉樹,是一棵平衡二叉樹,如果將這棵樹的最後一層不存在的節點定義爲null,補齊就可以看做是一棵滿二叉樹,就可以使用數組作爲底層進行表示。
SegmentTree.java(線段樹)
//線段樹
public class SegmentTree<E> {
private E[] data;// 底層數組
private E[] tree;// 構建線段樹
private Merger<E> merger;// 線段樹融合器
// 構造方法 傳入arr數組與融合器匿名函數
public SegmentTree(E[] arr, Merger<E> merger) {
// TODO Auto-generated constructor stub
this.merger = merger;
data = (E[]) new Object[arr.length];// 初始化arr數組
for (int i = 0; i < arr.length; i++)
data[i] = arr[i];
tree = (E[]) new Object[4 * arr.length];// 初始化tree數組存放線段樹
buildSegmentTree(0, 0, arr.length - 1);
}
// 在treeIndex的位置創建表示區間[l...r]的線段樹 遞歸函數
private void buildSegmentTree(int treeIndex, int l, int r) {
if (l == r) { // 遞歸的終止條件
tree[treeIndex] = data[l];// 當線段樹子樹長度爲1,也就是它本身
return;
}
int leftTreeIndex = leftChild(treeIndex);// 左子樹根節點
int rightTreeIndex = rightChild(treeIndex);// 右子樹根節點
int mid = l + (r - l) / 2;
buildSegmentTree(leftTreeIndex, l, mid);// 創建左子樹
buildSegmentTree(rightTreeIndex, mid + 1, r);// 創建右子樹
// tree[treeIndex]爲當前兩個孩子融合得到的結果
tree[treeIndex] = merger.meger(tree[leftTreeIndex], tree[rightTreeIndex]);
}
public E get(int index) {
if (index < 0 || index >= data.length)
try {
throw new Exception("index越界");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return data[index];
}
public int getSize() {
return data.length;
}
// 獲取該索引的左孩子的索引
private int leftChild(int index) {
return 2 * index + 1;
}
// 獲取該索引的右孩子的索引
private int rightChild(int index) {
return 2 * index + 2;
}
// 返回區間[queryL,queryR]的值
public E query(int queryL, int queryR) {
if (queryL < 0 || queryL >= data.length || queryR < 0 || queryR >= data.length || queryL > queryR)
try {
throw new Exception("[queryL,queryR]區間錯誤");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return query(0, 0, data.length - 1, queryL, queryR);
}
// 在以treeIndex爲根節點的線段樹中[l,r]的範圍內,搜索[queryL,queryR]的值
// 遞歸函數
private E query(int treeIndex, int l, int r, int queryL, int queryR) {
// TODO Auto-generated method stub
if (l == queryL && r == queryR)
return tree[treeIndex];
int mid = l + (r - l) / 2;
int leftTreeIndex = leftChild(treeIndex);
int rightTreeIndex = rightChild(treeIndex);
// [queryL,queryR]完全在左子樹區間或右子樹區間
if (queryL >= mid + 1)// 完全在右子樹查詢
return query(rightTreeIndex, mid + 1, r, queryL, queryR);
else if (queryR <= mid)// 完全在左子樹查詢
return query(leftTreeIndex, l, mid, queryL, queryR);
// [queryL,queryR]不完全在左子樹區間或右子樹區間
E leftResult = query(leftTreeIndex, l, mid, queryL, mid);// 在左邊區間查詢
E rightResult = query(rightTreeIndex, mid + 1, r, mid + 1, queryR);// 在右邊區間查詢
return merger.meger(leftResult, rightResult);// 融合返回
}
// 將index位置的值,更新爲e
public void set(int index, E e) {
if (index < 0 || index >= data.length)
try {
throw new Exception("index越界");
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
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; // 當線段樹子樹長度爲1,也就是它本身
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 // 即 index<=mid 在左子樹進行修改
set(leftTreeIndex, l, mid, index, e);
// 由於index節點進行了修改,index的祖輩節點也應該被修改,即重新調用融合器
tree[treeIndex] = merger.meger(tree[leftTreeIndex], tree[rightTreeIndex]);
}
public String toString() {
StringBuilder res = new StringBuilder();
res.append('[');
for (int i = 0; i < tree.length; i++) {
if (tree[i] != null)
res.append(tree[i]);
else
res.append("null");
if (i != tree.length - 1)
res.append(',');
}
res.append(']');
return res.toString();
}
}
Merger.java(融合器接口)
//融合器接口
public interface Merger<E> {
E meger(E a,E b);//通過一個meger操作把a和b兩個元素轉換成一個元素返回
}
Main.java(測試)
public class Main {
public static void main(String[] args) {
Integer[] nums = { -2, 0, 3, -5, 2, -1 };
//匿名函數
// SegmentTree<Integer> segTree = new SegmentTree<>(nums, new Merger<Integer>()
// {
//
// @Override
// public Integer meger(Integer a, Integer b) {
// // TODO Auto-generated method stub
// return a+b;
// }
// });
SegmentTree<Integer> segTree = new SegmentTree<>(nums, (a, b) -> a + b);//傳入a和b,返回a+b
System.out.println(segTree);//輸出線段樹
System.out.println(segTree.query(0, 2));//輸出線段樹[0-2]區間的和
segTree.set(0, 6);//將data[0]修改爲6
System.out.println(segTree.query(0,2));//輸出線段樹[0-2]區間的和
}
}
測試結果
總結
線段樹查詢與更新操作時間複雜度爲O(logn)
利用線段樹,我們可以高效地詢問和修改一個數列中某個區間的信息,並且代碼也不算特別複雜。
但是線段樹也是有一定的侷限性的,其中最明顯的就是數列中數的個數必須固定,即不能添加或刪除數列中的數。