看得見的數據結構Android版之二分搜索樹篇

零、前言

1.我的感受這個二叉搜索樹實現的仍是很不錯的,基本操做都涵蓋了
2.在Activity中對view設置監聽函數,能夠動態傳入數據,只要可比較,均可以生成二分搜索樹
3.二分搜索樹的價值:搜索、添加、刪除、更新速度快,最佳狀態複雜度logn,但極端狀況下會退化成單鏈表
4.本例操做演示源碼:但願你能夠和我在Github一同見證:DS4Android的誕生與成長,歡迎starnode

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

二分搜索樹操做合集.gif


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

二叉樹樹形.png


三、二分搜索樹簡介

二分搜索樹是一種特殊的二叉樹形的數據結構
存儲的數據必須具備可比較性git

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

二分搜索樹.png


1、準備工做

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;
        }
    }
}
複製代碼

2、節點(Node)的添加操做

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

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


2、最值操做:

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

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);
從根節點開始,它們都在等待左側值,直到發現到最左邊了,便將最小值節點的右側節點返回出去
這時前面等待的人接到了最小值的右側,而後最小值被從樹上移除了。canvas

/**
 * 從二分搜索樹中刪除最大值所在節點
 *
 * @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.刪除最大值

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

最大值操做.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;
}
複製代碼

3、查找是否包含元素

想一下一羣西瓜按二分搜索樹排列,怎麼看是否包含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


4、二叉樹的遍歷:

層序遍歷、前序遍歷、中序遍歷、後序遍歷,聽起來挺嚇人其實就是摸瓜的時候何時記錄一下
這裏是用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);
        }
    }
}
複製代碼

5、二叉樹的移除指定元素:
移除節點:首先相似包含操做,找一下與傳入元素相同是的節點  
刪除的最大難點在於對目標節點孩子的處理,按照樹型可分爲:
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
相關文章
相關標籤/搜索