1. 程式人生 > >java實現二叉查詢樹(插入、刪除、遍歷、查詢)

java實現二叉查詢樹(插入、刪除、遍歷、查詢)

閒話:

繼續擼資料結構和演算法。看資料結構推薦一個視覺化工具吧(http://visualgo.net/),沒有圖憑腦袋想是很痛苦的。

正文:

二叉查詢樹,也叫二叉搜尋樹、有序二叉樹,排序二叉樹,滿足以下性質(非嚴謹描述):

      1.對於每個節點,其左子節點要麼為空,要麼值小於該節點值。

      2.對於每個節點,其右子節點要麼為空,要麼值大於該節點值。

      3.沒有鍵值相等的點。

      通俗的歸納一下性質,二叉查詢樹中每個節點的值都大於其左子節點,小於其右子節點(如果左右子節點存在的話)。所以二叉查詢樹中每個節點的左邊,整棵左樹都是小於它的節點;右邊,整棵右樹都是大於它的節點。

      例圖:

     

  基於這樣的特性,查詢的時候就很好操作了,從根節點開始,查詢,如果值大於節點值,往右找;如果值小於節點值,往左找;如果值剛好相等,就找到了。是不是看著就能寫出程式碼了?這種查詢過程很像二分查詢法,但是那個是陣列結構,這個是樹結構。

      二叉查詢樹的操作基本概括為:插入值,刪除值,查詢值以及二叉樹的遍歷。

      這裡面,刪除是最麻煩的。

      (本來覺得寫資料結構還是用c語言最好的,直接可以操作指標,清晰明瞭效率高,但是c確實丟了太久了,而且現在主要目的是溫習資料結構和演算法的知識,所以只能放棄用c的想法,以後如果需要再學習,先用最熟悉的java來實現程式碼)

      下面來看具體的操作和邏輯,附帶貼上程式碼。

      首先是準備工作,java寫,沒指標,只有利用引用了,節點類是少不了的:

/**
	 * 節點
	 * 
	 * @author zhangyu
	 *
	 * @param <T>
	 */
	public class Node<T extends BaseData> {
		// 節點資料
		T data;
		// 父節點,左右子節點
		Node<T> fatherNode, leftChildNode, rightChildNode;
		//是否是左節點、是否是右節點
		boolean isLeftChild = false, isRightChild = false;

		//左節點是否存在
		public boolean haveLeftChild() {
			return !(leftChildNode == null);
		}

		//右節點是否存在
		public boolean haveRightChild() {
			return !(rightChildNode == null);
		}

		//構造方法
		public Node(boolean isLeft, boolean isRight) {
			isLeftChild = isLeft;
			isRightChild = isRight;
		}
	}
      然後是插入操作,根據特性,邏輯和查詢差不多,從根節點比較,小於則繼續比較其左節點;大於則比較其右節點;直到當某節點左或右節點為空時,在空值處,插入新節點。

例圖(插入65,黃色線為比較的軌跡):

      

      程式碼:

/**
	 * 插入節點
	 * 
	 * @param insertData 待插入的資料
	 * @param node 開始比較的節點
	 */
	private void insertNode(T insertData, Node<T> node) {

		int compareResult = insertData.compareTo(node.data);
		if (compareResult == 0)// 相等
			return;
		else if (compareResult > 0) {// 大於節點值
			if (node.rightChildNode == null) {
				node.rightChildNode = new Node<T>(false, true);
				node.rightChildNode.data = insertData;// 插入值
				node.rightChildNode.fatherNode = node;
				return;
			} else
				insertNode(insertData, node.rightChildNode);// 繼續對比右子節點
		} else {// 小於節點值
			if (node.leftChildNode == null) {
				node.leftChildNode = new Node<T>(true, false);
				node.leftChildNode.data = insertData;// 插入值
				node.leftChildNode.fatherNode = node;
				return;
			} else
				insertNode(insertData, node.leftChildNode);// 繼續對比左子節點
		}
	}

	/**
	 * 插入節點
	 * 
	 * @param insertData 待插入的資料
	 */
	public void insertNode(T insertData) {
		if (treeRoot.data == null) {
			treeRoot.data = insertData;
			return;
		}
		insertNode(insertData, treeRoot);
	}
      然後來看查詢操作,跟插入邏輯幾乎是一樣的,直接看程式碼吧:
/**
	 * 從某個節點開始搜尋
	 * 
	 * @param target 目標值
	 * @param startSearchNode 開始搜尋的節點
	 * @return
	 */
	public Node searchNode(T target, Node startNode) {
		int compareResult = target.compareTo(startNode.data);

		if (compareResult == 0)
			return startNode;
		else if (compareResult > 0 && startNode.rightChildNode != null)
			return searchNode(target, startNode.rightChildNode);
		else if (compareResult < 0 && startNode.leftChildNode != null)
			return searchNode(target, startNode.leftChildNode);
		else
			return null;
	}

	/**
	 * 查詢資料所在節點
	 * 
	 * @param target 目標資料
	 * @return null或資料所在節點
	 */
	public Node searchNode(T target) {
		if (treeRoot.data == null)
			return null;
		return searchNode(target, treeRoot);
	}

	/**
	 * 查詢資料
	 * @param target 目標資料(有部分檢索需要的資訊即可)
	 * @return 完整目標資料
	 */
	public BaseData searchData(T target) {
		Node node = searchNode(target);
		if (node != null)
			return node.data;
		return null;
	}

      然後看刪除操作,這個刪除真的是有點麻煩,為了把這部分理清楚,把程式碼調通,幾乎花費了一整天的時間,慢慢捋來~

    刪除分為以下幾種情況:

    1.被刪除的節點只有左節點或者只有右節點,這種情況好辦,因為節點在一條鏈上,沒有分叉,就像處理連結串列一樣把這個節點摘掉就行了。讓它的父節點關聯它的子節點,它的子節點關聯它的父節點就完事。如果它沒有父節點,說明它是根節點,直接將其子節點作為根節點就行。

    2.被刪除的節點沒有子節點,這種情況也很簡單,它是葉子節點,直接置空,將其父節點對應的子節點也置空,就完事。

   3.被刪除的節點有左右子節點。這種情況就有點麻煩了。

    這裡需要了解兩個概念,叫“前驅”和“後繼”。分別是樹中小於它的最大值和大於它的最小值,如果把樹結構中的所有節點按順序拍好的話,它的前驅和它的後繼兩個節點剛好在它左右緊挨著它。當一個節點被刪除時,為了保證二叉樹的結構不被破壞,要讓它的前驅或者後繼節點來代替它的位置,然後將它的前驅或者後繼節點同樣做刪除操作。

    那麼怎樣找前驅或者後繼呢。小於它的最大值,就是在樹中在它左邊最靠右的那個節點。同樣,大於它的最小值,就是在樹中在它右邊最靠左的那個節點。當一個節點既有左子節點又有右子節點時,前驅就是它的左子節點的右子節點的右子節點...直到最右子節點;後繼就是它的右子節點的左子節點的左子節點...直到最左子節點。上個圖吧:

    23的後繼是32;98的前驅是76。


    上圖中,當23被刪除,則可以用32代替它,也可以用12代替它,然後刪除掉32(或者12)的原節點就行了;同理,當98被刪除時,可以用76代替它,或者用99代替它,然後刪除76(或者99)的原節點就行了。當然,如果被刪除節點是根節點,就用代替它的節點作為根節點然後刪除代替節點的原節點就行了。再次推薦你用視覺化工具http://zh.visualgo.net/bst來看二叉樹的各種操作動畫,簡單明瞭。

    所以刪除操作程式碼如下(程式碼用的是後繼節點替代待刪除節點):

/**
	 * 刪除節點
	 * 
	 * @param node 待刪除節點
	 */
	private void deleteNode(Node node) {
		// 如果按順序排列好節點,它的前驅和後繼就是這個序列上緊挨著它左右兩側的節點.

		// 如果節點只有左節點或者只有右節點

		if (node.haveLeftChild() && !node.haveRightChild()) {// 只有左節點
			if (node.isLeftChild) {
				node.fatherNode.leftChildNode = node.leftChildNode;

			} else if (node.isRightChild) {
				node.fatherNode.rightChildNode = node.leftChildNode;
			} else// 待刪除節點是根節點
				treeRoot = node.leftChildNode;
			node.leftChildNode.fatherNode = node.fatherNode;
		} else if (node.haveRightChild() && !node.haveLeftChild()) {// 只有右節點
			if (node.isLeftChild) {
				node.fatherNode.leftChildNode = node.rightChildNode;

			} else if (node.isRightChild) {
				node.fatherNode.rightChildNode = node.rightChildNode;
			} else// 待刪除節點是根節點
				treeRoot = node.rightChildNode;
			node.rightChildNode.fatherNode = node.fatherNode;
		} else if (node.haveLeftChild() && node.haveRightChild()) {// 有左右子節點
			Node successorNode = getSuccessorNode(node);
			if (successorNode == node.rightChildNode) {// 後繼節點是右子節點
				successorNode.fatherNode = node.fatherNode;
				if (node.isLeftChild)
					node.fatherNode.leftChildNode = successorNode;
				else if (node.isRightChild)
					node.fatherNode.rightChildNode = successorNode;
				else {// 是根節點
					successorNode = treeRoot;
				}

				successorNode.fatherNode = node.fatherNode;
				successorNode.leftChildNode = node.leftChildNode;
				node.leftChildNode.fatherNode = successorNode;

			} else {// 後繼節點是右子節點的最左子節點
				if (successorNode.haveRightChild()) {// 左子節點有右子樹
					successorNode.fatherNode.leftChildNode = successorNode.rightChildNode;
					successorNode.rightChildNode.fatherNode = successorNode.fatherNode;

					replaceNode(node, successorNode);

				} else {// 左子節點沒有右子樹
						// 葉節點,直接刪除
					successorNode.fatherNode.leftChildNode = null;
					replaceNode(node, successorNode);
				}
			}

		} else {// 沒有子節點
			if (node.isLeftChild) {
				node.fatherNode.leftChildNode = null;
			} else if (node.isRightChild) {
				node.fatherNode.rightChildNode = null;
			}

		}

		node = null;
	}

	/**
	 * 非相鄰節點的替換邏輯(非相鄰加粗!)
	 * @param node 被替換節點
	 * @param replaceNode 替換的節點
	 */
	private void replaceNode(Node node, Node replaceNode) {
		if (node.isLeftChild)
			node.fatherNode.leftChildNode = replaceNode;
		else if (node.isRightChild)
			node.fatherNode.rightChildNode = replaceNode;
		else {// node是根節點
			treeRoot = replaceNode;
		}

		node.leftChildNode.fatherNode = node.rightChildNode.fatherNode = replaceNode;
		replaceNode.leftChildNode = node.leftChildNode;
		replaceNode.rightChildNode = node.rightChildNode;
	}

	/**
	 * 獲取一個節點的後繼節點
	 * @param node
	 * @return
	 */
	private Node getSuccessorNode(Node node) {
		if (!node.haveRightChild()) {// 沒有右子樹
			return null;
		}

		Node targetNode = node.rightChildNode;
		while (targetNode.haveLeftChild()) {// 找右子樹的最左孩子,保證返回的節點一定沒有左子樹
			targetNode = targetNode.leftChildNode;
		}

		return targetNode;
	}

	/**
	 * 刪除數中的資料
	 * @param baseData
	 */
	public void deleteData(T baseData) {
		Node node = searchNode(baseData);
		deleteNode(node);
	}
    然後還有一個遍歷操作,中規中矩的先序遍歷,遞迴操作:
/**
	 * 遍歷節點
	 * @param node
	 */
	private void preOrder(Node node) {
		System.out.println("" + node.data.toString());
		if (node.haveLeftChild())
			preOrder(node.leftChildNode);

		if (node.haveRightChild())
			preOrder(node.rightChildNode);
	}

	/**
	 * 遍歷樹(前序遍歷)
	 */
	public void preOrder() {
		if (treeRoot == null)
			return;

		preOrder(treeRoot);

	}
      二叉搜尋樹大概就這樣吧,附上文中程式碼資源:點選開啟連結