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樹內:
- 找到插入新記錄的位置: 這裏要調用Choose Leaf方法,選擇一個葉節點L來存放E。 把記錄E加入到葉節點中: 這裏須要進行判斷。
- 若是L.nodes <= M(即L的條目數量此時小於等於規定的最大值M),則下一步;
- 不然, 須要分裂,調用Split Node方法。把葉子節點L分裂成2個新節點L和LL(2個新節點包含了原來的L節點的全部條目和新條目E)。
- 向上傳遞變化:調用Adjust Tree方法對L節點操做。若是上一步是分裂操做,則對2個新節點調用Adjust Tree方法。
- 判斷:是否樹增高。若是節點的分裂致使了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); };
裏面的私有方法:
- pcik_linear返回數組,把原來數組內的條目,分紅2組。每組的條目屬於一個新的節點。
- pick_next則是把最好的MBR插入到節點a, b。
具體代碼見:https://github.com/imbcmdth/RTree/blob/master/src/rtree.js
關於插入操做就講解完了。