看得見的資料結構Android版之二分搜尋樹篇

張風捷特烈發表於2018-11-25

零、前言

1.個人感覺這個二叉搜尋樹實現的還是很不錯的,基本操作都涵蓋了
2.在Activity中對view設定監聽函式,可以動態傳入資料,只要可比較,都可以生成二分搜尋樹
3.二分搜尋樹的價值:搜尋、新增、刪除、更新速度快,最佳狀態複雜度logn,但極端情況下會退化成單連結串列
4.本例操作演示原始碼:希望你可以和我在Github一同見證:DS4Android的誕生與成長,歡迎star

1.留圖鎮樓:二分搜尋樹的最終實現的操作效果:

二分搜尋樹操作合集.gif


2、二叉樹簡介
二叉樹特性
1.一個二叉樹一定有且僅有一個根節點
2.一個二叉樹除了資料之外,還有[左子]、[右子]的引用,節點本身稱為[父]
3.樹形:
    |---殘樹:
        |---左殘:[左子]為空,[右子]非空
        |---右殘:[右子]為空,[左子]非空
        |---葉:[右子]為空,[左子]為空
    |---滿樹:[左子]、[右子]非空
4.二叉系:
    |---二叉系是天然存在的無限全空二叉樹
    |---節點的二叉系座標:(x,y)  x:該層的第幾個元素 y:該層層數
5.二叉樹的分類:
    |---二分搜尋樹:
    |---平衡二叉樹:最大樹深-最小樹深<=1
        |---完全二叉樹:按二叉系座標排放元素
            |---堆
        |---線段樹
複製程式碼

二叉樹樹形.png


3、二分搜尋樹簡介

二分搜尋樹是一種特殊的二叉樹形的資料結構
儲存的資料必須具有可比較性

特性:對於每個節點      
1.[父]的值都要大於[左子]的值。
2.[父]的值都要小於[右子]的值。
複製程式碼

二分搜尋樹.png


一、準備工作

1.建立類
/**
 * 作者:張風捷特烈
 * 時間:2018/10/7 0007:7:36
 * 郵箱:1981462002@qq.com
 * 說明:
 */
public class BinarySearchTree<T extends Comparable<T>> {
    private Node root;//根節點
    private int size;//節點個數

    public Node getRoot() {//----!為方便檢視繪製:暴露此方法
        return root;
    }

    /**
     * 獲取節點個數
     *
     * @return 節點個數
     */
    public int size() {
        return size;
    }

    /**
     * 二分搜尋樹是否為空
     *
     * @return 是否為空
     */
    public boolean isEmpty() {
        return size == 0;
    }
}

複製程式碼
2.節點類的設計
/**
 * 節點類----!為方便檢視繪製---private 改為 public
 */
public class Node {

    public T el;//儲存的資料元素
    public Node left;//左子
    public Node right;//右子

    public int deep;//!為方便檢視繪製---增加節點樹深


    /**
     * 建構函式
     *
     * @param left  左子
     * @param right 右子
     * @param el    儲存的資料元素
     */
    private Node(Node left, Node right, T el) {
        this.el = el;
        this.left = left;
        this.right = right;
    }

    public NodeType getType() {
        if (this.right == null) {
            if (this.left == null) {
                return NodeType.LEAF;
            } else {
                return NodeType.RIGHT_NULL;
            }
        }

        if (this.left == null) {
            return NodeType.LEFT_NULL;
        } else {
            return NodeType.FULL;
        }
    }
}
複製程式碼

二、節點(Node)的新增操作

感覺就像順藤插瓜,一個瓜,兩個叉,比較[待插入瓜]和[當前瓜]的個頭大小
大了放右邊,小了放左邊,直到摸不到瓜了,就把待插入的插上。

1.二分搜尋樹新增元素
/**
 * 新增節點
 *
 * @param el 節點元素
 */
public void add(T el) {
    root = addNode(root, el);
}

/**
 * 返回插入新節點後的二分搜尋樹的根
 *
 * @param target 目標節點
 * @param el     插入元素
 * @return 插入新節點後的二分搜尋樹的根
 */
private Node addNode(Node target, T el) {
    if (target == null) {
        size++;
        return new Node(null, null, el);
    }
    if (el.compareTo(target.el) <= 0) {
        target.left = addNode(target.left, el);
        target.left.deep = target.deep + 1;//!為方便檢視繪製---維護deep
    } else if (el.compareTo(target.el) > 0) {
        target.right = addNode(target.right, el);
        target.right.deep = target.deep + 1;//!為方便檢視繪製---維護deep
    }
    return target;
}
複製程式碼
2.新增測試:6, 2, 8, 1, 4, 3

二分搜尋樹新增.gif

[ 6, 2, 8, 1, 4, 3 ]
複製程式碼

插入的形象演示:其中表示null

    6             6                6              6                   6                  6
 /    \ --->   /    \    --->   /   \   --->   /     \    --->     /     \   --->     /     \
。     。     2       。       2     8         2      8            2      8           2      8
           /    \           /  \  /  \      /  \    /  \        /  \    /  \       /  \    /  \
          。     。        。   。。   。   1    。 。   。      1   4  。   。     1   4   。   。
                                          / \                 / \  / \          / \  / \
                                         。  。              。  。。  。        。 。。  3         
複製程式碼
3.用棧來分析插入元素5時的遞迴:
searchTree.add(5);
複製程式碼

二叉樹插入遞迴分析.png

插入5時.png


二、最值操作:

這真是正宗的順藤摸瓜,想找最小值,一直往左摸,想找最大值,一直往右摸。

1.尋找最小值
/**
 * 獲取最小值:暴露的方法
 *
 * @return 樹的最大元素
 */
public E getMin() {
    return getMinNode(root).el;
}
 
/**
 * 獲取最小值所在的節點 :內部方法
 *
 * @param node 目標節點
 * @return 最小值節點
 */
private Node<E> getMinNode(Node<E> node) {
    //左子不為空就一直拿左子,直到左子為空
    if (node.left == null) {
        return node;
    }
    node = getMinNode(node.left);
    return node;
}
複製程式碼

最小值.png

查詢最小值遞迴.png


2.刪除最小值:

node.left == null說明一直再往左找,整個遞迴過程中node.left = removeMinNode(node.left);
從根節點開始,它們都在等待左側值,直到發現到最左邊了,便將最小值節點的右側節點返回出去
這時前面等待的人接到了最小值的右側,然後最小值被從樹上移除了。

/**
 * 從二分搜尋樹中刪除最大值所在節點
 *
 * @return 刪除的元素
 */
public E removeMin() {
    E ret = getMin();
    root = removeMinNode(root);
    return ret;
}

/**
 * 刪除掉以node為根的二分搜尋樹中的最小節點 返回刪除節點後新的二分搜尋樹
 *
 * @param node 目標節點
 * @return 刪除節點後新的二分搜尋樹的根
 */
private Node removeMinNode(Node node) {
    if (node.left == null) {
        Node rightNode = node.right;
        node.right = null;
        return rightNode;
    }
    
    node.left = removeMinNode(node.left);
    return node;
}
複製程式碼

刪除最小值.gif

移除最小值遞迴分析.png


3.尋找最大值

原理基本一致,就不畫圖了。

/**
 * 獲取最大值:暴露的方法
 *
 * @return 樹的最大元素
 */
public E getMax() {
    return getMaxNode(root).el;
}

/**
 * 獲取最大值所在的節點:內部方法
 *
 * @param node 目標節點
 * @return 最小值節點
 */
private Node<E> getMaxNode(Node<E> node) {
    //右子不為空就一直拿右子,直到右子為空
    return node.right == null ? node : getMaxNode(node.right);
}
複製程式碼
4.刪除最大值

原理基本一致,就不畫圖了。

最大值操作.gif

/**
 * 從二分搜尋樹中刪除最大值所在節點
 *
 * @return 刪除的元素
 */
public E removeMax() {
    E ret = getMax();
    root = removeMaxNode(root);
    return ret;
}

/**
 * 刪除掉以node為根的二分搜尋樹中的最小節點 返回刪除節點後新的二分搜尋樹的根
 *
 * @param node 目標節點
 * @return 刪除節點後新的二分搜尋樹的根
 */
private Node removeMinNode(Node node) {
    if (node.left == null) {
        Node rightNode = node.right;
        node.right = null;
        return rightNode;
    }
    node.left = removeMinNode(node.left);
    return node;
}
複製程式碼

三、查詢是否包含元素

想一下一群西瓜按二分搜尋樹排列,怎麼看是否包含10kg的西瓜?
和root西瓜比較:小了就往往左走,因為右邊的都比root大,一下就排除一半,很爽有沒有
然後繼續比較,直到最後也沒有,那就不包含。

    /**
     * 否存在el元素
     * @param el 元素
     * @return 是否存在
     */
    public boolean contains(E el) {
        return contains(el, root);
    }

    /**
     * 以root為根節點的二叉樹是否存在el元素
     *
     * @param el   待測定元素
     * @param node 目標節點
     * @return 是否存在el元素
     */
    private boolean contains(E el, Node<E> node) {
        if (node == null) {
            return false;
        }

        if (el.compareTo(node.el) == 0) {
            return true;
        }

        boolean isSmallThan = el.compareTo(node.el) < 0;
        //如果小於,向左側查詢
        return contains(el, isSmallThan ? node.left : node.right);

    }
複製程式碼

包含方法遞迴分析.png

包含.png


四、二叉樹的遍歷:

層序遍歷、前序遍歷、中序遍歷、後序遍歷,聽起來挺嚇人其實就是摸瓜的時候什麼時候記錄一下
這裡是用List裝一下,方便獲取結果,你也可以用列印來看,不過感覺有點low

1.前序遍歷、中序遍歷、後序遍歷

程式碼基本一致,就是在遍歷左右子時,放到籃子裡的時機不同,分為了前、中、後
前序遍歷:父-->左-->右(如:6父,2左,2為父而左1,1非父,2右4,4為父而左3,以此循之)
中序遍歷:左-->父-->右
後序遍歷:左-->右-->父

遍歷.png

/**
 * 二分搜尋樹的前序遍歷(使用者使用)
 */
public void orderPer(List<T> els) {
    orderPerNode(root, els);
}
/**
 * 二分搜尋樹的中序遍歷(使用者使用)
 */
public void orderIn(List<T> els) {
    orderNodeIn(root, els);
}
/**
 * 二分搜尋樹的後序遍歷(使用者使用)
 */
public void orderPost(List<T> els) {
    orderNodePost(root, els);
}

/**
 * 前序遍歷以target為根的二分搜尋樹
 *
 * @param target 目標樹根節點
 */
private void orderPerNode(Node target, List<T> els) {
    if (target == null) {
        return;
    }
    els.add(target.el);
    orderPerNode(target.left, els);
    orderPerNode(target.right, els);
}

/**
 * 中序遍歷以target為根的二分搜尋樹
 *
 * @param target 目標樹根節點
 */
private void orderNodeIn(Node target, List<T> els) {
    if (target == null) {
        return;
    }
    orderNodeIn(target.left, els);
    els.add(target.el);
    orderNodeIn(target.right, els);
}
/**
 * 後序遍歷以target為根的二分搜尋樹
 *
 * @param target 目標樹根節點
 */
private void orderNodePost(Node target, List<T> els) 
    if (target == null) {
        return;
    }
    orderNodePost(target.left, els);
    orderNodePost(target.right, els);
    els.add(target.el);
}

複製程式碼
2.層序遍歷(佇列模擬):

感覺挺有意思的:還是用個栗子說明吧

6元素先入隊,排在最前面,然後走了登個記(放在list裡),把左右兩個孩子2,8留下了,佇列:8-->2    
然後2登個記(放在list裡)走了,把它的孩子1,4放在隊尾,這時候排隊的是:4-->1-->8,集合裡6,2  
然後8登個記(放在list裡)走了,它沒有孩子,這時候排隊的是:4-->1,集合裡6,2,8  
然後1登個記(放在list裡)走了,它沒有孩子,這時候排隊的是:4,集合裡6,2,8,1  
然後4登個記(放在list裡)走了,把它的孩子3,5放在隊尾,這時候排隊的是:5-->3,集合裡6,2,8,1,4    
都出隊過後:6,2,8,1,4,3,5-------------一層一層的遍歷完了,是不是很神奇
複製程式碼

層序遍歷.png

/**
 * 二分搜尋樹的層序遍歷,使用佇列實現
 */
public void orderLevel( List<T> els) {
    Queue<Node> queue = new LinkedList<>();
    queue.add(root);
    while (!queue.isEmpty()) {
        Node cur = queue.remove();
        els.add(cur.el);
        //節點出隊時將孩子入隊
        if (cur.left != null) {
            queue.add(cur.left);
        }
        if (cur.right != null) {
            queue.add(cur.right);
        }
    }
}
複製程式碼

五、二叉樹的移除指定元素:
移除節點:首先類似包含操作,找一下與傳入元素相同是的節點  
刪除的最大難點在於對目標節點孩子的處理,按照樹型可分為:
RIGHT_NULL:如果目標只有一個左子,可以按照刪除最小值的思路  
LEFT_NULL:只有一個右子,可以按照刪除最大值的思路  
LEAF:如果本身就是葉子節點,就不用考慮那麼多了,愛怎麼刪怎麼刪  
FULL:如果左右都有孩子,你總得找個繼承人接班吧,才能走..
複製程式碼
1.看一下移除2時:

首先2走了,要找到繼承人:這裡用後繼節點,將它右側的樹中的最小節點當做繼承人

移除2.gif

//找後繼節點
Node successor = getMinNode(target.right);
successor.right = removeMinNode(target.right);
successor.left = target.left;
target.left = target.right = null;
return successor;
複製程式碼
2.移除的程式碼實現
/**
 * 移除節點
 *
 * @param el 節點元素
 */
public void remove(T el) {
    root = removeNode(root, el);
}
複製程式碼
/**
 * 刪除掉以target為根的二分搜尋樹中值為e的節點, 遞迴演算法 返回刪除節點後新的二分搜尋樹的根
 *
 * @param target
 * @param el
 * @return
 */
private Node removeNode(Node target, T el) {
    if (target == null) {
        return null;
    }
    if (el.compareTo(target.el) < 0) {
        target.left = removeNode(target.left, el);
    } else if (el.compareTo(target.el) > 0) {
        target.right = removeNode(target.right, el);
        return target;
    } else {//相等時
        switch (target.getType()) {
            case LEFT_NULL://左殘--
            case LEAF:
                Node rightNode = target.right;
                target.right = null;
                size--;
                return rightNode;
            case RIGHT_NULL:
                Node leftNode = target.left;
                target.left = null;
                size--;
                return leftNode;
            case FULL:
                //找後繼節點
                Node successor = getMinNode(target.right);
                successor.right = removeMinNode(target.right);
                successor.left = target.left;
                target.left = target.right = null;
                return successor;
        }
    }
    return target;
}
複製程式碼

好了,二叉樹的基本操作都講了以遍,下面說說繪圖的核心方法:

核心繪製方法:

核心繪製思路

/**
 * 繪製結構
 *
 * @param canvas
 */
private void dataView(Canvas canvas) {
    if (!mTreeBalls.isEmpty()) {
        canvas.save();
        canvas.translate(ROOT_X, ROOT_Y);
        BinarySearchTree<TreeNode<E>>.Node root = mTreeBalls.getRoot();
        canvas.drawCircle(0, 0, NODE_RADIUS, mPaint);
        canvas.drawText(root.el.data.toString(), 0, 10, mTxtPaint);
        drawNode(canvas, root);
        canvas.restore();
    }
}

private void drawNode(Canvas canvas, BinarySearchTree<TreeNode<E>>.Node node) {
    float thta = (float) ((60 - node.deep * 10) * Math.PI / 180);//父節點與子節點豎直方向夾角
    int lineLen = (int) (150 / ((node.deep + .5)));//線長
    float offsetX = (float) (NODE_RADIUS * Math.sin(thta));//將起點偏移圓心X,到圓上
    float offsetY = (float) (NODE_RADIUS * Math.cos(thta));//將起點偏移圓心X,到圓上
    
    //畫布移動的X
    float translateOffsetX = (float) ((lineLen + 2 * NODE_RADIUS) * Math.sin(thta));
    //畫布移動的Y
    float translateOffsetY = (float) ((lineLen + 2 * NODE_RADIUS) * Math.cos(thta));
    
    float moveX = (float) (lineLen * Math.sin(thta));//線移動的X
    float moveY = (float) (lineLen * Math.cos(thta));//線移動的Y
    if (node.right != null) {
        canvas.save();
        canvas.translate(translateOffsetX, translateOffsetY);//每次將畫布移到右子的圓心
        canvas.drawCircle(0, 0, NODE_RADIUS, mPaint);//畫圓
        mPath.reset();//畫線
        mPath.moveTo(-offsetX, -offsetY);
        mPath.lineTo(-offsetX, -offsetY);
        mPath.rLineTo(-moveX, -moveY);
        canvas.drawPath(mPath, mPathPaint);
        canvas.drawText(node.right.el.data.toString(), 0, 10, mTxtPaint);//畫字
        drawNode(canvas, node.right);
        canvas.restore();
    }
    if (node.left != null) {//同理
        canvas.save();
        canvas.translate(-translateOffsetX, translateOffsetY);
        mPath.reset();
        mPath.moveTo(offsetX, -offsetY);
        mPath.rLineTo(moveX, -moveY);
        canvas.drawPath(mPath, mPathPaint);
        canvas.drawCircle(0, 0, NODE_RADIUS, mPaint);
        canvas.drawText(node.left.el.data.toString(), 0, 10, mTxtPaint);
        drawNode(canvas, node.left);
        canvas.restore();
    }
}
複製程式碼

後記:捷文規範

本系列後續更新連結合集:(動態更新)

看得見的資料結構Android版之開篇前言
看得見的資料結構Android版之陣列表(資料結構篇)
看得見的資料結構Android版之陣列表(檢視篇)
看得見的資料結構Android版之單連結串列篇
看得見的資料結構Android版之雙連結串列篇
看得見的資料結構Android版之棧篇
看得見的資料結構Android版之佇列篇
看得見的資料結構Android版之二分搜尋樹篇
更多資料結構---以後再說吧

2.本文成長記錄及勘誤表
專案原始碼 日期 備註
V0.1--github 2018-11-25 看得見的資料結構Android版之二分搜尋樹結構的實現
3.更多關於我
筆名 QQ 微信 愛好
張風捷特烈 1981462002 zdl1994328 語言
我的github 我的簡書 我的掘金 個人網站
4.宣告

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援


icon_wx_200.png

相關文章