前面探討的各種二叉樹,使用一個鍵值在樹中導航以執行必要的操作,二叉樹中每個節點都有唯一的一個key值,通過key我們可以組織二叉查詢樹、平衡樹、自適應樹、堆等,從某種意義上講,這是一維的二叉樹。假如我們現在要研究二維平面上n個點的性質,怎麼將它們組織成二叉樹呢?如果是3維空間或者k維空間呢?對於一個節點來說,它不僅僅只有一個key值,如果它處於k維空間,那麼會有k個key值。我們必須探討出一種二叉樹,可以組織k維空間上的節點。這就是kd二叉樹,全稱是k-dimension二叉樹,k維二叉樹。
本文以3維空間為例來探討kd二叉樹的建立、查詢、新增、刪除。
假設某3維空間上存在7個點,分別是(x1,y2,z3)、(x2,y3,z1)、(x3,y1,z2)、(x4,y4,z4)、(x5,y6,z7)、(x6,y7,z5)、(x7,y5,z6)。其中x(n)<x(n+1),y(n)<y(n+1),z(n)<z(n+1)
- 建立
如果使用一維二叉查詢樹來組織以上7個節點,我們可以使用x座標作為鍵值(也可以使用y或者z),判斷在哪裡插入這個點,從而儲存所有的點。為了能夠獨立地使用3個鍵值,我們組建kd二叉樹時可以交替使用x,y,z。在第一層,用x座標作為識別符號,在第二層,使用y座標,在第三層,使用z座標,在第四層繼續使用x,以此類推,迴圈使用3個key值。最終的kd二叉查詢樹可能是下面這樣的:
其中,藍色的是x層,紅色的是y層,黑色的是z層。對於x層中的節點P來講,左子樹中任一節點的x的值都小於P節點的x值,右子樹中任一節點的x的值都大於P節點的x值。y層和z層同理。
以上的kd二叉查詢樹是完美平衡的。類似一維二叉查詢樹,相同的資料流,可以構造出各種各樣的二叉查詢樹。我們希望構造出來的二叉查詢樹儘可能平衡,平衡意味著在查詢資料時可以跳過更多的節點,更加有效率的查詢。一維二叉查詢樹是怎麼建立的呢?可以看資料結構與演算法-二叉查詢樹(DSW)。類比其中的建立邏輯,可以探討出kd二叉樹的建立邏輯。
首先,我們決定按照x->y->z的順序建立kd樹,根節點處於x層,那我們將當前7個節點按照x的大小進行排序:
(x1,y2,z3)<(x2,y3,z1)<(x3,y1,z2)<(x4,y4,z4)<(x5,y6,z7)<(x6,y7,z5)<(x7,y5,z6)
取出中間節點(x4,y4,z4)作為根節點,這樣做的好處是保證根節點的左右子樹節點個數相差不大於1。
第二層是根據節點的y值排序,左子樹節點排序如下:
(x3,y1,z2)<(x1,y2,z3)<(x2,y3,z1)
取出中間節點(x1,y2,z3)作為左子樹的根節點。
以此類推,可以將右子樹以及剩餘的節點安插到kd樹中,最終,kd樹是高度平衡的。
虛擬碼如下:
def kd_tree(points, depth):
if len(points) == 0:
return None
j = depth mod k
將points陣列中節點按照j維度大小排序
獲取陣列中間節點下標medium_index
node = Node(points[medium_index])
node.left = kd_tree(points[:medium_index], depth + 1)
node.right = kd_tree(points[medium + 1:], depth +1)
return node 複製程式碼
虛擬碼中使用遞迴來簡化邏輯,易於理解,實踐中不建議使用。
- 查詢
查詢的邏輯比較簡單、直觀。如果查詢(x7,y5,z6),逐層進行比較即可,核心邏輯在於不同層次比較的key值不同。動態圖如下:
- 新增
新增的的邏輯也是相當直觀,就像查詢一樣,這裡就不多說了。
- 刪除
無論是一維的二叉查詢樹還是kd二叉查詢樹,刪除邏輯都是比較複雜的。一維二叉查詢樹刪除看這裡資料結構與演算法-二叉查詢樹。類比一維二叉查詢樹的刪除,從最直觀的角度出發,我們將kd樹刪除分成3種情況來考察。
1、刪除葉子節點
毫無疑問,刪除葉子節點直接釋放葉子節點空間即可,因為葉子節點沒有子樹需要處理,所以直接刪除。
2、刪除度為1的節點
在一維二叉查詢樹中,刪除度為1的節點也是比較簡單的,直接將唯一的子樹提升到被刪除節點層次即可。但是在kd樹中,這樣處理是不行的。因為被刪除節點以及子樹處於不同的層次,提升子樹到被刪除節點的層次是不可行的。假如現在有被刪除節點P,子樹根節點為Q,Q存在子節點R,其中,P層比較x值,Q層比較y值,如果節點Q是節點P的左節點,那麼x(Q)<x(P),節點R是節點Q的左節點,那麼y(R)<y(Q)。假設刪除P之後,提升Q節點到P原有的位置,需要保證x(R)<x(Q),但是我們只能保證y(R)<y(Q),所以直接提升子樹的層次是不可行的。
3、刪除度為2的節點
類比一維二叉查詢樹中度為2節點的刪除邏輯,無非是合併刪除或者複製刪除,其中合併刪除明顯是不可行的,原因也是子樹合併之後依然要提升子樹到被刪除節點的層次,這是不可行的。複製刪除可以嗎?假設被刪除節點為P,左子樹為Q,右子樹為R。複製刪除的核心邏輯是在Q中找到最大的節點替換P或者在R中找到最小的節點替換P。本質是,能夠替換P的節點要大於Q中任意節點,小於R中任意節點。將其擴充套件到kd二叉查詢樹,什麼樣的節點可以替換被刪除節點呢?
假設被刪除節點為P,我們將以P節點為根的子樹擷取出來,問題就轉化為如何刪除kd二叉查詢樹的根節點。當前的難點在於找到一個什麼樣的節點來替換根節點,這就需要觀察根節點的特性了。還記得插入節點的邏輯嗎?假設插入新節點Q,首先將Q節點的x維度的值和根節點x維度的值比較,大於則轉向右子樹,小於則轉向左子樹,在此過程中,其他維度的值不起作用。也就是說,根節點的x維度的值大於左子樹中任意節點的x維度的值,小於右子樹中任意節點的x維度的值,其他維度的值大小沒有要求。那麼答案就顯而易見了,能夠替換根節點的只有兩個,首先是左子樹中x維度值最大的節點,其次是右子樹中x維度值最小的節點。怎麼找呢?沒什麼好的辦法,因為這兩個可替換節點的位置沒有固定的規律,只能遍歷所有節點來尋找。就算找到了,如果該節點也是度為2的節點,那麼刪除它的邏輯和上面是一樣的,依然要遍歷尋找可替換的節點,直到找到葉子節點,才可以直接刪除。
刪除度為1的節點和刪除度為2的節點邏輯是一樣的,找到可替換節點才行。
虛擬碼如下:
假設q節點是被刪除節點右子樹的根節點,下面是查詢q子樹中i維度值最小節點的邏輯
smallest(q, i) {
min = q;
if q->left != 0
lt = smallest(q->left, i);
if min->el.keys[i] >= lt->el.keys[i]
min = lt;
if q->right != 0
rt = smallest(q->right, i);
if min->el.keys[i] >= rt->el.keys[i]
min = rt;
return min;
}複製程式碼
虛擬碼的邏輯是使用遞迴,逐個比較每個節點的x維度值的大小。顯然,這種方式不夠優秀,我們還可以繼續改進。改進點在於,如果我們知道某節點R是比較x維度的值,那我們就不用遍歷R節點的右子樹了,因為R節點的左子樹任意節點的x維度的值小於R節點x維度的值,而R節點x維度的值是小於R節點右子樹任意節點x維度值的。所以,如果我們檢測到當前比較節點比較的是x維度的值,直接選擇它的左子樹即可。
虛擬碼如下:
假設q節點是被刪除節點右子樹的根節點,下面是查詢q子樹中i維度值最小節點的邏輯
smallest(q, i, j) {
min = q;
if i == j
if q->left != 0
min = q = q->left;
j = j + 1
else
return q;
if q->left != 0
lt = smallest(q->left, i, (j + 1) mode k);
if min->el.keys[i] >= lt->el.keys[i]
min = lt;
if q->right != 0
rt = smallest(q->right, i, (j + 1) mode k);
if min->el.keys[i] >= rt->el.keys[i]
min = rt;
return min;
}複製程式碼
到此為止,我們已經介紹了kd二叉查詢樹的建立、查詢、新增、刪除演算法。但是我們依然有很多困惑沒有解決,比如說新增或者刪除會破壞kd樹的平衡,怎麼處理?刪除演算法複雜且效率不高,怎麼改進?kd二叉查詢樹有哪些應用?等等,這些問題在下一篇文章進行探討。