初入數據結構的線索二叉樹以及Java代碼實現
- 前提概念
- 什麼是線索二叉樹
- 線索二叉樹的由來
- 線索二叉樹相比普通二叉樹的好處
- 線索二叉樹
- 普通二叉樹中序線索化
- 線索二叉樹中序遍歷
- Java代碼實現
前提概念
什麼是線索二叉樹?
我們知道二叉樹的是一棵樹的度小於等於2
的有序樹
;那麼線索二叉樹又是什麼呢?其實線索二叉樹實際是一棵變形的二叉樹。
如果有一種算法需要經常的對一棵二叉樹進行遍歷,那麼遍歷的過程就是在頻繁的利用遞歸或棧做重複性的操作,而線索二叉樹不需要如此,通過使用二叉樹空閒的內存空間記錄某些結點的前趨和後繼結點,在遍歷的時候,就可以利用好這些保存的結點信息,提高遍歷效率。
那麼使用這種方式(利用好二叉樹空閒內存的方式)構建的二叉樹就是一顆線索二叉樹
線索二叉樹的由來
普通二叉樹中存在指針浪費的問題:
我們知道線索二叉樹相比普通二叉樹而言,就是利用了普通二叉樹的空閒內存,記錄了某些結點的前趨和後繼元素信息。我們先來了解一個前提,這個空閒內存到底指的是什麼呢?
我們看到上面的圖,該樹就是一顆很普通的二叉樹,結點結構也很簡單,就是數據域,左孩子指針,右孩子指針。該樹有7
個結點,所以該樹就有2*7 = 14個指針空間,這14
個指針空間就是我們所說的內存空間。
而空閒的內存空間又指的是什麼呢?很簡單,就是14個指針空間中指向null的空間,比如D,E,F
結點的左右孩子指針和C
結點的左孩子指針。因爲這些空間並沒有指向左右孩子,本質上就是一種浪費。本着有好東西就不能浪費的理念,所以就出現了線索二叉樹
空閒指針的規律:
- 存在n個結點的二叉鏈表必定存在n + 1個空指針域
- 不要問爲什麼,數一下就知道了
所以線索二叉樹實際上就是一棵利用普通二叉樹剩下的這些空指針域去存儲結點之間的前趨和後繼關係的特殊二叉樹
線索二叉樹相比普通二叉樹的好處
- 更好的利用了普通二叉樹的空指針域,相比普通二叉樹,並沒有增加結構開銷
- 在進行二叉樹遍歷的時候,利用好前趨結點和後繼結點的信息,可以更快的進行前,中,後序等遍歷
線索二叉樹
線索二叉樹的結點結構
線索二叉樹的結點結構:
我們知道,通常二叉樹在代碼實現中,是通過二叉鏈的形式去表達的,通常都是一個數據域,兩個指針域
但我們知道,在線索二叉樹中,如果結點有左孩子,那麼
lchild
就會指向左孩子,否則lchild
就會指向該結點的直接前趨結點;同樣,如果該結點有右孩子,那麼rchild
就會指向右孩子,否則rchild
就會指向該結點的直接後繼結點。
也就是說,左右孩子指針是有雙重意義的,這很容易讓人產生迷惑,所以爲了避免指針域指向結點的意義混淆,需要在普通二叉樹的結點構造上做一些小小的改變,增加兩個標誌位
- 我們在原有的基礎上,增加了兩個標誌位
- ltag值爲0時,表示lchild指向的是該結點的左孩子;爲1的時候,表示lchild指向的是該結點的直接前趨結點
- rtag值爲0時,表示rchild指向的是該結點的右孩子;爲1的時候,表示rchild指向的是該結點的直接後繼結點
結點代碼:
public static class TreeNode<E> {
/**
* 數據域
*/
private E data;
/**
* 子結點域
*/
private TreeNode<E> lchild, rchild;
/**
* tag域
* ltag爲0時,表示lchild指向的是左孩子,如果ltag爲1時,表示指向的是直接前趨結點
* rtag爲0時,表示rchild指向的是右孩子,如果rtag爲1時,表示指向的是直接後繼結點
*/
private int ltag, rtag;
}
普通二叉樹中序線索化
什麼是普通二叉樹線索化?就是給定一棵普通的二叉樹,假如有n
個結點,那麼這棵樹必然有2n
個指針域,n-1
個空指針域。通過中序的方式線索化就是,通過中序遍歷的順序依次將該樹的空指針域指向前趨或後繼結點,填充空指針域,讓一棵普通二叉樹變成一棵
/**
* 通過中序線索化一顆普通二叉樹
* 讓其從普通二叉樹變成線索二叉樹
*/
public void createThreadTreeByMidOrder() {
if (this.rootNode == null) {
return;
}
createThreadTreeByMidOrder(this.rootNode);
}
/**
* 通過中序線索化一顆普通二叉樹
* 讓其從普通二叉樹變成線索二叉樹
* 邏輯就跟遞歸實現的中序遍歷差不多
*/
private void createThreadTreeByMidOrder(TreeNode<T> root) {
//遞歸推出條件
if (root == null) {
return;
}
/**
* 1.先遞歸左子樹
*/
createThreadTreeByMidOrder(root.lchild);
/**
* 2. 再到相對根節點
* 一開始的前趨後繼鏈的第一個前趨結點必然爲空
*/
//如果當前結點的左孩子等於null,則代表它是空指針域
if (root.lchild == null) {
//設置前趨結點和狀態,前趨結點是一個臨時全局變量
root.ltag = 1;
root.lchild = preNode;
}
//如果前趨結點不爲null(不是鏈表頭),就看前趨結點的右孩子等不等於null,如果是null,則填寫後繼結點
if (preNode != null && preNode.rchild == null) {
//前趨結點的右孩子爲後繼結點,後繼結點是當前結點 | 要區分前趨結點和當前結點分別是什麼,且該前趨是當前結點的前趨
preNode.rtag = 1;
preNode.rchild = root;
}
//每處理完一個結點,當前root結點就是下一個結點的前趨結點
preNode = root;
/**
* 3. 遞歸右子樹
*/
createThreadTreeByMidOrder(root.rchild);
}
- 代碼實現其實也不難,整體的中序線索化就是一個遞歸的中序遍歷
- 遞歸推出條件就是,
相對根結點==null
- 需要有一個全局的臨時變量記錄線索後的每一步的前趨結點
preNode
, 默認初始爲null - 而每次到了遍歷相對根結點階段,就需要做兩個步驟
- 首先找到
最左葉子結點A
的左指針,它必然是空指針域,指向null, 這個最左子結點A
就是的當前相對根結點
,其ltag變爲1,代表lchild指向前趨結點,非左孩子。同時結點A的lchild指向臨時變量preNode(第一次必然爲null) - 然後判斷
當前相對根結點
前趨結點preNode
是否爲null, 且preNode
的右指針rchild
是否也是空指針域。爲什麼要判斷前趨結點的右指針呢?因爲每一輪遍的第1步都是先解決左指針,而過了這輪遍歷,下一輪開始,上輪的當前結點就會成爲本輪的前趨結點, 我們就要解決上一輪相對根結點(本輪的前趨結點)的右指針rchild
; 即本輪解決當前結點A的左指針,下一輪就會解決本輪前趨結點(結點A)的右指針 - 每一輪結束,當前相對結點就會賦值給臨時全局遍歷前趨結點preNode
- 首先找到
線索二叉樹中序線索遍歷
什麼是線索二叉樹的中序遍歷呢?普通二叉樹的遍歷,我們都知道,左->根->右
;但是線索二叉樹的中序遍歷還需要這樣子嗎?那必須有更加高效的方式啦!
/**
* 中序遍歷線索二叉樹 | 迭代方式
* 1. 找到最左葉子節點,然後向右開始線索遍歷,找後繼結點
* 2. 因爲有中斷結點,所以無法一直都向右遍歷,因爲中斷結點本身有右子樹,所以無法找到後繼結點
* 3. 因爲中斷結點的影響,所以需要找到右孩子,跳到第一層while,重寫開始循環,即迭代子樹
*/
public void midOrderWithIterator() {
//如果是空樹,直接返回
if (this.rootNode == null) {
return;
}
//從根節點開始
TreeNode<T> node = this.rootNode;
//只要當前節點不爲null,就一直遍歷
while (node != null) {
//找到最左葉子結點,ltag == 1就退出循環
while (node.ltag == 0) {
node = node.lchild;
}
//輸出
System.out.println(node.data);
//只要當前結點不是中斷結點,那麼直接輸出可遍歷到的後繼結點
while (node.rtag == 1 && node.rchild != null) {
node = node.rchild;
System.out.println(node.data);
}
//如果被打破了條件,證明鏈表中斷,當前node結點是中斷結點,直接找中斷結點的右孩子,然新的結點成爲根結點,重新開始迭代,遍歷子樹
node = node.rchild;
}
}
- 找到最左葉子節點,然後向右開始線索遍歷,找後繼結點
- 因爲有中斷結點,所以無法一直都向右遍歷,因爲中斷結點本身有右子樹,所以無法找到後繼結點
- 因爲中斷結點的影響,所以需要找到右孩子,跳到第一層while,重寫開始循環,即迭代子樹
這裏盜個圖來自@夢想拒絕零風險
- 從上圖看,結點1就是本樹的最左葉子結點,它的ltag必然爲1,所以會打破第一層內循環
- 而結點2就是一箇中斷結點,它的左右指針都是指向左右孩子,並不屬於原指針域,此時我們能做的就只是找到她的右孩子,重新開始一輪外循環
- 總結起來就是,有兩層循環,一層外循環,一層內循環,內循環有兩個;外循環是爲了解決鏈表中斷,出現中斷結點,那就以中斷結點的右孩子作爲一棵新的樹,重新遍歷;第一層內循環的目的就是找到這個樹的最左葉子結點,第二層內循環就是爲了順着後繼結點一直遍歷,只要發現鏈表中斷,就打破第二層循環,重新開始外循環
Java代碼實現
主要功能:
- 中序將普通二叉樹線索化 -> 線索二叉樹
- 中序遍歷一棵線索二叉樹
TreeNode(線索二叉樹結點)
public class TreeNode<E> {
/**
* 數據域
*/
private E data;
/**
* 子結點域
*/
private TreeNode<E> lchild, rchild;
/**
* tag域
* ltag爲0時,表示lchild指向的是左孩子,如果ltag爲1時,表示指向的是直接前趨結點
* rtag爲0時,表示rchild指向的是右孩子,如果rtag爲1時,表示指向的是直接後繼結點
*/
private int ltag, rtag;
public TreeNode(E data) {
this.data = data;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof TreeNode)) return false;
TreeNode<?> treeNode = (TreeNode<?>) o;
if (ltag != treeNode.ltag) return false;
if (rtag != treeNode.rtag) return false;
if (data != null ? !data.equals(treeNode.data) : treeNode.data != null) return false;
if (lchild != null ? !lchild.equals(treeNode.lchild) : treeNode.lchild != null) return false;
return rchild != null ? rchild.equals(treeNode.rchild) : treeNode.rchild == null;
}
@Override
public int hashCode() {
int result = data != null ? data.hashCode() : 0;
result = 31 * result + (lchild != null ? lchild.hashCode() : 0);
result = 31 * result + (rchild != null ? rchild.hashCode() : 0);
result = 31 * result + ltag;
result = 31 * result + rtag;
return result;
}
}
ThreadTree(線索二叉樹)
/**
* 線索二叉樹
*
* @author liwenjie
*/
public class ThreadTree<T> {
/**
* 根節點
*/
private TreeNode<T> rootNode;
/**
* 前驅節點 | 臨時存放,方法的公共變量
*/
private TreeNode<T> preNode;
/**
* 構造只有一個根節點的樹
*
* @param rootNode
*/
public ThreadTree(TreeNode<T> rootNode) {
this.rootNode = rootNode;
}
/**
* 爲某個節點添加左孩子
*
* @param parent
*/
public void addLChild(TreeNode<T> parent, TreeNode<T> lchild) {
if (this.rootNode == null || parent == null || parent.lchild != null) {
return;
}
parent.lchild = lchild;
}
/**
* 爲某個節點添加右孩子
*
* @param parent
*/
public void addRChild(TreeNode<T> parent, TreeNode<T> rchild) {
if (this.rootNode == null || parent == null || parent.rchild != null) {
return;
}
parent.rchild = rchild;
}
/**
* 通過中序線索化一顆普通二叉樹
* 讓其從普通二叉樹變成線索二叉樹
*/
public void createThreadTreeByMidOrder() {
if (this.rootNode == null) {
return;
}
createThreadTreeByMidOrder(this.rootNode);
}
/**
* 通過中序線索化一顆普通二叉樹
* 讓其從普通二叉樹變成線索二叉樹
* 邏輯就跟遞歸實現的中序遍歷差不多
*/
private void createThreadTreeByMidOrder(TreeNode<T> root) {
//遞歸推出條件
if (root == null) {
return;
}
/**
* 1.先遞歸左子樹
*/
createThreadTreeByMidOrder(root.lchild);
/**
* 2. 再到相對根節點
* 一開始的前趨後繼鏈的第一個前趨結點必然爲空
*/
//如果當前結點的左孩子等於null,則代表它是空指針域
if (root.lchild == null) {
//設置前趨結點和狀態,前趨結點是一個臨時全局變量
root.ltag = 1;
root.lchild = preNode;
}
//如果前趨結點不爲null(不是鏈表頭),就看前趨結點的右孩子等不等於null,如果是null,則填寫後繼結點
if (preNode != null && preNode.rchild == null) {
//前趨結點的右孩子爲後繼結點,後繼結點是當前結點 | 要區分前趨結點和當前結點分別是什麼,且該前趨是當前結點的前趨
preNode.rtag = 1;
preNode.rchild = root;
}
//每處理完一個結點,當前root結點就是下一個結點的前趨結點
preNode = root;
/**
* 3. 遞歸右子樹
*/
createThreadTreeByMidOrder(root.rchild);
}
/**
* 中序遍歷線索二叉樹 | 迭代方式
* 1. 找到最左葉子節點,然後向右開始線索遍歷,找後繼結點
* 2. 因爲有中斷結點,所以無法一直都向右遍歷,因爲中斷結點本身有右子樹,所以無法找到後繼結點
* 3. 因爲中斷結點的影響,所以需要找到右孩子,跳到第一層while,重寫開始循環,即迭代子樹
*/
public void midOrderWithIterator() {
//如果是空樹,直接返回
if (this.rootNode == null) {
return;
}
//從根節點開始
TreeNode<T> node = this.rootNode;
//只要當前節點不爲null,就一直遍歷
while (node != null) {
//找到最左葉子結點,ltag == 1就退出循環
while (node.ltag == 0) {
node = node.lchild;
}
//輸出
System.out.println(node.data);
//只要當前結點不是中斷結點,那麼直接輸出可遍歷到的後繼結點
while (node.rtag == 1 && node.rchild != null) {
node = node.rchild;
System.out.println(node.data);
}
//如果被打破了條件,證明鏈表中斷,當前node結點是中斷結點,直接找中斷結點的右孩子,然新的結點成爲根結點,重新開始迭代,遍歷子樹
node = node.rchild;
}
}
public static void main(String[] args) {
TreeNode<Integer> node1 = new TreeNode<>(1);
TreeNode<Integer> node2 = new TreeNode<>(2);
TreeNode<Integer> node3 = new TreeNode<>(3);
TreeNode<Integer> node4 = new TreeNode<>(4);
TreeNode<Integer> node5 = new TreeNode<>(5);
TreeNode<Integer> node6 = new TreeNode<>(6);
TreeNode<Integer> node7 = new TreeNode<>(7);
ThreadTree<Integer> threadTree = new ThreadTree<>(node1);
threadTree.addLChild(node1, node2);
threadTree.addRChild(node1, node3);
threadTree.addLChild(node2, node4);
threadTree.addRChild(node2, node5);
threadTree.addLChild(node3, node6);
threadTree.addRChild(node3, node7);
threadTree.createThreadTreeByMidOrder();
threadTree.midOrderWithIterator();
}
}