來來來!熱乎的二叉排序樹(搜索樹)查找、增加、刪除操作


前言

這是這個系列上的第二篇文章,如果你還沒有了解二叉樹的話,可以先看我的文章中閱讀量最高的二叉樹(從建樹、遍歷到存儲)Java.

一、定義

滿是學究氣息的文字定義我們先不看,還是沿用看圖說話的慣例。
在這裏插入圖片描述
數無形時少直覺,形少數時難入微”,所以文字上的定義也是不能少的,其實我們通過上圖可以自己總結出來。

對於二叉樹上的任意節點,若它的左子樹不爲空,那麼左子樹上所有節點的值均小於它的值,若它的右子樹不爲空,那麼右子樹上所有節點的值均大於它的值。

構造一棵二叉排序樹的目的,其實並不是爲了排序,而是爲了提高查找、增加和刪除數據的速度。因爲在一個有序的集合中查找數據的速度總是快於在無序集合中,而且二叉樹排序樹這種非線性的結構也有利於增加和刪除節點(類似鏈表)。

二、查找

在一本很暢銷的數據結構書上這一部分的算法實現用的遞歸,我不是很贊同,雖然遞歸寫起來代碼很簡潔,但是對於初學者而言比較難理解,隨着一層一層的遞歸調用下去,跟着跟着就滿腦漿糊了。

所以這裏我們還是用循環來寫,方便理解。

代碼

public boolean contains(int e) {
		//用current記錄根節點
		Node current = root;
		while (current != null) {
			//相等,說明找到了,返回true
			if (e == current.data) {
				return true;
				//要查找的數比根節點大,指向根節點的右兒子
			} else if (e > current.data) {
				current = current.right;
				//要查找的數比根節點下,指向根節點的左兒子
			} else {
				current = current.left;
			}
		}
		//找不到,返回false
		return false;
	}

這只是一個代碼片段,最後提供完整代碼,更具有邏輯性。

圖解

圖一:初始狀態,若查找80。
在這裏插入圖片描述
圖二:
(1)node爲根節點,不爲空,進入while循環;
(2)要查找的是80,node.data的值是60,80大於60,node等於node.right。
在這裏插入圖片描述
圖三:
(1)此時node爲下圖中的紅色節點,不爲空,進入while循環;
(2)要查找的是80,node.data是75,80大於75,node等於node.right。
在這裏插入圖片描述
圖四:
(1)此時node爲下圖中的紅色節點,不爲空,進入while循環;
(2)要查找的80,node.data是80,80等於80,返回true。
在這裏插入圖片描述

三、增加節點

當要插入的數據在二叉排序樹中不存在時,插入成功並返回true,否則返回false。

代碼

	public boolean add(int e) {
		if (root == null) {
			root = new Node(e);
			return true;
		}
		//記錄current的父節點
		Node father = null;
		Node current = root;
		while (current != null) {
			if (e > current.data) {
				father = current;
				current = current.right;
			} else if (e < current.data) {
				father = current;
				current = current.left;
			} else {
				return false;
			}
		}
		if (e > father.data) {
			father.right = new Node(e);
		} else {
			father.left = new Node(e);
		}
		return true;
	}

對比查找代碼:

public boolean contains(int e) {
		Node current = root;
		while (current != null) {
			if (e == current.data) {
				return true;
			} else if (e > current.data) {
				current = current.right;
			} else {
				current = current.left;
			}
		}
		return false;
	}

所以如果已經理解查找的代碼,對於增加的代碼也不難理解。

四、刪除節點

“請佛容易送佛難”,從上面的代碼我們知道增加節點的操作其實並不麻煩,一半代碼還和“查找”操作的代碼相同,但是要刪除節點的話就沒那麼輕鬆了,因爲要考慮到刪除該節點後整個二叉樹還滿住二叉排序樹的要求。

1.單身狗

即如下圖中的深色節點,這個要刪除的節點是一個可憐的單身狗。。。
在這裏插入圖片描述
那麼就把它直接刪除!我們可以看到直接刪除後對整個二叉樹排序樹並沒有什麼影響。

2.獨生子

這個節點的後代雖然只有一棵獨苗,但也是有的。
在這裏插入圖片描述
當他駕崩後,這皇位要傳給誰呢?當然是他的獨生子69。而且我們把69放到62的位置發現二叉排序樹的性質並沒有被破壞,如下圖。
在這裏插入圖片描述
當然,若這顆獨苗也有後代,情況是一樣的。
在這裏插入圖片描述

3.多子多孫

“哈哈哈哈!我可是多福多壽,多子多孫之人!”又來了一個節點。這個節點可是兒孫滿堂,那麼他駕崩後,“奪嫡”自然是不可避免的。
在這裏插入圖片描述
30他駕崩了,留下了大好河山,宗室諸王們蠢蠢欲動,誰都想過一把皇帝癮,經過一番比拼,發現25或者35都可以繼承皇位,那就二者選其一吧。

“等等!怎麼就25或者35了?”一個外國人不明所以地問道。原來在二叉排序樹內部皇位的繼承不是看誰最強而是看誰最像的。在30的衆多子孫中只有25和35是最像他的(和他的差距最小)。

但是在實際操作中我們不一定要真的把30刪掉,可以稍稍變通一下,先找到繼承人將他的值賦給要刪除的節點,再將繼承人刪除。

看到這裏有些人不禁會想這個方法聽上去簡單,實際中要怎麼用算法找到繼承人呢?之後會與大家分享兩句口訣,保證讓大家按圖索驥,順利找到這兩個節點。

(1)25繼承

在這裏插入圖片描述

口訣一:左轉,向右到盡頭。

我們再來看一個例子,如下圖:
在這裏插入圖片描述
注意:對於繼承人來說他只有單身狗和左獨生子這兩種情況,不存在右獨生子和多子多孫的情況,不明的話,再看看口訣就明白了,如果他存在右子,那麼皇位還能輪到他繼承嗎?口訣可是向右到盡頭!

(2)35繼承

在這裏插入圖片描述

口訣二:右轉,向左到盡頭。

同樣我們再來看一個例子,如下圖:
在這裏插入圖片描述
注意:同理,對於第二種情況的繼承人來說他只有單身狗和右獨生子這兩種情況,不存在左獨生子和多子多孫的情況,如果他存在左子,就不是他繼承皇位了。

五、完整代碼

這是我在學習過程中模仿Java集合類寫的一個二叉排序樹類,歡迎大家指導。

BinarySortTree類:

import java.util.LinkedList;

public class BinarySortTree {
	//記錄根節點
	Node root;

	public BinarySortTree() {
	}

	//增
	public boolean add(int e) {
		if (root == null) {
			root = new Node(e);
			return true;
		}
		//記錄current的父節點
		Node father = null;
		Node current = root;
		while (current != null) {
			if (e > current.data) {
				father = current;
				current = current.right;
			} else if (e < current.data) {
				father = current;
				current = current.left;
			} else {
				return false;
			}
		}
		if (e > father.data) {
			father.right = new Node(e);
		} else {
			father.left = new Node(e);
		}
		return true;
	}

	/* 刪
	 * 
	 * */
	public boolean remove(int e) {
		Node father = null;
		Node current = root;
		while (current != null) {
			if (current.data == e)
				return removeNode(current, father, e);
			else if (e > current.data) {
				father = current;
				current = current.right;
			} else {
				father = current;
				current = current.left;
			}
		}
		return false;
	}

	final boolean removeNode(Node current, Node father, int e) {
		//若左兒子爲空,右兒子繼承
		if (current.left == null) {
			if (current.data > father.data) {
				father.right = current.right;
			} else {
				father.left = current.right;
			}
			//方便垃圾回收器回收
			current = null;
		}
		//若右兒子爲空,左兒子繼承
		else if (current.right == null) {
			if (current.data > father.data) {
				father.right = current.left;
			} else {
				father.left = current.left;
			}
			current = null;
		}
		//多子多孫
		else {
			//用於記錄heir的父節點
			Node heirFather = null;
			/* heir(繼承人)
			 * heir = current.left即口訣一中的左轉
			 * */
			Node heir = current.left;
			//即口訣一中的向右到盡頭
			while (heir.right != null) {
				heirFather = heir;
				heir = heir.right;
			}
			//繼承人的值賦給要被刪除的節點
			current.data = heir.data;
			if (heir.data > heirFather.data) {
				heirFather.right = heir.left;
			} else {
				heirFather.left = heir.left;
			}
			heir = null;
		}
		return true;
	}

	//查找
	public boolean contains(int e) {
		Node current = root;
		while (current != null) {
			if (e == current.data) {
				return true;
			} else if (e > current.data) {
				current = current.right;
			} else {
				current = current.left;
			}
		}
		return false;
	}

	//層次遍歷
	public void levelOrder() {
		if (root == null) {
			return;
		}
		LinkedList<Node> q = new LinkedList<Node>();
		q.addFirst(root);
		Node current;
		while (!q.isEmpty()) {
			current = q.removeLast();
			System.out.print(current.data + " -> ");
			if (current.left != null)
				q.addFirst(current.left);
			if (current.right != null)
				q.addFirst(current.right);
		}
	}

	private static class Node {
		int data;
		Node right;
		Node left;

		Node(int data) {
			this.data = data;
		}
	}
}

測試類:

public class Demo {
	public static void main(String[] args) {
	
		BinarySortTree tree = new BinarySortTree();

		tree.add(60);
		tree.add(30);
		tree.add(75);
		tree.add(15);
		tree.add(50);
		tree.add(80);
		tree.add(10);
		tree.add(25);
		tree.add(40);
		tree.add(55);
		tree.add(19);
		tree.add(35);

		System.out.print("層次遍歷(刪除前):");
		tree.levelOrder();
		System.out.println();

		//刪除
		tree.remove(30);

		System.out.print("層次遍歷(刪除後):");
		tree.levelOrder();
		System.out.println();

		System.out.print("是否已存在:");
		System.out.print(tree.contains(80));
	}
}

測試類中構造出的二叉樹排序樹長這樣:
在這裏插入圖片描述
結果:

層次遍歷(刪除前)60 -> 30 -> 75 -> 15 -> 50 -> 80 -> 10 -> 25 -> 40 -> 55 -> 19 -> 35 -> 
層次遍歷(刪除後)60 -> 25 -> 75 -> 15 -> 50 -> 80 -> 10 -> 19 -> 40 -> 55 -> 35 -> 
是否已存在:true

這裏我只測試了一種情況,有興趣的可以將代碼粘貼複製到編譯器上多測試幾種情況。

寫在最後

分享這篇文章的目的不僅僅是與大家一起討論二叉排序樹的使用,更大的目的是爲了對付JDK1.8中的HashMap,其中用到了紅黑樹這種數據結構。

所以接下來還會分享其他類型二叉樹的使用,總的路線爲:二叉樹——>二叉排序樹(二叉搜索樹)——>紅黑樹,再加一個哈希表(hash table)。

這是這條路線上的第二篇文章,之前還有一篇二叉樹的二叉樹(從建樹、遍歷到存儲)Java.有興趣的話可以看看,如果喜歡這個系列的話可以持續關注哦。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章