1. 程式人生 > >資料結構: 線段樹

資料結構: 線段樹

線段樹

為什麼使用線段樹

  • 區間染色
  • 有一面牆,長度為n,每次選擇一段牆進行染色
  • M次操作後,我們可以可見多少種顏色
  • M次操作後,在[i,j]區間能看見多少種顏色

涉及的操作

  • 染色操作 (更新區間)
  • 查詢操作 (查詢區間)
陣列 線段樹
染色操作 O(N) O(logN)
查詢操作 O(N) O(logN)

什麼是線段樹

  • 以求和為例,每個節點就是儲存的每個區間的和

  • 線段樹是平衡二叉樹,但不一定是滿二叉樹

  • 堆也是平衡二叉樹

  • 可以把線段樹看做滿二叉樹 : 將沒有區間的看做[]

  • 若區間有n個元素,陣列需要 4n 的空間節點

  • 使用陣列實現:

    我們的線段樹不考慮新增元素,即區間固定,使用4n的靜態空間即可

線段樹的基本操作

線段樹的基本操作主要包括構造線段樹,區間查詢和區間修改

1. 構造線段樹

構造線段樹是一個遞迴的過程

C++實現:

// 返回完全二叉樹陣列表示中,一個索引表示的節點的左孩子的索引
int leftChild(int index) {
    return 2 * index + 1;
}
// 返回完全二叉樹陣列表示中,一個索引表示的節點的右孩子的索引
int rightChild(int index) {
    return 2 * index + 2;
}

// 在treeIdex位置建立區間表示[l...r]的線段樹
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; // (l+r)/2 會有溢位問題
    buildSegmentTree(leftTreeIndex, l, mid);
    buildSegmentTree(rightTreeIndex, mid + 1, r);

    // 給tree[]賦值,與業務相關,以求和為例
    tree[treeIndex] = tree[leftTreeIndex] + tree[rightTreeIndex];
}

2.區間查詢

區間查詢指的是使用者指定一個區間,獲得這個區間的相關資訊,如區間的最大值,最小值,和等.

查詢的C++程式碼如下:

  • 查詢一下是否能夠找到對應的區間
  • 若沒有向下繼續遍歷
// 線段樹查詢
T query(int queryL, int queryR) {
    if (queryR >= 0 && queryR < data.size() 
        && queryL >= 0 && queryL < data.size()
        && queryR> queryL) {
        return query(0,0,data.size()-1,queryL,queryR);
    }
}
T query(int treeIndex, int l, int r, int queryL, int queryR) {
    if (l == queryL&&r == queryR) {
        return tree[treeIndex];
    }

    int leftTreeIndex = leftChild(treeIndex);
    int rightTreeIndex = rightChild(treeIndex);

    int mid = l + (r - l) / 2;

    if (queryL > mid) {
        return query(rightTreeIndex, mid + 1, r, queryL, queryR);
    } else if (queryR <= mid) {
        return query(leftTreeIndex, l, mid, queryL, queryR);
    }
    // [queryL..mid] + [mid+1,queryR]
    T leftRes = query(leftTreeIndex, l, mid, queryL, mid);
    T rightRes = query(rightTreeIndex, mid + 1, r, mid + 1, queryR);
    return leftRes + rightRes;
}

3.區間更新

3.1 更新某個葉節點:

// 將index位置的值,更新為e
void set(int index, T val) {
    if (index >= 0 && index < data.size()) {
        data[index] = e;
        // 更新線段樹,葉子節點
        set(0, 0, data.size() - 1, index, val);
    }
}
void set(int treeIndex, int l, int r, int index, T val) {
    if (l == r) {
        tree[treeIndex] = val;
        return;
    }
    // 去找線段樹中葉子節點位置的索引在哪裡
    int leftTreeIndex = leftChild(treeIndex);
    int rightTreeIndex = rightChild(treeIndex);

    int mid = l + (r - l) / 2;

    if (index <= mid) {
        set(leftTreeIndex, l, mid, index, val);
    } else {
        set(rightTreeIndex, mid + 1, r, index, val);
    }
    // 更新過程
    tree[treeIndex] = tree[leftTreeIndex] + tree[rightTreeIndex];
}

3.2 更新某個區間:

採用懶惰更新,延遲標記

待續

LeetCode 307

class NumArray {
    public:

    vector<int> data;
    vector<int> tree;
    int size;

    // 1. 構建線段樹
    void buildSegmentTree(int treeIdx, int l, int r) {
        if (l == r) {
            tree[treeIdx] = data[l];
            return;
        }
        int mid = l + (r - l) / 2;
        int leftChildTreeIdx = treeIdx * 2+1;
        int rightChildTreeIdx = treeIdx * 2 + 2;
        buildSegmentTree(leftChildTreeIdx, l, mid);
        buildSegmentTree(rightChildTreeIdx, mid + 1, r);
        tree[treeIdx] = tree[leftChildTreeIdx] + tree[rightChildTreeIdx];
    }

    // 2.查詢線段樹
    int query(int treeIdx, int l, int r, int queryL, int queryR) {
        if (l == queryL&&r == queryR) {
            return tree[treeIdx];
        }
        int mid = l + (r - l) / 2;
        int leftChildTreeIdx = treeIdx * 2+1;
        int rightChildTreeIdx = treeIdx * 2 + 2;

        if (queryR <= mid) {
            return query(leftChildTreeIdx, l, mid, queryL, queryR);
        }
        else if (queryL > mid) {
            return query(rightChildTreeIdx, mid+1, r, queryL, queryR);
        }
        int leftResult = query(leftChildTreeIdx, l, mid, queryL, mid);
        int rightResult = query(rightChildTreeIdx, mid + 1, r, mid+1, queryR);
        return leftResult + rightResult;
    }

    int query(int queryL, int queryR) {
        if (queryL >= 0 && queryL < size
            && queryR >= 0 && queryR < size
            && queryR >= queryL) {
            return query(0, 0, size - 1, queryL, queryR);
        }
        return 0;
    }

    // 3.設定某個單個節點的值
    void set(int treeIdx, int l, int r, int index, int val) {
        if (l == r) {
            tree[treeIdx] = val;
            return;
        }
        int mid = l + (r - l) / 2;
        int leftChildTreeIdx = treeIdx * 2+1;
        int rightChildTreeIdx = treeIdx * 2 + 2;

        if (index <= mid) {
            set(leftChildTreeIdx, l, mid, index, val);
        }
        else {
            set(rightChildTreeIdx, mid + 1,r, index, val);
        }
        tree[treeIdx] = tree[leftChildTreeIdx] + tree[rightChildTreeIdx];
    }

    void set(int index, int val) {
        if (index >= 0 && index <size) {
            data[index] = val;
            set(0, 0, size - 1, index, val);
        }
    }

    // 下面是題目的:
    NumArray(vector<int> nums) {
        // 1. 先拷貝整個陣列
        if (nums.size() == 0) {
            return;
        }
        size = nums.size();
        data = vector<int>(nums.begin(), nums.end());
        // 2. 建立這個線段樹
        tree.resize(4 * size);
        buildSegmentTree(0, 0, size - 1);
    }

    void update(int i, int val) {
        set(i, val);
    }

    int sumRange(int i, int j) {
        return query(i, j);
    }
};