【從蛋殼到滿天飛】JS 數據結構解析和算法實現-線段樹

思惟導圖

前言

【從蛋殼到滿天飛】JS 數據結構解析和算法實現,所有文章大概的內容以下: Arrays(數組)、Stacks(棧)、Queues(隊列)、LinkedList(鏈表)、Recursion(遞歸思想)、BinarySearchTree(二分搜索樹)、Set(集合)、Map(映射)、Heap(堆)、PriorityQueue(優先隊列)、SegmentTree(線段樹)、Trie(字典樹)、UnionFind(並查集)、AVLTree(AVL 平衡樹)、RedBlackTree(紅黑平衡樹)、HashTable(哈希表)html

源代碼有三個:ES6(單個單個的 class 類型的 js 文件) | JS + HTML(一個 js 配合一個 html)| JAVA (一個一個的工程)git

所有源代碼已上傳 github,點擊我吧,光看文章可以掌握兩成,動手敲代碼、動腦思考、畫圖才能夠掌握八成。github

本文章適合 對數據結構想了解而且感興趣的人羣,文章風格一如既往如此,就以爲手機上看起來比較方便,這樣顯得比較有條理,整理這些筆記加源碼,時間跨度也算將近半年時間了,但願對想學習數據結構的人或者正在學習數據結構的人羣有幫助。面試

線段樹(區間樹)

  1. 堆(Heap)是一種樹結構
  2. 線段樹(Segment Tree)也是一種樹結構
    1. 也叫作區間樹(Interval Tree)

線段樹簡介

爲何使用線段樹

  1. 爲何使用線段樹,線段樹解決什麼樣的特殊問題
    1. 對於有一類的問題,只須要關心的是一個線段(或者區間),
    2. 有一道競賽的題目,也是最經典的線段樹問題:區間染色,
    3. 它是一個很是好的應用線段樹的場景。
    4. 有一面牆,長度爲 n,每次選擇一段兒牆進行染色,
    5. m 次操做後,能夠看到多少種顏色?
    6. m 次操做後,能夠在[i,j]區間內看到多少種顏色?
    7. 染色操做(更新區間)、查詢操做(查詢區間)。
  2. 徹底可使用數組實現來解決這個問題
    1. 若是你要對某一段區間進行染色,
    2. 那麼就遍歷這一段區間,把相應的值修改爲新的元素就 ok 了,
    3. 染色操做(更新區間)相應的複雜度就是O(n)級別的,
    4. 查詢操做(查詢區間)也只須要遍歷一遍這個區間就 ok 了,
    5. 相應的複雜度也是O(n)級別的,
    6. 若是對於一種數據結構,其中某一些操做是O(n)級別的話,
    7. 若是動態的使用這種數據結構,相應的性能頗有多是不夠的,
    8. 在實際的環境中頗有可能須要性能更加好的這樣一種時間複雜度,
    9. 這就是使用數組來實現區間染色問題相應的一個侷限性
  3. 在這樣的問題中主要關注的是區間或一個個的線段,
    1. 因此此時線段樹這樣的數據結構就有用武之地了,
    2. 在平時使用計算機來處理數據的時候,
    3. 有一類很經典的同時也是應用範圍很是廣的的問題,
    4. 就是進行區間查詢,相似統計操做的查詢,
    5. 查詢一個區間[i,j]的最大值、最小值,或者區間數字和。
  4. 實質:基於區間的統計查詢
    1. 問題一:2017 年註冊用戶中到如今爲止消費最高的用戶、消費最少的用戶、
    2. 學習時間最長的用戶?
    3. 2017 年註冊的到如今爲止,這個數據其實還在不斷的變化,
    4. 是一種動態的狀況,此時線段樹就是一個好的選擇。
    5. 問題二:某個太空區間中天體總量?
    6. 因爲天體不斷的在運動,總會有一個天體從一個區間來到另一個區間,
    7. 甚至發生爆炸消失之類的物理現象,在某個區間中或某幾個區間中
    8. 都多了一些天體,會存在這樣的現象的,因此就須要使用線段樹了。
  5. 對於這面牆有一個不停的對一個區間進行染色這樣的一個操做,
    1. 就是更新這個操做,於此同時基於整個數據不時在不停的在更新,
    2. 還須要進行查詢這樣的兩個操做,同理其實對於這些問題,
    3. 可使用數組來實現,不過它的複雜度都是O(n)級別的,
    4. 可是若是使用線段樹的話,那麼在區間類的統計查詢這一類的問題上,
    5. 更新和查詢這兩個操做均可以在O(logn)這個複雜度內完成。
  6. 對於線段樹來講它必定也是一種二叉樹的結構
    1. 對於線段樹抽象出來就是解決這樣的一類問題,
    2. 對於一個給定的區間,相應的要支持兩個操做,
    3. 更新:更新區間中一個元素或者一個區間的值,
    4. 查詢:查詢一個區間[i,j]的最大值、最小值、或者區間數字和,
    5. 對於一個區間能夠查詢的內容會不少,要根據實際的業務邏輯進調整,
    6. 不過總體是基於一個區間進行這種統計查詢的。
  7. 如何使用 logn 複雜度去實現這一點
    1. 首先對於給定的數組進行構建,假若有八個元素,
    2. 把它們構建成一棵線段樹,對於線段樹來講,
    3. 不考慮往線段樹中添加元素或者刪除元素的。
    4. 在大多數狀況下線段樹所解決的問題它的區間是固定的,
    5. 好比一面牆進行區間染色,那面牆自己是固定的,
    6. 不去考慮這面牆後面又建起了新的一面牆這種狀況,
    7. 只考慮給定的這一面牆進行染色;
    8. 好比統計 2017 年註冊的用戶,那麼這個區間是固定的;
    9. 或者觀察天體,觀察的外太空所劃分的區間已經固定了,
    10. 只是區間中的元素可能會發生變化;
    11. 因此對於這個數組直接使用靜態數組就行了。
  8. 線段樹也是一棵樹,每個節點表示一個區間內相應的信息
    1. 好比 以線段樹來統計區間內的和爲例,
    2. 在這樣的狀況下,
    3. 線段樹每個節點存儲的就是一段區間的數字和,
    4. 根節點存儲的就是整個區間相應的數字和,
    5. 以後從根節點平均將整個的區間分紅兩段,
    6. 這兩段表明着兩個節點,相應的這兩個節點會再分出兩個區間,
    7. 直至最後每個葉子節點只會存一個元素,
    8. 從區間的角度上來說,每一個元素自己就是一個區間,
    9. 只不過每個區間的長度爲 1 而已,
    10. 也就是對於整棵線段樹來講每個節點存儲的是
    11. 一個區間中相應的統計值,好比使用線段樹求和,
    12. 每一個節點存儲的是一個區間中數字和,
    13. 當你查詢某個區間的話,相應的你只須要找到對應的某個節點便可,
    14. 一步就到了這個節點,並不須要將全部的元素所有都遍歷一遍了,
    15. 可是並非全部的節點每次都知足這樣的條件,
    16. 因此有時候須要到兩個節點,將兩個節點進行相應的結合,
    17. 結合以後就能夠獲得你想要的結果,儘管如此,當數據量很是大的時候,
    18. 依然能夠經過線段樹很是快的找到你所關心的那個區間對應的一個或者多個節點,
    19. 而後在對那些節點的內容進行操做,
    20. 而不須要對那個區間中全部的元素中每個元素相應的進行一次遍歷,
    21. 這一點也是線段樹的優點。

線段樹基礎表示

  1. 線段樹就是二叉樹每個節點存儲的是一個線段(區間)相應的信息
    1. 這個相應的信息不是指把這個區間中全部的元素都存進去,
    2. 好比 以求和操做爲例,
    3. 那麼每個節點相應的存儲的就是這個節點所對應的區間的那個數字和。
  2. 線段樹不必定是滿的二叉樹,
    1. 線段樹也不必定是一棵徹底二叉樹,
    2. 線段樹是一棵平衡二叉樹,
    3. 也就是說對於整棵樹來講,最大的深度和最小的深度,
    4. 它們之間的差最多隻能爲 1,堆也是一棵平衡二叉樹,
    5. 徹底二叉樹自己也是一棵平衡二叉樹,
    6. 線段樹雖然不是一棵徹底二叉樹,可是它知足平衡二叉樹的定義,
    7. 二分搜索樹不必定是一棵平衡二叉樹,
    8. 由於二分搜索樹沒有任何機制可以保證最大深度和最小深度之間差不超過 1。
  3. 平衡二叉樹的優點
    1. 不會像二分搜索樹那樣在最差的狀況下退化爲一個鏈表,
    2. 一棵平衡二叉樹整棵樹的高度和它的節點之間的關係必定是一個 log 之間的關係,
    3. 這使得在平衡二叉樹上搜索查詢是很是高效的。
  4. 線段樹雖然不是徹底二叉樹
    1. 可是這樣的一個平衡二叉樹,
    2. 也可使用數組的方式來表示,
    3. 對線段樹來講其實能夠把它看做是一棵滿二叉樹,
    4. 可是可能在最後一層不少節點是不存在的,
    5. 對於這些不存在的節點只須要把它看做是空便可,
    6. 這樣一來就是一棵滿二叉樹了,滿二叉樹是一棵特殊的徹底二叉樹,
    7. 那麼它就必定可使用數組來表示。
  5. 滿二叉樹的性質
    1. 滿二叉樹每層的節點數與層數成次方關係,0 層就是 2^0,1 層就是 2^1,
    2. 最後一層的節點數是 前面全部層的節點之和 而後再加上一
    3. (當前層節點數是 前面全部層節點數的總和 而後另外再加一),
    4. 最後一層的節點數是 前面一層節點的兩倍
    5. (當前層節點數是 前一層節點數的兩倍)
    6. 整棵滿二叉樹實際的節點個數就是2^h-1
    7. (最後一層也就是(h-1層),有2^(h-1)個節點,
    8. 最後一層節點數是 前面全部層節點數的總和 另外再加一,
    9. 因此總節點數也就是2 * 2^(h-1)-1個節點,這樣一來就是2^h-1個)。
  6. 那麼就有一個問題了,若是區間中有 n 個元素
    1. 那麼使用數組表示時那麼數組的空間大小是多少,
    2. 也就是這棵線段樹上應該有多少個節點,
    3. 對於一棵滿的二叉樹,這一棵的層數和每一層的節點之間是有規律的,
    4. 第 0 層節點數爲 1,第 1 層節點數爲 2,第 2 層節點數爲 4,第 3 層節點數爲 8,
    5. 那麼第(h-1)層節點數爲2^(h-1),下層節點的數量是上層節點數量的 2 倍,
    6. 第 3 層的節點數量是第 2 層的節點數量的 2 倍,
    7. 因此對於滿二叉樹來講,h 層,一共有2^h-1個節點(大約是2^h),
    8. 這是等比數列求和的公式,
    9. 那麼當數組的空間爲2^h時必定能夠裝下滿二叉樹全部的元素,
    10. 最後一層(h-1層),有2^(h-1)個節點,
    11. 那麼最後一層的節點數大體等於前面全部層節點之和。
  7. 那麼原來的問題是若是區間有 n 個元素,數組表示須要有多少節點?
    1. 答案是 log 以 2 爲底的 n 爲多少,也就是 2 的多少次方爲 n,
    2. 若是這 n 是 2 的整數次冪,那麼只須要 2n 的空間,
    3. 這是由於除了最後一層以外,上層的全部節點大概也等於 n,
    4. 雖然實際來講是 n-1,可是這一個空間富餘出來沒有關係,
    5. 只須要 2n 的空間就足以存儲整棵樹了,
    6. 可是關鍵是一般這個 n 不必定是 2 的 k 次方冪,
    7. 也就是這個 n 不必定是 2 的整數次冪,如 n=2^k+r,r 確定不等於 2^k,
    8. 那麼在最壞的狀況下,若是 n=2^k+1,
    9. 那麼最後一層不足以存儲整個葉子節點的,
    10. 由於葉子節點的索引範圍會超出 2n 的數組範圍內,n=2^k+3 就會超出,
    11. 那麼葉子節點確定是在倒數的兩層的範圍裏,
    12. 那麼就還須要再加一層,加的這一層若是使用滿二叉樹的方式存儲的話,
    13. 那麼就在原來的基礎上再加一倍的空間,此時整棵滿二叉樹須要 4n 的空間,
    14. 這樣才能夠存儲全部的節點,對於建立的這棵線段樹來講,
    15. 若是你考慮的這個區間一共有 n 個元素,
    16. 那麼選擇使用數組的方式進行存儲的話,
    17. 只須要有 4n 的空間就能夠存儲整棵線段樹了,
    18. 在這 4n 的空間裏並非全部的空間都被利用了,
    19. 由於這個計算自己是一個估計值,
    20. 在計算的過程當中不是嚴格的正好能夠存儲整個線段樹的全部的節點,
    21. 其實作了一些富餘,對於線段樹來講並不必定是一棵滿二叉樹,
    22. 因此纔在最後一層的地方,頗有可能不少位置都是空的,
    23. 這 4n 的空間有多是有浪費掉的,
    24. 在最壞的狀況下至少有一半的空間是被浪費掉的,
    25. 可是不過分的考慮這些浪費的狀況,
    26. 對於現代計算機來講存儲空間自己並非問題,
    27. 作算法的關鍵就是使用空間來換時間,但願在時間性能上有巨大的提高,
    28. 這部分浪費自己也是能夠避免的,不使用數組來存儲整棵線段樹,
    29. 而使用鏈式的結構如二分搜索樹那種節點的方式來存儲整棵線段樹,
    30. 就能夠避免這種空間的浪費。
  8. 若是區間有 n 個元素,數組須要開 4n 的空間就行了,
    1. 於此同時這 4n 的空間是一個靜態的空間,
    2. 由於對於線段樹來講並不考慮添加元素,
    3. 也就是說考慮的整個區間是固定的,這個區間的大小不會再改變了,
    4. 真正改變的是區間中的元素,因此不須要使用本身實現的動態數組,
    5. 直接開 4n 的靜態空間便可。

代碼示例(class: MySegmentTree)

  1. MySegmentTree算法

    // 自定義線段樹 SegmentTree
    class MySegmentTree {
       constructor(array) {
          // 拷貝一份參數數組中的元素
          this.data = new Array(array.length);
          for (var i = 0; i < array.length; i++) this.data[i] = array[i];
    
          // 初始化線段樹 開4倍的空間 這樣才能在全部狀況下存儲線段樹上全部的節點
          this.tree = new Array(4 * this.data.length);
       }
    
       // 獲取線段樹中實際的元素個數
       getSize() {
          return this.data.length;
       }
    
       // 根據索引獲取元素
       get(index) {
          if (index < 0 || index >= this.getSize())
             throw new Error('index is illegal.');
          return this.data[index];
       }
    
       // 輔助函數:返回徹底二叉樹的數組表示中,一個索引所表示的元素的左孩子節點的索引
       // 計算出線段樹中指定索引位置的元素其左孩子節點的索引 -
       calcLeftChildIndex(index) {
          return index * 2 + 1;
       }
    
       // 輔助函數:返回徹底二叉樹的數組表示中,一個索引所表示的元素的右孩子節點的索引
       // 計算出線段樹中指定索引位置的元素其右孩子節點的索引 -
       calcRightChildIndex(index) {
          return index * 2 + 2;
       }
    }
    複製代碼

建立線段樹

  1. 將線段樹看做是一棵滿的二叉樹
    1. 這樣一來就可使用數組來存儲整個線段樹上全部的節點了,
    2. 若是考慮的這個區間中有 n 個元素,那麼這個數組就須要開 4n 個空間。
  2. 在數組中存儲什麼才能夠構建出一棵線段樹
    1. 這個邏輯是一個很是典型的遞歸邏輯,對於這個線段樹的定義,
    2. 根節點所存儲的信息實際上就是它的兩個孩子所存儲的信息相應的一個綜合,
    3. 怎麼去綜合是以業務邏輯去定義的,
    4. 好比是以求和爲例,建立這棵線段樹是爲了查詢區間中數據的元素和這樣的一個操做,
    5. 相應的每個節點存儲的就是相應的一個區間中全部元素的和,
    6. 好比有十個元素,那麼根節點存儲的就是這十個元素的和,
    7. 相應它分出兩個孩子節點,左孩子就是這十個元素中前五個元素相應的和,
    8. 右孩子就是這十個元素中後五個元素相應的和,
    9. 這兩個節點下面的左右孩子節點依次再這樣的劃分,直到到達葉子節點。
  3. 這整個過程至關因而建立這棵線段樹根
    1. 建立這棵線段樹的根必須先建立好這個根節點對應的左右兩個子樹,
    2. 只要有了這左右兩個子樹的根節點,
    3. 那麼這個線段樹的根節點對應的這個值就是它的兩個孩子所對應的值進行一下加法運算便可,
    4. 對於左右兩棵子樹的建立也是如此,爲了要建立它們的根節點,
    5. 那麼仍是要建立這個根節點對應的左右兩個子樹,依此類推,直到遞歸到底爲止,
    6. 也就是這個節點所對應的區間不可以再劃分了,該節點所存儲的這個區間的長度只爲 1 了,
    7. 這個區間只有一個元素,對於這一個元素,它的和就是這一個元素自己,那麼就遞歸到底了,
    8. 總體這個遞歸結構就是如此清晰的。
  4. BuildingSegmentTree 的方法
    1. 有三個參數;
    2. 第一個參數是在初始的時候這個線段樹對應的索引,索引應該爲 0,表示從 0 開始;
    3. 第2、三參數是指對於這個節點它所表示的那個線段(區間)左右端點是什麼,初始的時候,
    4. 左端點的索引應該爲 0,右端點的索引應該爲原數組的長度減 1;
    5. 遞歸使用的時候,
    6. 也就是在 treeIndex 的位置建立表示區間[l...r]的線段樹。
  5. BuildingSegmentTree 的方法的邏輯
    1. 若是真的要表示一個區間的話,那麼相應的處理方式是這樣的,
    2. 先獲取這個區間的左右節點的索引,這個節點必定會有左右孩子,
    3. 先建立和這個節點的左右子樹,基於兩個區間才能建立線段樹,
    4. 計算這個區間的左右範圍,計算公式:mid = (left + right) / 2
    5. 這個計算可能會出現整型溢出的問題,可是機率很低,
    6. 那麼計算公式能夠換一種寫法:mid = left + (right - left) / 2
    7. 左子樹區間爲 left至mid,右子樹區間爲 mid+1至right
    8. 遞歸建立線段樹,以後進行業務處理操做,
    9. 例如 求和、取最大值、取最小值,綜合左右兩個線段的信息,
    10. 來獲得當前的更大的這個線段相應的信息,若是去綜合,是根據你的業務邏輯來決定的,
    11. 使用一個如何去綜合的接口,這樣一來就會根據你傳入的方法來進行綜合的操做。
    12. 這個和 自定義的優先隊列中的 updateCompare 傳入的 方法的意義是同樣的,
    13. 只不過 updateCompare 是傳入比較的方法,用來在優先隊列中如何比較兩個元素值,
    14. 而 updateMerge 是傳入融合的方法,用來線段樹中構建線段樹時兩個元素如何去融合。

代碼示例

  1. (class: MySegmentTree, class: Main)數組

  2. MySegmentTree:線段樹數據結構

    // 自定義線段樹 SegmentTree
    class MySegmentTree {
       constructor(array) {
          // 拷貝一份參數數組中的元素
          this.data = new Array(array.length);
          for (var i = 0; i < array.length; i++) this.data[i] = array[i];
    
          // 初始化線段樹 開4倍的空間 這樣才能在全部狀況下存儲線段樹上全部的節點
          this.tree = new Array(4 * this.data.length);
    
          // 開始構建線段樹
          this.buildingSegmentTree(0, 0, this.data.length - 1);
       }
    
       // 獲取線段樹中實際的元素個數
       getSize() {
          return this.data.length;
       }
    
       // 根據索引獲取元素
       get(index) {
          if (index < 0 || index >= this.getSize())
             throw new Error('index is illegal.');
          return this.data[index];
       }
    
       // 構建線段樹
       buildingSegmentTree(treeIndex, left, right) {
          // 解決最基本問題
          // 當一條線段的兩端相同時,說明這個區間只有一個元素,
          // 那麼遞歸也到底了
          if (left === right) {
             this.tree[treeIndex] = this.data[left];
             return;
          }
    
          // 計算當前線段樹的左右子樹的索引
          const leftChildIndex = this.calcLeftChildIndex(treeIndex);
          const rightChildIndex = this.calcRightChildIndex(treeIndex);
    
          // 將一個區間拆分爲兩段,而後繼續構建其左右子線段樹
          let middle = Math.floor(left + (right - left) / 2); //(left + right) / 2
    
          // 構建左子線段樹
          this.buildingSegmentTree(leftChildIndex, left, middle);
          // 構建右子線段樹
          this.buildingSegmentTree(rightChildIndex, middle + 1, right);
    
          // 融合左子線段樹和右子線段樹
          this.tree[treeIndex] = this.merge(
             this.tree[leftChildIndex],
             this.tree[rightChildIndex]
          );
       }
    
       // 輔助函數:返回徹底二叉樹的數組表示中,一個索引所表示的元素的左孩子節點的索引
       // 計算出線段樹中指定索引位置的元素其左孩子節點的索引 -
       calcLeftChildIndex(index) {
          return index * 2 + 1;
       }
    
       // 輔助函數:返回徹底二叉樹的數組表示中,一個索引所表示的元素的右孩子節點的索引
       // 計算出線段樹中指定索引位置的元素其右孩子節點的索引 -
       calcRightChildIndex(index) {
          return index * 2 + 2;
       }
    
       // 輔助函數: 融合兩棵線段樹,也就是對線段樹進行業務邏輯的處理
       merge(treeElementA, treeElmentB) {
          // 默認進行求和操做
          return treeElementA + treeElmentB;
       }
    
       // 輔助函數:更新融合的方法,也就是自定義處理線段樹融合的業務邏輯
       updateMerge(mergeMethod) {
          this.merge = mergeMethod;
       }
    
       // @Override toString() 2018-11-7 jwl
       toString() {
          let segmentTreeConsoleInfo = ''; // 控制檯信息
          let segmentTreePageInfo = ''; // 頁面信息
    
          // 輸出頭部信息
          segmentTreeConsoleInfo += 'SegmentTree:';
          segmentTreePageInfo += 'SegmentTree:';
          segmentTreeConsoleInfo += '\r\n';
          segmentTreePageInfo += '<br/><br/>';
    
          // 輸出傳入的數據信息
          segmentTreeConsoleInfo += 'data = [';
          segmentTreePageInfo += 'data = [';
    
          for (let i = 0; i < this.data.length - 1; i++) {
             segmentTreeConsoleInfo += this.data[i] + ',';
             segmentTreePageInfo += this.data[i] + ',';
          }
    
          if (this.data != null && this.data.length != 0) {
             segmentTreeConsoleInfo += this.data[this.data.length - 1];
             segmentTreePageInfo += this.data[this.data.length - 1];
          }
          segmentTreeConsoleInfo += '],\r\n';
          segmentTreePageInfo += '],<br/><br/>';
    
          // 輸出生成的線段樹信息
          segmentTreeConsoleInfo += 'tree = [';
          segmentTreePageInfo += 'tree = [';
          let treeSize = 0;
          for (let i = 0; i < this.tree.length - 1; i++) {
             if (this.tree[i] !== undefined) treeSize++;
             segmentTreeConsoleInfo += this.tree[i] + ',';
             segmentTreePageInfo += this.tree[i] + ',';
          }
          if (this.tree != null && this.tree.length != 0) {
             if (this.tree[this.tree.length - 1] !== undefined) treeSize++;
             segmentTreeConsoleInfo += this.tree[this.tree.length - 1];
             segmentTreePageInfo += this.tree[this.tree.length - 1];
          }
          segmentTreeConsoleInfo += '],\r\n';
          segmentTreePageInfo += '],<br/><br/>';
          segmentTreeConsoleInfo += 'originArraySize:' + this.getSize() + ',';
          segmentTreePageInfo += 'originArraySize:' + this.getSize() + ',';
          segmentTreeConsoleInfo +=
             'treeCapacity: ' + this.tree.length + ',treeSize: ' + treeSize;
          segmentTreePageInfo +=
             'treeCapacity: ' + this.tree.length + ',treeSize: ' + treeSize;
    
          // 返回輸出的總信息
          document.body.innerHTML += segmentTreePageInfo;
          return segmentTreeConsoleInfo;
       }
    }
    複製代碼
  3. Main:主函數ide

    // main 函數
    class Main {
       constructor() {
          this.alterLine('MySegmentTree Area');
          // 初始數據
          const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
          // 初始化線段樹,將初始數據和融合器傳入進去
          let mySegmentTree = new MySegmentTree(nums);
          // 指定線段樹的融合器
          mySegmentTree.updateMerge((a, b) => a + b);
    
          // 輸出
          console.log(mySegmentTree.toString());
       }
    
       // 將內容顯示在頁面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展現分割線
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 頁面加載完畢
    window.onload = function() {
       // 執行主函數
       new Main();
    };
    複製代碼

線段樹查詢

  1. 要有兩個查詢方法,一個普通查詢,一個是遞歸查詢。
  2. 普通查詢
    1. 有兩個參數,也就是你要查詢的區間,左端點與右端點的索引,
    2. 先檢查 待查詢的區間左右兩端的索引是否符合要求,有沒有越界,
    3. 而後調用遞歸查詢,
    4. 首次遞歸函數調用時,須要從根節點開始,也就是第一個參數索引爲 0,
    5. 以及搜索範圍從根節點的左右兩端開始也就是從 0 到原數組的長度減 1,
    6. 而後就是你要指定要查詢的線段(區間),也就是從一個大範圍內找到一個小線段(區間),
    7. 最後也是獲取這個線段(區間),
    8. 其實就是獲取這個線段(區間)在進行過業務處理操做後獲得的結果,
    9. 如 求和、取最大值、取最小值,綜合線段(區間)樹的信息返回最終結果。
  3. 遞歸查詢
    1. 有五個參數,
    2. 第一個 當前節點所對應的索引,
    3. 第二個第三個 當前節點它所表示的那個線段(區間)左右端點是什麼,
    4. 第四個第五個 待查詢的線段(區間),也就是要查詢的這個線段(區間)的左右端點。
  4. 遞歸查詢的邏輯
    1. 若是查詢範圍的左右端點恰好與待查詢的線段(區間)的左右端點一致,
    2. 那麼就說明當前正好就查詢到了待查詢的這個線段了,那麼直接返回當前當前節點便可,
    3. 不一致的話,說明還須要向下縮小可查詢範圍,從而可以匹配到待查詢的這個線段(區間)。
    4. 向下縮小範圍的方式,就是當前這個節點的左右端點之和除以 2,獲取左右端點的中間值,
    5. 求出 middle 以後,再繼續遞歸,查詢當前節點的左右孩子節點,
    6. 查詢範圍是當前節點的左端點到 middle 以及 middle+1 到右端點,
    7. 可是查詢以前要判斷待查詢的線段(區間)到底在當前節點左子樹中仍是右子樹中,
    8. 若是在左子樹中那麼就直接把查詢範圍定位到當前節點左孩子節點中,
    9. 若是在右子樹中那麼就直接把查詢範圍定位到當前節點右孩子節點中,
    10. 這樣就完成了在一個節點的左子線段樹或右子線段樹中再繼續查找了,
    11. 這個查詢範圍在很明確狀況下開始收縮,
    12. 直到查詢範圍的左右端點恰好與待查詢的線段(區間)的左右端點徹底一致,
    13. 遞歸查詢就完畢了,直接返回那個線段(區間)的節點便可。
    14. 可是問題來了,若是待查詢的線段(區間)很不巧的同時分佈在
    15. 某一個線段(區間)左右子線段樹中,這樣一來就永遠都沒法匹配到
    16. 查詢範圍的左右端點恰好與待查詢的線段(區間)的左右端點一致的狀況,
    17. 那就麻煩了,那麼就須要同時在某一個線段(區間)左右子線段樹中查詢,
    18. 查詢的時候待查詢的線段(區間)也要作相應的縮小,由於查詢的範圍也縮小了,
    19. 若是待查詢的線段(區間)不作相應的縮小,那就會造成死遞歸,
    20. 由於永遠沒法徹底匹配,隨着查詢的範圍縮小,待查詢的線段(區間)會大於這個查詢範圍,
    21. 待查詢的線段(區間)縮小的方式和查詢範圍縮小的方式一致,
    22. 從待查詢的線段(區間)左端點到 middle 以及 middle+1 到右端點,
    23. 最後將查詢到的兩個結果進行一下融合,最終返回這個融合的結果,
    24. 同樣能夠達到如此的效果。

代碼示例

  1. (class: MySegmentTree, class: Main)函數

  2. MySegmentTree:線段樹性能

    // 自定義線段樹 SegmentTree
    class MySegmentTree {
       constructor(array) {
          // 拷貝一份參數數組中的元素
          this.data = new Array(array.length);
          for (var i = 0; i < array.length; i++) this.data[i] = array[i];
    
          // 初始化線段樹 開4倍的空間 這樣才能在全部狀況下存儲線段樹上全部的節點
          this.tree = new Array(4 * this.data.length);
    
          // 開始構建線段樹
          this.buildingSegmentTree(0, 0, this.data.length - 1);
       }
    
       // 獲取線段樹中實際的元素個數
       getSize() {
          return this.data.length;
       }
    
       // 根據索引獲取元素
       get(index) {
          if (index < 0 || index >= this.getSize())
             throw new Error('index is illegal.');
          return this.data[index];
       }
    
       // 構建線段樹
       buildingSegmentTree(treeIndex, left, right) {
          // 解決最基本問題
          // 當一條線段的兩端相同時,說明這個區間只有一個元素,
          // 那麼遞歸也到底了
          if (left === right) {
             this.tree[treeIndex] = this.data[left];
             return;
          }
    
          // 計算當前線段樹的左右子樹的索引
          const leftChildIndex = this.calcLeftChildIndex(treeIndex);
          const rightChildIndex = this.calcRightChildIndex(treeIndex);
    
          // 將一個區間拆分爲兩段,而後繼續構建其左右子線段樹
          let middle = Math.floor(left + (right - left) / 2); //(left + right) / 2
    
          // 構建左子線段樹
          this.buildingSegmentTree(leftChildIndex, left, middle);
          // 構建右子線段樹
          this.buildingSegmentTree(rightChildIndex, middle + 1, right);
    
          // 融合左子線段樹和右子線段樹
          this.tree[treeIndex] = this.merge(
             this.tree[leftChildIndex],
             this.tree[rightChildIndex]
          );
       }
    
       // 查詢指定區間的線段樹數據
       // 返回區間[queryLeft, queryRight]的值
       query(queryLeft, queryRight) {
          if (
             queryLeft < 0 ||
             queryRight < 0 ||
             queryLeft > queryRight ||
             queryLeft >= this.data.length ||
             queryRight >= this.data.length
          )
             throw new Error('queryLeft or queryRight is illegal.');
    
          // 調用遞歸的查詢方法
          return this.recursiveQuery(
             0,
             0,
             this.data.length - 1,
             queryLeft,
             queryRight
          );
       }
    
       // 遞歸的查詢方法 -
       // 在以treeIndex爲根的線段樹中[left...right]的範圍裏,
       // 搜索區間[queryLeft...queryRight]的值
       recursiveQuery(treeIndex, left, right, queryLeft, queryRight) {
          // 若是查詢範圍 與 指定的線段樹的區間 相同,那麼說明徹底匹配,
          // 直接返回當前這個線段便可,每個節點表明 一個線段(區間)處理後的結果
          if (left === queryLeft && right === queryRight)
             return this.tree[treeIndex];
    
          // 求出當前查詢範圍的中間值
          const middle = Math.floor(left + (right - left) / 2);
    
          // 滿二叉樹確定有左右孩子節點
          // 上面的判斷沒有徹底匹配,說明須要繼續 縮小查詢範圍,也就是要在左右子樹中進行查詢了
          const leftChildIndex = this.calcLeftChildIndex(treeIndex);
          const rightChildIndex = this.calcRightChildIndex(treeIndex);
    
          // 判斷:
          // 1. 從左子樹中查仍是右子樹中查,又或者從左右子樹中同時查,而後將兩個查詢結果融合。
          // 2. 若是 待查詢的區間的左端點大於查詢範圍的中間值,說明只須要從右子樹中進行查詢便可。
          // 3. 若是 待查詢的區間的右端點小於查詢範圍的中間值 + 1,說明只須要從左子樹中進行查詢。
          // 4. 若是 待查詢的區間在左右端點各分部一部分,說明要同時從左右子樹中進行查詢。
          if (queryLeft > middle)
             return this.recursiveQuery(
                rightChildIndex,
                middle + 1,
                right,
                queryLeft,
                queryRight
             );
          else if (queryRight < middle + 1)
             return this.recursiveQuery(
                leftChildIndex,
                left,
                middle,
                queryLeft,
                queryRight
             );
          else {
             // 求出 左子樹中一部分待查詢區間中的值
             const leftChildValue = this.recursiveQuery(
                leftChildIndex,
                left,
                middle,
                queryLeft,
                middle
             );
             // 求出 右子樹中一部分待查詢區間中的值
             const rightChildValue = this.recursiveQuery(
                rightChildIndex,
                middle + 1,
                right,
                middle + 1,
                queryRight
             );
             // 融合左右子樹種的數據並返回
             return this.merge(leftChildValue, rightChildValue);
          }
       }
    
       // 輔助函數:返回徹底二叉樹的數組表示中,一個索引所表示的元素的左孩子節點的索引
       // 計算出線段樹中指定索引位置的元素其左孩子節點的索引 -
       calcLeftChildIndex(index) {
          return index * 2 + 1;
       }
    
       // 輔助函數:返回徹底二叉樹的數組表示中,一個索引所表示的元素的右孩子節點的索引
       // 計算出線段樹中指定索引位置的元素其右孩子節點的索引 -
       calcRightChildIndex(index) {
          return index * 2 + 2;
       }
    
       // 輔助函數: 融合兩棵線段樹,也就是對線段樹進行業務邏輯的處理 -
       merge(treeElementA, treeElmentB) {
          // 默認進行求和操做
          return treeElementA + treeElmentB;
       }
    
       // 輔助函數:更新融合的方法,也就是自定義處理線段樹融合的業務邏輯 +
       updateMerge(mergeMethod) {
          this.merge = mergeMethod;
       }
    
       // @Override toString() 2018-11-7 jwl
       toString() {
          let segmentTreeConsoleInfo = ''; // 控制檯信息
          let segmentTreePageInfo = ''; // 頁面信息
    
          // 輸出頭部信息
          segmentTreeConsoleInfo += 'SegmentTree:';
          segmentTreePageInfo += 'SegmentTree:';
          segmentTreeConsoleInfo += '\r\n';
          segmentTreePageInfo += '<br/><br/>';
    
          // 輸出傳入的數據信息
          segmentTreeConsoleInfo += 'data = [';
          segmentTreePageInfo += 'data = [';
    
          for (let i = 0; i < this.data.length - 1; i++) {
             segmentTreeConsoleInfo += this.data[i] + ',';
             segmentTreePageInfo += this.data[i] + ',';
          }
    
          if (this.data != null && this.data.length != 0) {
             segmentTreeConsoleInfo += this.data[this.data.length - 1];
             segmentTreePageInfo += this.data[this.data.length - 1];
          }
          segmentTreeConsoleInfo += '],\r\n';
          segmentTreePageInfo += '],<br/><br/>';
    
          // 輸出生成的線段樹信息
          segmentTreeConsoleInfo += 'tree = [';
          segmentTreePageInfo += 'tree = [';
          let treeSize = 0;
          for (let i = 0; i < this.tree.length - 1; i++) {
             if (this.tree[i] !== undefined) treeSize++;
             segmentTreeConsoleInfo += this.tree[i] + ',';
             segmentTreePageInfo += this.tree[i] + ',';
          }
          if (this.tree != null && this.tree.length != 0) {
             if (this.tree[this.tree.length - 1] !== undefined) treeSize++;
             segmentTreeConsoleInfo += this.tree[this.tree.length - 1];
             segmentTreePageInfo += this.tree[this.tree.length - 1];
          }
          segmentTreeConsoleInfo += '],\r\n';
          segmentTreePageInfo += '],<br/><br/>';
          segmentTreeConsoleInfo += 'originArraySize:' + this.getSize() + ',';
          segmentTreePageInfo += 'originArraySize:' + this.getSize() + ',';
          segmentTreeConsoleInfo +=
             'treeCapacity: ' + this.tree.length + ',treeSize: ' + treeSize;
          segmentTreePageInfo +=
             'treeCapacity: ' + this.tree.length + ',treeSize: ' + treeSize;
    
          // 返回輸出的總信息
          document.body.innerHTML += segmentTreePageInfo;
          return segmentTreeConsoleInfo;
       }
    }
    複製代碼
  3. Main:主函數

    // main 函數
    class Main {
       constructor() {
          this.alterLine('MySegmentTree Area');
          // 初始數據
          const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
          // 初始化線段樹,將初始數據和融合器傳入進去
          let mySegmentTree = new MySegmentTree(nums);
          // 指定線段樹的融合器
          mySegmentTree.updateMerge((a, b) => a + b);
    
          // 輸出
          console.log(mySegmentTree.toString());
          this.show('');
          this.alterLine('MySegmentTree Queue Area');
          console.log('查詢區間[0, 2]:' + mySegmentTree.query(0, 2));
          this.show('查詢區間[0, 2]:' + mySegmentTree.query(0, 2));
          console.log('查詢區間[3, 9]:' + mySegmentTree.query(3, 9));
          this.show('查詢區間[3, 9]:' + mySegmentTree.query(3, 9));
          console.log('查詢區間[0, 9]:' + mySegmentTree.query(0, 9));
          this.show('查詢區間[0, 9]:' + mySegmentTree.query(0, 9));
       }
    
       // 將內容顯示在頁面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展現分割線
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 頁面加載完畢
    window.onload = function() {
       // 執行主函數
       new Main();
    };
    複製代碼

Leetcode 上與線段樹相關的問題

  1. 303.區域和檢索-數組不可變
    1. https://leetcode-cn.com/problems/range-sum-query-immutable/
    2. 方式一:使用線段樹
    3. 方式二:對數組進行必定的預處理
  2. 307.區域和檢索 - 數組可修改
    1. https://leetcode-cn.com/problems/range-sum-query-mutable/
    2. 方式一:對數組進行必定的預處理,可是性能不是很好

代碼示例

  1. 303 方式一 和 方式二

    // 答題
    class Solution {
       // leetcode 303. 區域和檢索-數組不可變
       NumArray(nums) {
          /** * @param {number[]} nums * 處理方式一:對原數組進行預處理操做 */
          var NumArray = function(nums) {
             if (nums.length > 0) {
                this.data = new Array(nums.length + 1);
                this.data[0] = 0;
                for (var i = 0; i < nums.length; i++) {
                   this.data[i + 1] = this.data[i] + nums[i];
                }
             }
          };
    
          /** * @param {number} i * @param {number} j * @return {number} */
          NumArray.prototype.sumRange = function(i, j) {
             return this.data[j + 1] - this.data[i];
          };
    
          /** * Your NumArray object will be instantiated and called as such: * var obj = Object.create(NumArray).createNew(nums) * var param_1 = obj.sumRange(i,j) */
    
          /** * @param {number[]} nums * 處理方式二:使用線段樹 */
          var NumArray = function(nums) {
             if (nums.length > 0) {
                this.mySegmentTree = new MySegmentTree(nums);
             }
          };
    
          /** * @param {number} i * @param {number} j * @return {number} */
          NumArray.prototype.sumRange = function(i, j) {
             return this.mySegmentTree.query(i, j);
          };
    
          return new NumArray(nums);
       }
    }
    複製代碼
  2. 307 方式一

    // 答題
    class Solution {
       // leetcode 307. 區域和檢索 - 數組可修改
       NumArray2(nums) {
          /** * @param {number[]} nums * 方式一:對原數組進行預處理操做 */
          var NumArray = function(nums) {
             // 克隆一份原數組
             this.data = new Array(nums.length);
             for (var i = 0; i < nums.length; i++) {
                this.data[i] = nums[i];
             }
    
             if (nums.length > 0) {
                this.sum = new Array(nums.length + 1);
                this.sum[0] = 0;
                for (let i = 0; i < nums.length; i++)
                   this.sum[i + 1] = this.sum[i] + nums[i];
             }
          };
    
          /** * @param {number} i * @param {number} val * @return {void} */
          NumArray.prototype.update = function(i, val) {
             this.data[i] = val;
    
             for (let j = 0; j < this.data.length; j++)
                this.sum[j + 1] = this.sum[j] + this.data[j];
          };
    
          /** * @param {number} i * @param {number} j * @return {number} */
          NumArray.prototype.sumRange = function(i, j) {
             return this.sum[j + 1] - this.sum[i];
          };
    
          /** * Your NumArray object will be instantiated and called as such: * var obj = Object.create(NumArray).createNew(nums) * obj.update(i,val) * var param_2 = obj.sumRange(i,j) */
       }
    }
    複製代碼
  3. Main

    // main 函數
    class Main {
       constructor() {
          this.alterLine('leetcode 303. 區域和檢索-數組不可變');
          let s = new Solution();
          let nums = [-2, 0, 3, -5, 2, -1];
          let numArray = s.NumArray(nums);
    
          console.log(numArray.sumRange(0, 2));
          this.show(numArray.sumRange(0, 2));
          console.log(numArray.sumRange(2, 5));
          this.show(numArray.sumRange(2, 5));
          console.log(numArray.sumRange(0, 5));
          this.show(numArray.sumRange(0, 5));
       }
    
       // 將內容顯示在頁面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展現分割線
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 頁面加載完畢
    window.onload = function() {
       // 執行主函數
       new Main();
    };
    複製代碼

線段樹中的更新操做

  1. 經過 leetcode 上 303 及 307 號題目能夠分析出
    1. 使用數組實現時,更新的操做是O(n)級別的,
    2. 查詢的操做是O(1)級別的,只不過初始化操做時是O(n)級別的。
    3. 使用線段樹實現時,更新和查詢的操做都是O(logn)級別的,
    4. 可是線段樹在建立的時候是O(n)的複雜度,
    5. 更準確的說是 4 倍的 O(n)的複雜度,
    6. 由於所用的空間是 4n 個,而且要對每一個空間進行賦值。
  2. 對於線段樹來講,要考慮區間這樣的這樣的一種數據,
    1. 尤爲是要查詢區間相關的統計信息的時候,
    2. 同時數據是動態的,不時的還須要更新你的數據,在這樣的狀況下,
    3. 線段樹是一種很是好的數據結構,不過對於線段樹來講,
    4. 大多數本科甚至是研究生的算法教材中都不會涉及這種數據結構,
    5. 它自己是一種高級的數據結構,更多的應用於算法競賽中。
  3. 更新操做和構建線段樹的操做相似。
    1. 若是要修改某個索引位置的值,
    2. 那麼就須要知道這個索引位置所對應的葉子節點,
    3. 遞歸到底後就可以知道這個葉子節點,這時候只須要賦值一下,
    4. 而後 從新進行融合操做,由於該索引位置所在的區間須要進行更新,
    5. 只有這樣纔可以達到修改線段樹中某一個節點的值後
    6. 也能夠改變相應的線段(區間)。

代碼示例

  1. (class: MySegmentTree, class: NumArray2, class: Main)

  2. MySegmentTree

    // 自定義線段樹 SegmentTree
    class MySegmentTree {
       constructor(array) {
          // 拷貝一份參數數組中的元素
          this.data = new Array(array.length);
          for (var i = 0; i < array.length; i++) this.data[i] = array[i];
    
          // 初始化線段樹 開4倍的空間 這樣才能在全部狀況下存儲線段樹上全部的節點
          this.tree = new Array(4 * this.data.length);
    
          // 開始構建線段樹
          this.buildingSegmentTree(0, 0, this.data.length - 1);
       }
    
       // 獲取線段樹中實際的元素個數
       getSize() {
          return this.data.length;
       }
    
       // 根據索引獲取元素
       get(index) {
          if (index < 0 || index >= this.getSize())
             throw new Error('index is illegal.');
          return this.data[index];
       }
    
       // 構建線段樹
       buildingSegmentTree(treeIndex, left, right) {
          // 解決最基本問題
          // 當一條線段的兩端相同時,說明這個區間只有一個元素,
          // 那麼遞歸也到底了
          if (left === right) {
             this.tree[treeIndex] = this.data[left];
             return;
          }
    
          // 計算當前線段樹的左右子樹的索引
          const leftChildIndex = this.calcLeftChildIndex(treeIndex);
          const rightChildIndex = this.calcRightChildIndex(treeIndex);
    
          // 將一個區間拆分爲兩段,而後繼續構建其左右子線段樹
          let middle = Math.floor(left + (right - left) / 2); //(left + right) / 2
    
          // 構建左子線段樹
          this.buildingSegmentTree(leftChildIndex, left, middle);
          // 構建右子線段樹
          this.buildingSegmentTree(rightChildIndex, middle + 1, right);
    
          // 融合左子線段樹和右子線段樹
          this.tree[treeIndex] = this.merge(
             this.tree[leftChildIndex],
             this.tree[rightChildIndex]
          );
       }
    
       // 查詢指定區間的線段樹數據
       // 返回區間[queryLeft, queryRight]的值
       query(queryLeft, queryRight) {
          if (
             queryLeft < 0 ||
             queryRight < 0 ||
             queryLeft > queryRight ||
             queryLeft >= this.data.length ||
             queryRight >= this.data.length
          )
             throw new Error('queryLeft or queryRight is illegal.');
    
          // 調用遞歸的查詢方法
          return this.recursiveQuery(
             0,
             0,
             this.data.length - 1,
             queryLeft,
             queryRight
          );
       }
    
       // 遞歸的查詢方法 -
       // 在以treeIndex爲根的線段樹中[left...right]的範圍裏,
       // 搜索區間[queryLeft...queryRight]的值
       recursiveQuery(treeIndex, left, right, queryLeft, queryRight) {
          // 若是查詢範圍 與 指定的線段樹的區間 相同,那麼說明徹底匹配,
          // 直接返回當前這個線段便可,每個節點表明 一個線段(區間)處理後的結果
          if (left === queryLeft && right === queryRight)
             return this.tree[treeIndex];
    
          // 求出當前查詢範圍的中間值
          const middle = Math.floor(left + (right - left) / 2);
    
          // 滿二叉樹確定有左右孩子節點
          // 上面的判斷沒有徹底匹配,說明須要繼續 縮小查詢範圍,也就是要在左右子樹中進行查詢了
          const leftChildIndex = this.calcLeftChildIndex(treeIndex);
          const rightChildIndex = this.calcRightChildIndex(treeIndex);
    
          // 判斷:
          // 1. 從左子樹中查仍是右子樹中查,又或者從左右子樹中同時查,而後將兩個查詢結果融合。
          // 2. 若是 待查詢的區間的左端點大於查詢範圍的中間值,說明只須要從右子樹中進行查詢便可。
          // 3. 若是 待查詢的區間的右端點小於查詢範圍的中間值 + 1,說明只須要從左子樹中進行查詢。
          // 4. 若是 待查詢的區間在左右端點各分部一部分,說明要同時從左右子樹中進行查詢。
          if (queryLeft > middle)
             return this.recursiveQuery(
                rightChildIndex,
                middle + 1,
                right,
                queryLeft,
                queryRight
             );
          else if (queryRight < middle + 1)
             return this.recursiveQuery(
                leftChildIndex,
                left,
                middle,
                queryLeft,
                queryRight
             );
          else {
             // 求出 左子樹中一部分待查詢區間中的值
             const leftChildValue = this.recursiveQuery(
                leftChildIndex,
                left,
                middle,
                queryLeft,
                middle
             );
             // 求出 右子樹中一部分待查詢區間中的值
             const rightChildValue = this.recursiveQuery(
                rightChildIndex,
                middle + 1,
                right,
                middle + 1,
                queryRight
             );
             // 融合左右子樹種的數據並返回
             return this.merge(leftChildValue, rightChildValue);
          }
       }
    
       // 設置指定索引位置的元素 更新操做
       set(index, element) {
          if (index < 0 || index >= this.data.length)
             throw new Error('index is illegal.');
    
          this.recursiveSet(0, 0, this.data.length - 1, index, element);
       }
    
       // 遞歸的設置指定索引位置元素的方法 -
       // 在以treeIndex爲根的線段樹中更新index的值爲element
       recursiveSet(treeIndex, left, right, index, element) {
          // 解決最基本的問題 遞歸到底了就結束
          // 由於找到了該索引位置的節點了
          if (left === right) {
             this.tree[treeIndex] = element;
             this.data[index] = element;
             return;
          }
    
          // 求出當前查詢範圍的中間值
          const middle = Math.floor(left + (right - left) / 2);
    
          // 滿二叉樹確定有左右孩子節點
          // 上面的判斷沒有徹底匹配,說明須要繼續 縮小查詢範圍,也就是要在左右子樹中進行查詢了
          const leftChildIndex = this.calcLeftChildIndex(treeIndex);
          const rightChildIndex = this.calcRightChildIndex(treeIndex);
    
          // 若是指定的索引大於 查詢範圍的中間值,那就說明 該索引的元素在右子樹中
          // 不然該索引元素在左子樹中
          if (index > middle)
             this.recursiveSet(
                rightChildIndex,
                middle + 1,
                right,
                index,
                element
             );
          // index < middle + 1
          else this.recursiveSet(leftChildIndex, left, middle, index, element);
    
          // 將改變後的左右子樹再進行一下融合,由於遞歸到底時修改了指定索引位置的元素,
          // 那麼指定索引位置所在的線段(區間)也須要再次進行融合操做,
          // 從而達到修改一個值改變 相應的線段(區間)
          this.tree[treeIndex] = this.merge(
             this.tree[leftChildIndex],
             this.tree[rightChildIndex]
          );
       }
    
       // 輔助函數:返回徹底二叉樹的數組表示中,一個索引所表示的元素的左孩子節點的索引
       // 計算出線段樹中指定索引位置的元素其左孩子節點的索引 -
       calcLeftChildIndex(index) {
          return index * 2 + 1;
       }
    
       // 輔助函數:返回徹底二叉樹的數組表示中,一個索引所表示的元素的右孩子節點的索引
       // 計算出線段樹中指定索引位置的元素其右孩子節點的索引 -
       calcRightChildIndex(index) {
          return index * 2 + 2;
       }
    
       // 輔助函數: 融合兩棵線段樹,也就是對線段樹進行業務邏輯的處理 -
       merge(treeElementA, treeElmentB) {
          // 默認進行求和操做
          return treeElementA + treeElmentB;
       }
    
       // 輔助函數:更新融合的方法,也就是自定義處理線段樹融合的業務邏輯 +
       updateMerge(mergeMethod) {
          this.merge = mergeMethod;
       }
    
       // @Override toString() 2018-11-7 jwl
       toString() {
          let segmentTreeConsoleInfo = ''; // 控制檯信息
          let segmentTreePageInfo = ''; // 頁面信息
    
          // 輸出頭部信息
          segmentTreeConsoleInfo += 'SegmentTree:';
          segmentTreePageInfo += 'SegmentTree:';
          segmentTreeConsoleInfo += '\r\n';
          segmentTreePageInfo += '<br/><br/>';
    
          // 輸出傳入的數據信息
          segmentTreeConsoleInfo += 'data = [';
          segmentTreePageInfo += 'data = [';
    
          for (let i = 0; i < this.data.length - 1; i++) {
             segmentTreeConsoleInfo += this.data[i] + ',';
             segmentTreePageInfo += this.data[i] + ',';
          }
    
          if (this.data != null && this.data.length != 0) {
             segmentTreeConsoleInfo += this.data[this.data.length - 1];
             segmentTreePageInfo += this.data[this.data.length - 1];
          }
          segmentTreeConsoleInfo += '],\r\n';
          segmentTreePageInfo += '],<br/><br/>';
    
          // 輸出生成的線段樹信息
          segmentTreeConsoleInfo += 'tree = [';
          segmentTreePageInfo += 'tree = [';
          let treeSize = 0;
          for (let i = 0; i < this.tree.length - 1; i++) {
             if (this.tree[i] !== undefined) treeSize++;
             segmentTreeConsoleInfo += this.tree[i] + ',';
             segmentTreePageInfo += this.tree[i] + ',';
          }
          if (this.tree != null && this.tree.length != 0) {
             if (this.tree[this.tree.length - 1] !== undefined) treeSize++;
             segmentTreeConsoleInfo += this.tree[this.tree.length - 1];
             segmentTreePageInfo += this.tree[this.tree.length - 1];
          }
          segmentTreeConsoleInfo += '],\r\n';
          segmentTreePageInfo += '],<br/><br/>';
          segmentTreeConsoleInfo += 'originArraySize:' + this.getSize() + ',';
          segmentTreePageInfo += 'originArraySize:' + this.getSize() + ',';
          segmentTreeConsoleInfo +=
             'treeCapacity: ' + this.tree.length + ',treeSize: ' + treeSize;
          segmentTreePageInfo +=
             'treeCapacity: ' + this.tree.length + ',treeSize: ' + treeSize;
    
          // 返回輸出的總信息
          document.body.innerHTML += segmentTreePageInfo;
          return segmentTreeConsoleInfo;
       }
    }
    複製代碼
  3. NumArray2

    // 答題
    class Solution {
       // leetcode 307. 區域和檢索 - 數組可修改
       NumArray2(nums) {
          /** * @param {number[]} nums * 方式一:對原數組進行預處理操做 */
          var NumArray = function(nums) {
             // 克隆一份原數組
             this.data = new Array(nums.length);
             for (var i = 0; i < nums.length; i++) {
                this.data[i] = nums[i];
             }
    
             if (nums.length > 0) {
                this.sum = new Array(nums.length + 1);
                this.sum[0] = 0;
                for (let i = 0; i < nums.length; i++)
                   this.sum[i + 1] = this.sum[i] + nums[i];
             }
          };
    
          /** * @param {number} i * @param {number} val * @return {void} */
          NumArray.prototype.update = function(i, val) {
             this.data[i] = val;
    
             for (let j = 0; j < this.data.length; j++)
                this.sum[j + 1] = this.sum[j] + this.data[j];
          };
    
          /** * @param {number} i * @param {number} j * @return {number} */
          NumArray.prototype.sumRange = function(i, j) {
             return this.sum[j + 1] - this.sum[i];
          };
    
          /** * Your NumArray object will be instantiated and called as such: * var obj = Object.create(NumArray).createNew(nums) * obj.update(i,val) * var param_2 = obj.sumRange(i,j) */
    
          /** * @param {number[]} nums * 方式二:對原數組進行預處理操做 */
          var NumArray = function(nums) {
             this.tree = new MySegmentTree(nums);
          };
    
          /** * @param {number} i * @param {number} val * @return {void} */
          NumArray.prototype.update = function(i, val) {
             this.tree.set(i, val);
          };
    
          /** * @param {number} i * @param {number} j * @return {number} */
          NumArray.prototype.sumRange = function(i, j) {
             return this.tree.query(i, j);
          };
    
          return new NumArray(nums);
       }
    }
    複製代碼
  4. Main

    // main 函數
    class Main {
       constructor() {
          this.alterLine('leetcode 307. 區域和檢索 - 數組可修改');
          let s = new Solution();
          let nums = [1, 3, 5];
          let numArray = s.NumArray2(nums);
    
          console.log(numArray.sumRange(0, 2));
          this.show(numArray.sumRange(0, 2));
          numArray.update(1, 2);
          console.log(numArray.sumRange(0, 2));
          this.show(numArray.sumRange(0, 2));
       }
    
       // 將內容顯示在頁面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展現分割線
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    // 頁面加載完畢
    window.onload = function() {
       // 執行主函數
       new Main();
    };
    複製代碼

更多線段樹相關的話題

  1. 在 leetcode 上能夠找到線段樹相關的題目
    1. https://leetcode-cn.com/tag/segment-tree/
    2. 題目整體難度都是困難的,因此線段樹是一種高級數據結構,
    3. 在通常的面試環節是不會看到線段樹的影子的,
    4. 線段樹的題目總體是有必定的難度的,
    5. 尤爲是這些問題在具體使用線段樹的時候,
    6. 不必定是直接的使用線段樹,頗有可能須要繞幾個彎子,
    7. 若是你不去參加算法競賽的話,線段樹不是一個重點。
  2. 線段樹雖然不是一個徹底二叉樹,可是能夠把它看做是一棵滿二叉樹,
    1. 進而可使用數組的方式去存儲這些結構,
    2. 這和以前實現的堆相應的存儲方式是一致的,
    3. 對線段樹的學習能夠深刻理解樹這種結構,
    4. 當節點中存儲的內容不同的時候它所表示的意義也不同的時候,
    5. 相應的就能夠來解決各類各樣的問題,
    6. 它使用的範圍是很是普遍的,對於線段樹的構建,
    7. 對於線段樹節點存儲的是什麼,它的左右子樹表明的是什麼意思,
    8. 其實和二分搜索樹是徹底不一樣的,
    9. 當你賦予這種結構合理的定義以後,就能夠很是高效的處理一些特殊的問題,
    10. 好比說對於線段樹來講,就能夠很是高效的處理了和線段(區間)有關的問題。
  3. 自定義線段樹實現了三個方法
    1. 建立線段樹、查詢線段樹、更新線段樹中一個元素,
    2. 這三個方法都使用了遞歸的操做,
    3. 同時這個遞歸的寫法在有一些層面和以前的二分搜索樹是不一樣的,
    4. 很大程度的不一樣是表如今遞歸以後
    5. 最終仍是要對線段樹中左右兩個孩子的節點進行一個融合的操做,
    6. 這其實是一種後序遍歷的思想。
  4. 遞歸的代碼不管是在宏觀的角度上仍是從微觀的角度
    1. 都可以更深一步的對遞歸有進一步的認識。
  5. 對於線段樹來講其實還有不少能夠深刻挖掘的東西
    1. 例如對線段樹中一個區間進行更新,對應的時間複雜度是O(n)級別的,
    2. 由於這個區間裏全部的元素都要訪問到,這個操做相對來講是比較慢的,
    3. 爲了解決這個問題,在線段樹中有一個專門的方式來解決它,
    4. 對應的方法一般稱之爲懶惰更新,也能夠叫作懶惰的傳播,
    5. 在本身實現的動態數組中有一個縮容的操做,
    6. 就有使用到懶惰的這個概念,在線段樹中也可使用這樣的思想,
    7. 在更新了中間節點的時候其實還要更新下面的葉子節點,
    8. 可是先不進行這個更新,這就是懶的地方,
    9. 先使用另一個叫作 lazy 的數組記錄此次未更新的內容,
    10. 有了這個記錄,就不須要實際的去更新這些節點,
    11. 當你再有一次更新或者查詢操做的時候,
    12. 也就是當你再碰到這些節點的時候,
    13. 那麼碰到這些節點以前都要先查一下已經記錄的這個 lazy 數組中
    14. 是否有以前須要更新的內容,若是沒有更新,那麼在訪問它們以前,
    15. 先將 lazy 數組中記錄的未更新的內容進行一下更新,
    16. 更新之後再來進行應該進行的訪問操做,這樣作在更新一個區間的內容的時候,
    17. 就又變成了 logn 的複雜度了,只須要訪問到中間節點就夠了,
    18. 不須要對底層的全部節點都進行訪問,
    19. 於此同時對於其餘的查詢或者新的更新操做,也依然是這樣的一個複雜度,
    20. 只不過碰到相應的節點的時候看一下 lazy 數組中有沒有記錄相應的內容就行了,
    21. 這個思想在實現的時候,有相應的不少細節須要注意,
    22. 這一點也是一個比較高級的話題,有一個印象有一個概念就 ok。
  6. 本身實現的線段樹本質上是一個一維的線段樹
    1. 線段樹還能夠擴充到二維,
    2. 一維線段樹就是指處理的空間是在一個一維空間中,是在一個座標軸中,
    3. 若是根節點是一個線段的話,左邊半段就是它的左節點,
    4. 右邊半段就是它的右節點,可是能夠把這個思想擴展成二維空間中,
    5. 對於根節點能夠記錄的是一個矩陣的內容,而後對這個矩陣進行分塊兒,
    6. 把它分紅四塊兒,分別是左上、右上、左下、右下這四塊兒,
    7. 這樣一來就可讓每個節點有四個孩子,
    8. 對於這四個孩子每一個孩子表示這個矩陣中相應的一起,
    9. 對於每個孩子它們依舊是一個更小的矩陣,
    10. 對於這個更小的矩陣又能夠把它分紅四塊兒,
    11. 相應的每個節點有四個孩子,依此類推,
    12. 直到在葉子節點的時候每個節點只表示一個元素,
    13. 這樣的一種線段樹就叫作二維線段樹,因此對於二維區間相應的查詢問題,
    14. 也可使用線段樹這樣的思路來解決。
    15. 因此不只是二維線段樹,其實也能夠設計出三維線段樹,
    16. 那麼對於一個三維的矩陣,或者是對於一個立方體上的數據,
    17. 能夠把這個立方體切成八塊兒,那麼每個節點能夠分紅八個節點,
    18. 對於每個小的節點,它是一個更小的立方體,而後能夠這樣繼續細分下去,
  7. 線段樹自己它就是一個思想,是在如何使用樹這種數據結構,
    1. 將一個大的數據單元拆分紅不一樣的小的數據單元,遞歸的來表示這些數據,
    2. 同時利用這種遞歸的結構能夠高效的進行訪問,
    3. 從而進行諸如像更新查詢這樣的操做,這自己就是樹這種結構的一個實質。
  8. 本身實現的線段樹是一個數組的存儲方式,
    1. 使用數組的存儲方式,相應的就會出現若是你有 n 個元素,
    2. 那麼就須要開闢 4n 個存儲空間,在這個空間中其實有不少空間是被浪費的,
    3. 對於線段樹其實可使用鏈式的方式進行存儲,
    4. 能夠設計一個單獨的線段樹所使用的節點類,
    5. 在這個節點類中就能夠存儲所表示的區間
    6. 它的左邊界是誰、右邊界是誰、相應的元素值是誰、以及它的左右孩子,
    7. 對於這樣的一個節點也可使用鏈式的方式也能夠建立出這個線段樹,
    8. 在這種狀況下,不須要浪費任何的空間,
    9. 若是你的線段樹要處理的節點很是多的話,
    10. 有可能開 4n 的空間對你的電腦的存儲資源負擔比較大,
    11. 這時候就能夠考慮使用鏈式這種所謂動態線段樹。
  9. 實際上對於動態線段樹來講有一個更加劇要的應用
    1. 在本身所實現的線段樹,
    2. 對於一個區間中相應的每個元素都要使用一個節點來表達,
    3. 這樣的結果就是整個線段樹所佔的空間大小是 4n,
    4. 若是想要探討的這個區間特別大的話,例若有一億這麼大的一個區間,
    5. 可是其實頗有可能你並不會對這麼大的一個區間中每個長度爲 1 的子區間都感興趣,
    6. 在這種狀況下頗有可能不須要一上來就創建一個巨大的線段樹,
    7. 就從根節點開始,初始的時候就這一個節點,它表示從 0 到一億這樣的一個區間,
    8. 若是你關注[5,16]這樣的一個區間,在這種狀況下再開始動態的建立這個線段樹,
    9. 那麼這個動態建立的方法,多是首先將這個線段樹根節點分紅兩部分,
    10. 左孩子表示[0,4]這樣的一個區間,右孩子表示 5 到一億這樣的一個區間,
    11. 進而對 5 到一億這樣的區間再給分紅兩部分,左半部分表示[5,16]
    12. 右半部分表示 17 到一億這個區間。至此對於這棵線段樹來講,
    13. 只有 5 個節點,也能夠很是快速的關注到[5,16]這個區間相應的內容,
    14. 那麼使用這樣的方式,若是你的區間很是大,
    15. 可是你關注的區間其實並不會分佈到這個大區間中每個小部分的時候,
    16. 能夠實現這樣的一個動態線段樹,由於更加的有利。

區間操做相關-另一個重要的數據結構

  1. 樹狀數組(Binary Index Tree)
    1. 對區間這種數據進行操做時,就可能會使用到這種數據結構了,
    2. 也就是樹狀數組,也被簡稱爲 BIT,也叫二叉索引樹,
    3. 樹狀數組也是一個很是經典的樹狀結構,
    4. 也是算法競賽中的常客,在某一些問題上樹狀數組解決的問題和線段樹是重疊的,
    5. 不過在另一些問題上樹狀數組也有它獨特的優點。

區間相關的問題

  1. 對於區間相關的問題不必定使用線段樹或者樹狀數組這樣的專門的數據結構來解決
    1. 和區間相關的有一類很是經典的問題,叫作 RMQ(Range Minimum Query),
    2. 也就是在一個區間中去相應的查詢最小值,
    3. 其實使用本身實現的線段樹徹底能夠解決這個問題,
    4. 不過對於 RMQ 問題因爲它太過經典,
    5. 有很是多個研究相應的也產生了很是多的其它辦法來解決這個問題,
    6. 而不只僅是使用線段樹或者是使用樹狀數組。
相關文章
相關標籤/搜索