二叉樹(順序儲存二叉樹,線索化二叉樹)

馬化騰的爸爸碼ming發表於2020-10-29

順序儲存二叉樹

順序儲存二叉樹的概念

a.基本說明
從資料儲存來看,陣列儲存方式和樹的儲存方式可以相互轉換,即陣列可以轉換成樹,樹也可以轉換成陣列,看下面的示意圖。
在這裡插入圖片描述
b. 要求:

  1. 右圖的二叉樹的結點,要求以陣列的方式來存放arr : [1, 2, 3, 4, 5, 6, 6]
  2. 要求在遍歷陣列arr 時,仍然可以以前序遍歷,中序遍歷和後序遍歷的方式完成結點的遍歷

c.順序儲存二叉樹的特點:

  1. 順序二叉樹通常只考慮完全二叉樹
  2. 第n 個元素的左子節點為2 * n + 1
  3. 第n 個元素的右子節點為2 * n + 2
  4. 第n 個元素的父節點為(n-1) / 2
  5. n : 表示二叉樹中的第幾個元素(按0 開始編號如圖所示)

順序儲存二叉樹遍歷

需求: 給你一個陣列{1,2,3,4,5,6,7},要求以二叉樹前序遍歷的方式進行遍歷。前序遍歷的結果應當為1,2,4,5,3,6,7
程式碼實現:

public class ArrBinaryTreeDemo {

	public static void main(String[] args) {
		int[] arr = { 1, 2, 3, 4, 5, 6, 7 };
		//建立一個 ArrBinaryTree
		ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr);
		arrBinaryTree.preOrder(); // 1,2,4,5,3,6,7
	}

}

//編寫一個ArrayBinaryTree, 實現順序儲存二叉樹遍歷

class ArrBinaryTree {
	private int[] arr;//儲存資料結點的陣列

	public ArrBinaryTree(int[] arr) {
		this.arr = arr;
	}
	
	//過載preOrder
	public void preOrder() {
		this.preOrder(0);
	}
	
	//編寫一個方法,完成順序儲存二叉樹的前序遍歷
	/**
	 * 
	 * @param index 陣列的下標 
	 */
	public void preOrder(int index) {
		//如果陣列為空,或者 arr.length = 0
		if(arr == null || arr.length == 0) {
			System.out.println("陣列為空,不能按照二叉樹的前序遍歷");
		}
		//輸出當前這個元素
		System.out.println(arr[index]); 
		//向左遞迴遍歷
		if((index * 2 + 1) < arr.length) {
			preOrder(2 * index + 1 );
		}
		//向右遞迴遍歷
		if((index * 2 + 2) < arr.length) {
			preOrder(2 * index + 2);
		}
	}
	
}

順序儲存二叉樹應用例項

八大排序演算法中的堆排序,就會使用到順序儲存二叉樹, 關於堆排序,我們放在<<樹結構實際應用>> 章節講解。

線索化二叉樹

先看一個問題

將數列{1, 3, 6, 8, 10, 14 } 構建成一顆二叉樹. n+1=7
在這裡插入圖片描述

問題分析:

  1. 當我們對上面的二叉樹進行中序遍歷時,數列為{8, 3, 10, 1, 6, 14 }
  2. 但是6, 8, 10, 14 這幾個節點的左右指標,並沒有完全的利用上.
  3. 如果我們希望充分的利用各個節點的左右指標, 讓各個節點可以指向自己的前後節點,怎麼辦?
  4. 解決方案-線索二叉樹

線索二叉樹基本介紹

  1. n 個結點的二叉連結串列中含有n+1 【公式2n-(n-1)=n+1】個空指標域。利用二叉連結串列中的空指標域,存放指向該結點在某種遍歷次序下的前驅和後繼結點的指標(這種附加的指標稱為"線索")
  2. 這種加上了線索的二叉連結串列稱為線索連結串列,相應的二叉樹稱為線索二叉樹(Threaded BinaryTree)。根據線索性質
    的不同,線索二叉樹可分為前序線索二叉樹、中序線索二叉樹和後序線索二叉樹三種
  3. 一個結點的前一個結點,稱為前驅結點
  4. 一個結點的後一個結點,稱為後繼結點

線索二叉樹應用案例

應用案例說明:將下面的二叉樹,進行中序線索二叉樹。中序遍歷的數列為{8, 3, 10, 1, 14, 6}
思路分析: 中序遍歷的結果:{8, 3, 10, 1, 14, 6}
在這裡插入圖片描述
 說明: 當線索化二叉樹後,Node 節點的屬性left 和right ,有如下情況:

  1. left 指向的是左子樹,也可能是指向的前驅節點. 比如① 節點left 指向的左子樹, 而⑩ 節點的left 指向的就是前驅節點.
  2. right 指向的是右子樹,也可能是指向後繼節點,比如① 節點right 指向的是右子樹,而⑩ 節點的right 指向的是後繼節點.

遍歷線索化二叉樹

說明:對前面的中序線索化的二叉樹, 進行遍歷
分析:因為線索化後,各個結點指向有變化,因此原來的遍歷方式不能使用,這時需要使用新的方式遍歷線索化二叉樹,各個節點可以通過線型方式遍歷,因此無需使用遞迴方式,這樣也提高了遍歷的效率。 遍歷的次序應當和中序遍歷保持一致。

 程式碼實現:

public class ThreadedBinaryTreeDemo {

	public static void main(String[] args) {
		// 測試一把中序線索二叉樹的功能
		HeroNode root = new HeroNode(1, "tom");
		HeroNode node2 = new HeroNode(3, "jack");
		HeroNode node3 = new HeroNode(6, "smith");
		HeroNode node4 = new HeroNode(8, "mary");
		HeroNode node5 = new HeroNode(10, "king");
		HeroNode node6 = new HeroNode(14, "dim");

		// 二叉樹,後面我們要遞迴建立, 現在簡單處理使用手動建立
		root.setLeft(node2);
		root.setRight(node3);
		node2.setLeft(node4);
		node2.setRight(node5);
		node3.setLeft(node6);

		// 測試中序線索化
		ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
		threadedBinaryTree.setRoot(root);
		threadedBinaryTree.threadedNodes();

		// 測試: 以10號節點測試
		HeroNode leftNode = node5.getLeft();
		HeroNode rightNode = node5.getRight();
		System.out.println("10號結點的前驅結點是 =" + leftNode); // 3
		System.out.println("10號結點的後繼結點是=" + rightNode); // 1

		// 當線索化二叉樹後,能在使用原來的遍歷方法
		// threadedBinaryTree.infixOrder();
		System.out.println("使用線索化的方式遍歷 線索化二叉樹");
		threadedBinaryTree.threadedList(); // 8, 3, 10, 1, 14, 6

	}

}

//定義ThreadedBinaryTree 實現了線索化功能的二叉樹
class ThreadedBinaryTree {
	private HeroNode root;

	// 為了實現線索化,需要建立要給指向當前結點的前驅結點的指標
	// 在遞迴進行線索化時,pre 總是保留前一個結點
	private HeroNode pre = null;

	public void setRoot(HeroNode root) {
		this.root = root;
	}

	// 過載一把threadedNodes方法
	public void threadedNodes() {
		this.threadedNodes(root);
	}

	// 遍歷線索化二叉樹的方法
	public void threadedList() {
		// 定義一個變數,儲存當前遍歷的結點,從root開始
		HeroNode node = root;
		while (node != null) {
			// 迴圈的找到leftType == 1的結點,第一個找到就是8結點
			// 後面隨著遍歷而變化,因為當leftType==1時,說明該結點是按照線索化
			// 處理後的有效結點
			while (node.getLeftType() == 0) {
				node = node.getLeft();
			}

			// 列印當前這個結點
			System.out.println(node);
			// 如果當前結點的右指標指向的是後繼結點,就一直輸出
			while (node.getRightType() == 1) {
				// 獲取到當前結點的後繼結點
				node = node.getRight();
				System.out.println(node);
			}
			// 替換這個遍歷的結點
			node = node.getRight();

		}
	}

	// 編寫對二叉樹進行中序線索化的方法
	/**
	 * 
	 * @param node 就是當前需要線索化的結點
	 */
	public void threadedNodes(HeroNode node) {

		// 如果node==null, 不能線索化
		if (node == null) {
			return;
		}

		// (一)先線索化左子樹
		threadedNodes(node.getLeft());
		// (二)線索化當前結點[有難度]

		// 處理當前結點的前驅結點
		// 以8結點來理解
		// 8結點的.left = null , 8結點的.leftType = 1
		if (node.getLeft() == null) {
			// 讓當前結點的左指標指向前驅結點
			node.setLeft(pre);
			// 修改當前結點的左指標的型別,指向前驅結點
			node.setLeftType(1);
		}

		// 處理後繼結點
		if (pre != null && pre.getRight() == null) {
			// 讓前驅結點的右指標指向當前結點
			pre.setRight(node);
			// 修改前驅結點的右指標型別
			pre.setRightType(1);
		}
		// !!! 每處理一個結點後,讓當前結點是下一個結點的前驅結點
		pre = node;

		// (三)線上索化右子樹
		threadedNodes(node.getRight());

	}

}

//先建立HeroNode 結點
class HeroNode {
	private int no;
	private String name;
	private HeroNode left; // 預設null
	private HeroNode right; // 預設null
	// 說明
	// 1. 如果leftType == 0 表示指向的是左子樹, 如果 1 則表示指向前驅結點
	// 2. 如果rightType == 0 表示指向是右子樹, 如果 1表示指向後繼結點
	private int leftType;
	private int rightType;

	public int getLeftType() {
		return leftType;
	}

	public void setLeftType(int leftType) {
		this.leftType = leftType;
	}

	public int getRightType() {
		return rightType;
	}

	public void setRightType(int rightType) {
		this.rightType = rightType;
	}

	public HeroNode(int no, String name) {
		this.no = no;
		this.name = name;
	}

	public int getNo() {
		return no;
	}

	public void setNo(int no) {
		this.no = no;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public HeroNode getLeft() {
		return left;
	}

	public void setLeft(HeroNode left) {
		this.left = left;
	}

	public HeroNode getRight() {
		return right;
	}

	public void setRight(HeroNode right) {
		this.right = right;
	}

	@Override
	public String toString() {
		return "HeroNode [no=" + no + ", name=" + name + "]";
	}
}

線索二叉樹優勢與不足

優勢
(1)利用線索二叉樹進行中序遍歷時,不必採用堆疊處理,速度較一般二叉樹的遍歷速度快,且節約儲存空間。
(2)任意一個結點都能直接找到它的前驅和後繼結點。

不足
(1)結點的插入和刪除麻煩,且速度也較慢。
(2)線索子樹不能共用。

線索二叉樹存在的意義

(來自: https://blog.csdn.net/Tangs_/article/details/83040502)

  • 百度,google了二十分鐘也沒看到關於線索二叉樹的應用。

    線索二叉樹減少了的空指標域的同時又對每個節點增加了兩個標誌位。

    如果要遍歷樹可以用棧或者佇列或者遞迴,那線索二叉樹的意義是什麼?莫不是學者們強迫症犯了就為了減少空指標域的個數。

    書上寫著引入線索二叉樹是為了加快查詢節點前驅和後繼的速度,而個人覺得線索二叉樹在建立的時候使得樹的建立變得複雜了一點點,從邏輯上去想也變得複雜,覺得有點吃力不討好。

    除了考試時可能會考到線索二叉樹,其他的用處暫時沒發現,有緣再見線索二叉樹吧。

    終於,發現了一個實際的應用:

    當路由器使用CIDR,選擇下一跳的時候,或者轉發分組的時候,通常會用最長字首匹配(最佳匹配)來得到路由表的一行資料,為了更加有效的查詢最長字首匹配,使用了一種層次的資料結構中,通常使用的資料結構為二叉線索。

    闊以闊以,先留個懸念,後續詳解CIDR以及線索二叉樹。

相關文章