日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

如何在vs中创建r树索引代码_线段树详解与实现

發布時間:2025/3/15 编程问答 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 如何在vs中创建r树索引代码_线段树详解与实现 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

此篇文章用于記錄《玩轉數據結構》課程的學習筆記

什么是線段樹

線段樹也被稱為區間樹,英文名為Segment Tree或者Interval tree,是一種高級的數據結構。這種數據結構更多出現在競賽中,在常見的本科數據結構教材里沒有介紹這種數據結構。但是,在面試中卻有可能碰到和線段樹相關的問題。那么為什么會產生線段樹這種數據結構,線段樹到底是為了解決什么樣的一種問題呢?

其實這里的線段可以理解為區間,線段樹就是為了解決區間問題的。

有一個很經典的線段樹問題是:區間染色。

假設有一面墻,長度為 n,每次選擇一段墻進行染色。

在區間染色的過程中,每次選擇一段區間進行染色,這時新的顏色可能會覆蓋之前的顏色。

最后的問題是:

  • 在經過 m 次染色操作后,我們可以在整個區間看見多少種顏色?

更加普遍的說法是:

  • 在經過 m 次染色操作后,我們可以在區間 [i, j]內看見多少種顏色?

由于第一個問題是第二個問題的一個特例,我們采用第二種問題來思考解決方法。

從上面可以看出,我們對于區間,有 2 種操作,分別是染色操作查詢區間的顏色,使用更加一般的說法,染色操作就是更新區間查詢區間的顏色就是查詢區間

這類問題里面,更加常見的的是區間查詢:一個數組存放的不再是顏色,而是具體的數字,查詢某個區間[i, j]的統計值。這里的統計值是指:區間內最大值、最小值、或者這個區間的數字和。

比如:

  • 查詢 2018 年注冊的用戶中消費最高的用戶
  • 查詢 2018 年注冊的用戶中消費最低的用戶

注意上面兩種情況都是動態查詢,我們查詢的消費數據不只是 2018 的消費數據

如果我們想查詢 2018 年中消費最高的用戶,那么 2018 年的數據已經固定了,我們直接在這一年的數據中進行統計分析就行了。

但是一個 2018 年注冊的用戶,在 2019 年、2020 年都可能會有消費。我們實際上查詢的是:2018 年注冊的用戶中,到現在為止,消費最高的用戶。

這種情況下,數據是在動態變化的, 也就是說:2017 年注冊的用戶中,每個用戶的消費額是會更新的,這就對應到更新區間的操作。

此時線段樹就是一種好的選擇。

按照通常的思路,使用數組存儲上述的元素是比較好的,思考上面兩個操作的時間復雜度:

  • 更新區間:每次根據需要更新的區間的首尾索引,逐個遍歷區間中的元素進行更新,時間復雜度為O(n)。
  • 查詢區間:每次根據需要更新的區間的首尾索引,逐個遍歷區間種的元素進行查詢,時間復雜度為O(n)。

兩個操作的時間復雜度均為O(n),對于需要多次動態使用的場景來說,性能可能是不夠好的。

在這類問題中,我們關注的是一個個區間內的元素的情況,線段樹就有用武之地了,線段樹的優點就是把兩個操作的時間復雜度降到了O(logn)

操作使用數組使用線段樹
更新O(n)O(logn)
查詢O(n)O(logn)

這里提一點,如果你看到一個算法的時間復雜度是O(logn),那這個算法大多與二叉樹分治算法有關。

這里也不例外,線段樹就是使用二叉樹來實現的。

那么一個區間是如何被構建成為一個二叉樹的?

對于一個數組 A,如下所示:

對應的線段樹就是:

二叉樹中每個非葉子節點表示的是區間內元素的統計值葉子節點存儲的就是元素本身。上面說了統計值是指:區間內最大值、最小值、或者這個區間的數字和。比如你要求區間的最大值,每個每個節點存儲的就是這個區間內元素的最大值。像下面這樣:

假設你要查詢[4,7]區間內的最大值,那么不用查到葉子節點,而是查到A[4, 7]這個節點就行了。

當然,并不是所有的區間都恰好落在一個節點,比如你要求[2, 5]區間內的最大值。那么就要分別找到A[2, 3]和A[4,5]的最大值,再進行比較。

可以看出,線段樹的查詢區間操作不需要遍歷區間中的每一個元素,只要找到對應的樹節點就可以返回,時間復雜度為O(logn)。

總結

從更加抽象的角度來講,線段樹的使用場景就是,對于給定區間,進行更新區間和查詢區間操作:

  • 更新區間:更新區間中的一個元素或者一個區間的值。
  • 查詢區間:查詢一個區間[i, j]的最大值、最小值、或者區間的數字和。

注意,在大多數情況下,我們是不考慮區間里添加元素和刪除元素的,我們假設區間的大小是固定的。

線段樹的表示

樹的一般表示方法是鏈式存儲,每個節點有兩個指針,一個指向左孩子,一個指向右孩子。但是滿二叉樹,和完全二叉樹,除了使用鏈表法來存儲,還可以使用數組來表示。

在滿二叉樹和完全二叉樹的數組表示中,假設一個節點的所以是i,那么左孩子的索引就是,右孩子的索引就是。

那么線段樹是不是滿二叉樹或者完全二叉樹呢? 能不能使用數組來表示呢?

在上面的例子中,我們的二叉樹恰好是一棵滿二叉樹,這是因為我們的數組大小恰好是 8,也就是,只有數組大小恰好是 2 的 n 次冪,所對應的線段樹才會是一個滿二叉樹。在大部分情況下,線段樹并不是一個滿二叉樹。如果一個數組的大小是 10 ,對應的線段樹如下圖所示。

所以線段樹不是滿二叉樹,也不是完全二叉樹。但實際上:線段樹是平衡二叉樹,是可以保證O(logn)的時間復雜度的,這里就不證明了。

其實平衡二叉樹可以看作特殊的滿二叉樹,進而使用數組來表示。

下一步就是確定:對于大小為 n 的數組,需要多大的空間來存儲線段樹。

首先說一個結論,對于高為 h 層的滿二叉樹,一共有個節點,而最后一層有個節點。那么:。也就是:前面所有層的節點數之和等于最后一層的節點數減 1。

那么線段樹所需要的節點數量,分兩種情況來討論:

  • 如果 n 恰好是 2 的 k 次冪,由于線段樹最后一層的葉子節點存儲的是數組元素本身,最后一層的節點數就是 n,而根據上面的結論,前面所有層的節點數之和是,那么總節點數就是。為了方便起見,分配的空間。

  • 如果 ?n 不是 2 的 k 次冪,最壞的情況就是,那么有一個元素需要開辟新的一層來存儲,需要的大小。為了方便起見,我們分配的空間,已經足夠了。

綜上,首先需要判斷數組的大小是否為 ,是則使用 的空間,否則使用的 空間。下面線段樹的實現是基于數組來實現的,不過為了簡便起見,下面的實現統一使用 空間來存儲線段樹。

使用數組來存儲線段樹,會有一定的空間浪費,但是換來的時間復雜度的降低是可以接受的。同時,我也在最后會介紹鏈式存儲的實現方式。

線段樹的實現

首先需要兩個數組,其中data存放原來的數據,tree就是存放線段樹。

基本的 API ?有getSize():返回數組元素個數;get(int index):根據索引獲取數據。

其中每個元素使用泛型E表示,這是為了考慮可擴展性:如果你的數組元素不是數字,而是自定義的類,那么使用泛型就是比較好的選擇。

public class SegmentTree { private E[] tree; //線段樹 private E[] data; //數據 public SegmentTree(E[] arr) { data = (E[]) new Object[arr.length]; tree = (E[]) new Object[arr.length * 4]; //大小為 4 * n for (int i = 0; i < arr.length; i++) { data[i] = arr[i]; } } // 返回數組元素個數 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]; }}

由于把線段樹看作一棵完全二叉樹,應該定義兩個 API,根據一個節點獲取到它的左孩子和右孩子。

// 根據一個節點的索引 index,返回這個節點的左孩子的索引private int leftChild(int index) { return 2 * index + 1;}// 根據一個節點的索引 index,返回這個節點的右孩子的索引private int rightChild(int index) { return 2 * index + 2;}

線段樹的構建

下面考慮的就是構造線段樹的每個節點。這里以求區間的最大值為例。

根節點存儲的是整個[0,7]區間的最大值,左孩子存儲的是[0,3]區間內的最大值,右孩子存儲的是[4,7]區間內的最大值。

我們要求根節點的值,首先求得左右兩個孩子的值,再從左右兩個孩子中取出較大的值作為根節點的值。要求父節點區間的值,需要先求孩子節點區間的值,這是遞歸的性質。所以可以通過遞歸來創建線段樹。

那么這個遞歸的終止條件,也就是 base case,是什么呢?

  • base case:如果一個節點的區間長度為1,不能再劃分,也就是遞歸到底了,就返回這個元素本身。

明確了思路,那么我們的遞歸函數需要幾個參數?

首先,既然是創建節點,那么需要節點在tree數組中的索引;其次,這個節點對應的區間的左邊界和右邊界。

總共需要 3 個參數,寫出的代碼如下:

// 在 treeIndex 的位置創建表示區間 [l,r] 的線段樹private void buildSegmentTree(int treeIndex, int l, int r) { // base case:遞歸到葉子節點了 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] = Math.max(tree[leftTreeIndex], tree[rightTreeIndex]);}

當然這里最后一句是會報錯的。因為tree的元素類型是泛型,不支持Math.max()函數。

還有一個問題是:如果你在這里把區間合并的邏輯寫成了只取最大值,那么這個線段樹就只能求某個區間的最大值,不能用于求取區間的最小值、或者區間的和,限制了線段樹的應用場景。

一個更好的方法是:用戶可以根據自己的業務場景,自由選擇合并區間的邏輯。

要達成這個目的,我們需要創建一個接口,用戶需要實現這個接口來實現自己的區間合并邏輯。

//融合器,表示如何合并兩個區間的統計值public interface Merger { // a 表示左區間的統計值,b 表示有區間的統計值 //返回整個[左區間+右區間] 的統計值 E merge(E a, E b);}

在線段樹的構造函數中,添加一個Merger參數,并且調用buildSegmentTree()構建線段樹。

public class SegmentTree { private E[] tree; //線段樹 private E[] data; //數據 private Merger merger;//融合器 public SegmentTree(E[] arr, Merger merger) { this.merger = merger; data = (E[]) new Object[arr.length]; tree = (E[]) new Object[arr.length * 4]; //大小為 4 * n for (int i = 0; i < arr.length; i++) { data[i] = arr[i]; } //構建線段樹 buildSegmentTree(0, 0, data.length - 1); } . . .}

然后,修改buildSegmentTree()方法的最后一行。

// 在 treeIndex 的位置創建表示區間 [l,r] 的線段樹private void buildSegmentTree(int treeIndex, int l, int r) { // base case:遞歸到葉子節點了 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]);}

線段樹的查詢

我們用下面的數組構建一棵線段樹,查詢區間[2,5]為例。

  • 我們首先從根節點開始查詢:在區間[0,7]中查詢[2,5]。
  • 將根節點的區間分為左孩子[0,3]和右孩子[4,7],而我們的查詢的區間[2,5],不能其中一個孩子的區間完全包括

    于是上述問題轉化為兩個子問題:

    • 在區間[0,3]中查詢[2,3];
    • 在區間[4,7]中查詢[4,5];
    • 最后將[2,3]和[4,5]結果合并,得到區間[2,5]的結果。

    將[0,3]劃分為左孩子[0,1]和右孩子[2,3],此時孩子剛好和查詢的區間重合,返回結果。

    將[4,7]劃分為左孩子[4,5]和右孩子[6,7],此時孩子剛好和查詢的區間重合,返回結果。

    從上面可以看出,在線段樹的區間查找過程中,不需要遍歷區間中的每個元素,只需要在線段樹中找到對應的所有子區間,再將這些子區間的結果合并即可。而查詢所經過的節點數最多是樹的高度,時間復雜度為O(logn)。

    而且上面的過程也是遞歸的過程。

    • 遞歸終止條件就是:查詢區間的邊界和節點的邊界完全重合,就返回該節點的統計值。

    如果不重合,那怎么辦?

    這時應該分 3 種情況:

    • 如果查詢區間的左邊界大于中間節點,那么就查詢右區間

    • 如果查詢區間的右邊界小于等于中間節點,那么就查詢左區間

    • 如果不屬于上述兩種情況,那么查詢的區間就要根據中間節點拆分

    遞歸函數的參數應該有 4 個,當前節點所在的區間的左邊界和右邊界用戶要查詢的的區間的左邊界和右邊界

    代碼如下:

    //在以 treeIndex 為根的線段樹中 [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) return query(rightTreeIndex, mid + 1, r, queryL, queryR); // 如果右邊界小于等于中間節點,則查詢左區間 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);}

    線段樹的更新

    如下圖所示,更新A[i]=100,那么將需要更新的索引 i和區間的終點mid,分為兩種情況。

    • 如果i>mid,那么索引i落在右區間,更新右區間;
    • 如果i<=mid,那么索引i落在左區間,更新左區間;

    那么遞歸的終止條件是什么呢?

    • 當遞歸到葉子節點的時候,就值更新這個節點:葉子節點就是區間長度為 1 的節點。

    當更新完葉子節點后,還需要回溯,更新父節點區間的統計值。

    代碼如下:

    //將 index 位置的值,更新為 epublic void update(int index, E e) { if (index < 0 || index >= data.length) throw new IllegalArgumentException("Index is illegal"); data[index] = e; //更新線段樹相應的節點 updateTree(0, 0, data.length - 1, index, e);}// 在以 treeIndex 為根的線段樹中,更新 index 的值為 eprivate void updateTree(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) updateTree(rightTreeIndex, mid + 1, r, index, e); else //index <= mid updateTree(leftTreeIndex, l, mid, index, e); //更新當前節點 tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);}

    完整代碼

    public class SegmentTree { private E[] tree; //線段樹 private E[] data; //數據 private Merger merger;//融合器 public SegmentTree(E[] arr, Merger merger) { this.merger = merger; data = (E[]) new Object[arr.length]; tree = (E[]) new Object[arr.length * 4]; //大小為 4 * n for (int i = 0; i < arr.length; i++) { data[i] = arr[i]; } //構建線段樹 buildSegmentTree(0, 0, data.length - 1); } // 返回數組元素個數 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]; } //根據一個節點的索引 index,返回這個節點的左孩子的索引 private int leftChild(int index) { return 2 * index + 1; } //根據一個節點的索引 index,返回這個節點的右孩子的索引 private int rightChild(int index) { return 2 * index + 2; } // 在 treeIndex 的位置創建表示區間 [l,r] 的線段樹 private void buildSegmentTree(int treeIndex, int l, int r) { // base case:遞歸到葉子節點了 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]); } //查詢區間,返回區間 [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); } //在以 treeIndex 為根的線段樹中 [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) return query(rightTreeIndex, mid + 1, r, queryL, queryR); // 如果右邊界小于等于中間節點,則查詢左區間 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 update(int index, E e) { if (index < 0 || index >= data.length) throw new IllegalArgumentException("Index is illegal"); data[index] = e; //更新線段樹相應的節點 updateTree(0, 0, data.length - 1, index, e); } // 在以 treeIndex 為根的線段樹中,更新 index 的值為 e private void updateTree(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) updateTree(rightTreeIndex, mid + 1, r, index, e); else //index <= mid updateTree(leftTreeIndex, l, mid, index, e); //更新當前節點 tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]); } public String toString() { StringBuffer res = new StringBuffer(); 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(); }}

    使用例子

    定義一個求區間的最大值的線段樹,代碼如下:

    public class Main { public static void main(String[] args) { Integer[] nums = new Integer[]{34, 65, 8, 10, 21, 86, 79, 30}; SegmentTree segTree = new SegmentTree<>(nums, new Merger() { @Override public Integer merge(Integer a, Integer b) { //返回 a 和 b 的最大值 return Math.max(a, b); } }); // 查詢區間 [2,5] 的最大值 System.out.println(segTree.query(4, 7)); }}

    當然,你也可以定義一個求區間內元素的和的線段樹,只需要修改merge()方法的實現即可:

    public class Main { public static void main(String[] args) { Integer[] nums = new Integer[]{34, 65, 8, 10, 21, 86, 79, 30}; SegmentTree segTree = new SegmentTree<>(nums, new Merger() { @Override public Integer merge(Integer a, Integer b) { //返回 a 和 b 的和 return a + b; } }); // 查詢區間 [2,5] 的和 System.out.println(segTree.query(4, 7)); }}

    LeetCode 上相關的題目

    303. 區域檢索和-數組不可變

    題目鏈接:303. 區域和檢索 - 數組不可變

    線段樹求解

    這道題是求取區間和,可以使用線段樹來實現。時間復雜度為O(logn),空間復雜度為O(n)。

    class NumArray { private int[] tree; private int[] data; public NumArray(int[] nums) { data = nums; tree = new int[nums.length * 4]; //當數組長度大于 0 時,才創建線段樹 if (nums.length > 0) //創建線段樹 buildSegmentTree(0, 0, nums.length - 1); } //根據一個節點的索引 index,返回這個節點的左孩子的索引 private int leftChild(int index) { return 2 * index + 1; } //根據一個節點的索引 index,返回這個節點的右孩子的索引 private int rightChild(int index) { return 2 * index + 2; } // 在 treeIndex 的位置創建表示區間 [l,r] 的線段樹 private void buildSegmentTree(int treeIndex, int l, int r) { //遞歸終止條件:區間長度為 1 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] = tree[leftTreeIndex] + tree[rightTreeIndex]; } public int sumRange(int i, int j) { //tree.length == 1 表示數組沒有元素,直接返回 0 if (tree.length == 1) return 0; return queryRange(0, 0, data.length - 1, i, j); } //在以 treeIndex 為根的線段樹中 [l,r] 的范圍里,搜索區間 [queryL, queryR] private int queryRange(int treeIndex, int l, int r, int queryL, int queryR) { if (queryL == l && queryR == r) return tree[treeIndex]; int mid = l + (r - l) / 2; int leftTreeIndex = leftChild(treeIndex); int rightTreeIndex = rightChild(treeIndex); // 如果左邊界大于中間節點,則查詢右區間 if (queryL > mid) return queryRange(rightTreeIndex, mid + 1, r, queryL, queryR); // 如果右邊界小于等于中間節點,則查詢左區間 if (queryR <= mid) return queryRange(leftTreeIndex, l, mid, queryL, queryR); // 如果上述兩種情況都不是,則根據中間節點,拆分為兩個查詢區間 int leftResult = queryRange(leftTreeIndex, l, mid, queryL, mid); int rightResult = queryRange(rightTreeIndex, mid + 1, r, mid + 1, queryR); //合并左右區間的查詢結果 return leftResult + rightResult; }}

    前綴和求解

    其實這道題有更加高效的解法,那就是前綴和

    前綴和的定義是:定義一個前綴和數組sum,每個元素sum[i]表示的是nums[0...i]區間中的元素的和。

    那么我們要求[i,j]區間的和,就可以使用sum[j]-sum[i-1]得到。

    注意當i=0時,i-1=-1會溢出。因此sums數組應該整體向后移動一位。

    sum[0]=0表示前面沒有元素,和應該是 0。

    此時[i,j]區間的和應該是sum[j+1]-sum[i]。

    代碼如下:

    class NumArray { //前綴和數組 private int[] sums; public NumArray(int[] nums) { //邊界條件判斷 if (nums == null || nums.length == 0) { sums = new int[]{}; } int n = nums.length; //由于整體后移了一位,長度應該為 n+1 sums = new int[n + 1]; //構建前綴和 for (int i = 0; i < n; i++) { sums[i + 1] = sums[i] + nums[i]; } } public int sumRange(int i, int j) { if (sums.length == 0) return 0; //直接返回前綴和相減的結果 return sums[j + 1] - sums[i]; }}

    使用前綴和數組的空間復雜度依然是O(n),但時間復雜度是O(1),優于線段樹。

    那既然這樣,區間問題為什么還要用線段樹呢?

    因為這道題目加了一個限制:數組不可變,也就是說數組里的元素是固定的。

    如果數組的內容是可變的,那么每次更新索引[i]的數據,相應的[i...n]區間的前綴和都需要更新。前綴和數組更新的時間時間復雜度是O(n),而線段樹的更新復雜度是O(logn)。

    因此,在數組內容可變的情況下,線段樹依然是更優的選擇。

    303. 區域檢索和-數組可修改

    題目鏈接:307. 區域和檢索 - 數組可修改

    前綴和求解

    根據上面前綴和的做法,我們只需要添加更新數據和對應的前綴和的邏輯即可。

    class NumArray { int[] sums; int[] data; public NumArray(int[] nums) { //邊界條件判斷 if (nums == null || nums.length == 0) { sums = new int[]{}; } data = nums; int n = nums.length; //由于整體后移了一位,長度應該為 n+1 sums = new int[n + 1]; //構建前綴和 for (int i = 0; i < n; i++) { sums[i + 1] = sums[i] + nums[i]; } } public void update(int i, int val) { // 更新數組 data[i] = val; //更新從 i 到 n 的前綴和 for (int j = i; j < data.length; j++) { sums[j + 1] = sums[j] + data[j]; } } public int sumRange(int i, int j) { if (sums.length == 0) return 0; //直接返回前綴和相減的結果 return sums[j + 1] - sums[i]; }}

    update()方法的時間復雜度是O(n),sumRange()方法的時間復雜度是O(1)。

    線段樹求解

    同理,這里只需要在上面 303. 區域和檢索 - 數組不可變 的線段樹解法上,添加更新的線段樹的邏輯即可。

    class NumArray { int[] tree; int[] data; public NumArray(int[] nums) { data = nums; int n = nums.length; if (nums == null || nums.length == 0) { tree = new int[]{}; return; } tree = new int[n * 4]; buildSegmentTree(0, 0, data.length - 1); } private void buildSegmentTree(int treeIndex, int l, int r) { // base case:遞歸到葉子節點了 if (l == r) { tree[treeIndex] = data[l]; return; } //劃分區間 int mid = l + (r - l) / 2; int leftTreeIndex = leftChild(treeIndex); int rightTreeIndex = rightChild(treeIndex); // 求(左孩子)左區間的統計值 buildSegmentTree(leftTreeIndex, l, mid); // 求(右孩子)右區間的統計值 buildSegmentTree(rightTreeIndex, mid + 1, r); //求當前節點 [左區間+右區間] 的統計值 tree[treeIndex] = tree[leftTreeIndex] + tree[rightTreeIndex]; } private int leftChild(int treeIndex) { return 2 * treeIndex + 1; } private int rightChild(int treeIndex) { return 2 * treeIndex + 2; } //將 index 位置的值,更新為 e public void update(int i, int val) { // 更新數組 data[i] = val; //更新線段樹 updateTree(0, i, 0, data.length - 1); } // 在以 treeIndex 為根的線段樹中,更新 index 的值為 e private void updateTree(int treeIndex, int index, int l, int r) { //遞歸終止條件 if (l == r) { tree[treeIndex] = data[l]; return; } int mid = l + (r - l) / 2; int leftTreeIndex = leftChild(treeIndex); int rightTreeIndex = rightChild(treeIndex); if (index <= mid) updateTree(leftTreeIndex, index, l, mid); else //index <= mid updateTree(rightTreeIndex, index, mid + 1, r); //更新當前節點 tree[treeIndex] = tree[leftTreeIndex] + tree[rightTreeIndex]; } public int sumRange(int i, int j) { if (data.length == 0) return 0; return queryRange(0, 0, data.length - 1, i, j); } private int queryRange(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) return queryRange(rightTreeIndex, mid + 1, r, queryL, queryR); if (queryR <= mid) return queryRange(leftTreeIndex, l, mid, queryL, queryR); int leftResult = queryRange(leftTreeIndex, l, mid, queryL, mid); int rightResult = queryRange(rightTreeIndex, mid + 1, r, mid + 1, queryR); return leftResult + rightResult; }}

    update()方法和sumRange()方法的時間復雜度都是O(n)。

    總結與擴展

    線段樹,雖然不是滿二叉樹。但是我們卻可以把它看作一棵滿二叉樹,進而使用數組來存儲這棵樹。

    其次,在線段樹中,我們定義了每個節點,存儲的數據是這個節點對應區間的統計值(最大值、最小值、區間和)。通過這一點,你可以體會到,對于樹中的節點,你可以賦予它獨特的定義,進而可以高效地解決各種各樣的問題。因此,樹的使用范圍是非常廣泛的。

    對于,線段樹的構建、更新區間、和查詢區間,這 3 個操作,都是先遞歸訪問葉子節點,然后再回溯訪問父節點,合并左右孩子區間的結果,這本質上是一種后序遍歷的思想。

    對一個區間進行更新

    在本文的例子中,每次更新都是對一個元素進行更新。現在考慮另一種更新:對某個區間內的所有元素進行更新。

    比如:將[2,5]區間中的所有元素都加 3,就需要遍歷這個區間中的每個元素,區間更新的時間復雜度就變為了O(n)。為了降低區間更新的復雜度,有一種專門的方法:懶惰更新

    懶惰更新的思想是:在每次更新區間時,我們實際上先不更新實際的數據,而是使用另一個lazy數組,來標記這些未更新的內容。

    那么什么時候才會更新這些節點呢?

    當我們下一次更新或者查詢到這些數據時,先查一下lazy數組中是否有數據未更新,然后將未更新的內容進行更新,再訪問對應的數據。

    例如:

    • 第一次:將[2,5]區間中的所有元素都加 3,實際上lazy數組就會標記[2,5]區間的數據未更新;
    • 第二次:將[4,7]區間中的所有元素都減 5。這時,查詢lazy數組中,發現[4,5]區間的數據未更新,那么先更新[4,5]區間的內容,[2,3]區間的標記不變。然后標記[4,7]區間中的內容未更新。

    通過懶惰更新,時間復雜度降為了O(logn)。

    二維線段樹

    在這篇文章中,我們處理的都是一維的線段樹,實際中還可以產生二位線段樹。

    一維線段樹,就是數據都是一維數組,每個節點記錄的區間只有左右兩個邊界。

    在二維線段樹中,數據是二維數組,也就是一個矩陣,每個節點記錄的區間有上下左右兩個邊界。那么每個節點就有 4 個孩子。

    進一步擴展,你也可以設計出 3 維的線段樹,甚至更高維的線段樹。

    線段樹是一種設計思想,利用樹這種數據結構,如何把一個大的數據單元,遞歸地拆分為小的數據單元,同時,利用樹這種數據結構,可以高效地進行查詢、更新等操作。

    動態線段樹

    在這篇文章中,我使用了數組這種數據結構來存儲樹,造成了空間的浪費。實際上,我們可以也使用鏈式存儲,可以更好地利用空間。

    實際上,動態線段樹有一個更加重要的應用,。例如,我們要存儲 10000000 大小的線段樹,但是不會對這么大一個區間中的每一個部分都進行訪問,可能只會對一種某個小部分進行訪問。

    那么我們可以不用一開始就創建這么巨大的線段樹,而是只創建一個根節點,等到實際訪問某個區間時,在根據區間的邊界,動態地創建線段樹。

    樹狀數組

    這篇文章講的線段樹,實際上是對區間操作的一種數據結構。與區間操作相關的數據結構,還有另外一個:樹狀數組,英文名為Binary Index Tree。感興趣可以自行查閱。

    總結

    以上是生活随笔為你收集整理的如何在vs中创建r树索引代码_线段树详解与实现的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。