R樹-javascript代碼實現過程分析(插入操做)

R Tree

第一步,建立R樹類。

構建一個RTree生成器。用以建立tree對象。node

例子:var tree = new RTree(12)git

var RTree = function(width){ var _Min_Width = 3;  // Minimum width of any node before a merge
    var _Max_Width = 6;  // Maximum width of any node before a split
    if(!isNaN(width)){ _Min_Width = Math.floor(width/2.0); _Max_Width = width;}
    // Start with an empty root-tree
    var _T = {x:0, y:0, w:0, h:0, id:"root", nodes:[] }; var isArray = function(o) { return Object.prototype.toString.call(o) === '[object Array]'; }; var _attach_data = function(node, more_tree){ node.nodes = more_tree.nodes; node.x = more_tree.x; node.y = more_tree.y; node.w = more_tree.w; node.h = more_tree.h; return(node); }; //選擇適合的節點來存放插入的條目。
  //@private。
  var _choose_leaf_subtree = (rect, root) => {...} //內部插入函數。
  //[] = _insert_subtree(rectangle, object to insert, root to begin insertion at) 
  //@private。即私有函數,只能用RTree的方法調用它。
  var _insert_subtree = (node, root) => {...} this.get_tree = function() { return _T} //new_tree表明新的子樹節點,where表明要替代的位置。 
  this.set_tree = (new_tree, where) => { if (!where) { where = _T } return (_attach_data(where, new_tree)) } //rect是邊界矩陣,對象是葉子節點。 
  this.insert = (rect, obj) => { if (arguments.length < 2) { throw "Wrong number of arguments" } return (_insert_subtree({x:rect.x, y:rect.y, w:rect.w, h:rect.h, leaf:obj}, _T)) } // End of RTree 
}

 tree.insert方法,用以向生成的R樹,插入數據。方法見下:github

把一個新的索引條目E插入一個R樹內:

  1. 找到插入新記錄的位置: 這裏要調用Choose Leaf方法,選擇一個葉節點L來存放E。
  2. 把記錄E加入到葉節點中: 這裏須要進行判斷。
    • 若是L.nodes <= M(即L的條目數量此時小於等於規定的最大值M),則下一步;
    • 不然, 須要分裂,調用Split Node方法。把葉子節點L分裂成2個新節點L和LL(2個新節點包含了原來的L節點的全部條目和新條目E)。
  3. 向上傳遞變化:調用Adjust Tree方法對L節點操做。若是上一步是分裂操做,則對2個新節點調用Adjust Tree方法。
  4. 判斷:是否樹增高。若是節點的分裂致使了root的分裂,則須要生成新的root,而且讓它的兩個孩子節點爲原來的root分裂後產生的2個節點。

 

第二步:生成R樹。

var tree = new RTree(12)、 tree.insert(rect, obj)

調用tree.insert(rect, obj)方法, 向R樹插入數據,參數有2個,rect是邊界矩陣對象,obj是節點對象。算法

 

由此,引入rectangle的構建器。

//Rectangle - 生成rectangle對象。 
RTree.Rectangle = function(ix, iy, iw, ih) { var x, x2, y, y2, w, h; if(ix.x) { x = ix.x;y = ix.y; //獲得左下角座標
    
    if(ix.w !== 0 && !ix.w && ix.x2){ //若是長,寬不存在,則計算出來。
      w = ix.x2-ix.x;    h = ix.y2-ix.y; } else { w = ix.w;    h = ix.h; } x2 = x + w; y2 = y + h; //獲得第右上角的座標
    } else { x = ix; y = iy;    //獲得左下角座標
    w = iw;    h = ih; x2 = x + w; y2 = y + h;  //獲得第右上角的座標
 } this.x1 = this.x = x; this.y1 = this.y = y; this.x2 = x2; this.y2 = y2; this.w =  w; this.h =  h; //矩陣a和當前矩陣產生部分重合則,返回true。
  this.overlap = (a) => {...} //擴展當前矩陣。根據傳入的矩陣a,來擴展,包含矩陣a. this.expand = (a) => {...} //重置當前矩陣的座標和長寬。代碼同初始化Rectangle的代碼。 this.setRect = (ix, iy, iw ,ih) {...} // End of Rectangle 
}

 

overlap方法的解釋(代碼):數組

 

expand方法的解釋:函數

插入一個矩陣,到葉子節點,對應的父親的最小邊界矩陣由此可能要擴展。矩陣b要把插入的a的矩陣包含進本身。優化

this.expand = function(a) { var nx = Math.min(this.x(), a.x()); var ny = Math.min(this.y(), a.y()); w = Math.max(this.x2(), a.x2()) - nx; h = Math.max(this.y2(), a.y2()) - ny; x = nx; y = ny; return(this); };

 

第三步:插入方法講解

這裏要調用插入方法。this

//獲得一個最小邊界矩陣。
var rect = new RTree.Rectangle(2,2,3,3)
var tree = new RTree(12) tree.insert(rect, obj)

 

在插入一個rect後,爲了要把rect放到正確的葉節點中。首先要找到這個葉節點,須要調用choose leaf方法。url

//在insert方法內的_insert方法內調用
 var tree_stack = _choose_leaf_subtree(node, root) //node參數就是最開始傳入insert()的第一個參數

 

方法_choose_leaf_subtree:spa

//選擇適合的節點來存放插入的條目。
  //從root節點開始一路向下,每次找到當前節點的條目中,那個被插入新矩陣後,須要擴展最小的條目。就是被選擇的條目。
  //直到到達葉子節點。最後返回:從root節點到葉子節點,通過的節點的集合數組。
  var _choose_leaf_subtree = (rect, root) => { var best_choice_index = -1;  //記錄最合適的節點索引,並用它來控制do..while循環
    var best_choice_stack = [];  //方法結束後,返回從root節點到葉子節點,通過的節點的集合數組。
    var best_choice_area;        //用於比較擴展的區域大小。
 best_choice_stack.push(root); //返回的變量的第一個元素是root。
    var nodes = root.nodes;    //首先,變量nodes記錄根節點的全部的條目,用於循環代碼。
    
    do { if(best_choice_index != -1){ best_choice_stack.push(nodes[best_choice_index]); //儲存當前最合適的條目。
        nodes = nodes[best_choice_index].nodes;  //修改nodes,爲被選中的條目的nodes集合。其實就是準備下一層的條目的判斷。
        best_choice_index = -1; } //第一次循環,遍歷root的全部項目。
      //變量:當前節點的全部條目/項目。找到添加rect後,擴展最小的那個條目。 
      //當i等於-1,當前節點的全部條目的判斷結束,
      for(var i = nodes.length-1; i >= 0; i--) { var ltree = nodes[i]; //若是到達葉節點,結束for循環。leaf是hash對象的索引,由_insert_subtree傳入。
        if("leaf" in ltree) { best_choice_index = -1; //經過變量,同時也保證會退出do..while循環。
          break; } 
        //下面的代碼用於計算當前條目被插入新矩陣後,擴展的面積。而後用best_choice_area記錄最小的擴張面積。
        //這裏使用一種特殊的算法。 
        //原矩陣正方化。一種算法。
        var old_lratio = RTree.Rectangle.squarified_ratio(ltree.w, ltree.h, ltree.nodes.length+1); // 擴展矩陣。
        var nw = Math.max(ltree.x+ltree.w, rect.x+rect.w) - Math.min(ltree.x, rect.x); var nh = Math.max(ltree.y+ltree.h, rect.y+rect.h) - Math.min(ltree.y, rect.y); // 新擴展的矩陣的正方化。
        var lratio = RTree.Rectangle.squarified_ratio(nw, nh, ltree.nodes.length+2); //擴展的面積的比較,咱們須要用變量記錄最小擴展面積。
        if(best_choice_index < 0 || Math.abs(lratio - old_lratio) < best_choice_area) { best_choice_area = Math.abs(lratio - old_lratio); best_choice_index = i; } } } while(best_choice_index != -1) return(best_choice_stack) }

 

找到要插入的位置後,進行第2步判斷是否須要分裂節點。

再而後進行第3步,向上調整邊界矩陣。

  • 這時要判斷第二步是否有分裂節點的狀況。若是是,那麼對分裂出來的2個新節點的矩陣都要進行性調整。即調用RTree.Rectangle.expand_rectangle方法。
  • 若是false。就對原來的節點調整。

最後一路到達根節點,一樣對根節點進行第2步的判斷,第3步的調整,最後完成插入操做。

 

看_insert_subtree

//[] = _insert_subtree(rectangle, object to insert, root to begin insertion at) 
  //@private。即私有函數,只能用RTree的方法調用它。
  var _insert_subtree = (node, root) => { var bc //Best current node
    
    // 初始化插入。若是根節點尚未兒子,那麼這個節點的最小邊界矩陣,就是root節點的MBR。
    if (root.nodes.length == 0) { root.x = node.x; root.y = node.y; root.w = node.w; root.h = node.h; root.nodes.push(node); return; } //找到最適合的葉子節點來插入條目
    var tree_stack = _choose_leaf_subtree(node, root) //return獲得從root到葉子,通過的全部節點的集合。
    var ret_obj = node //{x: rect.x, y:rect.y, w:rect.w, h:rect.h, leaf:obj}, 這個變量表明循環內要調整的條目。
    
    //向上傳遞變化。包括插入的第2-4步驟,對tree_stack增減其中的元素,控制循環次數。
    do { //第一次循環會調用else塊的語句,bc被賦值爲葉節點對象, 同時tree_stack也發生變化,用於控制循環。
            if(bc && "nodes" in bc && bc.nodes.length == 0) {  //handle the case of an empty node (from a split) 。 刪除空節點。
                var pbc = bc; // Past bc
                bc = tree_stack.pop(); for(var t=0; t<bc.nodes.length; t++)     //for循環沒有帶{},⚠️這種寫法 if(bc.nodes[t] === pbc || bc.nodes[t].nodes.length == 0) { bc.nodes.splice(t, 1);  //刪除這個條目。
                      break;  } } else { bc = tree_stack.pop(); } // If there is data attached to this ret_obj,
      // 若是rec_obj對象含有屬性"leaf",或"nodes",或一個數組(內含多2個新節點/條目)
            if("leaf" in ret_obj || "nodes" in ret_obj || isArray(ret_obj)) { // 調整和插入。
                if(isArray(ret_obj)) {  //若是上一輪循環是分裂狀況,那麼須要把分裂的節點放入父親點,並調整矩陣。
                    for(var ai = 0; ai < ret_obj.length; ai++) {      //讓bc擴展到能夠包含全部ret_obj內的條目。
 RTree.Rectangle.expand_rectangle(bc, ret_obj[ai]); } bc.nodes = bc.nodes.concat(ret_obj);              //葉節點bc的條目增長
                } else { //正常狀況,也是調整矩陣,而後插入。 RTree.Rectangle.expand_rectangle(bc, ret_obj); bc.nodes.push(ret_obj); // 插入一個條目到節點bc。
 }      //當插入完成後,第二步判斷bc的條目是否超出最大值限制。
             // true: rec_obj被從新賦值,由於沒有"leaf",'nodes'屬性,後續輪循環表明一路向上調整最小限定矩陣。
             // false: 則須要分裂。而後也要對分裂後的節點進行調整。
                if(bc.nodes.length <= _Max_Width) { ret_obj = {x:bc.x,y:bc.y,w:bc.w,h:bc.h};  //後續循環只需調整MBR。
                }    else { // 不然,要分裂
                    // 調用linear_split(),返回包括2個新節點的數組。
                    // formed from the split of the previous node's overflow
                    var a = _linear_split(bc.nodes); ret_obj = a; //這時ret_obj是一個數組。
                    
                    if(tree_stack.length < 1)    { // 若是正在分裂root節點, tree_stack已經爲空。這是插入操做的第4步。
                        bc.nodes.push(a[0]); tree_stack.push(bc); //從新考慮root元素。
                        ret_obj = a[1]; } /*else { delete bc; }*/ } } else {     //插入操做第3步驟。
            //若是不是上面的狀況:rect_obj只是一個含有矩陣信息的對象。就只更新bc的最小限定矩陣。
 RTree.Rectangle.expand_rectangle(bc, ret_obj); ret_obj = {x:bc.x,y:bc.y,w:bc.w,h:bc.h}; } }while(tree_stack.length > 0) }

 

根據插入操做的流程,可理解代碼。這裏尚未講解分裂方法:_linear_split()。

爲了優化R樹,大神們開發了多種分裂算法。這裏使用的是linear split。

具體看 Hilbert R樹發展 這篇文章講解了分裂算法的發展歷史。

⚠️R*樹的方法是對R樹最好的優化。

 

分裂算法也是很複雜的。沒有仔細理解這個分裂算法。

 //split方法:分裂一個節點的條目,把它們放到2個新的節點中。⚠️分裂方法不一樣,放置也不一樣。這裏使用linear split。
    // [ an array of two new arrays of nodes ] = linear_split(array of nodes)
    // @private
    var _linear_split = function(nodes) { var n = _pick_linear(nodes); while(nodes.length > 0) { _pick_next(nodes, n[0], n[1]); } return(n); };

 

裏面的私有方法:

  1. pcik_linear返回數組,把原來數組內的條目,分紅2組。每組的條目屬於一個新的節點。
  2. pick_next則是把最好的MBR插入到節點a, b。

具體代碼見:https://github.com/imbcmdth/RTree/blob/master/src/rtree.js

 

關於插入操做就講解完了。

相關文章
相關標籤/搜索