首頁 > 軟體

Java資料結構學習之二元樹

2021-06-21 16:00:53

一、背景知識:樹(Tree)

在之前的筆記中,我們介紹的連結串列、棧、佇列、陣列和字串都是以線性結構來組織資料的。本篇筆記要介紹的採用的是樹狀結構,這是一種非線性的資料組織形式。

樹結構由節點構成,且不存在。我們曾線上性表型的資料結構中介紹過迴圈連結串列和迴圈佇列,這兩種資料結構使得儲存容器中的元素形成一個閉環,具體可參看「資料結構學習筆記」系列的相關博文,連結貼在下面:

連結串列:https://www.jb51.net/article/215278.htm

佇列:https://www.jb51.net/article/211502.htm

樹狀結構與線性結構最重要的區別在於:樹只能有分叉,不能有閉環。如下圖所示:

樹結構不允許有環其實是樹的層級性決定的。樹結構中最頂端的結點是根節點, 根節點即整棵樹的頂級父節點。除了根節點只有子節點,最底層的節點只有父節點,其餘各層的節點都是上層節點的子節點、下層節點的父節點。也就是說,樹中的資料只與其上下層的資料有關,同層資料間不能有直接聯絡,這也就是樹結構不能有環的原因。

樹層級的多少往往被描述為樹的高度(height),由於我們是從上往下觀察樹結構的,因此也被描述為樹的深度(depth)。上面圖示中兩顆樹的深度都是3.

二、何為二元樹(Binray Tree)

2.1 二元樹的概念與結構

二元樹顧名思義,即每個父節點最多隻有兩個分叉的樹,這是資料結構領域使用頻率極高的一種樹結構,這與我們常常用二元對立的觀點認識世界的思維習慣有關。

二元樹的結構不僅具有層級性,還具有遞迴性,一個父節點連線左子節點右子節點,而左右子節點又可以作為父節點再各自連線兩個子節點,以此類推。因此二元樹是一種層次巢狀的資料結構,除了根節點外,樹中任意一個父節點都能作為一棵子樹,位於上層父節點左側的子樹被稱為左子樹,位於右側的子樹被稱為右子樹

二元樹體現了人們用二元思維認識自然的方式。筆者的本行是語言學,語言學界主流的對句法結構的分析方法就是類似於二元樹的二分法。拿漢語的句法結構來說,有主謂、述賓、定中、狀中、述補等基本的結構型別。句法結構具有層次巢狀遞迴的特點,同時也有對語序的要求,即句法二元樹中的左右節點的位置並不是任意的。這種分析方法語言學上被稱為層次分析法,如果我們用該方法分析句子「文程同學熱愛程式設計」,傳統圖示和句法樹圖示分別如下:

2.2 滿二元樹與完全二元樹

二元樹中有兩個特殊的結構型別:滿二元樹完全二元樹。滿二元樹的結構特點是除了最後一層外,所有層級的節點都有兩個子節點;完全二元樹的結構特點是除了最後兩層外,所有層級的節點都有兩個子節點,倒數第二層的子節點(即最後一層節點)全部靠左排列。如下圖所示:

由此可見,滿二元樹一定是完全二元樹,完全二元樹可滿可不滿。這兩種二元樹體現了我們採用樹狀結構儲存資料時,對於空間利用率的追求。比如我們設計一個深度為n的二元樹,那麼整個二元樹能容納的最大節點數為2^n-1,滿二元樹就是達到了最大節點數,用足了二元樹的容量。完全二元樹除了n層沒有子節點,除n-1層外各層父節點都充分利用了自己擁有子節點的名額,也算是儘可能做到了對空間的充分利用。

為了更好地理解完全二元樹的空間利用率,我們看一個非完全二元樹的例子,如下圖所示:

上圖是一個深度為4的非完全二元樹,前3層的父節點都預留了左右兩個子節點的位置,然而第二層的第2個結點只使用了右子節點的空間,浪費了左子節點的空間。如果二元樹的深度很深,其中很多層級的父節點都存在浪費子節點「名額」的現象,那麼會造成相當大的空間資源的浪費,二元樹也失去了「二叉」的意義。但是完全二元樹最多浪費倒數第二層父節點的子節點名額, 整體上能夠保證較高的空間利用率。

2.3 二元樹的三種遍歷方式

二元樹的形狀整體上構成一個三角形,最小的二元樹由一個位於中間的父節點和位於左右兩側的子節點構成,這導致遍歷存取一棵二元樹的所有節點有三種順序:前序遍歷(Preorder Traversal , VLR)、中序遍歷(Inorder Traversal , LDR)和後序遍歷(Inorder Traversal , LRD)。

無論哪種遍歷方式,二元樹都是從上到下、從左到右遍歷的,即從父節點層到子節點層、從左子樹到右子樹。2.1解釋了二元樹的遞迴性,遍歷二元樹時採用的也是遞迴(recursion)的方式。對於整棵樹或某一子樹,都是從根開始,先遍歷其左子樹,再遍歷其右子樹;分別遍歷左右子樹時,同樣是從根開始,從左向右遍歷;以此類推,直到遍歷到最後一個右子節點。

如果我們以列印節點資料的方式來表示對節點的存取,那麼前序、中序和後序的區別就在於列印節點的時機不同。前序遍歷的操作順序是列印節點在遍歷左子樹和遍歷右子樹之前中序遍歷的操作順序是列印節點在遍歷左子樹和遍歷右子樹之間後序遍歷的操作順序是列印節點在遍歷左子樹和遍歷右子樹之後。子樹遍歷的過程是遞迴實現的。 

如果我們想遍歷2.1演示的「文程同學熱愛程式設計」的句法二元樹,那麼用三種遍歷方法得到的遍歷結果分別如下:

三、二元樹及其遍歷的簡單實現(Java)

我們用Java語言實現「文程同學熱愛程式設計」這個句子對應的句法二元樹,設計思路是:將同層級的父節點(二元樹及其各子樹的根節點)存入陣列中,陣列中存入的是結點,包括資料左右指標,左右指標分別指向位於下一層節點的左右子節點,如果沒有子節點則指標為空指標。

用陣列的好處是,可以通過節點所在的索引建立上下層級父節點和子節點的指標聯絡。假設父節點在它所在的層級陣列中的索引為i,那麼左子節點在它所在層級陣列中的索引為(i+1)*2-2,右子節點的索引為(i+1)*2-1,即左子節點的索引+1。

遍歷預設從整棵二元樹的根節點開始,通過方法的重寫實現預設引數的效果。

準備工作1:MyBinaryTree.java,建立一個二元樹的類

package com.notes.data_structure6;
 
import com.notes.data_structure6.NumberOfNodesException;
 
public class MyBinaryTree {
	// 樹的根結點
	private Node[] root;
	// 樹的深度(當前層級數)
	private int depth;
	// 將每一層所有的 結點 都儲存在陣列中,結點數是 2的 層數 次冪
	private Node[] currentLevel;
	
	
	public MyBinaryTree(String data) {
		Node[] rootArray = new Node[] {new Node(data)};
		this.root = rootArray;
		this.currentLevel = rootArray;
	}
 
	// 定義一個結點類
	private class Node{
		private String data; // 資料
		private Node leftNext; // 左指標
		private Node rightNext; // 右指標
		
		// 構造方法:Node範例化時傳入資料
		public Node(String data) {
			this.data = data;
		}	
	}
	
	// 向樹中增加一層結點
	public void add(String[] datas) throws NumberOfNodesException {
		// 層級數增加1
		depth++;
		// 新增 層級 的最大結點數
		int nodeNum = numberOfNextNodes();
		// 如果傳入的 資料數 與 當前層級 最大結點數 不符,丟擲異常
		if(datas.length != nodeNum) {
			throw new NumberOfNodesException("第"+depth+"層最大父節點數為"+nodeNum);
		}
		// 將傳入的 資料陣列 轉換為 結點陣列
		Node[] newLevel = new Node[nodeNum];
		// 當前 層級的 結點數量(新增層級的父)
		int nodeNum2 = (int) Math.pow(2, depth-1);
		// 讓每一個結點都與上層 父結點 建立連線
		for(int i=0;i<nodeNum2;i++) {
			// 讓父結點 的左指標 指向 左子結點
			int leftIndex = (i+1)*2-2; // 計算左子結點對應的新層級陣列的索引
			currentLevel[i].leftNext = new Node(datas[leftIndex]); // 建立指標指向
			newLevel[leftIndex] = currentLevel[i].leftNext; // 將結點加入新層級結點陣列
			// 讓父結點 的右指標 指向 右子結點
			int rightIndex = leftIndex+1; // 計算右子結點對應的新層級陣列的索引
			currentLevel[i].rightNext = new Node(datas[rightIndex]); // 建立指標指向
			newLevel[rightIndex] = currentLevel[i].rightNext; // 將結點加入新層級結點陣列
		}
		// 讓新增層級的陣列成為當前層級的陣列
		currentLevel =  newLevel;
	}
	
	// 前序遍歷所有結點
	public void preTraversal(Node node) {
		if(node==null) {
			return;
		}
		System.out.print(node.data+" ");
		preTraversal(node.leftNext);
		preTraversal(node.rightNext);
	}
	
	// 重寫 前序遍歷 方法,讓遍歷從 根結點 開始
	public void preTraversal() {
		Node node = root[0];
		if(node==null) {
			return;
		}
		// 遞迴時呼叫帶引數的方法
		System.out.print(node.data+" ");
		preTraversal(node.leftNext);
		preTraversal(node.rightNext);
	}
	
	// 中序遍歷所有結點
	public void midTraversal(Node node) {
		if(node==null) {
			return;
		}
		midTraversal(node.leftNext);
		System.out.print(node.data+" ");
		midTraversal(node.rightNext);
	}
	
	// 重寫中序遍歷 方法,讓遍歷從 根結點 開始
	public void midTraversal() {
		Node node = root[0];
		if(node==null) {
			return;
		}
		// 遞迴時呼叫帶引數的方法
		midTraversal(node.leftNext);
		System.out.print(node.data+" ");
		midTraversal(node.rightNext);
	}
	
	// 後序遍歷所有結點
	public void posTraversal(Node node) {
		if(node==null) {
			return;
		}
		posTraversal(node.leftNext);
		posTraversal(node.rightNext);
		System.out.print(node.data+" ");
	}
	
	// 重寫後序遍歷 方法,讓遍歷從 根結點 開始
	public void posTraversal() {
		Node node = root[0];
		if(node==null) {
			return;
		}
		// 遞迴時呼叫帶引數的方法
		posTraversal(node.leftNext);
		posTraversal(node.rightNext);
		System.out.print(node.data+" ");
	}
	
	// 檢視 新增層級 的最大結點數
	public int numberOfNextNodes() {
		return (int) Math.pow(2,depth);
	}
	
	// 檢視 樹 的深度(層級數)
	public int getDepth() {
		return depth;
	}
	
	
}

準備工作2:NumberOfNodesException.java,為add()方法建立一個自定義異常,如果傳入的資料數與當前層級最大結點數不符,則丟擲該異常(如果二元樹不滿,在陣列的相應位置傳入null)。

package com.notes.data_structure6;
 
public class NumberOfNodesException extends Exception{
	public NumberOfNodesException(String message) {
		super(message);
	}
}

句法二元樹的實現及其遍歷:TreeDemo.java

package com.notes.data_structure6;
 
public class TreeDemo {
	public static void main(String[] args) throws NumberOfNodesException {
		// 範例化二元樹類,並且傳入根節點的資料
		MyBinaryTree tree = new MyBinaryTree("句子");
		// 加入第一層節點的資料
		tree.add(new String[] {"主語","謂語"});
		// 加入第二層節點的資料
		tree.add(new String[] {"定語","中心語","述語","賓語"});
		
		// 前序遍歷
		tree.preTraversal(); // 句子 主語 定語 中心語 謂語 述語 賓語 
		System.out.println();
		// 中序遍歷
		tree.midTraversal(); // 定語 主語 中心語 句子 述語 謂語 賓語 
		System.out.println();
		// 後序遍歷
		tree.posTraversal(); // 定語 中心語 主語 述語 賓語 謂語 句子 
	}
}

到此這篇關於Java資料結構學習之二元樹的文章就介紹到這了,更多相關Java二元樹內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com