我所知道的數據結構之樹

前面咱們學了不一樣的數據結構,今天學習的是一種特別的數據結構: node

首先咱們思考一下:什麼是樹呢?爲何咱們須要學習這種數據結構呢?segmentfault

1、爲何須要樹這種數據結構

咱們對比以前學習的數據結構,分析看看以前的數據結構有什麼特色又有什麼缺陷數組

1、數組存儲方式的分析數據結構

優勢:經過下標方式訪問元素,速度快。對於有序數組,可使用二分查找提升檢索速度。ide

缺點:若是要操做具體插入值,那麼會總體移動(按必定順序),效率較低post

假如我當前有數組arr {1,3,5,8,10},若此時插入數據:6 那麼能放的進去嗎?學習

image.png

其實是不能的,由於數組是事先分配空間的,指說原建立好空間長度就不能動態增加,可是數組在動態添加數據的時候,底層有一個動做:數組擴容測試

那麼是如何擴容的呢?建立新的數組,並將數據拷貝,以及插入數據後移優化

那麼這時會有小夥伴提出:咱們使用集合ArrayList不是能夠動態增加嗎?this

其實咱們觀察集合ArrayList,發現也維護了數組擴容,只是策略不一樣。

image.png

那麼咱們一塊兒看看ArrayList 源碼

private static final Object [] DEFAULTCAPACITY_ EMPTY_ ELEMENTDATA = {};

//ArrayList的構造器
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_ EMPTY_ ELEMENTDATA ;
}

咱們發現ArrayList無參構造器,上來時將空數值給到elementData數組。

那麼elementData是什麼?

transient 0bject [] elementData;

實際上是一個對象Object數組,也就是說ArrayList維護的0bject [] elementData數組

在ArrayList容量不夠的時候,有方法grow()按照不一樣的策略進行擴容,但仍然是一個數組擴容

private void grow(int minCapacity) {
    
    int oldCapacity = elementData.length;
    int newCapacity - oldCapacity + (oldCapacity >> 1);
    
    if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;
     
    if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);
    
    elementData = Arrays.copy0f(elementData, newCapacity);
}

小結

  • ArrayList 中維護了一個Object類型的數組elementData.
  • 當建立對象時, 若是使用的是無參構造器,則初始elementData容量爲0 (jdk7是10)
  • 若是使用的是指定容量capacity的構造器,則初始elementData 容量爲capacity.
  • 添加元素時: 先判斷是否須要擴容,若是須要擴容,則調用grow方法,不然直接添加元素到合適位置
  • 若是使用的是無參構造器,若是第一次添加須要擴容的話,則擴容elementData爲10,若是須要再次擴容的話,則擴容elementData爲1.5倍
  • 若是使用的是指定容量capacity的構造器,若是還須要擴容,則直接擴容elementData爲1.5倍。

咱們發現ArrayList爲了解決擴容,按照一種策略進行的,仍是會總體移動的,效率比較低,因而咱們看看鏈式存儲方式能不能更好解決問題

2、鏈式存儲方式的分析

優勢:在必定程度上對數組存儲方式有優化(好比:插入數值節點,只須要將插入節點,連接到鏈表中便可,刪除效率也很好)。

缺點:在進行檢索時,效率仍然較低(好比:檢索某個值,須要從頭節點開始遍歷)

咱們發現數組與鏈式都有各自的優勢與缺點,那麼接下來介紹新的數據結構: 有什麼不一樣呢?

2、什麼是樹?

image.png
image.png

在樹的家族中,有一種高頻使用的一種樹結構二叉樹

二叉樹中,每一個節點最多有兩個分支,即每一個節點最多有兩個節點,分別稱爲左節點與右節點

在二叉樹中,還有兩個特殊的類型:滿二叉樹與徹底二叉樹

滿二叉樹:除了葉子節點外,全部節點都有兩個節點

徹底二叉樹:除了最後一層之外,其餘層節點個數都達到最大,而且最後一層的葉子節點向左排列

image.png

徹底二叉樹看上去並不徹底,爲何這麼稱呼它?

這和存儲二叉樹的兩種存儲方法:鏈式存儲法與數組順序法。有關了

基於指針的鏈式存儲法,也就是像鏈表同樣,每一個節點有三個字段一個存儲數據,兩外兩個分別存儲指向左右節點的指針,以下圖所示

image.png

基於數組的順序存儲法,就是按照規律把節點存放在數組裏,以下圖所示。爲了方便按照規律計算,把起始數據放在下標爲一的位置上

image.png

image.png

如果非徹底二叉樹則會浪費大量的數組存儲空間,以下圖所示

image.png

樹的基本操做

二叉樹爲例介紹樹的操做

  • 對比以前的數據結構,發現有些都是"一對一"的關係,即前面的數據只跟下面的一個數據產生鏈接關係。如鏈表、棧、隊列等
  • 樹結構則是"一對多"的關係,即前面的父節點跟若干個子節點產生了鏈接關係

與以前的數據結構相比,遍歷一個樹

有很是經典的三種方法,分別是前序遍歷、中序遍歷、後序遍歷

  • 前序是先輸出父節點,再遍歷左子樹和右子樹
  • 中序是先遍歷左子樹,再輸出父節點,再遍歷右子樹
  • 後序是先遍歷左子樹,再遍歷右子樹,最後輸出父節點

image.png

3、認識二叉樹不一樣遍歷方式

示例一:使用前序、中序、後序對下面二叉樹進行遍歷

圖片.png

咱們根據圖片定義英雄節點HeroNode 信息

//建立英雄節點HeroNode
class HeroNode {

    private int no;         //英雄節點編號
    private String name;    //英雄節點名稱

    private HeroNode left;  //默認null 左節點
    private HeroNode right; //默認null 右節點

    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);

    }
}

咱們建立一顆二叉樹 BinaryTree 信息

//定義二叉樹
class BinaryTree{

    private HeroNode root;  //根節點

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

    //root根節點前序遍歷的方法
    public void preOrder(){
        if(this.root!=null){
            this.root.preOrder();
        }else{
            System.out.println("二叉樹爲空,沒法遍歷");
        }
    }

    //root根節點中序遍歷的方法
    public void infixOrder(){
        if(this.root!=null){
            this.root.infixOrder();
        }else{
            System.out.println("二叉樹爲空,沒法遍歷");
        }
    }


    //root根節點後序遍歷的方法
    public void postOrder(){
        if(this.root!=null){
            this.root.postOrder();
        }else{
            System.out.println("二叉樹爲空,沒法遍歷");
        }
    }
}

如今讓咱們如圖所示建立二叉樹與英雄節點來測試遍歷看看

public class BinaryTreeDemo {

    public static void  main(String [] agrs){

        //建立二叉樹
        BinaryTree tree =new BinaryTree();

        //建立英雄節點
        HeroNode root = new HeroNode(1,"松江");
        HeroNode node2 =new HeroNode(2,"吳用");
        HeroNode node3 =new HeroNode(3,"盧俊");
        HeroNode node4 =new HeroNode(4,"林沖");

        //手動建立二叉樹依賴
        root.setLeft(node2);
        root.setRight(node3);
        node3.setRight(node4);
        
        //將root 節點給到二叉樹
        tree.setRoot(root);
    }
}

image.png

咱們測試前序遍歷,看看是否結果是[宋江-->吳用-->盧俊-->林沖]

//前序遍歷
tree.preOrder();

HeroNode [no =1, name =松江]
HeroNode [no =2, name =吳用]
HeroNode [no =3, name =盧俊]
HeroNode [no =4, name =林沖]

咱們測試中序遍歷,看看是否結果是[吳用-->松江-->盧俊-->林沖]

//中序遍歷
tree.infixOrder();

HeroNode [no =2, name =吳用]
HeroNode [no =1, name =松江]
HeroNode [no =3, name =盧俊]
HeroNode [no =4, name =林沖]

咱們測試後序遍歷,看看是否結果是[吳用-->林沖-->盧俊-->松江]

//後序遍歷
tree.postOrder();

HeroNode [no =2, name =吳用]
HeroNode [no =4, name =林沖]
HeroNode [no =3, name =盧俊]
HeroNode [no =1, name =松江]

增強小練習

圖片.png

1.上圖的3號節點"盧俊",添加左節點[5,關勝]
2.使用前序、中序、後序 寫出各自輸出順序是啥?

4、認識二叉樹不一樣遍歷的查找方式

示例二:使用前序、中序、後序不一樣遍歷對下面二叉樹進行查找

圖片.png

分析前序遍歷查找思路

  • 判斷當前節點是否符合要求等於要查找的,相等則返回當前節點
  • 不相等則,判斷當前節點的左節點是否爲空,不爲空遞歸前序查找
  • 找到符合要求的節點則返回,不然繼續遞歸查找
  • 不相等則,判斷當前節點的右節點是否爲空,不爲空遞歸前序查找

image.png

分析中序遍歷查找思路

  • 判斷當前節點的左節點是否爲空,不爲空遞歸中序查找
  • 符合要求的節點則返回,沒有則和當前節點進行比較,知足則返回
  • 不相等則進行右遞歸中序查找

image.png

分析後序遍歷查找思路

  • 判斷當前節點的左節點是否爲空,不爲空遞歸後序查找
  • 符合要求的節點則返回,沒有則進行右遞歸後序查找
  • 符合要求的節點則返回,沒有則和當前節點進行比較,知足則返回

image.png

接下來咱們在分別在HeroNode、BinaryTree添加代碼

//添加前序、中序、後序查找代碼
class HeroNode {
    
    //省略以前序、中序、後序遍歷代碼
    
    /**
     * @param no 查找no
     * @return 若是找到返回node,沒有返回null
     */
    public HeroNode preOrderSearch(int no){

        System.out.println("進入前序遍歷查找~~~~");

        //比較當前節點看看是否是
        if(this.no == no){
            return this;
        }

        //1.判斷當前節點的左節點是否爲空,不爲空遞歸前序查找,找到符合要求的節點則返回
        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;
    }

    /**
     * @param no 查找no
     * @return 若是找到返回node,沒有返回null
     */
    public HeroNode infixOrderSearch(int no){

        //1.判斷當前節點的左節點是否爲空,不爲空遞歸中序查找,找到符合要求的節點則返回
        HeroNode resnode = null;
        if(this.left!=null){
            resnode = this.left.infixOrderSearch(no);
        }

        //說明符合要求的節點找到了,相等
        if(resnode != null){
            return resnode;
        }

        System.out.println("進入中序遍歷查找~~~~");
        //沒有則和當前節點進行比較,知足則返回
        if(this.no == no){
            return this;
        }

        //不相等則,判斷當前節點的右節點是否爲空,不爲空遞歸前序查找
        if(this.right != null){
            resnode = this.right.infixOrderSearch(no);
        }

        return resnode;
    }


    /**
     * @param no 查找no
     * @return 若是找到返回node,沒有返回null
     */
    public HeroNode postOrderSearch(int no){

        //1.判斷當前節點的左節點是否爲空,不爲空遞歸後序查找,找到符合要求的節點則返回
        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;
        }

        
        System.out.println("進入後序遍歷查找~~~~");
        //沒有則和當前節點進行比較,知足則返回
        if(this.no == no){
            return this;
        }

        return resnode;
    }
}
//添加前序、中序、後序查找代碼
class BinaryTree{
       
    //省略以前序、中序、後序遍歷代碼
    
    //root節點前序查找方法
    public HeroNode preOrderSearch(int no){
        if(this.root != null){
            return this.root.preOrderSearch(no);
        }else{
            return null;
        }
    }

    //root節點中序查找方法
    public HeroNode infixOrderSearch(int no){
        if(this.root != null){
            return this.root.infixOrderSearch(no);
        }else{
            return null;
        }
    }

    //root節點後序查找方法
    public HeroNode postOrderSearch(int no){
        if(this.root != null){
            return this.root.postOrderSearch(no);
        }else{
            return null;
        }
    }
}

[舒適提示]:小夥伴必定要添加好關勝英雄數據並關聯起樹的關係

//建立英雄節點
HeroNode node5 =new HeroNode(5,"關勝");

//手動關聯二叉樹依賴關係
node3.setLeft(node5);

使用前序遍歷查找測試[編號:5 關勝]看看,看看是否找到並是否四次找到

System.out.println("==========================使用前序遍歷查找方式");
HeroNode resNode = tree.preOrderSearch(5);
if (resNode != null) {
    System. out.printf("找到了,信息爲no=%d name=%s", resNode.getNo(), resNode. getName());
} else {
    System.out. printf("沒有找到no = %d的英雄",5);
}

運行輸出結果以下:
==========================使用前序遍歷查找方式
進入前序遍歷查找~~~~
進入前序遍歷查找~~~~
進入前序遍歷查找~~~~
進入前序遍歷查找~~~~
找到了,信息爲no=5 name=關勝

使用中序遍歷查找測試[編號:5 關勝]看看,看看是否找到並是否三次找到

System.out.println("==========================使用中序遍歷查找方式~~~");
HeroNode resNode = tree.infixOrderSearch(5);
if (resNode != null) {
    System. out.printf("找到了,信息爲no=%d name=%s", resNode.getNo(), resNode. getName());
} else {
    System.out. printf("沒有找到no = %d的英雄",5);
}

運行輸出結果以下:
==========================使用中序遍歷查找方式~~~
進入中序遍歷查找~~~~
進入中序遍歷查找~~~~
進入中序遍歷查找~~~~
找到了,信息爲no=5 name=關勝

使用後序遍歷查找測試[編號:5 關勝]看看,看看是否找到並是否二次找到

System.out.println("==========================使用後序遍歷查找方式~~~");
HeroNode resNode = tree.postOrderSearch(5);
if (resNode != null) {
    System. out.printf("找到了,信息爲no=%d name=%s", resNode.getNo(), resNode. getName());
} else {
    System.out. printf("沒有找到no = %d的英雄",5);
}

運行輸出結果以下:
==========================使用後序遍歷查找方式~~~
進入後序遍歷查找~~~~
進入後序遍歷查找~~~~
找到了,信息爲no=5 name=關勝

5、認識二叉樹的刪除節點

由於目前的二叉樹:暫時是沒有規則的,後邊深刻時再解決怎麼把左節點或者右節點提高上去的問題。

示例三:

  • 規則一:若是刪除的節點是葉子節點,則刪除該節點
  • 規則二:若是刪除的節點是非葉子節點,則刪除該子樹.
  • 目標:刪除葉子節點五號和子樹三號.

image.png

思路分析

  1. 若是樹自己爲空,只有一個root節點則等價於二叉樹置空
  2. 由於咱們的二叉樹是鏈表單向的,因此刪除目標節點時不能直接判斷是否刪除該節點。
  3. 若是當前節點左子節點不爲空,而且左子節點是須要刪除的節點就將this.left = null,並結束返回
  4. 若是當前節點右子節點不爲空,而且右子節點是須要刪除的節點就將this.right = null,並結束返回
  5. 若是當前節點沒有刪除的節點,則判斷左節點是否爲空,進行左遞歸繼續刪除
  6. 若是左節點左遞歸沒有刪除的節點,則判斷右節點是否爲空,進行右遞歸繼續刪除

接下來咱們在分別在HeroNode、BinaryTree添加代碼

//添加刪除節點代碼
class HeroNode {
    
    //省略以前序、中序、後序遍歷代碼
    //省略以前序、中序、後序查找代碼代碼
    //遞歸刪除結點
    //1.若是刪除的節點是葉子節點,則刪除該節點
    //2.若是刪除的節點是非葉子節點,則刪除該子樹
    public void delHerNode(int no){

        //思路
//        由於咱們的二叉樹是鏈表單向的,因此刪除目標節點時不能直接判斷是否刪除該節點。

//        1.若是當前節點左子節點不爲空,而且左子節點是須要刪除的節點就將this.left = null,並結束返回
          if(this.left != null && this.left.no == no){
              this.left = null;
              return;
          }
//        2.若是當前節點右子節點不爲空,而且右子節點是須要刪除的節點就將this.right = null,並結束返回
          if(this.right != null && this.right.no == no){
              this.right = null;
              return;
          }
//        3.若是當前節點沒有刪除的節點,則判斷左節點是否爲空,進行左遞歸繼續刪除
          if(this.left != null){
              this.left.delHerNode(no);
          }
//        4.若是左節點左遞歸沒有刪除的節點,則判斷右節點是否爲空,進行右遞歸繼續刪除
          if(this.right != null){
              this.right.delHerNode(no);
          }
    }
}
//添加刪除節點代碼
class BinaryTree{ 

    //省略以前序、中序、後序遍歷代碼
    //省略以前序、中序、後序查找代碼代碼
    public void delHerNode(int no){
        //若是樹自己爲空,只有一個root節點則等價於二叉樹置空
        if(root !=null){
            //若是隻有一個root結點,這裏當即判斷root是否是就是要刪除結點
            if(root.getNo() == no){
               root = null;
            }else{
                root.delHerNode(no);
            }
        }else{
            System.out.println("空樹!不能刪除");
        }
   }
}

使用刪除節點測試[編號:5 關勝]看看,看看是否成功

System.out.println("==============前序遍歷顯示刪除前數據");
tree.preOrder();

System.out.println("==========================================刪除葉子節點五號:關勝");
tree.delHerNode(5);

System.out.println("==============前序遍歷顯示刪除後數據");
tree.preOrder();

運行結果以下:
==============前序遍歷顯示刪除前數據
HeroNode [no =1, name =松江]
HeroNode [no =2, name =吳用]
HeroNode [no =3, name =盧俊義]
HeroNode [no =5, name =關勝]
HeroNode [no =4, name =林沖]
==========================================刪除葉子節點五號:關勝
==============前序遍歷顯示刪除後數據
HeroNode [no =1, name =松江]
HeroNode [no =2, name =吳用]
HeroNode [no =3, name =盧俊義]
HeroNode [no =4, name =林沖]

圖解分析刪除節點

執行刪除[葉子節點五號:關勝],看看是如何進行的吧!

當執行方法時,先判斷當前root 是否爲空,緊接着判斷當前root 節點no 是否等於須要刪除的節點no,不知足條件進入delNode方法
image.png

delNode方法裏,判斷當前節點宋江左節點是不是[五號:關勝],但左節點當前爲[二號:吳用],不知足因而接着判斷右節點
image.png

但右節點當前爲[三號:盧俊義],不知足條件判斷
image.png

因而判斷當前左節點是否爲空,不爲空則進行左遞歸查詢再次進入delNode方法,那麼當前節點爲[二號:吳用]
image.png

delNode方法裏,判斷當前節點吳用左節點是不是[五號:關勝],但當前吳用節點左節點爲空因而接着判斷右節點
image.png

但右節點當前爲空,不知足條件判斷
image.png

因而判斷當前左節點是否爲空,不爲空則進行左遞歸查詢再次進入,可是很遺憾,吳用的左邊是null,則進行判斷右節點
image.png

可是吳用右邊也是null,則往回溯回到宋江的左遞歸查詢
image.png

接着判斷當前宋江右節點是否爲空,不爲空則進行右遞歸查詢再次進入delNode方法,那麼當前節點爲[三號:盧俊義]
image.png

delNode方法裏,判斷當前節點吳用左節點是不是[五號:關勝],知足條件則盧俊義左節點更改成:null

image.png
image.png

相關文章
相關標籤/搜索