數據結構和算法:二叉樹

二叉樹

二叉樹(Binary tree)是樹形結構的一個重要類型。許多實際問題抽象出來的數據結構每每是二叉樹形式,即便是通常的樹也能簡單地轉換爲二叉樹,並且二叉樹的存儲結構及其算法都較爲簡單,所以二叉樹顯得特別重要。二叉樹特色是每一個節點最多隻能有兩棵子樹,即樹的度最大爲2,且有左右之分node

二叉樹是n個有限元素的集合,該集合或者爲空、或者由一個稱爲根(root)的元素及兩個不相交的、被分別稱爲左子樹和右子樹的二叉樹組成,是有序樹。當集合爲空時,稱該二叉樹爲空二叉樹。算法

特殊類型

滿二叉樹:二叉樹內只有度爲2和0的節點,且度爲0的節點在同一層,即除了樹的最後一層的節點沒有子節點,其餘節點都有2個子節點,則這個二叉樹就是滿二叉樹。滿二叉樹的節點數量爲2^k-1(k爲樹的深度)數組

徹底二叉樹:徹底二叉樹的節點順序是由上到下,由左到右的。即葉子節點所在的層級差異不能大於1,且右節點不爲空時,左節點也不能爲空。滿二叉樹必定就是徹底二叉樹,但徹底二叉樹不必定是滿二叉樹數據結構

代碼實例ide

使用二叉樹來存儲水滸英雄好漢,模擬水滸英雄排名。post

首先須要先建立二叉樹裏的節點,用於存儲水滸英雄的編號、名字和節點的左、右子節點。ui

//水滸英雄節點
class HeroNode{
    private int id;
    private String name;
    private HeroNode left;//節點的左子節點
    private HeroNode right;//節點的右子節點
    
    //3種遍歷樹的方法,在《數據結構與算法:樹》的一章有過講解
    //前序遍歷(中-左-右)
    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);
    }

    public HeroNode(int id, String name) {
        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 + '\'' +
                '}';
    }

}

有了節點,接下來就須要建立二叉樹類了,而二叉樹的起點是根節點,咱們須要靠根節點才能夠實現增刪改查的操做,因此二叉樹最重要的屬性就是根節點。this

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

    //實例化根節點,即實例化二叉樹
    public void setRoot(HeroNode root) {
        this.root = root;
    }
    
      //由於是經過二叉樹去操做節點,因此要爲節點的遍歷方法進行封裝調用
    public void preOrder(){
        if (this.root != null){
            root.preOrder();
        }else {
            System.out.println("二叉樹爲空,沒法遍歷");
        }
    }

    public void midOrder(){
        if (this.root != null){
            root.midOrder();
        }else {
            System.out.println("二叉樹爲空,沒法遍歷");
        }
    }

    public void postOrder(){
        if (this.root != null){
            root.postOrder();
        }else {
            System.out.println("二叉樹爲空,沒法遍歷");
        }
    }
}

接下來咱們就能夠建立一個二叉樹,由於咱們沒有爲二叉樹設定任何節點順序規則,因此這裏咱們先手動爲二叉樹添加節點,等到後面章節再講解根據規則來自動添加節點和刪除節點操做。spa

public class BinaryTreeDemo {
    public static void main(String[] args) {
        BinaryTree binaryTree = new BinaryTree();

        HeroNode root = new HeroNode(1, "宋江");
        HeroNode node1 = new HeroNode(2, "盧俊義");
        HeroNode node2 = new HeroNode(3, "吳用");
        HeroNode node3 = new HeroNode(4, "公孫勝");
        HeroNode node4 = new HeroNode(5, "關勝");

        //爲各個節點設置關係
        root.setLeft(node1);
        root.setRight(node2);
        node2.setLeft(node4);
        node2.setRight(node3);
        //實例化二叉樹根節點
        binaryTree.setRoot(root);
        
        //使用3種遍歷方式遍歷二叉樹
        System.out.println("前序遍歷:");
        binaryTree.preOrder();
        System.out.println("中序遍歷:");
        binaryTree.midOrder();
        System.out.println("後序遍歷:");
        binaryTree.postOrder();
    }
}

若是咱們要根據id查找單個水滸英雄信息時,只需將遍歷方法修改一下便可,下面之前序遍歷查找爲例:3d

//前序遍歷查找(修改節點方法只需添加個修改參數,並把返回代碼改爲修改代碼)
public HeroNode preOrderSearch(int id){
    //若是當前節點就是查找的節點時,返回該節點(中)
    if (id == this.getId()){
        return this;
    }
    HeroNode result = null;
    //往左子節點遞歸遍歷查找
    if (this.left != null){
        result = this.left.preOrderSearch(id);
    }
    //若是result不爲空則代表已經查找到該節點,直接返回result
    if (result != null){
        return result;
    }
    //往右子節點遞歸遍歷查找
    if (this.right != null){
        result = this.right.preOrderSearch(id);
    }
    return result;
}

 

順序存儲二叉樹

二叉樹的存儲方式有倆種,一種是鏈式存儲(上面代碼所演示的),另外一種是順序存儲。順序存儲指的是用數組來存儲二叉樹,即根據層級,從第一層到最後一層由左到右的順序存儲在數組中。

順序存儲二叉樹的節點總數量:數組的長度

下標爲n的節點的左節點下標:n*2+1

下標爲n的節點的右節點下標:n*2+2

下標爲n的節點的父節點下標:(n-1)/2

注意:順序存儲方式只能存儲徹底二叉樹,若是要存儲非徹底二叉樹,需將它轉換成徹底二叉樹,即補值爲0的節點,直到該二叉樹變成徹底二叉樹

代碼實例

//順序存儲二叉樹
class ArrayBinaryTree{
    //順序存儲的數組
    private int[] array;

    public ArrayBinaryTree(int[] array) {
        this.array = array;
    }

    public void preOrder(){
        preOrder(0);
    }

    public void midOrder(){
        midOrder(0);
    }

    public void postOrder(){
        postOrder(0);
    }

    //前序遍歷
    public void preOrder(int index){
        if (array == null || array.length == 0){
            System.out.println("二叉樹數組爲空,沒法遍歷");
            return;
        }
        //輸出該節點(中)
        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 midOrder(int index){
        if (array == null || array.length == 0){
            System.out.println("二叉樹數組爲空,沒法遍歷");
            return;
        }
        if (index*2+1 < array.length){
            midOrder(index*2+1);
        }
        System.out.println(array[index]);
        if (index*2+2 < array.length){
            midOrder(index*2+2);
        }
    }

    //後序遍歷
    public void postOrder(int index){
        if (array == null || array.length == 0){
            System.out.println("二叉樹數組爲空,沒法遍歷");
            return;
        }
        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]);
    }
}

 

線索化二叉樹

在上圖的二叉樹遍歷中,當咱們遍歷到葉子節點時,須要一層一層的返回到下一個輸出的節點,效率並不高,那咱們能不能直接讓葉子節點指向在對應遍歷方法中它的下一個節點呢?這時咱們就須要線索化二叉樹了。

對於n個結點的二叉樹,在二叉鏈存儲結構中有n+1個空鏈域,利用這些空鏈域存放在某種遍歷次序下該結點的前驅結點和後繼結點的指針,這些指針稱爲線索,加上線索的二叉樹稱爲線索二叉樹。

這種加上了線索的二叉鏈表稱爲線索鏈表,相應的二叉樹稱爲線索二叉樹(Threaded BinaryTree)。根據線索性質的不一樣,線索二叉樹可分爲前序線索二叉樹中序線索二叉樹後序線索二叉樹三種。

代碼實例

//中序線索化二叉樹
class ThreadedBinaryTree{
    private HeroNode2 root;
    //在中序線索化時,保存前一個節點,默認爲空
    private HeroNode2 pre;

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

    public void threadedNodes(){
        threadedNodes(root);
    }

    //中序線索化二叉樹
    public void threadedNodes(HeroNode2 node){
        //若是傳入的參數節點爲空,即已經超出二叉樹範圍,則直接返回
        if (node == null){
            return;
        }

        /**
         * 按照中序遍歷(左中右)的順序,依次爲各個節點線索化
         */
        //先向節點的左節點遞歸,依次線索化左節點
        threadedNodes(node.getLeft());

        //爲節點線索化
        /*
        若是該節點的左節點爲空,則將該節點的左節點設置爲中序線索化的前一個節點pre,
        並將該節點的左節點類型leftType設置爲前驅節點1
        注意: 在第一次線索化時,左節點會設置爲空,但由於是中序線索化,因此不影響
         */
        if (node.getLeft() == null){
            node.setLeft(pre);
            node.setLeftType(1);
        }
        /*
        若是前一個節點pre的右節點爲空,則將中序線索化的前一個節點pre的右節點設置爲當前節點,
        並將pre節點的右節點類型設爲後繼節點1
        邏輯:pre只有當它的右節點爲空時纔會進入線索化,而這時遞歸已經返回到中序遍歷時pre節點的
        下一個遍歷節點node,因此node就是在中序線索化中pre的下一個節點,即後繼節點
         */
        if (pre != null && pre.getRight() == null){
            pre.setRight(node);
            pre.setRightType(1);
        }
        //線索化後要將當前節點設置會上一個節點
        pre = node;

        //再向節點的右節點遞歸,依次線索化右節點
        threadedNodes(node.getRight());
    }

    //遍歷中序線索化二叉樹(即輸出中序遍歷的結果)
    public void threadedList(){
        HeroNode2 node = root;

        //當node爲空時,即中序線索化二叉樹已經遍歷完成
        while (node != null){
            //若是左節點的類型是子樹0的話,則繼續往節點的左節點走
            while (node.getLeftType() == 0){
                node = node.getLeft();
            }
            //當退出循環時,即node沒有左子節點了,則輸出node節點
            System.out.println(node);

            //若是右節點的類型是後繼節點1時,則直接跳到後繼節點並輸出
            while (node.getRightType() == 1){
                node = node.getRight();
                System.out.println(node);
            }
            //當退出循環時,即node還有右子節點,則繼續往節點的右子節點走
            node = node.getRight();
        }
    }
}

//線索化二叉樹節點
/**
 * 由於二叉樹線索化後,
 * 左節點的指向有兩種狀況:指向左子樹或指向前驅節點;右節點的指向也有兩種狀況:指向右子樹或指向後繼節點;
 * 因此要分別爲左、右節點建立一個屬性,該屬性能夠說明左、右節點的指向狀況。
 */
class HeroNode2{
    private int id;
    private String name;
    private HeroNode2 left;
    private HeroNode2 right;
    //左指向的類型,當爲0時,指向的是左子樹,當爲1時,指向的是前驅節點
    private int leftType;
    //右指向的類型,當爲0時,指向的是右子樹,當爲1時,指向的是後繼節點
    private int rightType;

    public HeroNode2(int id, String name) {
        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 "HeroNode2{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}
相關文章
相關標籤/搜索