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

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

3、二分搜尋樹簡介
二分搜尋樹是一種特殊的二叉樹形的資料結構
儲存的資料必須具有可比較性
特性:對於每個節點 1.[父]的值都要大於[左子]的值。 2.[父]的值都要小於[右子]的值。 複製程式碼

一、準備工作
1.建立類
/** * 作者:張風捷特烈 * 時間:2018/10/7 0007:7:36 * 郵箱:[email protected] * 說明: */ 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

[ 6, 2, 8, 1, 4, 3 ] 複製程式碼
插入的形象演示:其中 。
表示 null
666666 /\ --->/\--->/\--->/\--->/\--->/\ 。。2。28282828 /\/\/\/\/\/\/\/\/\ 。。。。。。1。 。。14。。14。。 / \/ \/ \/ \/ \ 。。。。。。。 。。3 複製程式碼
3.用棧來分析插入元素 5
時的遞迴:
searchTree.add(5); 複製程式碼


二、最值操作:
這真是正宗的 順藤摸瓜
,想找最小值,一直往左摸,想找最大值,一直往右摸。
1.尋找最小值
/** * 獲取最小值:暴露的方法 * * @return 樹的最大元素 */ public E getMin() { return getMinNode(root).el; } /** * 獲取最小值所在的節點 :內部方法 * * @param node 目標節點 * @return 最小值節點 */ private Node<E> getMinNode(Node<E> node) { //左子不為空就一直拿左子,直到左子為空 if (node.right == null) { return node; } node = getMinNode(node.left); return node; } 複製程式碼


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


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.刪除最大值
原理基本一致,就不畫圖了。

/** * 從二分搜尋樹中刪除最大值所在節點 * * @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); } 複製程式碼


四、二叉樹的遍歷:
層序遍歷、前序遍歷、中序遍歷、後序遍歷,聽起來挺嚇人其實就是摸瓜的時候什麼時候記錄一下
這裡是用List裝一下,方便獲取結果,你也可以用列印來看,不過感覺有點low
1.前序遍歷、中序遍歷、後序遍歷
程式碼基本一致,就是在遍歷左右子時,放到籃子裡的時機不同,分為了前、中、後
前序遍歷:父-->左-->右(如:6父,2左,2為父而左1,1非父,2右4,4為父而左3,以此循之)
中序遍歷:左-->父-->右
後序遍歷:左-->右-->父

/** * 二分搜尋樹的前序遍歷(使用者使用) */ 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-------------一層一層的遍歷完了,是不是很神奇

/** * 二分搜尋樹的層序遍歷,使用佇列實現 */ 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走了,要找到繼承人:這裡用後繼節點,將它右側的樹中的最小節點當做繼承人

//找後繼節點 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版之二分搜尋樹篇(待完成)
更多資料結構---以後再說吧
2.本文成長記錄及勘誤表
專案原始碼 | 日期 | 備註 |
---|---|---|
V0.1--github | 2018-11-25 | 看得見的資料結構Android版之二分搜尋樹結構的實現 |
3.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的掘金 | 個人網站 |
4.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援
