「乾貨」面試官問我如何快速搜尋10萬個矩形?——我說RBUSH

前言

親愛的coder們,我又來了,一個喜歡圖形的程式設計師‍,前幾篇文章一直都在教大家怎麼畫地圖、畫折線圖、畫煙花,難道圖形就是這樣嘛,當然不是,一個很簡單的問題, 如果我在canvas中畫了10萬個點,滑鼠在畫布上移動,靠近哪一個點,哪一個點高亮。有同學就說遇事不決 用for迴圈遍歷哇,我也知道可以用迴圈解決哇,迴圈解決幾百個點可以,如果是幾萬甚至幾百萬個點你還迴圈,你想讓使用者等死?這時就引入今天的主角他來了就是Rbush

RBUSH

我們先看下定義,這個rbush到底能幫我們解決了什麼問題?

RBush是一個high-performanceJavaScript庫,用於點和矩形的二維空間索引。它基於優化的R-tree資料結構,支援大容量插入。空間索引是一種用於點和矩形的特殊資料結構,允許您非常高效地執行“此邊界框中的所有專案”之類的查詢(例如,比在所有專案上迴圈快數百倍)。它最常用於地圖和資料視覺化。

看定義他是基於優化的R-tree資料結構,那麼R-tree又是什麼呢?

R-trees是用於空間訪問方法的樹資料結構,即用於索引多維資訊,例如地理座標矩形多邊形。R-tree 在現實世界中的一個常見用途可能是儲存空間物件,例如餐廳位置或構成典型地圖的多邊形:街道、建築物、湖泊輪廓、海岸線等,然後快速找到查詢的答案例如“查詢我當前位置 2 公里範圍內的所有博物館”、“檢索我所在位置 2 公里範圍內的所有路段”(以在導航系統中顯示它們)或“查詢最近的加油站”(儘管不將道路進入帳戶)。

R-tree的關鍵思想是將附近的物件分組,並在樹的下一個更高級別中用它們的最小邊界矩形表示它們;R-tree 中的“R”代表矩形。由於所有物件都位於此邊界矩形內,因此不與邊界矩形相交的查詢也不能與任何包含的物件相交。在葉級,每個矩形描述一個物件;在更高級別,聚合包括越來越多的物件。這也可以看作是對資料集的越來越粗略的近似。說著有點抽象,還是看一張圖:

我來詳細解釋下這張圖:

  1. 首先我們假設所有資料都是二維空間下的點,我們從圖中這個R8區域說起,也就是那個shape of data object。別把那一塊不規則圖形看成一個數據,我們把它看作是多個數據圍成的一個區域。為了實現R樹結構,我們用一個最小邊界矩形恰好框住這個不規則區域,這樣,我們就構造出了一個區域:R8。R8的特點很明顯,就是正正好好框住所有在此區域中的資料。其他實線包圍住的區域,如R9,R10,R12等都是同樣的道理。這樣一來,我們一共得到了12個最最基本的最小矩形。這些矩形都將被儲存在子結點中。

  2. 下一步操作就是進行高一層次的處理。我們發現R8,R9,R10三個矩形距離最為靠近,因此就可以用一個更大的矩形R3恰好框住這3個矩形。

  3. 同樣道理,R15,R16被R6恰好框住,R11,R12被R4恰好框住,等等。所有最基本的最小邊界矩形被框入更大的矩形中之後,再次迭代,用更大的框去框住這些矩形。

演算法

插入

為了插入一個物件,樹從根節點遞迴遍歷。在每一步,檢查當前目錄節點中的所有矩形,並使用啟發式方法選擇候選者,例如選擇需要最少放大的矩形。搜尋然後下降到這個頁面,直到到達葉節點。如果葉節點已滿,則必須在插入之前對其進行拆分。同樣,由於窮舉搜尋成本太高,因此採用啟發式方法將節點一分為二。將新建立的節點新增到上一層,這一層可以再次溢位,並且這些溢位可以向上傳播到根節點;當這個節點也溢位時,會建立一個新的根節點並且樹的高度增加。

搜尋

範圍搜尋中,輸入是一個搜尋矩形(查詢框)。搜尋從樹的根節點開始。每個內部節點包含一組矩形和指向相應子節點的指標,每個葉節點包含空間物件的矩形(指向某個空間物件的指標可以在那裡)。對於節點中的每個矩形,必須確定它是否與搜尋矩形重疊。如果是,則還必須搜尋相應的子節點。以遞迴方式進行搜尋,直到遍歷所有重疊節點。當到達葉節點時,將針對搜尋矩形測試包含的邊界框(矩形),如果它們位於搜尋矩形內,則將它們的物件(如果有)放入結果集中。

讀著就複雜,但是社群裡肯定有大佬替我們封裝好了,就不用自己再去手寫了,寫了寫估計不一定對哈哈哈。

RBUSH 用法

用法

 
 
 
 
 
 
 
// as a ES module
import RBush from 'rbush';
// as a CommonJS module
const RBush = require('rbush');
 

建立一個樹

 
 
 
 
 
 
 
const tree = new RBush(16);
 

後面的16 是一個可選項,RBush 的一個可選引數定義了樹節點中的最大條目數。 9(預設使用)是大多數應用程式的合理選擇。 更高的值意味著更快的插入和更慢的搜尋,反之亦然

插入資料

 
 
 
 
 
 
 
const item = {
    minX: 20,
    minY: 40,
    maxX: 30,
    maxY: 50,
    foo: 'bar'
};
tree.insert(item);
 

刪除資料

 
 
 
 
 
 
 
tree.remove(item);
 

預設情況下,RBush按引用移除物件。但是,您可以傳遞一個自定義的equals函式,以便按刪除值進行比較,當您只有需要刪除的物件的副本時(例如,從伺服器載入),這很有用:

 
 
 
 
 
 
 
tree.remove(itemCopy, (a, b) => {
    return a.id === b.id;
});
 

刪除所有資料

 
 
 
 
 
 
 
tree.clear();
 

搜尋

 
 
 
 
 
 
 
const result = tree.search({
    minX: 40,
    minY: 20,
    maxX: 80,
    maxY: 70
});
 

api 介紹完畢下面開始進入實戰環節一個簡單的小案例——canvas中畫布搜尋的。

用圖片填充畫布

填充畫布的的過程中,這裡和大家介紹一個canvas點的api ——createPattern

**CanvasRenderingContext2D**.createPattern()是 Canvas 2D API 使用指定的影象 (CanvasImageSource)建立模式的方法。 它通過repetition引數在指定的方向上重複元影象。此方法返回一個CanvasPattern物件。

第一個引數是填充畫布的資料來源可以是下面這:

第二個引數指定如何重複影象。允許的值有:

如果為空字串 ('') 或null (但不是undefined),repetition將被當作"repeat"。

程式碼如下:

這邊有個小提醒的就是圖片載入成功的回撥裡面去給畫布建立模式,然後就是this 指向問題, 最後就是填充畫布。

如圖:

資料的生成

資料生成主要在畫布的寬度 和長度的範圍內隨機生成10萬個矩形。插入到rbush資料的格式就是有minX、maxX、minY、maxY。這個實現的思路也是非常的簡單哇, minX用畫布的長度Math.random minY 就是畫布的高度Math.random. 然後最大再此基礎上隨機*20 就OK了,一個矩形就形成了。這個實現的原理就是左上和右下兩個點可以形成一個矩形。程式碼如下:

然後迴圈加入10萬條資料:

畫布填充

這裡我建立一個和當前畫布一抹一樣的canvas,但是裡面畫了n個矩形,將這個畫布 當做圖片填充到原先的畫布中。

然後在載入資料的時候,在當前畫布畫了10000個矩形。這時候新建的畫布有東西了,然後我們用一個drawImage api ,

這個api做了這樣的一個事,就是將畫布用特定資源填充,然後你可以改變位置,後面有引數可以修改,這裡我就不多介紹了,傳送門

this.ctx.drawImage(this.memCanv, 0, 0)

我們看下效果:

新增互動

新增互動, 就是對畫布新增mouseMove 事件, 然後呢我們以滑鼠的位置,形成一個搜尋的資料,然後我在統計花費的時間,然後你就會發現,這個Rbush 是真的快。程式碼如下:

 this.canvas.addEventListener('mousemove', this.handler.bind(this))
// mouseMove 事件
handler(e) {
this.clearRect()
const x = e.offsetX
const y = e.offsetY
this.bbox.minX = x - 20
this.bbox.maxX = x + 20
this.bbox.minY = y - 20
this.bbox.maxY = y + 20
const start = performance.now()
const res = this.tree.search(this.bbox)
this.ctx.fillStyle = this.pattern
this.ctx.strokeStyle = 'rgba(255,255,255,0.7)'
res.forEach((item) => {
this.drawRect(item)
})
this.ctx.fill()
this.res.innerHTML =
'Search Time (ms): ' + (performance.now() - start).toFixed(3)
}

這裡給大家講解一下,現在我們畫布是黑白的, 然後以滑鼠搜尋到資料後,然後我們畫出對應的矩形,這時候呢,可以將矩形的填充模式改成 pattern 模式,這樣便於我們看的更加明顯。fillStyle可以填充3種類型:

ctx.fillStyle = color;
ctx.fillStyle = gradient;
ctx.fillStyle = pattern;

分別代表的是:

OK講解完畢, 直接gif 看在1萬個矩形的搜尋中Rbush的表現怎麼樣。

這是1萬個矩形我換成10萬個矩形我們在看看效果:

我們發現增加到10萬個矩形,速度還是非常快的,增加到100萬個矩形,canvas 已經有點畫不出來了,整個頁面已經卡頓了,這邊涉及到canvas的效能問題,當圖形的數量過多,或者數量過大的時候,fps會大幅度下降的。

總結

最後總結下:rbush 是一種空間索引搜尋演算法,當你涉及到空間幾何搜尋的時候,尤其在地圖場景下,因為Rbush 實現的原理是比較搜尋物體的boundingBox 和已知的boundingBox 求交集, 如果不相交,那麼在樹的遍歷過程中就已經過濾掉了。最後文章寫作不易,如果有錯誤的話歡迎指正。如果看了對你有幫助的話,希望你能為我點個關注 和, 這是對我最大的支援!

學習交流

搜尋公眾號【前端圖形】,後臺回覆"加群"二字, 就可以加入視覺化學習交流群哦! 一起學習吧!

參考文獻

深入理解空間演算法

R樹詳細解釋

維基百科-R樹的介紹

Alex2wong