文章目錄
第九章 樹結構基礎
本章源碼:https://github.com/name365/Java-Data-structure
二叉樹
數組 鏈表 樹存儲方式分析
爲什麼需要樹這種數據結構?
-
數組存儲方式的分析
- 優點:通過下標方式訪問元素,速度快。對於有序數組,還可使用二分查找提高檢索速度。
- 缺點:如果要檢索具體某個值,或者插入值(按一定順序)會整體移動,效率較低 。
-
鏈式存儲方式的分析
- 優點:在一定程度上對數組存儲方式有優化(比如:插入一個數值節點,只需要將插入節點,鏈接到鏈表中即可, 刪除效率也很好)。
- 缺點:在進行檢索時,效率仍然較低,比如(檢索某個值,需要從頭節點開始遍歷) 。
-
樹存儲方式的分析
- 能提高數據存儲,讀取的效率, 比如利用 二叉排序樹(Binary Sort Tree),既可以保證數據的檢索速度,同時也可以保證數據的插入,刪除,修改的速度。
- 案例: [7, 3, 10, 1, 5, 9, 12]
二叉樹的概念和常用術語
樹示意圖
樹的常用術語(結合示意圖理解):
節點
根節點
父節點
子節點
葉子節點 (沒有子節點的節點)
節點的權(節點值)
路徑(從root節點找到該節點的路線)
層
子樹
樹的高度(最大層數)
森林 :多顆子樹構成森林
二叉樹的概念
1. 樹有很多種,每個節點最多隻能有兩個子節點的一種形式稱爲二叉樹。 2. 二叉樹的子節點分爲左節點和右節點。
3. 如果該二叉樹的所有葉子節點都在最後一層,並且結點總數= 2^n -1 , n 爲層數,則我們稱爲滿二叉樹。 4. 如果該二叉樹的所有葉子節點都在最後一層或者倒數第二層,而且最後一層的葉子節點在左邊連續,倒數第二層的葉子節點在右邊連續,我們稱爲完全二叉樹。
前序中序後序遍歷的二叉樹圖解與實現
使用前序,中序和後序對下面的二叉樹進行遍歷. 前序遍歷: 先輸出父節點,再遍歷左子樹和右子樹 中序遍歷: 先遍歷左子樹,再輸出父節點,再遍歷右子樹 後序遍歷: 先遍歷左子樹,再遍歷右子樹,最後輸出父節點 小結: 看輸出父節點的順序,就確定是前序,中序還是後序
分析二叉樹的前序,中序,後序的遍歷步驟:
1. 創建一顆二叉樹
2. 前序遍歷
2.1 先輸出當前節點(初始的時候是root節點)
2.2 如果左子節點不爲空,則遞歸繼續前序遍歷
2.3 如果右子節點不爲空,則遞歸繼續前序遍歷
3. 中序遍歷
3.1 如果當前節點的左子節點不爲空,則遞歸中序遍歷,
3.2 輸出當前節點
3.3 如果當前節點的右子節點不爲空,則遞歸中序遍歷
4.後序遍歷
4.1 如果當前節點的左子節點不爲空,則遞歸後序遍歷,
4.2 如果當前節點的右子節點不爲空,則遞歸後序遍歷
4.3 輸出當前節點
代碼實現如下:
public class BinaryTreeTest {
public static void main(String[] args) {
//先創建一棵二叉樹
BinaryTree binaryTree = new BinaryTree();
//創建需要的節點
HeroNode root = new HeroNode(1,"宋江");
HeroNode hero2 = new HeroNode(2,"吳用");
HeroNode hero3 = new HeroNode(3,"盧俊義");
HeroNode hero4 = new HeroNode(4,"關勝");
//說明,先手動創建二叉樹,再遞歸方式創建二叉樹
root.setLeft(hero2);
root.setRight(hero3);
hero3.setLeft(hero4);
binaryTree.setRoot(root);
//測試
System.out.println("前序遍歷:"); //1,2,3,4
binaryTree.perOrder();
//測試2
System.out.println("中序遍歷:"); //2,1,4,3
binaryTree.infixOrder();
//測試3
System.out.println("後序遍歷:"); //2,4,3,1
binaryTree.postOrder();
}
}
// 創建一個HeroNode結點
class HeroNode {
private int id;
private String name;
private HeroNode left; // 默認爲null,左子節點
private HeroNode right; // 默認爲null,右子節點
public HeroNode() {
super();
}
public HeroNode(int id, String name) {
super();
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
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 [id=" + id + ", name=" + name + "]";
}
// 編寫前序遍歷的方法
public void perOrder() {
System.out.println(this); // 輸出父結點
// 遞歸向左子樹前序遍歷
if (this.left != null) {
this.left.perOrder();
}
// 遞歸向右子樹前序遍歷
if (this.right != null) {
this.right.perOrder();
}
}
// 編寫中序遍歷的方法
public void infixOrder(){
//遞歸向左子樹中序遍歷
if(this.left != null){
this.left.infixOrder();
}
//輸出父節點
System.out.println(this);
//遞歸向右子樹中序遍歷
if(this.right != null){
this.right.infixOrder();
}
}
// 編寫後序遍歷的方法
public void postOrder(){
//遞歸向左子樹後序遍歷
if(this.left != null){
this.left.postOrder();
}
//遞歸向右子樹後序遍歷
if(this.right != null){
this.right.postOrder();
}
//輸出父節點
System.out.println(this);
}
}
//定義一個二叉樹
class BinaryTree{
private HeroNode root;
public void setRoot(HeroNode root) {
this.root = root;
}
//前序遍歷
public void perOrder() {
if(this.root != null){
this.root.perOrder();
}else{
System.out.println("二叉樹爲空,無法遍歷");
}
}
//中序遍歷
public void infixOrder(){
if(this.root != null){
this.root.infixOrder();
}else{
System.out.println("二叉樹爲空,無法遍歷");
}
}
//後序遍歷
public void postOrder(){
if(this.root != null){
this.root.postOrder();
}else{
System.out.println("二叉樹爲空,無法遍歷");
}
}
}
1)前上圖的 3號節點 “盧俊義” , 增加一個左子節點 [5, 林沖]
2)使用前序,中序,後序遍歷,請寫出各自輸出的順序是什麼?
代碼實現如下:
public class BinaryTreeTest {
public static void main(String[] args) {
//先創建一棵二叉樹
BinaryTree binaryTree = new BinaryTree();
//創建需要的節點
HeroNode root = new HeroNode(1,"宋江");
HeroNode hero2 = new HeroNode(2,"吳用");
HeroNode hero3 = new HeroNode(3,"盧俊義");
HeroNode hero4 = new HeroNode(4,"關勝");
HeroNode hero5 = new HeroNode(5,"林沖");
//說明,先手動創建二叉樹,再遞歸方式創建二叉樹
root.setLeft(hero2);
root.setRight(hero3);
hero3.setLeft(hero5);
hero3.setRight(hero4);
binaryTree.setRoot(root);
//測試
System.out.println("前序遍歷:"); //1,2,3,5,4
binaryTree.perOrder();
//測試2
System.out.println("中序遍歷:"); //2,1,5,3,4
binaryTree.infixOrder();
//測試3
System.out.println("後序遍歷:"); //2,5,4,3,1
binaryTree.postOrder();
}
}
// 創建一個HeroNode結點
class HeroNode {
private int id;
private String name;
private HeroNode left; // 默認爲null,左子節點
private HeroNode right; // 默認爲null,右子節點
public HeroNode() {
super();
}
public HeroNode(int id, String name) {
super();
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
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 [id=" + id + ", name=" + name + "]";
}
// 編寫前序遍歷的方法
public void perOrder() {
System.out.println(this); // 輸出父結點
// 遞歸向左子樹前序遍歷
if (this.left != null) {
this.left.perOrder();
}
// 遞歸向右子樹前序遍歷
if (this.right != null) {
this.right.perOrder();
}
}
// 編寫中序遍歷的方法
public void infixOrder(){
//遞歸向左子樹中序遍歷
if(this.left != null){
this.left.infixOrder();
}
//輸出父節點
System.out.println(this);
//遞歸向右子樹中序遍歷
if(this.right != null){
this.right.infixOrder();
}
}
// 編寫後序遍歷的方法
public void postOrder(){
//遞歸向左子樹後序遍歷
if(this.left != null){
this.left.postOrder();
}
//遞歸向右子樹後序遍歷
if(this.right != null){
this.right.postOrder();
}
//輸出父節點
System.out.println(this);
}
}
//定義一個二叉樹
class BinaryTree{
private HeroNode root;
public void setRoot(HeroNode root) {
this.root = root;
}
//前序遍歷
public void perOrder() {
if(this.root != null){
this.root.perOrder();
}else{
System.out.println("二叉樹爲空,無法遍歷");
}
}
//中序遍歷
public void infixOrder(){
if(this.root != null){
this.root.infixOrder();
}else{
System.out.println("二叉樹爲空,無法遍歷");
}
}
//後序遍歷
public void postOrder(){
if(this.root != null){
this.root.postOrder();
}else{
System.out.println("二叉樹爲空,無法遍歷");
}
}
}
前序中序後序查找思路圖解與實現
二叉樹-查找指定節點
要求
- 請編寫前序查找,中序查找和後序查找的方法。
- 並分別使用三種查找方式,查找 heroNo = 5 的節點
- 並分析各種查找方式,分別比較了多少次
- 思路分析圖解如下:
public class BinaryTreeTest {
public static void main(String[] args) {
//先創建一棵二叉樹
BinaryTree binaryTree = new BinaryTree();
//創建需要的節點
HeroNode root = new HeroNode(1,"宋江");
HeroNode hero2 = new HeroNode(2,"吳用");
HeroNode hero3 = new HeroNode(3,"盧俊義");
HeroNode hero4 = new HeroNode(4,"關勝");
HeroNode hero5 = new HeroNode(5,"林沖");
//說明,先手動創建二叉樹,再遞歸方式創建二叉樹
root.setLeft(hero2);
root.setRight(hero3);
hero3.setLeft(hero5);
hero3.setRight(hero4);
binaryTree.setRoot(root);
//前序遍歷查找
//前序遍歷查找的次數 :4
System.out.println("前序遍歷查找方式:");
HeroNode node = binaryTree.preOrderSearch(5);
if(node != null){
System.out.printf("找到了,信息爲 id = %d name = %s\n",node.getId(),node.getName());
} else {
System.out.printf("沒有找到,相關信息的人物。\n");
}
//中序遍歷查找
//中序遍歷查找次數 :3
System.out.println("中序遍歷查找方式:");
HeroNode node2 = binaryTree.infoxSearch(5);
if(node2 != null){
System.out.printf("找到了,信息爲 id = %d name = %s\n",node2.getId(),node2.getName());
} else {
System.out.printf("沒有找到,相關信息的人物。\n");
}
//後序遍歷查找
//後序遍歷查找次數 :2
System.out.println("中序遍歷查找方式:");
HeroNode node3 = binaryTree.postSearch(5);
if(node3 != null){
System.out.printf("找到了,信息爲 id = %d name = %s\n",node3.getId(),node3.getName());
} else {
System.out.printf("沒有找到,相關信息的人物。\n");
}
}
}
// 創建一個HeroNode結點
class HeroNode {
private int id;
private String name;
private HeroNode left; // 默認爲null,左子節點
private HeroNode right; // 默認爲null,右子節點
public HeroNode() {
super();
}
public HeroNode(int id, String name) {
super();
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
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 [id=" + id + ", name=" + name + "]";
}
//前序遍歷查找
/**
*
* @Description
* @param id 查找的id
* @return 如果找到就返回該Node,如果沒有找到返回 null
*/
public HeroNode preOrdersearch(int id){
// System.out.println("進入前序查找:"); //用於測試查找次數
//比較當前節點
if(this.id == id){
return this;
}
//1.判斷當前結點的左子節點是否爲空,如果不爲空,則遞歸前序查找
//2.如果左送歸前序查找,找到結點,則返回
HeroNode res = null;
if(this.left != null){
res = this.left.preOrdersearch(id);
}
if(res != null){ //找到了左子樹
return res;
}
//3.否則繼續判斷,當前的結點的右子節點是否爲空
//4.如果不空,則維續向右遞歸前序查找。
if(this.right != null){
res = this.right.preOrdersearch(id);
}
return res;
}
//中序遍歷查找
public HeroNode infoxSearch(int id){
//1.判斷當前結點的左子節點是否爲空,如果不爲空,則遞歸中序查找
HeroNode res = null;
if(this.left != null){
res = this.left.infoxSearch(id);
}
if(res != null){ //找到了左子樹
return res;
}
// System.out.println("進入中序查找:"); //用於測試查找次數
//2.如果找到,則返回,如果沒有找到,就和當前結點比較,
//如果是則返回當前結點,否則繼續進行右遞歸的中序查找
if(this.id == id){
return this;
}
//3.如果右遞歸中序查找,找到就返回,否則返回null
if(this.right != null){
res = this.right.infoxSearch(id);
}
return res;
}
//後序遍歷查找
public HeroNode postSearch(int id){
//1.判斷當前節點的左子節點是否爲空,如果不爲空,則遞歸後序查找
HeroNode res = null;
if(this.left != null){
res = this.left.postSearch(id);
}
if(res != null){ //找到了左子樹
return res;
}
//2.如果左子樹沒有找到,則向右遞歸進行後序查找,如果找到,就返回
if(this.right != null){
res = this.right.postSearch(id);
}
if(res != null){ //找到了右子樹
return res;
}
// System.out.println("進入後序查找:"); //用於測試查找次數
//3.如果左右子樹都沒有找到,就比較當前節點是不是
if(this.id == id){
return this;
}
return res;
}
}
//定義一個二叉樹
class BinaryTree{
private HeroNode root;
public void setRoot(HeroNode root) {
this.root = root;
}
//前序遍歷
public HeroNode preOrderSearch(int id){
if(root != null){
return root.preOrdersearch(id);
}else{
return null;
}
}
//中序遍歷
public HeroNode infoxSearch(int id){
if(root != null){
return root.infoxSearch(id);
}else{
return null;
}
}
//後序遍歷
public HeroNode postSearch(int id){
if(root != null){
return root.postSearch(id);
}else{
return null;
}
}
}
二叉樹刪除結點思路圖解與實現
要求:
1.如果刪除的節點是葉子節點,則刪除該節點
2.如果刪除的節點是非葉子節點,則刪除該子樹.
3.測試,刪除掉 5號葉子節點 和 3號子樹.
4.相關思路分析如下:
代碼實現:
public class BinaryTreeTest2 {
public static void main(String[] args) {
//先創建一棵二叉樹
BinaryTree2 binaryTree = new BinaryTree2();
//創建需要的節點
HeroNode2 root = new HeroNode2(1,"宋江");
HeroNode2 hero2 = new HeroNode2(2,"吳用");
HeroNode2 hero3 = new HeroNode2(3,"盧俊義");
HeroNode2 hero4 = new HeroNode2(4,"關勝");
HeroNode2 hero5 = new HeroNode2(5,"林沖");
//說明,先手動創建二叉樹,再遞歸方式創建二叉樹
root.setLeft(hero2);
root.setRight(hero3);
hero3.setLeft(hero5);
hero3.setRight(hero4);
binaryTree.setRoot(root);
//測試刪除
System.out.println("刪除前,前序遍歷:");
binaryTree.perOrder(); // 1,2,3,5,4
binaryTree.delNode(5);
// binaryTree.delNode(3);
System.out.println("刪除後,前序遍歷:");
binaryTree.perOrder(); // 1,2,3,4
}
}
// 創建一個HeroNode結點
class HeroNode2 {
private int id;
private String name;
private HeroNode2 left; // 默認爲null,左子節點
private HeroNode2 right; // 默認爲null,右子節點
public HeroNode2() {
super();
}
public HeroNode2(int id, String name) {
super();
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public HeroNode2 getLeft() {
return left;
}
public void setLeft(HeroNode2 left) {
this.left = left;
}
public HeroNode2 getRight() {
return right;
}
public void setRight(HeroNode2 right) {
this.right = right;
}
@Override
public String toString() {
return "HeroNode [id=" + id + ", name=" + name + "]";
}
//前序遍歷
public void perOrder() {
System.out.println(this); // 輸出父結點
// 遞歸向左子樹前序遍歷
if (this.left != null) {
this.left.perOrder();
}
// 遞歸向右子樹前序遍歷
if (this.right != null) {
this.right.perOrder();
}
}
// 編寫中序遍歷的方法
public void infixOrder(){
//遞歸向左子樹中序遍歷
if(this.left != null){
this.left.infixOrder();
}
//輸出父節點
System.out.println(this);
//遞歸向右子樹中序遍歷
if(this.right != null){
this.right.infixOrder();
}
}
// 編寫後序遍歷的方法
public void postOrder(){
//遞歸向左子樹後序遍歷
if(this.left != null){
this.left.postOrder();
}
//遞歸向右子樹後序遍歷
if(this.right != null){
this.right.postOrder();
}
//輸出父節點
System.out.println(this);
}
//遞歸刪除結點
//1.如果刪除的節點是葉子節點,則刪除該節點
//2.如果刪除的節點是非葉子節點,則刪除該子樹
public void delNode(int id){
/**
* 思路:
* 1.因爲我們的二叉樹是單向的,所以我們是判斷當前結點的子結點是否需要刪除結點,而不能去判斷當前這個結點是不是需要刪除結點.
* 2.如果當前結點的左子結點不爲空,並且左子結點就是要刪除結點,就將this.left = null;並且就返回(結束遞歸刪除)
* 3.如果當前結點的右子結點不爲空,並且右子結點就是要刪除結點,就將this.right = null;並且就返回(結束遞歸刪除)
* 4.如果第2和第3步沒有刪除結點,那麼我們就需要向左子樹進行遞歸刪除
* 5.如果第4步也沒有刪除結點,則應當向右子樹進行遞歸刪除.
*
*/
// 2.如果當前結點的左子結點不爲空,並且左子結點就是要刪除結點,就將this.left = null;並且就返回(結束遞歸刪除)
if(this.left != null && this.left.id == id){
this.left = null;
return;
}
//3.如果當前結點的右子結點不爲空,並且右子結點就是要刪除結點,就將this.right = null;並且就返回(結束遞歸刪除)
if(this.right != null && this.right.id == id){
this.right = null;
return;
}
//4.如果第2和第3步沒有刪除結點,那麼我們就需要向左子樹進行遞歸刪除
if(this.left != null){
this.left.delNode(id);
}
//5.如果第4步也沒有刪除結點,則應當向右子樹進行遞歸刪除.
if(this.right != null){
this.right.delNode(id);
}
}
}
//定義一個二叉樹
class BinaryTree2{
private HeroNode2 root;
public void setRoot(HeroNode2 root) {
this.root = root;
}
//前序遍歷
public void perOrder() {
if(this.root != null){
this.root.perOrder();
}else{
System.out.println("二叉樹爲空,無法遍歷");
}
}
//中序遍歷
public void infixOrder(){
if(this.root != null){
this.root.infixOrder();
}else{
System.out.println("二叉樹爲空,無法遍歷");
}
}
//後序遍歷
public void postOrder(){
if(this.root != null){
this.root.postOrder();
}else{
System.out.println("二叉樹爲空,無法遍歷");
}
}
//刪除節點
public void delNode(int id){
if(root != null){
//如果只有一個root結點,則等價將二叉樹置空
if(root.getId() == id){
root = null;
}else{
//遞歸刪除
root.delNode(id);
}
}else{
System.out.println("空樹,不能刪除");
}
}
}
思考題(課後練習)
1.如果要刪除的節點是非葉子節點,現在我們不希望將該非葉子節點爲根節點的子樹刪除,需要指定規則, 假如規定如下:
2.如果該非葉子節點A只有一個子節點B,則子節點B替代節點A
3.如果該非葉子節點A有左子節點B和右子節點C,則讓左子節點B替代節點A。————>具體參考思路及代碼:
第11章 二叉排序樹的刪除!!!!
順序存儲二叉樹
基本說明:
從數據存儲來看,數組存儲方式和樹的存儲方式可以相互轉換,即數組可以轉換成樹,樹也可以轉換成數組,
參考上面的示意圖。
要求:
1.上圖的二叉樹的結點,要求以數組的方式來存放 arr : [1, 2, 3, 4, 5, 6, 6]
2.要求在遍歷數組 arr時,仍然可以用前序遍歷,中序遍歷和後序遍歷的方式完成結點的遍歷
順序存儲二叉樹的特點:
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 BTreeTest {
public static void main(String[] args) {
int[] arr = { 1, 2, 3, 4, 5, 6, 7 };
//創建一個 BTree
BTree bTree = new BTree(arr);
bTree.preOrder();
}
}
//編寫一個BTree,實現順序存儲二叉樹遍歷
class BTree{
private int[] arr; //存儲數據結點的數組
public BTree(int[] arr) {
super();
this.arr = arr;
}
//重載方法
public void preOrder(){
this.preOrder(0);
}
//編寫方法,實現順序存儲二叉樹的前序遍歷
//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);
}
}
}
課後練習:請完成對數組以二叉樹中序,後序遍歷方式的代碼.
public class BTreeTest2 {
public static void main(String[] args) {
int[] arr = { 1, 2, 3, 4, 5, 6, 7 };
//創建一個 BTree
BTree2 bTree = new BTree2(arr);
bTree.infixOrder(0); //中序
System.out.println();
bTree.postOrder(0); //後序
}
}
//編寫一個BTree,實現順序存儲二叉樹遍歷
class BTree2{
private int[] arr; //存儲數據結點的數組
public BTree2(int[] arr) {
super();
this.arr = arr;
}
//編寫方法,實現順序存儲二叉樹的中序遍歷
public void infixOrder(int index){
//向左遞歸遍歷
if((index * 2 + 1) < arr.length){
infixOrder(2 * index + 1);
}
//如果數組爲空,或arr.length = 0
if(arr == null || arr.length == 0){
System.out.println("數組爲空,無法遍歷查找。");
}
//輸出當前數組的元素
System.out.print("中序遍歷:" + arr[index] + " ");
//向右遞歸遍歷
if((index * 2 + 2) < arr.length) {
infixOrder(2 * index + 2);
}
}
//編寫方法,實現順序存儲二叉樹的後序遍歷
public void postOrder(int index){
//向左遞歸遍歷
if((index * 2 + 1) < arr.length){
postOrder(2 * index + 1);
}
//向右遞歸遍歷
if((index * 2 + 2) < arr.length) {
postOrder(2 * index + 2);
}
//如果數組爲空,或arr.length = 0
if(arr == null || arr.length == 0){
System.out.println("數組爲空,無法遍歷查找。");
}
//輸出當前數組的元素
System.out.print("後序遍歷:" + arr[index] + " ");
}
}
順序存儲二叉樹應用實例
- 八大排序算法中的堆排序,就會使用到順序存儲二叉樹, 關於堆排序,請參考<< 第11章 樹結構實際應用 >> 。
線索化二叉樹
線索化二叉樹基本介紹
先看一個問題:
將數列 {1, 3, 6, 8, 10, 14 } 構建成一顆二叉樹. n+1=7
問題分析:
1.當我們對上面的二叉樹進行中序遍歷時,數列爲 {8, 3, 10, 1, 6, 14 }
2.但是 6, 8, 10, 14 這幾個節點的 左右指針,並沒有完全的利用上.
3.如果我們希望充分的利用 各個節點的左右指針, 讓各個節點可以指向自己的前後節點,怎麼辦?
4.解決方案 ---> 線索二叉樹
線索二叉樹基本介紹
- n個結點的二叉鏈表中含有n+1 【公式 2n-(n-1)=n+1】 個空指針域。利用二叉鏈表中的空指針域,存放指向該結點在某種遍歷次序下的前驅和後繼結點的指針(這種附加的指針稱爲"線索"
- 這種加上了線索的二叉鏈表稱爲線索鏈表,相應的二叉樹稱爲線索二叉樹(Threaded BinaryTree)。根據線索性質的不同,線索二叉樹可分爲前序線索二叉樹、中序線索二叉樹和後序線索二叉樹三種
- 一個結點的前一個結點,稱爲前驅結點
- 一個結點的後一個結點,稱爲後繼結點
線索化二叉樹思路圖解及實現
應用案例說明:將下面的二叉樹,進行中序線索二叉樹。中序遍歷的數列爲 {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 ThreadedBinaryTreeTest {
public static void main(String[] args) {
//測試中序線索二叉樹的功能
HeroNode root = new HeroNode(1, "Z1");
HeroNode node2 = new HeroNode(3, "S5");
HeroNode node3 = new HeroNode(6, "R6");
HeroNode node4 = new HeroNode(8, "G4");
HeroNode node5 = new HeroNode(10, "P8");
HeroNode node6 = new HeroNode(14, "Q1");
//二叉樹,後面需要要遞歸創建,所以先簡單處理使用手動創建
root.setLeft(node2);
root.setRight(node3);
node2.setLeft(node4);
node2.setRight(node5);
node3.setLeft(node6);
//測試中序線索化
ThreadTree threadTree = new ThreadTree();
threadTree.setRoot(root);
threadTree.treadNodes();
//測試:以10號節點測試
HeroNode left = node5.getLeft();
HeroNode right = node5.getRight();
System.out.println("10號節點的前驅是:" + left);
System.out.println("10號節點的後繼是:" + right);
}
}
//創建一個HeroNode結點
class HeroNode {
private int id;
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 HeroNode() {
super();
}
public HeroNode(int id, String name) {
super();
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
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;
}
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;
}
@Override
public String toString() {
return "HeroNode [id=" + id + ", name=" + name + "]";
}
}
//定義一個ThreadTree二叉樹,實現線索化功能
class ThreadTree{
private HeroNode root;
//爲了實現線索化,需要創建要給指向當前結點的前驅結點的指針
//在遞歸進行線索化時,pre 總是保留前一個結點
private HeroNode pre = null;
public void setRoot(HeroNode root) {
this.root = root;
}
//重載 線索化的方法treadNodes
public void treadNodes(){
this.treadNodes(root);
}
//編寫對二叉樹進行中序線索化的方法
//node 即當前需要線索化的結點
public void treadNodes(HeroNode node){
//如果node==null,無法線索化
if(node == null) {
return;
}
//1.先線索化左子樹
treadNodes(node.getLeft());
//2.線索化當前結點
//處理當前結點的前驅結點
//例如: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;
//3.再線索化右子樹
treadNodes(node.getRight());
}
}
遍歷線索化二叉樹實現
說明:對前面的中序線索化的二叉樹, 進行遍歷
分析:因爲線索化後,各個結點指向有變化,因此原來的遍歷方式不能使用,這時需要使用新的方式遍歷線索化二叉樹,各個節點可以通過線型方式遍歷,因此無需使用遞歸方式,這樣也提高了遍歷的效率。 遍歷的次序應當和中序遍歷保持一致。
- 代碼實現:
public class ThreadedBinaryTreeTest2 {
public static void main(String[] args) {
//測試中序線索二叉樹的功能
HeroNode2 root = new HeroNode2(1, "Z1");
HeroNode2 node2 = new HeroNode2(3, "S5");
HeroNode2 node3 = new HeroNode2(6, "R6");
HeroNode2 node4 = new HeroNode2(8, "G4");
HeroNode2 node5 = new HeroNode2(10, "P8");
HeroNode2 node6 = new HeroNode2(14, "Q1");
//二叉樹,後面需要要遞歸創建,所以先簡單處理使用手動創建
root.setLeft(node2);
root.setRight(node3);
node2.setLeft(node4);
node2.setRight(node5);
node3.setLeft(node6);
//測試中序線索化
ThreadTree2 threadTree = new ThreadTree2();
threadTree.setRoot(root);
threadTree.treadNodes();
//測試:以10號節點測試
HeroNode2 left = node5.getLeft();
HeroNode2 right = node5.getRight();
System.out.println("10號節點的前驅是:" + left);
System.out.println("10號節點的後繼是:" + right);
//當線索化二叉樹後,不能再使用初始遍歷方式
// threadTree.infixOrder(); //會報錯,死循環換溢出
System.out.println("使用線索化的方式遍歷線索化二叉樹:");
threadTree.threadedList();
}
}
//創建一個HeroNode2結點
class HeroNode2 {
private int id;
private String name;
private HeroNode2 left; // 默認爲null,左子節點
private HeroNode2 right; // 默認爲null,右子節點
//說明:
//1.如果leftType == 0 表示指向的是左子樹, 如果 1 則表示指向前驅結點
//2.如果rightType == 0 表示指向是右子樹, 如果 1表示指向後繼結點
private int leftType;
private int rightType;
public HeroNode2() {
super();
}
public HeroNode2(int id, String name) {
super();
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public HeroNode2 getLeft() {
return left;
}
public void setLeft(HeroNode2 left) {
this.left = left;
}
public HeroNode2 getRight() {
return right;
}
public void setRight(HeroNode2 right) {
this.right = right;
}
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;
}
@Override
public String toString() {
return "HeroNode [id=" + id + ", name=" + name + "]";
}
//中序遍歷
public void infixOrder(){
//遞歸向左子樹中序遍歷
if(this.left != null){
this.left.infixOrder();
}
//輸出父節點
System.out.println(this);
//遞歸向右子樹中序遍歷
if(this.right != null){
this.right.infixOrder();
}
}
}
//定義一個ThreadTree2二叉樹,實現線索化功能
class ThreadTree2{
private HeroNode2 root;
//爲了實現線索化,需要創建要給指向當前結點的前驅結點的指針
//在遞歸進行線索化時,pre 總是保留前一個結點
private HeroNode2 pre = null;
public void setRoot(HeroNode2 root) {
this.root = root;
}
//遍歷線索化二叉樹的方法
public void threadedList() {
//定義一個變量,用於|存儲當前遍歷的結點,從root開始
HeroNode2 node = root;
while(node != null){
//循環的找到leftType == 1的結點,第一個找到就是8結點
//後面隨着遍歷而變化,因爲當leftType==1時,說明該結點是按照線索化處理後的有效結點
while(node.getLeftType() == 0){
node = node.getLeft();
}
//打印當前節點
System.out.println(node);
//如果當前結點的右指針指向的是後繼結點,則會一直輸出
while(node.getLeftType() == 1){
//獲得當前節點的後繼節點
node = node.getRight();
System.out.println(node);
}
//替換這個遍歷節點
node = node.getRight();
}
}
//中序遍歷
public void infixOrder(){
if(this.root != null){
this.root.infixOrder();
}else{
System.out.println("二叉樹爲空,無法遍歷");
}
}
//重載 線索化的方法treadNodes
public void treadNodes(){
this.treadNodes(root);
}
//編寫對二叉樹進行中序線索化的方法
//node 即當前需要線索化的結點
public void treadNodes(HeroNode2 node){
//如果node==null,無法線索化
if(node == null) {
return;
}
//1.先線索化左子樹
treadNodes(node.getLeft());
//2.線索化當前結點
//處理當前結點的前驅結點
//例如: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;
//3.再線索化右子樹
treadNodes(node.getRight());
}
}
課後作業:
上面敘述了中序線索化二叉樹,請寫出前序線索化二叉樹和後序線索化二叉樹的實現.
- 前序化遍歷
public class ThreadedBinaryTreeTest {
public static void main(String[] args) {
// 測試前、後序線索二叉樹的功能
HeroNode root = new HeroNode(1, "Z1");
HeroNode node2 = new HeroNode(3, "S5");
HeroNode node3 = new HeroNode(6, "R6");
HeroNode node4 = new HeroNode(8, "G4");
HeroNode node5 = new HeroNode(10, "P8");
HeroNode node6 = new HeroNode(14, "Q1");
// 二叉樹,後面需要要遞歸創建,所以先簡單處理使用手動創建
root.setLeft(node2);
root.setRight(node3);
node2.setLeft(node4);
node2.setRight(node5);
node3.setLeft(node6);
// 測試前序線索化
ThreadTree threadTree = new ThreadTree();
threadTree.setRoot(root);
threadTree.endTreadNodes(root);
// 測試:以10號節點測試
HeroNode left = node5.getLeft();
HeroNode right = node5.getRight();
System.out.println("10號節點的前驅是:" + left);
System.out.println("10號節點的後繼是:" + right);
System.out.println("使用線索化的方式遍歷線索化二叉樹:");
threadTree.threadedList(); // 1,3,8,10,6,14
}
}
// 定義一個ThreadTree二叉樹,實現線索化功能
class ThreadTree {
private HeroNode root;
// 爲了實現線索化,需要創建要給指向當前結點的前驅結點的指針
// 在遞歸進行線索化時,pre 總是保留前一個結點
private HeroNode pre = null;
public void setRoot(HeroNode root) {
this.root = root;
}
// 前序遍歷線索化二叉樹的方法
public void threadedList() {
// 定義一個變量,用於|存儲當前遍歷的結點,從root開始
HeroNode node = root;
while (node != null) {
// 循環的找到leftType == 1的結點,第一個找到就是8結點
// 後面隨着遍歷而變化,因爲當leftType==1時,說明該結點是按照線索化處理後的有效結點
while (node.getLeftType() != 1) {
System.out.println(node);
node = node.getLeft();
}
System.out.println(node);
// 替換這個遍歷節點
node = node.getRight();
}
}
// 編寫對二叉樹進行前序線索化的方法
// node 即當前需要線索化的結點
public void endTreadNodes(HeroNode node) {
// 2.再線索化左子樹
if (node == null) {
return;
}
// 1.先線索化當前結點
// 處理當前結點的前驅結點
// 例如: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;
// 2.再線索化左子樹
if(node.getLeftType() != 1) {
endTreadNodes(node.getLeft());
}
// 3.再線索化右子樹
if(node.getRightType() != 1) {
endTreadNodes(node.getRight());
}
}
}
// 創建一個HeroNode結點
class HeroNode {
private int id;
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 HeroNode() {
super();
}
public HeroNode(int id, String name) {
super();
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
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;
}
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;
}
@Override
public String toString() {
return "HeroNode [id=" + id + ", name=" + name + "]";
}
}
- 後序化遍歷——(
有錯誤!!!待修改
)
public class ThreadedBinaryTreeTest2 {
public static void main(String[] args) {
// 測試前、後序線索二叉樹的功能
HeroNode2 root = new HeroNode2(1, "Z1");
HeroNode2 node2 = new HeroNode2(3, "S5");
HeroNode2 node3 = new HeroNode2(6, "R6");
HeroNode2 node4 = new HeroNode2(8, "G4");
HeroNode2 node5 = new HeroNode2(10, "P8");
HeroNode2 node6 = new HeroNode2(14, "Q1");
// 二叉樹,後面需要要遞歸創建,所以先簡單處理使用手動創建
root.setLeft(node2);
root.setRight(node3);
node2.setLeft(node4);
node2.setRight(node5);
node3.setLeft(node6);
//添加父節點
node2.setParent(root);
node3.setParent(root);
node4.setParent(node2);
node5.setParent(node2);
node6.setParent(node3);
// 測試後序線索化
ThreadTree2 threadTree = new ThreadTree2();
threadTree.setRoot(root);
threadTree.endTreadNodes(root);
// 測試:以10號節點測試
HeroNode2 left = node5.getLeft();
HeroNode2 right = node5.getRight();
System.out.println("10號節點的前驅是:" + left);
System.out.println("10號節點的後繼是:" + right);
System.out.println("使用線索化的方式遍歷線索化二叉樹:");
threadTree.threadedList(); //
}
}
// 定義一個ThreadTree2二叉樹,實現線索化功能
class ThreadTree2 {
private HeroNode2 root;
// 爲了實現線索化,需要創建要給指向當前結點的前驅結點的指針
// 在遞歸進行線索化時,pre 總是保留前一個結點
private HeroNode2 pre = null;
public void setRoot(HeroNode2 root) {
this.root = root;
}
// 後序遍歷線索化二叉樹的方法
public void threadedList() {
//定義一個變量,用於|存儲當前遍歷的結點,從root開始
HeroNode2 node = root;
while(node != null && node.getLeftType() != 1){
node = node.getLeft();
}
HeroNode2 pre = null;
while(node != null){
if(node.getRightType()!= 0){
System.out.println(node);
pre = node;
node = node.getRight();
}else{
if(node.getRight() == pre){
System.out.println(node);
if(node == root){
return;
}
pre = node;
node = node.getParent();
}else{
node = node.getRight();
while(node.getLeftType() != 1){
node = node.getLeft();
}
}
}
}
}
// 編寫對二叉樹進行後序線索化的方法
// node 即當前需要線索化的結點
public void endTreadNodes(HeroNode2 node) {
// 如果node==null,無法線索化
if (node == null) {
return;
}
// 1.線索化左子樹
endTreadNodes(node.getLeft());
// 2.再線索化右子樹
endTreadNodes(node.getRight());
// 3.線索化當前結點
if (node.getLeft() == null) {
// 讓當前結點的左指針指向前驅結點
node.setLeft(pre);
// 修改當前結點的左指針的類型,指向前驅結點
node.setLeftType(1);
}
// 處理後繼結點
if (pre != null && pre.getRight() == null) {
pre.setRight(node); // 讓前驅結點的右指針指向當前結點
pre.setRightType(1); // 修改前驅結點的右指針類型
}
// 重要:每處理一個結點後,讓當前結點是下一個結點的前驅結點
pre = node;
}
}
//創建一個HeroNode2結點
class HeroNode2 {
private int id;
private String name;
private HeroNode2 left; // 默認爲null,左子節點
private HeroNode2 right; // 默認爲null,右子節點
private HeroNode2 parent; //父結點指針
// 說明:
// 1.如果leftType == 0 表示指向的是左子樹, 如果 1 則表示指向前驅結點
// 2.如果rightType == 0 表示指向是右子樹, 如果 1表示指向後繼結點
private int leftType = 0;
private int rightType = 0;
public HeroNode2() {
super();
}
public HeroNode2(int id, String name) {
super();
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public HeroNode2 getLeft() {
return left;
}
public void setLeft(HeroNode2 left) {
this.left = left;
}
public HeroNode2 getRight() {
return right;
}
public void setRight(HeroNode2 right) {
this.right = right;
}
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 HeroNode2 getParent() {
return parent;
}
public void setParent(HeroNode2 parent) {
this.parent = parent;
}
@Override
public String toString() {
return "HeroNode [id=" + id + ", name=" + name + "]";
}
}
參考鏈接:二叉樹的線索化與線索二叉樹的遍歷(Java實現)
這節對於初學的我來說,比較不易理解,雖然寫完了,但仍有諸多不解,做此紀錄,便於查閱!!!歡迎指正!!!