樹結構基礎部分
二叉樹
為什麼需要該資料結構
數組存儲方式的分析
- 優點:
- 通過 下标 方式通路元素,速度快
- 對于 有序數組,還可以使用二分查找提高檢索速度
- 缺點:如果要檢索具體某個值或插入值(按一定順序)會整體移動,效率較低,如下的示意圖
連結清單存儲方式的分析
優點:在一定程度上對數組存儲方式有優化
例如:插入一個數值節點,隻需要将插入節點,連結到連結清單中即可,同理,删除效率也很好
-
缺點:檢索效率較低
需要從頭結點開始周遊查找。
簡單說:
- 數組通路快,增删慢
- 連結清單增删快,通路慢
那麼就出現了 樹 這種資料結構
樹存儲資料方式分析
提供資料 存儲 、讀取 效率。
例如:利用 二叉排序樹(Binary Sort Tree),既可以保證資料的檢索速度,同時也可以保證資料的插入、删除、修改 的速度
如圖所示:
- 插入時,小的數在 左節點、大的數在 右節點
- 查找時:根據插入事的特性,基本上就類似折半查找了,每次都過濾一半的節點
- 删除時:隻需要移動相鄰的節點的引用
樹的常用術語
- 節點:每一個圓圈表示一個節點,也稱節點對象
- 根節點:最上面,最頂部的那個節點,也就是一棵樹的入口
- 父節點:有子節點的節點
- 子節點
- 葉子節點:沒有子節點的節點
-
節點的權:可以簡單的了解為節點值
有時候也用 路徑 來表示
- 路徑:從 root 節點找到該節點的路線
- 層
- 子樹:有子節點的父子兩層就可以稱為是一個子樹
- 樹的高度:最大層數
- 森林:多顆子樹構成森林
二叉樹的概念
- 樹有很多種,每個節點 最多隻能有兩個子節點 的一種形式稱為 二叉樹
- 二叉樹的子節點分為 左節點 和 右節點
- 如果該二叉樹的所有 葉子節點 都在 最後一層,并且 節點總數 = 2^n-1 (n 為層數),則我們稱為 滿二叉樹
- 如果該二叉樹的所有葉子節點都在最 後一層或倒數第二層,而且 最後一層的葉子節點在左邊連續,倒數第二層的葉子節點在右邊連續,我們稱為 完全二叉樹
二叉樹的周遊說明
有三種:
- 前序周遊:先輸出父節點,再周遊左子樹(遞歸)和右子樹(遞歸)
- 中序周遊:先周遊左子樹(遞歸),再輸出父節點,再周遊右子樹(遞歸)
- 後序周遊:先周遊左子樹(遞歸),再周遊右子樹(遞歸),最後輸出父節點
前、中、後序主要差別是輸出父節點的時間來區分
二叉樹周遊思路分析
- 前序周遊:
- 先輸出目前節點(初始節點是 root 節點)
- 如果左子節點不為空,則遞歸繼續前序周遊
- 如果右子節點不為空,則遞歸繼續前序周遊
- 中序周遊:
- 如果目前節點的左子節點不為空,則遞歸中序周遊
- 輸出目前節點
- 如果目前節點的右子節點不為空,則遞歸中序
- 後序周遊:
周遊代碼實作
//定義節點
class HeroNode{
private int no;//目前節點編号
private String name;//目前節點名字
private HeroNode left;//目前節點左子樹
private HeroNode right;//目前節點右子樹
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 + '\'' +
'}';
}
//前序周遊
public void preOrder(){
//先輸出目前節點
System.out.println(this);
//遞歸周遊左子節點
if(this.left != null){
this.left.preOrder();
}
//遞歸周遊右子節點
if(this.right != null){
this.right.preOrder();
}
}
//中序周遊
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 preOrder(){
if(this.root != null){
this.root.preOrder();
}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("目前二叉樹為空");
}
}
}
//建立二叉樹
BinaryTree binaryTree = new BinaryTree();
//手動建立二叉樹
HeroNode root = new HeroNode(1,"宋江");
HeroNode hero1 = new HeroNode(2,"吳用");
HeroNode hero2 = new HeroNode(3,"盧俊義");
HeroNode hero3 = new HeroNode(4,"林沖");
root.setLeft(hero1);
root.setRight(hero2);
hero2.setRight(hero3);
binaryTree.setRoot(root);
//周遊二叉樹
System.out.println("前序周遊");
binaryTree.preOrder();
//中序周遊
System.out.println("中序周遊");
binaryTree.infixOrder();
//後序周遊
System.out.println("後序周遊");
binaryTree.postOrder();
二叉樹查找
要求:
- 編寫前、中、後序查找方法
- 并分别使用三種查找方式,查找
的節點id=5
- 并分析各種查找方式,分别比較了多少次
由于二叉樹的查找是周遊查找,是以就簡單了,前面周遊規則已經寫過了,改寫成查找即可
//在節點中增加查找方法
//前序查找
public HeroNode preOrderSearch(int no){
if(this.no == no){
return this;
}
HeroNode resNode = null;
if(this.left != null){
resNode = this.left.preOrderSearch(no);
}
if(resNode != null){
return resNode;
}
if(this.right != null){
resNode = this.right.preOrderSearch(no);
}
return resNode;
}
//中序查找
public HeroNode infixOrderSearch(int no){
HeroNode resNode = null;
if(this.left != null){
resNode = this.left.infixOrderSearch(no);
}
if(resNode != null){
return resNode;
}
if(this.no == no){
return this;
}
if(this.right != null){
resNode = this.right.infixOrderSearch(no);
}
return resNode;
}
//後序查找
public HeroNode postOrderSearch(int no){
HeroNode resNode = null;
if(this.left != null){
resNode = this.left.postOrderSearch(no);
}
if(resNode != null){
return resNode;
}
if(this.right != null){
resNode = this.right.postOrderSearch(no);
}
if(resNode != null){
return resNode;
}
if(this.no == no){
return this;
}
return null;
}
//在二叉樹中增加查找方法
//前序查找
public HeroNode preOrderSearch(int no){
HeroNode resNode = null;
if(this.root != null){
resNode = this.root.preOrderSearch(no);
}else{
System.out.println("目前二叉樹為空");
}
return resNode;
}
//中序
public HeroNode infixOrderSearch(int no){
HeroNode resNode = null;
if(this.root != null){
resNode = this.root.infixOrderSearch(no);
}else{
System.out.println("目前二叉樹為空");
}
return resNode;
}
//後序
public HeroNode postOrderSearch(int no){
HeroNode resNode = null;
if(this.root != null){
resNode = this.root.postOrderSearch(no);
}else{
System.out.println("目前二叉樹為空");
}
return resNode;
}
可以看出:
- 找到的次數和 查找的順序 有關,而查找順序就是哪一序有關
- 找不到的次數則是相當于都周遊完成,是以是相等的次數
二叉樹删除
- 如果删除的節點是 葉子節點,則删除該節點
- 如果删除的節點是非葉子節點,則删除該子樹
測試:删除 5 号葉子節點和 3 号子樹。
說明:目前的二叉樹不是規則的,如果不删除子樹,則需要考慮哪一個節點會被上提作為父節點。這個後續講解排序二叉樹時再來實作。先實作簡單的
思路分析:
- 由于我們的二叉樹是單向的
- 是以我們判定一個節點是否可以删除,是判斷它的 子節點,是否可删除,否則則沒法回到父節點删除了,因為要判斷被删除的節點滿足前面的兩點要求
- 目前節點的 左子節點 不為空,并且左子節點就是要删除的節點,則 left = null,并且傳回(結束遞歸删除)
- 目前節點的 右子節點 不為空,并且右子節點就是要删除的節點,則 right = null,并且傳回(結束遞歸删除)
如果前面都沒有删除,則繼續遞歸删除。上面的要求是 2 點,實際上是,找到符合條件的節點則直接删除(因為不考慮是否有子樹)
如果樹隻有一個 root 節點,則将 root 節點置空
//節點中增加删除方法
public void delete(int no){
if(this.left != null){
if(this.left.no == no){
this.left = null;
return;
}
}
if(this.right != null){
if(this.right.no == no){
this.right = null;
return;
}
}
//向左遞歸
if(this.left != null){
this.left.delete(no);
}
//向右遞歸
if(this.right != null){
this.right.delete(no);
}
}
//二叉樹中增加删除方法
public void delete(int no){
if(this.root != null){
if(this.root.getNo() == no){
this.root = null;
return;
}else{
this.root.delete(no);
}
}else{
System.out.println("目前二叉樹為空");
}
}
順序存儲二叉樹
基本概念
從資料存儲來看,數組存儲 方式和 樹 的存儲方式可以 互相轉換。即使數組可以轉換成樹,樹也可以轉換成數組。如下示意圖
上圖閱讀說明:
- 圓圈頂部的數字對應了數組中的索引
- 圓圈内部的值對應的數數組元素的值
現在有兩個要求:
- 上圖的二叉樹的節點,要求以數組的方式來存儲
arr=[1,2,3,4,5,6,7]
- 要求在周遊數組 arr 時,仍然可以以 前序、中序、後序的方式周遊
特點
想要 實作上面的兩個要求,需要知道順序存儲二叉樹的特點:
- 順序二叉樹 通常隻考慮 完全二叉樹
- 第 n 個元素的 左子節點 為
2*n+1
- 第 n 個元素的 右子節點 為
2*n+2
- 第 n 個元素的 父節點 為
(n-1)/2
注:n 表示二叉樹中的第幾個元素(按 0 開始編号)
比如:
- 元素 2 的左子節點為:
,對比上圖去檢視,的确是 32 * 1 + 1 = 3
- 元素 2 的右子節點為:
,也 就是元素 52 * 1 + 2 = 4
- 元素 3 的左子節點為:
,也就是元素 62 * 2 + 1 = 5
- 元素 3 的父節點為:
,也就是根節點 1(2-1)/2= 1/2 = 0
三種周遊方式
//順序存儲二叉樹
class ArrayBinaryTree{
private int[] array;
public ArrayBinaryTree(int[] array) {
this.array = array;
}
//前序周遊輸出順序存儲二叉樹
public void preOrder(int index){
if(array == null || array.length == 0){
System.out.println("數組為空,不能周遊完全二叉樹");
}
//先輸出目前元素
System.out.println(array[index]);
//再遞歸輸出左子元素
if((index*2+1) < array.length){
preOrder((index*2 + 1));
}
//再遞歸輸出右子元素
if((index*2+2) < array.length){
preOrder((index*2 +2));
}
}
public void preOrder(){
preOrder(0);
}
//中序周遊輸出順序存儲二叉樹
public void infixOrder(int index){
if(array == null || array.length == 0){
System.out.println("數組為空,不能周遊完全二叉樹");
}
if((index*2 +1) < array.length){
infixOrder((index*2 + 1));
}
System.out.println(array[index]);
if((index*2 + 2) < array.length){
infixOrder((index*2+2));
}
}
public void infixOrder(){
infixOrder(0);
}
//後序周遊順序存儲二叉樹
public void postOrder(int index){
if(array == null || array.length == 0){
System.out.println("數組為空,不能周遊完全二叉樹");
}
if((index*2 + 1) < array.length){
postOrder((index*2+1));
}
if((index*2 + 2) < array.length){
postOrder((index*2+2));
}
System.out.println(array[index]);
}
public void postOrder(){
postOrder(0);
}
}
線索化二叉樹
引出線索化二叉樹
看如下問題:将數列
{1,3,6,8,10,14}
構成一顆二叉樹
可以看到上圖的二叉樹為一顆 完全二叉樹。對他進行分析,可以發現如下的一些問題:
- 當對上面的二叉樹進行中序周遊時,數列為
8,3,10,1,14,6
- 但是
這幾個節點的左右指針,并沒有完全用上6,8,10,14
如果希望充分利用各個節點的左右指針,讓各個節點可以 指向自己的前後節點,這個時候就可以使用 線索化二叉樹 了
介紹
n 個節點的二叉樹連結清單中含有
n + 1
個空指針域,他的推導公式為
2n-(n-1) = n + 1
。
利用二叉連結清單中的空指針域,存放指向該節點在 某種周遊次序 *下的 **前驅** 和 **後繼** 節點的指針,這種附加的指針稱為*「線索」
- 前驅:一個節點的前一個節點
- 後繼:一個節點的後一個節點
如下圖,在中序周遊中,下圖的中序周遊為
8,3,10,1,14,6
,那麼 8 的後繼節點就為 3,3 的後繼節點是 10
這種加上了線索的二叉樹連結清單稱為 線索連結清單(節點存儲了下一個節點,組成了連結清單,并且一般的二叉樹本來就是用連結清單實作的),相應的二叉樹稱為 線索二叉樹(Threaded BinaryTree)。根據線索性質的不同,線索二叉樹可分為:前、中、後序線索二叉樹。
思路分析
将上圖的二叉樹,進行 中序線索二叉樹,中序周遊的數列為
8,3,10,1,14,6
那麼以上圖為例,線索化二叉樹後的樣子如下圖
- 的後繼節點為 3
- 3 由于 左右節點都有元素,不能線索化
- 10 的前驅節點為 3,後繼節點為 1
- 1 不能線索化
- 14 的前驅節點為 1,後繼節點為 6
- 6 有左節點,不能線索化
注意:當線索化二叉樹後,那麼一個 Node 節點的 left 和 right 屬性,就有如下情況:
- left 指向的是 左子樹,也可能是指向 前驅節點
代碼實作
//建立節點
class HeroNode{
private int no;
private String name;
private HeroNode left;
private HeroNode right;
//左右子樹的類型,如果是左右子樹就是0,如果是前驅或者後繼節點就是1
private int leftType;
private int 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;
}
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{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
}
//建立線索化二叉樹
class ThreadBinaryTree{
private HeroNode root;//表示跟節點
private HeroNode pre;//表示前驅節點
public void setRoot(HeroNode root) {
this.root = root;
}
public void threadedNodes(){
threadedNodes(root);
}
//中序線索化周遊
public void threadedList(){
if(root == null){
System.out.println("線索化二叉樹為空");
return;
}
HeroNode node = root;
while(node != null){
//找到第一個有前驅節點的元素
while(node.getLeftType() != 1){
node = node.getLeft();
}
//輸出第一個元素
System.out.println(node);
//當有後繼節點的情況會進入
while(node.getRightType() == 1){
node = node.getRight();
System.out.println(node);
}
//當一個節點沒有後繼節點的時候直接擷取右子樹
node = node.getRight();
}
}
//中序線索化
public void threadedNodes(HeroNode node){
if(node == null){
return;
}
//先線索化左子樹
threadedNodes(node.getLeft());
//再線索化目前節點
/*
* 1.設定前驅節點,
* 2.設定後繼節點
* */
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());
}
}
前序線索化和周遊
//前序線索化
public void preThreadedNodes(HeroNode node){
if(node == null){
return;
}
//先序列化自己
if(pre != null && node.getLeft() == null){
node.setLeft(pre);
node.setLeftType(1);
}
if(pre != null && pre.getRight() == null){
pre.setRight(node);
pre.setRightType(1);
}
pre = node;
//再序列化遞歸左子樹
if(node.getLeftType() == 0){
preThreadedNodes(node.getLeft());
}
//再遞歸序列化右子樹
if(node.getRightType() == 0){
preThreadedNodes(node.getRight());
}
}
//前序序列化周遊
public void preThreadedList(){
if(root == null){
System.out.println("線索化二叉樹為空");
return;
}
HeroNode node = root;
while(node != null){
System.out.println(node);
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();
}
}