1. 程式人生 > >【硬核】使用替罪羊樹實現KD-Tree的增刪改查

【硬核】使用替罪羊樹實現KD-Tree的增刪改查

本文始發於個人公眾號:TechFlow,原創不易,求個關注


今天是機器學習的第16篇文章,我們來繼續上週KD-Tree的話題。

如果有沒有看過上篇文章或者是最新關注的小夥伴,可以點選一下下方的傳送門:

【硬核】機器學習與資料結構的完美結合——KD-Tree

旋轉不可行分析

上週我們實現了KD-Tree建樹和查詢的核心功能,然後我們留了一個問題,如果我們KD-Tree的資料集發生變化,應該怎麼辦呢?

最樸素的辦法就是重新建樹,但是顯然我們每次資料發生變動都把整棵樹重建顯然是不科學的,因為絕大多數資料是沒有變化的,並且我們重新建樹的成本很高,如果變動稍微頻繁一些會導致大量的開銷,這明顯是不合理的。

另一個思路是借鑑平衡樹,比如AVL或者是紅黑樹等樹結構。在這些樹結構當中,當我們新增或者是刪除節點導致樹發生不平衡的情況時,平衡樹會進行旋轉操作在不改變二叉搜尋樹性質的前提下維護樹的平衡。看起來這是一個比較好的方法,但是遺憾的是,這並不太可行。因為KD-Tree和二叉搜尋樹不同,KD-Tree中的節點儲存的元素都是高維的。每一棵子樹的衡量的維度都不同,這會使得旋轉操作變得非常麻煩,甚至是不可行的。

我們來看下面這張圖:

這是平衡樹當中經典的左旋操作,它旋轉前後都滿足平衡樹的性質,即左子樹上所有元素小於根節點,小於右子樹上所有元素。通過旋轉操作,我們可以變更樹結構,但是不影響二叉搜尋樹的性質。

問題是KD-Tree當中我們在不同深度判斷元素大小的維度不同,我們旋轉之後節點的樹深會發生變化,會導致判斷標準發生變化。這樣會導致旋轉之後不再滿足KD-Tree的性質。

我們用剛才的圖舉個例子:

我們給每個節點標上了資料,在樹深為0的節點當中,劃分維度是0,樹深為1的節點劃分維度是1。當我們旋轉之後,很明顯可以發現KD-Tree的性質被打破了。

比如D節點的第0維是2,B節點是1,但是D卻放在了B的左子樹。再比如A節點的第1維是3,E節點的第1維是7,但是E同樣放在了A的左子樹。

這還只是二維的KD-Tree,如果維度更高,會導致情況更加複雜。

通過這個例子,我們證明了平衡樹旋轉的方式不適合KD-Tree。

那麼,除了平衡樹旋轉的方法之外,還有其他方法可以保持樹平衡嗎?

別說,還真有,這也是本篇文章的正主——替罪羊樹。

替罪羊樹

替罪羊樹其實也是平衡二叉樹,但是它和普通的平衡二叉樹不同,它維護平衡的方式不是旋轉,而是重建。

為什麼叫替罪羊樹呢,替罪羊是聖經裡的一個宗教術語,原本指的是將山羊獻祭作為贖罪的儀式,後來才衍生出了代人受過,背鍋俠的意思。替罪羊樹的意思是一個節點的變化可能會導致某一個子樹或者是整棵樹被摧毀並重建,相當於整棵子樹充當了某一個節點的”替罪羊“。

替罪羊樹的裡非常簡單粗暴,不強制保證所有子樹完全平衡,允許一定程度的不平衡存在。當我們插入或者刪除使得某一棵子樹的節點超過平衡底線的時候,我們將整棵樹拍平後重建。

比如下圖紅框當中表示一棵不平衡的子樹:

很明顯,它不平衡地十分嚴重,超過了我們的底線。於是我們將整棵子樹拍平,拍平的意思是將子樹當中所有的元素全部取出,然後重建該樹。

拍平之後的結果是:

拍平之後重建該子樹,得到:

我們把重建的這棵子樹插回到原樹上,代替之前不平衡的部分,這樣就保證了樹的平衡。

整個原理應該非常簡單,底層的細節也只有一個,就是我們怎麼衡量什麼時候應該執行拍平重建的操作呢?

這一點在替罪羊樹當中也非常簡單粗暴,我們維護每一棵子樹中的節點數,然後通過一個引數alpha來控制。當它的某一棵子樹的節點數的佔比超過alpha的時候,我們就認為不平衡性超過了限度,需要進行拍平和重建操作了。

一般alpha的取值在0.6-0.8之間。

刪除

在替罪羊樹當中刪除節點有很多種方法,但是大都大同小異,核心的思想是我們刪除節點並不是真的刪除,而是給節點打上標記,標記這個節點在查詢的時候不會被考慮進去。

但是節點被打上標記而不是真的刪除雖然實現起來簡單,但是也有隱患,畢竟一個節點被刪除了,我們把它留在樹上一段時間還可以接受,一直留著顯然就有問題了。不僅會佔用空間,也會給計算增加負擔。

針對這種情況,也有幾種不同的解決策略。一種策略是不用理會,等待某一次插入的時候發現樹不平衡,進行拍平重構的時候將已經刪除的節點移除。另一種策略是我們也刪除設定一個引數,當某棵子樹上被刪除的元素的比例超過這個閾值的時候,我們也同樣進行子樹的拍平重建。但是不論選擇哪一種,本質上來說都是惰性操作。

所謂的惰性操作一般是通過標記代替原本複雜的運算,等待以後需要的時候執行。這個所謂需要的時候可以是以後查詢到的時候,也可以是積累到一定閾值的時候。總之通過這樣的設計,我們可以簡化刪除操作,因為加上標記不會影響樹結構,所以也不用擔心不平衡的問題。

新增和修改

對於KD-Tree的常規實現來說,修改和新增是一回事,因為我們會通過刪除新增來代替修改。這麼做的原因也很簡單,因為修改某一個節點的資料可能會影響整個樹結構,尤其是KD-Tree中的資料是多維的,所以我們是不能隨意修改一個節點的。

實際上不只是KD-Tree如此,很多平衡樹都不支援修改,比如我們之前介紹過的LSMT就不支援。當然不支援的原因多種多樣,本質上來說都是因為價效比太低。

我們再來看新增操作,二叉搜尋樹的純新增操作其實是很簡單的,我們只需要遍歷樹找到可以插入的位置即可。KD-Tree當中的新增也是如此,雖然KD-Tree當中是多個維度,但是查詢節點的邏輯和之前相差並不大。我們就順著樹結構遍歷,找到需要插入的葉子節點即可。由於我們使用替罪羊樹的原理來維護樹的平衡,所以我們在插入的是時候也需要維護子樹當中節點的數量,以及會不會出發拍平操作。

如果存在子樹違反了平衡條件,我們需要找到最上層的滿足拍平條件的子樹來進行拍平,否則的話底層的子樹平衡了,但是上層的子樹可能仍然需要拍平。注意這兩個細節即可,其他的原理和普通的二叉樹插入節點一致。

我們來看下程式碼,尋找更多細節:

def _insert_data(self, node, data):
    # 子樹節點的數量+1
    node.size += 1
    axis = node.axis
    new_axis = (axis + 1) % self.K
    flat = False
    # 當前節點的判斷條件
    # 小於等於則進入左子樹,否則進入右子樹
    if data[axis] <= node.boundray:
        # 如果子節點為空,說明已經到葉子節點,建立新節點
        if node.lchild is None:
            new_node = KDTree.Node(
                data[new_axis], data, new_axis, node.depth + 1, 1, None, None)
            new_node.father = node
            node.lchild = new_node
        else:
            # 遞迴
            self._insert_data(node.lchild, data)
            # 回溯的時候判斷是否引發樹不平衡
            if node.lchild.size >= self.alpha * node.size:
                self.rebuildNode = node
    else:
        # 邏輯同上,找到葉子節點,回溯的時候判斷是否不平衡
        if node.rchild is None:
            new_node = KDTree.Node(
                data[new_axis], data, new_axis, node.depth + 1, 1, None, None)
            new_node.father = node
            node.rchild = new_node
        else:
            self._insert_data(node.rchild, data)
            if node.rchild.size >= self.alpha * node.size:
                self.rebuildNode = node

我們再來看下拍平的邏輯,拍平其實就是拿到子樹當中所有的節點。如果是二叉搜尋樹,我們可以通過中序遍歷保證元素的有序性,但是在KD-Tree當中,元素的維度太多,再加上存在被刪除的節點,所以有序性無法保證,所以我們可以忽略這點,拿到所有資料即可。

def flat_data(self, node, data):
    if node is None:
        return
    # 跳過刪除元素
    if not node.deleted:
        data.append(node.value)
    self.flat_data(node.lchild, data)
    self.flat_data(node.rchild, data)

拿到所有資料之後也簡單,我們只需要呼叫之前的建樹函式,獲得一棵新子樹,然後將新子樹插回到原樹上對應的位置。

def rebuild(self):
    data = []
    # 拍平以rebuildNode節點為根的子樹
    node = self.rebuildNode
    if node is None:
        return
    # 拿到所有資料
    self.flat_data(node, data)
    # 塞回到父節點當中去代替舊子樹
    father = node.father
    if father is None:
        # 如果父節點為空說明是整棵樹重建了
        self.root = self._build_model(data, node.depth)
        self.set_father(self.root, None)
    else:
        # 判斷是左孩子還是右孩子
        position = 'left' if node == father.lchild else 'right'
        node = self._build_model(data, node.depth)
        if position == 'left':
            father.lchild = node
        else:
            father.rchild = node
        self.set_father(node, father)

這樣一來,我們帶增刪改查功能的KD-Tree就實現好了。到這裡,我們還有一個問題沒有解決,就是複雜度的問題。

這樣做看起來可行,真的複雜度會降低嗎?很遺憾,這個問題涉及到非常複雜的數學證明,我暫時還沒有找到靠譜的證明過程,但是可以肯定的是,雖然我們每一次重建樹都需要nlogn次計算,但是並不是每一次插入和刪除都會引發重建。如果假設發生大量操作的話,那麼我們拍平重建的計算會分攤到每一次查詢上,分攤之後可以得到 級別的插入和刪除。實際上分攤的思路非常常見,像是紅黑樹也是利用了分攤操作。

總結

到這裡關於替罪羊樹在KD-Tree的應用就結束了,雖然這是一個全新的資料結構,並且和比較困難的平衡樹有關,但其實核心的思路並不困難,非但不困難,而且有些過於簡單了,但是效果卻又如此神奇,能解決一個如此棘手的問題,不得不說演算法的魅力實在是無窮。

另外,網路上絕大多數關於KD-Tree的部落格都只有建樹和查詢的部分,雖然實際場景當中,這也基本上足夠了。但是我個人覺得,學習的過程應該是飽和式的,不能僅僅停留在夠用上。畢竟我們努力保持學習的目的,並不只是為了讓這些知識派上用場,更是為了可以擁有更強的能力,成為一個更優秀的人。

最後,我把完整的程式碼放在ubuntu.paste當中,在公眾號裡回覆'kd-tree2',我把完整程式碼發給你,和你一起學習。

如果你也這麼覺得,請順手點個關注或者轉發吧,你們的舉手之勞對我來說很重要。

相關推薦

使用替罪羊實現KD-Tree刪改

本文始發於個人公眾號:TechFlow,原創不易,求個關注 今天是機器學習的第16篇文章,我們來繼續上週KD-Tree的話題。 如果有沒有看過上篇文章或者是最新關注的小夥伴,可以點選一下下方的傳送門: 【硬核】機器學習與資料結構的完美結合——KD-Tree 旋轉不可行分析 上週我們實現了KD-Tree建樹和

Visual Basic列表控制元件ListView的刪改、模態對話方塊、禁止視窗調整大小

列表控制元件ListView是窗體中核心元件之一,在各種窗體程式得到廣泛應用。在《【mfc】學生資訊管理,實現List控制元件節點的增刪改查》(點選開啟連結)中極其難以實現列表控制元件ListView的增刪改查,在vb中可以輕鬆實現,下面舉一個列子來說明這個問題。 如上圖

用開源硬體打一場冠狀病毒殲滅戰

由於在短時間內還無法研發出冠狀病毒特效藥或疫苗,所以就全靠人類自身的免疫力(抗體細胞)來對抗冠狀病毒。不過這些抗體細胞需要外部助力才行。因此,現在對抗冠狀病毒的主要手段(不包括口罩,口罩不算裝置)就是檢測和輔助呼吸。 對抗冠狀病毒的第一步需要儘可能多,儘可能快地找出被感染的人,這就需要大量的檢測裝置,如果已經

Dubbo常見面試題

有情懷,有乾貨,微信搜尋【三太子敖丙】關注這個不一樣的程式設計師。 本文 GitHub https://github.com/JavaFamily 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。 前言 Dubbo 整體介紹的差不多了,今天就開始面試環節了,我會列舉一些常見的 Dubbo 面試題,只

Android Sqlite萌動的sqlite資料庫,簡單實現:使用者刪改

sqlite資料的使用很常見呢,我們簡單學習一下app的登入、註冊、修改、刪除使用者吧! 下面就寫一個萌動的app註冊登入吧! 主頁面: 還有2個圖片按鈕(雖然不是很好看,但是將就吧→。→) EditView輸入框的監聽事件,2張圖片的切換 (為了找22,33的圖片

Machine learning決策(decision tree

三個問題: 怎樣選擇根節點 怎樣選擇後繼節點 什麼時候停止 (一顆決策樹=》一個分類準則=》一個模型) 基本的演算法: 對一開始提出來的三個問題進行解答: 1.選擇最優屬性 ID3: 最優屬性選擇資訊增益最大的屬性來作為最優屬性 設D為用

資料結構線段(Segment Tree

  假設我們現在拿到了一個非常大的陣列,對於這個數組裡面的數字要反覆不斷地做兩個操作。 1、(query)隨機在這個陣列中選一個區間,求出這個區間所有數的和。 2、(update)不斷地隨機修改這個陣列中的某一個值。 時間複雜度: 列舉: 列舉L~R的每個數並累加。

pandas[1] Series 入門(建立和刪改)

作者:lianghc 地址:http://blog.csdn.net/zutsoft/article/details/51482573 Series 是pandas兩大資料結構中(DataFrame,Series)的一種。使用pandas 前需要將pandas 模組引入,因

函數、文件操作實現數據刪改---low版本

腳本 python 首先說明這個腳本很low,目前水平有限,只能實現使用固定的語法對數據進行增刪改查。但是現在腳本不low,怎麽讓明年的我來嘲笑今年的自己需求 a.可進行模糊查詢,語法至少支持下面3種:    select name,age from staff_table where

Struts2+Spring+Hibernate實現員工管理刪改功能(一)之ssh框架整合

pri support scrip ext ack efault ring src 兩張 前言 轉載請標明出處:http://www.cnblogs.com/smfx1314/p/7795837.html 本項目是我寫的一個練習,目的是回顧ssh框架的整合以及

基於AT UI實現表格的刪改遇到的坑

data 基於 報錯 color CA cannot bsp 加載 http 基於AT UI實現表格的增刪改查遇到的坑 坑一、表格數據加載的渲染報錯 報錯:Error in render: "TypeError: Cannot read property ‘isC

python實現mongodb的刪改

pmd xxx rsn rpm vfat uwa paul dpf wdf python實現mongodb的增刪改查環境:192.168.122.1 pytho

Mybatis實現部門表刪改以及排序

tis hide 映射 類型 f2c cep interface mes sele 廢話不說,直接開門見山! 需要在WebContent下的lib下導入兩個包 mybatis-3.2.5.jar ojdbc6.jar 1 package com.xdl.entit

Spring Data jpa + extjs 實現簡單的刪改

公司最近的專案一部分是在使用MyBatis,還有一部分使用SpringJPA,jpa平時沒怎麼用過,今天閒來無事做個增刪改查的demo,記錄下來。 環境;jdk 1.8 編輯器: IDEA 資料庫:postgresql 首先貼上專案所需要的依賴 <?xml version=

ssm實現簡單的刪改demo

訂閱 Spring介紹: spring 使用基本的 JavaBean 來完成以前只可能由 EJB 完成的事情。然而, Spring的用途不僅限於伺服器端的開發。從簡單性、可測試性和鬆耦合的角度而言,任何Java 應用都可以

php實現資料庫的刪改總結

查詢語句的語法: select() * | 欄位列表 from 表列表 where 條件  查詢可以返回一個結果集 select * from mytable select id,name from mytable select * from mytable where id=3

純c語言實現連結串列,實現連結串列刪改

#include <stdio.h> #include <string.h> #include <stdlib.h> struct selflist{ int num; selflist* next; }; struct selflist* hea

java實現mysql資料庫刪改

1.連線資料庫: import java.sql.Connection; import java.sql.DriverManager; public class DBConnection { static String driver = "com.mysql.jdbc.Driver"; s

Springboot整合Thymeleaf、layui實現簡單的刪改

Springboot整合Thymeleaf、layui實現簡單的增刪改查 主頁面列表分頁顯示 搜尋功能 新增使用者 編輯使用者 Mapper對映 Controller層 list.html初始化載入表格資料 搜尋過載表

JDBC實現簡單的刪改

exceptio 介紹 cde sna arch png into 註冊 sets JDBC的開發步驟 * a: 步驟介紹 1.註冊驅動 告知JVM使用的是哪一個數據庫的驅動 2.獲得連接 使用JDBC中的類,完成對MySQL數據庫的連接 3.獲