【數據結構】之線段樹(區間樹)

1、引言

線段樹又名區間樹(segment tree),線段樹也是一種樹結構,該數據結構主要解決區間計算問題,葉子節點保存不可分割的最小區間的值,而非葉子節點保存的是當前區間的狀態,例如求區間最大值、最小值、求和等等;線段樹不是徹底二叉樹,但線段樹是一顆平衡二叉樹。segmentfault

一、什麼是線段樹

  • 假設有以下數組

數組.png

  • 用線段樹表示以上數組

線段樹.png

說明:以上線段樹構建於具備8個元素的數組,目的在於求任意區間的元素之和。根節點保存的是整個數組區間的元素之和,其左節點表示[0,3]子區間的元素之和,其右節點表示[4,7]子區間的元素之和,依次類推,直到葉子節點表示的是單個元素的值。這個示例方便與計算區間之和,計算區間的值不須要遍歷整個數組,沿着根節點往葉子節點方向尋找合適的區間。時間複雜度在O(logn)級別。數組

2、實現

一、基於靜態數組實現線段樹

  • 線段樹融合接口
/**
 * 融合接口
 * @param <E>
 */
public interface Merger<E> {

    /**
     * 融合操做
     * @param a 元素
     * @param b 元素
     * @return
     */
    E merge(E a, E b);

}
  • 線段樹實現
/**
 * 線段樹,基於靜態數組實現
 * @param <E>
 */
public class SegmentTree<E> {

    /**
     * 線段樹容器,靜態數組
     */
    private E[] tree;
    private E[] data;

    /**
     * 融合器,處理元素融合邏輯
     */
    private Merger<E> merger;

    public SegmentTree(E[] arr,Merger<E> merger) {
        data = (E[]) new Object[arr.length];
        this.merger = merger;
        for (int i = 0; i < arr.length; i++) {
            data[i] = arr[i];
        }

        //初始化靜態數組容器爲4n,足夠容納線段樹結構
        tree = (E[]) new Object[4 * arr.length];

        //構建線段樹
        buildSegmentTree(0, 0, arr.length - 1);
    }

    /**
     * 在treeIndex的位置建立表示區間[l,r]的線段樹
     * @param treeIndex 靜態數組的索引位置
     * @param l 左邊界
     * @param r 右邊界
     */
    private void buildSegmentTree(int treeIndex, int l, int r) {

        //葉子節點,終止遞歸
        if (l == r) {
            tree[treeIndex] = data[l];
            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] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
    }

    public int getSize() {
        return data.length;
    }

    public E get(int index) {
        if (index < 0 || index >= data.length) {
            throw new IllegalArgumentException("index is illegal");
        }
        return data[index];
    }

    /**
     * 返回徹底二叉樹的數組表示中,一個索引所表示的元素的左孩子節點的索引
     * @param index 當前元素索引
     * @return
     */
    private int leftChild(int index) {
        return 2 * index + 1;
    }

    /**
     * 返回徹底二叉樹的數組表示中,一個索引所表示的元素的右孩子節點的索引
     * @param index 當前元素索引
     * @return
     */
    private int rightChild(int index) {
        return 2 * index + 2;
    }

    /**
     * 返回區間[queryL,queryR]的值
     * @param queryL 區間左邊界
     * @param queryR 區間右邊界
     * @return
     */
    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);
    }

    /**
     * 在以treeIndex爲根的線段樹[l,r]的範圍裏,搜索區間[queryL,queryR]的值
     * @param treeIndex
     * @param l
     * @param r
     * @param queryL
     * @param queryR
     * @return
     */
    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
     * @param index
     * @param 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
     * @param treeIndex
     * @param l
     * @param r
     * @param index
     * @param 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]);

    }

    @Override
    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();
    }
}

3、複雜度分析

一、基於靜態數組的線段樹實現

  • 建立線段樹:根據靜態數組建立線段樹,額外用了4n的靜態數組空間,而後遞歸構建線段樹填充4n空間的靜態數組,時間複雜度是O(4n)。
  • 更新線段樹:更新線段樹某個元素值,須要遞歸到葉子節點修改葉子節點的值,而後還須要從新計算各個父節點的值。時間複雜度依然之和樹的深度相關,爲O(logn)。
  • 查詢線段樹:線段樹查詢合適的區間,是從根節點出發,往葉子節點方向遞歸,時間複雜度依然之和樹的深度相關,爲O(logn)。

4、其它數據結構

相關文章
相關標籤/搜索