看得見的資料結構Android版之二分搜尋樹結構的實現
零、前言
1.個人感覺這個二叉搜尋樹實現的還是很不錯的,基本操作都涵蓋了
2.在Activity中對view設定監聽函式,可以動態傳入資料,只要可比較,都可以生成二分搜尋樹
3.二分搜尋樹的價值:搜尋、新增、刪除、更新速度快,最佳狀態複雜度logn,但極端情況下會退化成單鏈表
4.本例操作演示原始碼:希望你可以和我在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
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);
複製程式碼
二、最值操作:
這真是正宗的
順藤摸瓜
,想找最小值,一直往左摸,想找最大值,一直往右摸。
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版之雙鏈表篇(待完成)
看得見的資料結構Android版之棧(待完成)
看得見的資料結構Android版之佇列(待完成)
看得見的資料結構Android版之二分搜尋樹篇(待完成)
更多資料結構---以後再說吧
2.本文成長記錄及勘誤表
專案原始碼 | 日期 | 備註 |
---|---|---|
V0.1--github | 2018-11-25 | 看得見的資料結構Android版之二分搜尋樹結構的實現 |
3.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的掘金 | 個人網站 |
4.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援