1. 程式人生 > 其它 >206. 反轉連結串列-力扣(leetcode)

206. 反轉連結串列-力扣(leetcode)

技術標籤:演算法與資料結構二叉樹遍歷二叉樹資料結構java

文章目錄

一、二叉樹概述

1.1 為什麼要有樹這種資料結構

在前面已經學習了陣列和連結串列這兩種資料結構,這兩種資料結構都有著鮮明的特點。

陣列儲存方式的優缺點如下:

  • 優點:通過下標方式訪問元素,速度快。對於有序陣列,還可使用二分查詢提高檢索速度。
  • 缺點:如果要檢索具體某個值,或者插入值(按一定順序)會整體移動, 效率較低。

連結串列儲存方式的優缺點如下:

  • 優點:在一定程度上對陣列儲存方式有優化。比如插入一個數值節點,只需要將插入節點連結到連結串列中即可,其刪除效率也較高。
  • 缺點:在進行檢索時,效率仍然較低。比如檢索某個值, 需要從頭節點開始遍歷。

從它們的特點我們可以總結出:陣列方式查詢快、增刪慢,而連結串列方式查詢慢、增刪快。這兩種資料儲存方式都相當於是魚與熊掌不可兼得。

而樹這種資料結構,可以在查詢速度快的同時又兼顧了增刪的效率,可以說是結合了陣列和連結串列的優點

樹儲存方式的核心特點為:能提高資料儲存、讀取的效率。比如利用二叉排序樹(Binary Sort Tree),既可以保證資料的檢索速度,同時也可以保證資料的插入、刪除、修改的速度。

在這裡插入圖片描述

1.2 樹的常用術語

樹這種資料結構有較多的常用術語,這些術語我們需要熟練記住。

在這裡插入圖片描述

這些常用術語包括:

  1. 節點
  2. 根節點
  3. 父節點
  4. 子節點
  5. 葉子節點(沒有子節點的節點)
  6. 節點的權(節點值)
  7. 路徑(從 root 節點找到該節點的路線)
  8. 子樹
  9. 樹的高度(最大層數)
  10. 森林(多顆子樹構成森林)

常用術語雖然比較多,但是對照著樹的示意圖,還是比較容易理解的。

1.3 什麼是二叉樹

樹有很多種,每個節點最多隻能有兩個子節點的一種形式稱為二叉樹。二叉樹的子節點分為左節點和右節點。

在這裡插入圖片描述

如上圖所示的三個樹,它們每個節點最多隻有兩個子節點,因此它們都是二叉樹。

在二叉樹中,還有兩種更為特殊的樹,分別是:

  • 滿二叉樹

    如果二叉樹的所有葉子節點都在最後一層

    ,並且結點總數= 2^n -1 , n 為層數,則我們稱其為滿二叉樹。

    在這裡插入圖片描述

  • 完全二叉樹

    如果二叉樹的所有葉子節點都在最後一層或者倒數第二層,而且最後一層的葉子節點在左邊(相對於根節點)連續,倒數第二層的葉子節點在右邊連續,我們稱為完全二叉樹。

    在這裡插入圖片描述

我們不難看出:滿二叉樹是完全二叉樹的特殊形態, 即如果一棵二叉樹是滿二叉樹, 則它必定是完全二叉樹

1.4 二叉樹的遍歷順序

二叉樹的遍歷共有三種情況:

  • 前序遍歷

    先遍歷根節點,再遍歷左子樹,最後遍歷右子樹。

  • 中序遍歷

    先遍歷左子樹,再遍歷根節點,最後遍歷右子樹。

  • 後序遍歷

    先遍歷左子樹,再遍歷右子樹,最後遍歷根節點。

根據這三種遍歷順序的定義,我們可以得知兩個結論:

  1. 左子樹總是在右子樹的前面遍歷;
  2. 前、中、後的遍歷順序指的是遍歷根節點的順序。

二、二叉樹的基本操作

本節的關於二叉樹的操作,都是對如下圖所示的二叉樹進行的:

在這裡插入圖片描述

如上所示的二叉樹,每一個節點都是一個英雄節點 HeroNode ,每個節點記錄著英雄的資訊(編號、姓名)。

英雄節點模型如下:

/**
 * 模擬二叉樹的節點
 */
class HeroNode{
    public int no;
    public String name;
    public HeroNode left;   // 左子節點
    public HeroNode right;  // 右子節點

    public HeroNode(int no, String name){
        this.no = no;
        this.name = name;
    }    
    // 新增左子節點
    public void addLeftNode(HeroNode node){
        this.left = node;
    }
    // 新增右子節點
    public void addRightNode(HeroNode node){
        this.right = node;
    }
    // ......
    
    @Override
    public String toString() {
        return "HeroNode{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }
}

除此之外,我們還需要建立一個二叉樹模型 BinaryTree,包含對二叉樹的操作:

class BinaryTree{
    private HeroNode root;  // 根節點

    public BinaryTree(HeroNode node){
        this.root = node;
    }
    // ......
}

為了方便後續的程式碼測試,我們手動建立一個上圖所示的二叉樹:

HeroNode node_1 = new HeroNode(1, "宋江");
HeroNode node_2 = new HeroNode(2, "盧俊義");
HeroNode node_3 = new HeroNode(3, "吳用");
HeroNode node_4 = new HeroNode(4, "公孫勝");
HeroNode node_5 = new HeroNode(5, "關勝");

BinaryTree binaryTree = new BinaryTree(node_1);
node_1.addLeftNode(node_2);
node_1.addRightNode(node_3);
node_3.addLeftNode(node_4);
node_3.addRightNode(node_5);

2.1 二叉樹的節點遍歷

【案例需求】

使用前序、中序、後序分別遍歷二叉樹。

【思路分析】

上面說過,二叉樹的遍歷包括:前序遍歷、中序遍歷、後序遍歷。

其中前序遍歷的思路如下:

  1. 首先輸出當前節點;
  2. 如果左子節點不為空,就對左子節點遞迴前序遍歷;
  3. 如果右子節點不為空,就對右子節點遞迴前序遍歷。

中序遍歷的思路如下:

  1. 首先判斷左子節點是否為空,如果不為空,就對左子節點遞迴中序遍歷;
  2. 然後輸出當前節點;
  3. 最後判斷右子節點是否為空,如果不為空,就對右子節點遞迴中序遍歷。

後序遍歷的思路如下:

  1. 首先判斷左子節點是否為空,如果不為空,就對左子節點遞迴後序遍歷;
  2. 然後判斷右子節點是否為空,如果不為空,就對右子節點遞迴後序遍歷;
  3. 最後輸出當前節點。

【程式碼實現】

英雄節點 HeroNode 負責對子樹的具體操作,二叉樹 BinaryTree 呼叫節點的具體操作來實現相關操作。

二叉樹的遍歷操作實現程式碼如下:

/**
 * 二叉樹
 */
class BinaryTree{
    private HeroNode root;  // 根節點

    public BinaryTree(HeroNode node){
        this.root = node;
    }
    
    // 前序遍歷
    public void preOrder(){
        if (root != null){
            root.preOrder();	// 根節點呼叫具體遍歷操作
        }else{
            System.out.println("二叉樹為空!");
        }
    }
    // 中序遍歷
    public void midOrder(){
        if (root != null){
            root.midOrder(); // 根節點呼叫具體遍歷操作
        }else{
            System.out.println("二叉樹為空!");
        }
    }
    // 後序遍歷
    public void postOrder(){
        if (root != null){
            root.postOrder(); // 根節點呼叫具體遍歷操作
        }else{
            System.out.println("二叉樹為空!");
        }
    }
}

/**
 * 模擬二叉樹的節點
 */
class HeroNode{
    public int no;
    public String name;
    public HeroNode left;   // 左子節點
    public HeroNode right;  // 右子節點

    public HeroNode(int no, String name){
        this.no = no;
        this.name = name;
    }
    // 新增左子節點
    public void addLeftNode(HeroNode node){
        this.left = node;
    }
    // 新增右子節點
    public void addRightNode(HeroNode node){
        this.right = node;
    }
    // 前序遍歷:根->左->右
    public void preOrder(){
        System.out.println(this);   // 列印本節點
        if (this.left != null){     // 遞迴列印左子節點
            this.left.preOrder();
        }
        if (this.right != null){    // 遞迴列印右子節點
            this.right.preOrder();
        }
    }
    // 中序遍歷:左->根->右
    public void midOrder(){
        if (this.left != null){ // 遞迴列印左子節點
            this.left.midOrder();
        }
        System.out.println(this);   // 列印本節點
        if (this.right != null){
            this.right.midOrder();  // 遞迴列印右子節點
        }
    }
    // 後序遍歷:左->右->根
    public void postOrder(){
        if (this.left != null){ // 遞迴列印左子節點
            this.left.postOrder();
        }
        if (this.right != null){    // 遞迴列印右子節點
            this.right.postOrder();
        }
        System.out.println(this);   // 列印本節點
    }

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

程式碼測試執行結果如下:

在這裡插入圖片描述

2.2 二叉樹的節點查詢

【案例需求】

分別使用前序、中序、後序查詢二叉樹中指定編號節點。

【思路分析】

前序查詢思路分析:

  1. 首先判斷當前節點的編號是否等於指定編號
  2. 如果相等,則直接返回當前節點;
  3. 如果不相等,再判斷左子節點是否為空,如果不為空則左子節點遞迴前序查詢;
  4. 如果左子節點遞迴前序查詢找到了節點,則返回節點;
  5. 否則,繼續判斷右子節點是否為空,如果不為空則對右子節點遞迴前序查詢;
  6. 如果找到則返回節點,否則返回 NULL。

中序查詢思路:

  1. 首先判斷判斷左子節點是否為空,如果不為空則左子節點遞迴中序查詢;
  2. 如果左子節點遞迴中序查詢找到了節點,則返回節點;
  3. 否則,判斷當前節點的編號是否等於指定編號,如果相等,則直接返回當前節點;
  4. 否則,繼續判斷右子節點是否為空,如果不為空則對右子節點遞迴中序查詢;
  5. 如果找到則返回節點,否則返回 NULL。

後序查詢思路:

  1. 首先判斷判斷左子節點是否為空,如果不為空則左子節點遞迴後序查詢;
  2. 如果左子節點遞迴後序查詢找到了節點,則返回節點;
  3. 否則,繼續判斷右子節點是否為空,如果不為空則對右子節點遞迴後序查詢;
  4. 如果右子節點遞迴後序查詢找到了節點,則返回節點;
  5. 否則,判斷當前節點的編號是否等於指定編號。如果相等,則直接返回當前節點,否則,返回 NULL。

【程式碼實現】

前、中、後序查詢指定編號的節點的程式碼實現如下:

/**
 * 二叉樹
 */
class BinaryTree{
    private HeroNode root;  // 根節點

    public BinaryTree(HeroNode node){
        this.root = node;
    }
    // 前序查詢
    public void preOrderSearch(int no){
        if (root != null){	// 判斷根節點是否為空
            HeroNode res = root.preOrderSearch(no);
            if (res != null){
                System.out.println(res);
            }else{
                System.out.println("未找到指定節點!");
            }
        }else{
            System.out.println("二叉樹為空!");
        }
    }
    // 中序查詢
    public void midOrderSearch(int no){
        if (root != null){
            HeroNode res = root.midOrderSearch(no);
            if (res != null){
                System.out.println(res);
            }else{
                System.out.println("未找到指定節點!");
            }
        }else{
            System.out.println("二叉樹為空!");
        }
    }
    // 後序查詢
    public void postOrderSearch(int no){
        if (root != null){
            HeroNode res = root.postOrderSearch(no);
            if (res != null){
                System.out.println(res);
            }else{
                System.out.println("未找到指定節點!");
            }
        }else{
            System.out.println("二叉樹為空!");
        }
    }
}

/**
 * 模擬二叉樹的節點
 */
class HeroNode{
    public int no;
    public String name;
    public HeroNode left;   // 左子節點
    public HeroNode right;  // 右子節點

    public HeroNode(int no, String name){
        this.no = no;
        this.name = name;
    }
    // 新增左子節點
    public void addLeftNode(HeroNode node){
        this.left = node;
    }
    // 新增右子節點
    public void addRightNode(HeroNode node){
        this.right = node;
    }
    // 前序查詢:根左右
    public HeroNode preOrderSearch(int no){
        if (this.no == no){ // 先查根
            return this;
        }
        HeroNode res = null;
        if (this.left != null){ // 再查左
            res = this.left.preOrderSearch(no);
            if (res != null){
                return res;
            }
        }
        if (this.right != null){    // 最後查右
            res = this.right.preOrderSearch(no);
        }
        return res;
    }
    // 中序查詢:左根右
    public HeroNode midOrderSearch(int no){
        HeroNode res = null;
        if (this.left != null){ // 先查左
            res = this.left.midOrderSearch(no);
        }
        if (res != null){
            return res;
        }
        if (this.no == no){ // 再查根
            return this;
        }
        if (this.right != null){    // 最後查右
            res = this.right.midOrderSearch(no);
        }
        return res;
    }
    // 後序查詢:左右根
    public HeroNode postOrderSearch(int no){
        HeroNode res = null;
        if (this.left != null){	// 先左子節點
            res = this.left.postOrderSearch(no);
        }
        if (res != null){
            return res;
        }
        if (this.right != null){	// 然後右子節點
            res = this.right.postOrderSearch(no);
        }
        if (res != null){
            return res;
        }
        if (this.no == no){	// 最後根節點
            return this;
        }
        return null;
    }
    @Override
    public String toString() {
        return "HeroNode{" +
            "no=" + no +
            ", name='" + name + '\'' +
            '}';
    }
}

2.3 二叉樹的節點刪除

【案例需求】

根據節點編號刪除指定節點:

  • 如果節點為葉子節點,直接刪除;
  • 如果節點為父節點,則把節點和節點的整個子樹都刪除。

【思路分析】

除了二叉樹的根節點之外,其餘的節點都是有父節點的。由於二叉樹的每個節點關係都是單向的,即每個節點記錄的都是自己的左、右子節點的資訊,所以是無法通過目標節點自己來刪除自己的,而是需要藉助目標節點的父節點來刪除目標節點

因此二叉樹的節點刪除思路如下:

  1. 如果二叉樹的根節點就是目標節點,那麼直接將根節點置空即可;
  2. 否則,如果當前根節點的左子節點不為空且是目標節點,就將左子節點置空 this.left = null 並返回;
  3. 如果第 2 步沒刪除,若當前根節點的右子節點不為空且是目標節點,就將右子節點置空 this.right = null 並返回;
  4. 如果第 3 步沒刪除,就遍歷左子樹遞迴查詢刪除;
  5. 如果第 4 步也沒刪除,就遍歷右子樹遞迴查詢刪除;

【程式碼實現】

刪除二叉樹指定節點的程式碼如下:

class BinaryTree{
    private HeroNode root;  // 根節點

    public BinaryTree(HeroNode node){
        this.root = node;
    }

    // 刪除指定編號的節點
    public void deleteNode(int no){
        if (root != null){  // 判斷根節點是否為空
            if (root.no == no){ // 判斷根節點是否為目標節點
                root = null;
            }else{
                root.deleteNode(no);    // 從根節點開始遞迴遍歷子節點
            }
        }else{
            System.out.println("二叉樹為空");
        }
    }
}

/**
 * 模擬二叉樹的節點
 */
class HeroNode{
    public int no;
    public String name;
    public HeroNode left;   // 左子節點
    public HeroNode right;  // 右子節點

    public HeroNode(int no, String name){
        this.no = no;
        this.name = name;
    }
    // 新增左子節點
    public void addLeftNode(HeroNode node){
        this.left = node;
    }
    // 新增右子節點
    public void addRightNode(HeroNode node){
        this.right = node;
    }
    // 刪除節點:由於節點只記錄下自己的左、右子節點的資訊,因為只能通過待刪除節點的父節點來刪除自己
    public void deleteNode(int no){
        if (this.left != null && this.left.no == no){   // 判斷左子節點是否是目標節點
            this.left = null;   // 刪除節點,如果節點為父節點,則子樹全部刪除
            return;
        }
        if (this.right != null && this.right.no == no){ // 判斷右子節點是否為目標節點
            this.right = null;  // 刪除節點,若節點為父節點,則子樹全部刪除
            return;
        }
        if (this.left != null){ // 如果左子樹不為空,左子樹遞迴執行
            this.left.deleteNode(no);
        }
        if (this.right != null){    // 如果右子樹不為空,右子樹遞迴執行
            this.right.deleteNode(no);
        }
    }
    @Override
    public String toString() {
        return "HeroNode{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }
}