四叉樹演算法用於將二維空間劃分成更多的矩形部分,將每個矩形劃分成四個大小相等的區域,常用於碰撞檢測演算法,d3中的forceCollide、forceManyBody等都用到了該資料結構。
d3.quadtree
/**
* d3.quadtree用於生成四叉樹
* @param {object} nodes 將要新增到quadtree中的所有節點
* @param {function} x 用於獲取節點的x座標的函式
* @param {function} y 用於獲取節點的y座標的函式
* @return {object} 該quadtree物件
*/
function quadtree(nodes, x, y) {
var tree = new Quadtree(x == null ? defaultX : x, y == null ? defaultY : y, NaN, NaN, NaN, NaN);
return nodes == null ? tree : tree.addAll(nodes);
}
/**
* 四叉樹的建構函式
* @param {function} x 獲取x座標
* @param {function} y 獲取y座標
* @param {number} x0 [x0, y0]到[x1, y1]為該quadtree的矩形區域的範圍
* @param {number} y0 [x0, y0]到[x1, y1]為該quadtree的矩形區域的範圍
* @param {number} x1 [x0, y0]到[x1, y1]為該quadtree的矩形區域的範圍
* @param {number} y1 [x0, y0]到[x1, y1]為該quadtree的矩形區域的範圍
*/
function Quadtree(x, y, x0, y0, x1, y1) {
this._x = x;
this._y = y;
this._x0 = x0;
this._y0 = y0;
this._x1 = x1;
this._y1 = y1;
this._root = undefined;
}
複製程式碼
quadtree.add
function tree_add(d) {
var x = +this._x.call(null, d),
y = +this._y.call(null, d);
return add(this.cover(x, y), x, y, d);
}
/* d3.quadtree的add方法,用於新增node
* add方法的執行流程如下:
* 1. 首次新增data0時,由於_root不存在,直接對其賦值data並返回。
* 2. 第二次新增data1時,node中此時是第一次新增的data0,執行do while迴圈,為_root賦值一個空陣列。判斷data1和data0是否是在一個象限(這裡將一個區域劃分成的四塊叫做象限),如果不在則根據索引分別新增至陣列中;
* 如果在則對該象限再次劃分四個區域,繼續判斷是否在同一個象限。
* 3. 之後每次新增data時,都會先查詢該節點屬於哪個象限,根據索引查詢node,如果是陣列則說明該區域有節點且已經再次劃分了象限,則繼續進入查詢;如果是物件,則說明該區域只有一個節點,此時會對該區域進行劃分執行步驟2中的過程;如果是undefined則直接插入該出。
*
*/
function add(tree, x, y, d) {
if (isNaN(x) || isNaN(y)) return tree;
var parent,
node = tree._root,
leaf = {data: d},
x0 = tree._x0,
y0 = tree._y0,
x1 = tree._x1,
y1 = tree._y1,
//(xm, ym)表示該區域的中心點
xm,
ym,
xp,
yp,
right,
bottom,
i,
j;
// 如果treenode當前不包含任何node,將leaf作為其節點
if (!node) return tree._root = leaf, tree;
// 看(x, y)是否和已有的點在一個象限中,若不是則直接插入,否則往下繼續執行
while (node.length) {
if (right = x >= (xm = (x0 + x1) / 2)) x0 = xm; else x1 = xm;
if (bottom = y >= (ym = (y0 + y1) / 2)) y0 = ym; else y1 = ym;
if (parent = node, !(node = node[i = bottom << 1 | right])) return parent[i] = leaf, tree;
}
// 判斷(x, y)是否已存在當前節點中
xp = +tree._x.call(null, node.data);
yp = +tree._y.call(null, node.data);
if (x === xp && y === yp) return leaf.next = node, parent ? parent[i] = leaf : tree._root = leaf, tree;
// 將當前區域進行劃分,直至(x, y)和之前的節點不在同一個象限內
do {
//parent = parent[i] = new Array(4)會為parent賦值一個空陣列,但是由於node = parent,node會形成一個多維陣列
parent = parent ? parent[i] = new Array(4) : tree._root = new Array(4);
if (right = x >= (xm = (x0 + x1) / 2)) x0 = xm; else x1 = xm;
if (bottom = y >= (ym = (y0 + y1) / 2)) y0 = ym; else y1 = ym;
} while ((i = bottom << 1 | right) === (j = (yp >= ym) << 1 | (xp >= xm)));
return parent[j] = node, parent[i] = leaf, tree;
}
複製程式碼
quadtree.addAll
//d3.quadtree的addAll方法,先計算資料的範圍調整quadtree,之後再新增資料
function addAll(data) {
var d, i, n = data.length,
x,
y,
xz = new Array(n),
yz = new Array(n),
x0 = Infinity,
y0 = Infinity,
x1 = -Infinity,
y1 = -Infinity;
// 根據_x和_y方法計算data值,得到x、y的範圍[x0, x1]和[y0, y1],即矩形區域的範圍
for (i = 0; i < n; ++i) {
// this._x和this._y是quadtree中定義的獲取x、y座標的方法
if (isNaN(x = +this._x.call(null, d = data[i])) || isNaN(y = +this._y.call(null, d))) continue;
xz[i] = x;
yz[i] = y;
if (x < x0) x0 = x;
if (x > x1) x1 = x;
if (y < y0) y0 = y;
if (y > y1) y1 = y;
}
// 無效點時的處理
if (x1 < x0) x0 = this._x0, x1 = this._x1;
if (y1 < y0) y0 = this._y0, y1 = this._y1;
// 為quadtree新增範圍
this.cover(x0, y0).cover(x1, y1);
// 新增node
for (i = 0; i < n; ++i) {
add(this, xz[i], yz[i], data[i]);
}
return this;
}
複製程式碼
quadtree.cover
//為quadtree設定區域範圍
function tree_cover(x, y) {
if (isNaN(x = +x) || isNaN(y = +y)) return this;
var x0 = this._x0,
y0 = this._y0,
x1 = this._x1,
y1 = this._y1;
// 如果該quadtree範圍不存在,則根據當前的(x, y)座標取範圍
if (isNaN(x0)) {
x1 = (x0 = Math.floor(x)) + 1;
y1 = (y0 = Math.floor(y)) + 1;
}
// 如果(x, y)在當前範圍之外,則擴充套件當前範圍
else if (x0 > x || x > x1 || y0 > y || y > y1) {
var z = x1 - x0,
node = this._root,
parent,
i;
//將該矩形區域的中心看做座標軸原點,根據x、y座標軸劃分成大小相等的四塊區域,0表示右下方,1表示左下方,2表示右上方,3表示左上方。
//成倍的增長z,擴大當前範圍直至(x, y)在當前區域內,在擴大範圍的同時不斷的構造node陣列
switch (i = (y < (y0 + y1) / 2) << 1 | (x < (x0 + x1) / 2)) {
case 0: {
do parent = new Array(4), parent[i] = node, node = parent;
while (z *= 2, x1 = x0 + z, y1 = y0 + z, x > x1 || y > y1);
break;
}
case 1: {
do parent = new Array(4), parent[i] = node, node = parent;
while (z *= 2, x0 = x1 - z, y1 = y0 + z, x0 > x || y > y1);
break;
}
case 2: {
do parent = new Array(4), parent[i] = node, node = parent;
while (z *= 2, x1 = x0 + z, y0 = y1 - z, x > x1 || y0 > y);
break;
}
case 3: {
do parent = new Array(4), parent[i] = node, node = parent;
while (z *= 2, x0 = x1 - z, y0 = y1 - z, x0 > x || y0 > y);
break;
}
}
if (this._root && this._root.length) this._root = node;
}
// 如果(x, y)已經在當前範圍內,則直接返回
else return this;
this._x0 = x0;
this._y0 = y0;
this._x1 = x1;
this._y1 = y1;
return this;
}
複製程式碼
quadtree.extend
//若有引數且_ = [[x0, y0], [x1, y1]]用於通過cover方法設定quadtree的範圍[x0, y0]和[x1, y1];若沒有引數,則以同樣的陣列形式返回當前區域的範圍。
function tree_extent(_) {
return arguments.length
? this.cover(+_[0][0], +_[0][1]).cover(+_[1][0], +_[1][1])
: isNaN(this._x0) ? undefined : [[this._x0, this._y0], [this._x1, this._y1]];
}
複製程式碼
quadtree.data
//返回quadtree中所有的node
function tree_data() {
var data = [];
this.visit(function(node) {
if (!node.length) do data.push(node.data); while (node = node.next)
});
return data;
}
複製程式碼
quadtree.find
//查詢以(x, y)為中心,radius為半徑的範圍內離中心最近的點
function tree_find(x, y, radius) {
var data,
//(x0, y0)和(x3, y3)表示以(x, y)為中心的矩形搜尋區域
x0 = this._x0,
y0 = this._y0,
//(x1, y1)和(x2, y2)表示當前node所在的區域範圍
x1,
y1,
x2,
y2,
x3 = this._x1,
y3 = this._y1,
quads = [],
node = this._root,
q,
i;
if (node) quads.push(new Quad(node, x0, y0, x3, y3));
//若沒有設定radius則預設為Infinity
if (radius == null) radius = Infinity;
else {
x0 = x - radius, y0 = y - radius;
x3 = x + radius, y3 = y + radius;
radius *= radius;
}
while (q = quads.pop()) {
// 如果node不存在或者(x, y)在該node範圍外則跳過執行
if (!(node = q.node)
//node所在區域與搜尋區域不重疊
//
//
|| (x1 = q.x0) > x3
|| (y1 = q.y0) > y3
|| (x2 = q.x1) < x0
|| (y2 = q.y1) < y0) continue;
// 如果node為陣列說明已對其進行了區域劃分,開始遞迴查詢
if (node.length) {
var xm = (x1 + x2) / 2,
ym = (y1 + y2) / 2;
// node陣列中0表示區域左上角,1表示右上角,2表示左下角,3表示右下角
quads.push(
new Quad(node[3], xm, ym, x2, y2),
new Quad(node[2], x1, ym, xm, y2),
new Quad(node[1], xm, y1, x2, ym),
new Quad(node[0], x1, y1, xm, ym)
);
// 判斷(x, y)所在的象限,並將該象限對應的node與棧頂的資料交換位置,如果是左上角區域及表示已是棧頂則不用處理
if (i = (y >= ym) << 1 | (x >= xm)) {
q = quads[quads.length - 1];
quads[quads.length - 1] = quads[quads.length - 1 - i];
quads[quads.length - 1 - i] = q;
}
}
// 當查詢到在搜尋範圍內的點後,縮小搜尋範圍
else {
var dx = x - +this._x.call(null, node.data),
dy = y - +this._y.call(null, node.data),
d2 = dx * dx + dy * dy;
if (d2 < radius) {
var d = Math.sqrt(radius = d2);
x0 = x - d, y0 = y - d;
x3 = x + d, y3 = y + d;
data = node.data;
}
}
}
return data;
}
複製程式碼
quadtree.remove
function tree_remove(d) {
if (isNaN(x = +this._x.call(null, d)) || isNaN(y = +this._y.call(null, d))) return this;
var parent,
node = this._root,
retainer,
previous,
next,
x0 = this._x0,
y0 = this._y0,
x1 = this._x1,
y1 = this._y1,
x,
y,
xm,
ym,
right,
bottom,
i,
j;
if (!node) return this;
// 當node中有多個點時,進入查詢
if (node.length) while (true) {
//計算(x, y)所在的象限
if (right = x >= (xm = (x0 + x1) / 2)) x0 = xm; else x1 = xm;
if (bottom = y >= (ym = (y0 + y1) / 2)) y0 = ym; else y1 = ym;
//如果對應的象限中沒有點,則說明查詢不到該點,直接返回。
if (!(parent = node, node = node[i = bottom << 1 | right])) return this;
//如果該象限只有一個點則跳出迴圈往下執行。
if (!node.length) break;
//
if (parent[(i + 1) & 3] || parent[(i + 2) & 3] || parent[(i + 3) & 3]) retainer = parent, j = i;
}
// TODO: 這裡存在一個問題,由於node.data和d都為陣列,但是兩個資料是不同的指標,因此這裡不會相等
while (node.data !== d) if (!(previous = node, node = node.next)) return this;
if (next = node.next) delete node.next;
// If there are multiple coincident points, remove just the point.
if (previous) return (next ? previous.next = next : delete previous.next), this;
// If this is the root point, remove it.
if (!parent) return this._root = next, this;
// Remove this leaf.
next ? parent[i] = next : delete parent[i];
// If the parent now contains exactly one leaf, collapse superfluous parents.
if ((node = parent[0] || parent[1] || parent[2] || parent[3])
&& node === (parent[3] || parent[2] || parent[1] || parent[0])
&& !node.length) {
if (retainer) retainer[j] = node;
else this._root = node;
}
return this;
}
複製程式碼
quadtree.visit
//採用先序遍歷的方式,如果callback返回true,則執行不再訪問其子節點;否則繼續訪問其子節點
function tree_visit(callback) {
var quads = [], q, node = this._root, child, x0, y0, x1, y1;
if (node) quads.push(new Quad(node, this._x0, this._y0, this._x1, this._y1));
while (q = quads.pop()) {
if (!callback(node = q.node, x0 = q.x0, y0 = q.y0, x1 = q.x1, y1 = q.y1) && node.length) {
var xm = (x0 + x1) / 2, ym = (y0 + y1) / 2;
if (child = node[3]) quads.push(new Quad(child, xm, ym, x1, y1));
if (child = node[2]) quads.push(new Quad(child, x0, ym, xm, y1));
if (child = node[1]) quads.push(new Quad(child, xm, y0, x1, ym));
if (child = node[0]) quads.push(new Quad(child, x0, y0, xm, ym));
}
}
return this;
}
複製程式碼
quadtree.visitAfter
//採用後序遍歷的方式,先將所有節點存入陣列中,然後依次對所有節點進行操作。
function tree_visitAfter(callback) {
var quads = [], next = [], q;
if (this._root) quads.push(new Quad(this._root, this._x0, this._y0, this._x1, this._y1));
while (q = quads.pop()) {
var node = q.node;
if (node.length) {
var child, x0 = q.x0, y0 = q.y0, x1 = q.x1, y1 = q.y1, xm = (x0 + x1) / 2, ym = (y0 + y1) / 2;
if (child = node[0]) quads.push(new Quad(child, x0, y0, xm, ym));
if (child = node[1]) quads.push(new Quad(child, xm, y0, x1, ym));
if (child = node[2]) quads.push(new Quad(child, x0, ym, xm, y1));
if (child = node[3]) quads.push(new Quad(child, xm, ym, x1, y1));
}
next.push(q);
}
while (q = next.pop()) {
callback(q.node, q.x0, q.y0, q.x1, q.y1);
}
return this;
}
複製程式碼
quadtree.copy
//對quadtree進行復制,但是是通過引用來複制的而不是複製值。
treeProto.copy = function() {
var copy = new Quadtree(this._x, this._y, this._x0, this._y0, this._x1, this._y1),
node = this._root,
nodes,
child;
if (!node) return copy;
if (!node.length) return copy._root = leaf_copy(node), copy;
nodes = [{source: node, target: copy._root = new Array(4)}];
while (node = nodes.pop()) {
for (var i = 0; i < 4; ++i) {
if (child = node.source[i]) {
if (child.length) nodes.push({source: child, target: node.target[i] = new Array(4)});
else node.target[i] = leaf_copy(child);
}
}
}
return copy;
};
//複製葉子節點
function leaf_copy(leaf) {
var copy = {data: leaf.data}, next = copy;
while (leaf = leaf.next) next = next.next = {data: leaf.data};
return copy;
}
複製程式碼
quad物件
在quadtree的一些方法中使用到了quad物件用於儲存quadtree中的node資訊,包括node的值和其所在區域的座標範圍。
/**
* Quad建構函式
* @param {object} node 節點資料
* @param {number} x0 該節點的區域座標範圍
* @param {number} y0 該節點的區域座標範圍
* @param {number} x1 該節點的區域座標範圍
* @param {number} y1 該節點的區域座標範圍
*
* quad物件在quadtree的node中的位置如下:
*
* |
* 0 | 1
* |
* -------|--------
* |
* 2 | 3
* |
*/
function Quad(node, x0, y0, x1, y1) {
this.node = node;
this.x0 = x0;
this.y0 = y0;
this.x1 = x1;
this.y1 = y1;
}
複製程式碼