天天看點

資料結構----二叉樹

五、二叉樹

一、二叉樹入門

之前我們實作的符号表中,不難看出,符号表的增删查操作,随着元素個數N的增多,其耗時也是線性增多的,時

間複雜度都是O(n),為了提高運算效率,接下來我們學習樹這種資料結構。

1.1 樹的基本定義

樹是我們計算機中非常重要的一種資料結構,同時使用樹這種資料結構,可以描述現實生活中的很多事物,例如家

譜、機關的組織架構、等等。

樹是由n(n>=1)個有限結點組成一個具有層次關系的集合。把它叫做“樹”是因為它看起來像一棵倒挂的樹,也就

是說它是根朝上,而葉朝下的。

資料結構----二叉樹

樹具有以下特點:

1.每個結點有零個或多個子結點;

2.沒有父結點的結點為根結點;

3.每一個非根結點隻有一個父結點;

4.每個結點及其後代結點整體上可以看做是一棵樹,稱為目前結點的父結點的一個子樹;

1.2 樹的相關術語

結點的度:

一個結點含有的子樹的個數稱為該結點的度;

葉結點:

度為0的結點稱為葉結點,也可以叫做終端結點

分支結點:

度不為0的結點稱為分支結點,也可以叫做非終端結點

結點的層次:

從根結點開始,根結點的層次為1,根的直接後繼層次為2,以此類推

結點的層序編号:

将樹中的結點,按照從上層到下層,同層從左到右的次序排成一個線性序列,把他們編成連續的自然數。

樹的度:

樹中所有結點的度的最大值

樹的高度(深度):

樹中結點的最大層次

森林:

m(m>=0)個互不相交的樹的集合,将一顆非空樹的根結點删去,樹就變成一個森林;給森林增加一個統一的根

結點,森林就變成一棵樹

資料結構----二叉樹

孩子結點:

一個結點的直接後繼結點稱為該結點的孩子結點

雙親結點(父結點):

一個結點的直接前驅稱為該結點的雙親結點

兄弟結點:

同一雙親結點的孩子結點間互稱兄弟結點

1.3 二叉樹的基本定義

二叉樹就是度不超過2的樹(每個結點最多有兩個子結點)

資料結構----二叉樹

滿二叉樹:

一個二叉樹,如果每一個層的結點樹都達到最大值,則這個二叉樹就是滿二叉樹。

資料結構----二叉樹

完全二叉樹:

葉節點隻能出現在最下層和次下層,并且最下面一層的結點都集中在該層最左邊的若幹位置的二叉樹

資料結構----二叉樹

1.4 二叉查找樹的建立

1.4.1二叉樹的結點類

根據對圖的觀察,我們發現二叉樹其實就是由一個一個的結點及其之間的關系組成的,按照面向對象的思想,我們

設計一個結點類來描述結點這個事物。

結點類API設計:

資料結構----二叉樹

代碼實作:

private static class Node<Key, Value> {
        public Key key;
        public Value value;
        public Node left;
        public Node right;

        public Node(Key key, Value value, Node left, Node right) {
            this.key = key;
            this.value = value;
            this.left = left;
            this.right = right;
        }
    }
           

1.4.2 二叉查找樹API設計

資料結構----二叉樹

1.4.3 二叉查找樹實作

插入方法put實作思想:

  1. 如果目前樹中沒有任何一個結點,則直接把新結點當做根結點使用
  2. 如果目前樹不為空,則從根結點開始:

2.1 如果新結點的key小于目前結點的key,則繼續找目前結點的左子結點;

2.2 如果新結點的key大于目前結點的key,則繼續找目前結點的右子結點;

2.3 如果新結點的key等于目前結點的key,則樹中已經存在這樣的結點,替換該結點的value值即可

資料結構----二叉樹

查詢方法get實作思想:

從根節點開始:

  1. 如果要查詢的key小于目前結點的key,則繼續找目前結點的左子結點;
  2. 如果要查詢的key大于目前結點的key,則繼續找目前結點的右子結點;
  3. 如果要查詢的key等于目前結點的key,則樹中傳回目前結點的value。

删除方法delete實作思想:

  1. 找到被删除結點;
  2. 找到被删除結點右子樹中的最小結點minNode
  3. 删除右子樹中的最小結點
  4. 讓被删除結點的左子樹稱為最小結點minNode的左子樹,讓被删除結點的右子樹稱為最小結點minNode的右

    子樹

  5. 讓被删除結點的父節點指向最小結點minNode
資料結構----二叉樹

代碼:

public class BinaryTree<Key extends Comparable<Key>, Value> {

    // 記錄根節點
    private Node root;
    // 記錄樹中元素的個數
    private int N;


    // 擷取樹中元素的個數
    public int size() {
        return N;
    }

    // 像樹中添加元素key-value
    public void put(Key key, Value value) {
        root = put(root, key, value);
    }

    // 向指定樹x中添加key-value,并傳回添加元素後新的樹
    private Node put(Node x, Key key, Value value) {
        // 如果x子樹為空
        if (x == null) {
            N++;
            return new Node(key, value, null, null);
        }

        // 如果x子樹不為空
        // 比較x結點的鍵,和key的大小
        int cmp = key.compareTo((Key) x.key);
        if (cmp > 0) {
            // 如果key>x結點的key,則繼續找x結點的右子樹
            x.right = put(x.right, key, value);
        } else if (cmp < 0) {
            // 如果key<x結點的key,則繼續找x結點的左子樹
            x.left = put(x.left, key, value);
        } else {
            // 如果key=x結點的key,則替換x結點的值為value即可
            x.value = value;
        }
        return x;
    }

    // 查詢樹中指定key對應的value
    public Value get(Key key) {
        return get(root, key);
    }

    // 從指定的樹x中,查找key對應的value
    private Value get(Node x, Key key) {
        // x樹為空
        if (x == null) {
            return null;
        }
        // x樹不為空
        // 比較x結點的鍵,和key的大小
        int cmp = key.compareTo((Key) x.key);
        if (cmp > 0) {
            // 如果key>x結點的key,則繼續找x結點的右子樹
            return get(x.right, key);
        } else if (cmp < 0) {
            // 如果key<x結點的key,則繼續找x結點的左子樹
            return get(x.left, key);
        } else {
            // 如果key=x結點的key,就是找到了,傳回x結點的值即可
            return (Value) x.value;
        }
    }

    // 删除樹中key對應的value
    public void delete(Key key) {
        delete(root, key);
    }

    // 删除指定樹x中的key對應的value,并傳回删除後的新樹
    public Node delete(Node x, Key key) {
        if (x == null) {
            return null;
        }

        int cmp = key.compareTo((Key) x.key);
        if (cmp > 0) {
            // 如果key>x結點的key,則繼續找x結點的右子樹
            x.right = delete(x.right, key);
        } else if (cmp < 0) {
            // 如果key<x結點的key,則繼續找x結點的左子樹
            x.left = delete(x.left, key);
        } else {
            // 讓元素個數-1
            N--;
            // 如果key=x結點的key,完成真正的删除結點
            // 找到右子樹中最小的結點
            if (x.right == null) {

                return x.left;
            }
            if (x.left == null) {
                return x.right;
            }

            Node minNode = x.right;
            while (minNode.left != null) {
                minNode = minNode.left;
            }
            // 删除右子樹中最小的結點,也就是找到的minNode
            Node n = x.right;
            while (n.left != null) {
                if (n.left.left == null) {
                    n.left = null;
                } else {
                    // 變換n結點
                    n = n.left;
                }
            }
            // 讓删除結點的父結點與删除結點的右結點相連接配接(連接配接大的一方)
            n.left = minNode.right;
            // 讓x結點的左子樹成為minNode的左子樹
            minNode.left = x.left;
            // 讓x結點的右子樹成為minNode的右子樹
            minNode.right = x.right;
            // 讓x結點的父結點指向minNode,因為前面
            // x.left = delete(x.left, key);
            // x.right = delete(x.right, key);  是以傳回x使得父節點下一個結點變為minNode
            x = minNode;
        }
        return x;
    }



    private static class Node<Key, Value> {
        public Key key;
        public Value value;
        public Node left;
        public Node right;

        public Node(Key key, Value value, Node left, Node right) {
            this.key = key;
            this.value = value;
            this.left = left;
            this.right = right;
        }
    }

}
           
public static void main(String[] args) {
        //建立二叉查找樹對象
        BinaryTree<Integer, String> tree = new BinaryTree<>();

        //測試插入
        tree.put(1,"張三");
        tree.put(7,"李四");
        tree.put(3,"王五");
        System.out.println("插入完畢後元素的個數:"+tree.size());

        //測試擷取
        System.out.println("鍵3對應的元素是:"+tree.get(3));

        //測試删除
        tree.delete(3);
        System.out.println("删除後的元素個數:"+tree.size());
        System.out.println("删除後鍵3對應的元素:"+tree.get(3));
    }
輸出:
插入完畢後元素的個數:3
鍵3對應的元素是:王五
删除後的元素個數:2
删除後鍵3對應的元素:null
           

1.4.4 二叉查找樹其他便捷方法

1.4.4.1 查找二叉樹中最小的鍵

在某些情況下,我們需要查找出樹中存儲所有元素的鍵的最小值,比如我們的樹中存儲的是學生的排名和姓名數

據,那麼需要查找出排名最低是多少名?這裡我們設計如下兩個方法來完成:

資料結構----二叉樹
/*
        找出樹中最小的鍵
     */
    public Key min() {
        return (Key) min(root).key;
    }

    /*
        找出指定樹x中,最小鍵所在的結點
     */
    private Node min(Node x) {
        if (x.left != null) {
            return min(x.left);
        } else {
            return x;
        }
    }
           

1.4.4.2 查找二叉樹中最大的鍵

在某些情況下,我們需要查找出樹中存儲所有元素的鍵的最大值,比如比如我們的樹中存儲的是學生的成績和學生

的姓名,那麼需要查找出最高的分數是多少?這裡我們同樣設計兩個方法來完成:

資料結構----二叉樹
/*
        找出樹中最大的鍵
     */
    public Key max() {
        return (Key) max(root).key;
    }

    /*
        找出指定樹x中,最小大鍵所在的結點
     */
    private Node max(Node x) {
        if (x.right != null) {
            return max(x.right);
        } else {
            return x;
        }
    }
           

1.5 二叉樹的基礎周遊

很多情況下,我們可能需要像周遊數組數組一樣,周遊樹,進而拿出樹中存儲的每一個元素,由于樹狀結構和線性

結構不一樣,它沒有辦法從頭開始依次向後周遊,是以存在如何周遊,也就是按照什麼樣的搜尋路徑進行周遊的問

題。

資料結構----二叉樹

我們把樹簡單的畫作上圖中的樣子,由一個根節點、一個左子樹、一個右子樹組成,那麼按照根節點什麼時候被訪

問,我們可以把二叉樹的周遊分為以下三種方式:

  1. 前序周遊;

先通路根結點,然後再通路左子樹,最後通路右子樹

  1. 中序周遊;

先通路左子樹,中間通路根節點,最後通路右子樹

  1. 後序周遊;

先通路左子樹,再通路右子樹,最後通路根節點

如果我們分别對下面的樹使用三種周遊方式進行周遊,得到的結果如下:

資料結構----二叉樹

1.5.1 前序周遊

我們在4.4中建立的樹上,添加前序周遊的API:

public Queue<Key> preErgodic():

使用前序周遊,擷取整個樹中的所有鍵

private void preErgodic(Node x,Queue<Key> keys):

使用前序周遊,把指定樹x中的所有鍵放入到keys隊

列中

實作過程中,我們通過前序周遊,把,把每個結點的鍵取出,放入到隊列中傳回即可。

實作步驟:

1.把目前結點的key放入到隊列中;

2.找到目前結點的左子樹,如果不為空,遞歸周遊左子樹

3.找到目前結點的右子樹,如果不為空,遞歸周遊右子樹

代碼:

/*
        使用前序周遊,擷取整個樹中的所有鍵
     */
    public Queue<Key> preErgodic() {
        Queue<Key> keys = new Queue<>();
        preErgodic(root, keys);
        return keys;
    }

    /*
        使用前序周遊,把指定樹x中的所有鍵放入到keys隊列中
     */
    private void preErgodic(Node x, Queue<Key> keys) {
        if (x == null) {
            return;
        }
        // 把x結點的key放到keys中
        keys.enqueue((Key) x.key);
        // 遞歸周遊x結點的左子樹
        if (x.left != null) {
            preErgodic(x.left, keys);
        }
        // 遞歸周遊x結點的右子樹
        if (x.right != null) {
            preErgodic(x.right, keys);
        }

    }
           

1.5.2 中序周遊

我們在4.4中建立的樹上,添加前序周遊的API:

public Queue<Key> midErgodic():

使用中序周遊,擷取整個樹中的所有鍵

private void midErgodic(Node x,Queue<Key> keys):

使用中序周遊,把指定樹x中的所有鍵放入到keys隊

列中

實作步驟:

1.找到目前結點的左子樹,如果不為空,遞歸周遊左子樹

2.把目前結點的key放入到隊列中;

3.找到目前結點的右子樹,如果不為空,遞歸周遊右子樹

代碼:

/*
        使用中序周遊,擷取整個樹中的所有鍵
     */
    public Queue<Key> midErgodic() {
        Queue<Key> keys = new Queue<>();
        midErgodic(root, keys);
        return keys;
    }

    /*
        使用中序周遊,把指定樹x中的所有鍵放入到keys隊列中
     */
    private void midErgodic(Node x, Queue<Key> keys) {

        if (x == null) {
            return;
        }
        // 先遞歸把左子樹中的鍵放到keys中
        if (x.left != null) {
            midErgodic(x.left, keys);
        }
        // 把目前結點x的鍵放到keys中
        keys.enqueue((Key) x.key);
        // 在遞歸把右子樹中的鍵放到keys中
        if (x.right != null) {
            midErgodic(x.right, keys);
        }


    }
           

1.5.3 後序周遊

我們在4.4中建立的樹上,添加前序周遊的API:

public Queue<Key> afterErgodic():

使用後序周遊,擷取整個樹中的所有鍵

private void afterErgodic(Node x,Queue<Key> keys):

使用後序周遊,把指定樹x中的所有鍵放入到keys隊列中

實作步驟:

1.找到目前結點的左子樹,如果不為空,遞歸周遊左子樹

2.找到目前結點的右子樹,如果不為空,遞歸周遊右子樹

3.把目前結點的key放入到隊列中;

代碼:

/*
           使用後序周遊,擷取整個樹中的所有鍵
        */
    public Queue<Key> afterErgodic() {
        Queue<Key> keys = new Queue<>();
        afterErgodic(root, keys);
        return keys;
    }

    /*
        使用後序周遊,把指定樹x中的所有鍵放入到keys隊列中
     */
    private void afterErgodic(Node x, Queue<Key> keys) {
        if (x == null) {
            return;
        }
        // 先遞歸把左子樹中的鍵放到keys中
        if (x.left!=null){
            afterErgodic(x.left,keys);
        }
        // 在遞歸把右子樹中的鍵放到keys中
        if (x.right!=null){
            afterErgodic(x.right,keys);
        }
        // 把目前結點x的鍵放到keys中
        keys.enqueue((Key) x.key);


    }

           

1.6 二叉樹的層序周遊

所謂的層序周遊,就是從根節點(第一層)開始,依次向下,擷取每一層所有結點的值,有二叉樹如下:

資料結構----二叉樹

那麼層序周遊的結果是:EBGADFHC

我們在4.4中建立的樹上,添加層序周遊的API:

public Queue<Key> layerErgodic():

使用層序周遊,擷取整個樹中的所有鍵

實作步驟:

  1. 建立隊列,存儲每一層的結點;
  2. 使用循環從隊列中彈出一個結點:

    2.1 擷取目前結點的key;

    2.2 如果目前結點的左子結點不為空,則把左子結點放入到隊列中

    2.3 如果目前結點的右子結點不為空,則把右子結點放入到隊列中

資料結構----二叉樹

代碼:

/**
     * 層序周遊
     * @return
     */
    public Queue<Key> layerErgodic(){
       // 定義兩個隊列。分别存儲樹中的鍵和樹中的結點
        Queue<Key> keys = new Queue<>();
        Queue<Node> nodes = new Queue<>();
        // 預設,往隊列中放入根結點
        nodes.enqueue(root);
        while (!nodes.isEmpty()){
            // 從隊列中彈出一個結點,把key放入到keys中
            Node n = nodes.dequeue();
            keys.enqueue((Key) n.key);
            // 判斷目前結點還有沒有左子節點,如果有,則放入到nodes中
            if (n.left!=null){
                nodes.enqueue(n.left);
            }
            // 判斷目前結點還有沒有右子節點,如果有,則放入到nodes中
            if (n.right!=null){
                nodes.enqueue(n.right);
            }
        }
        return keys;
    }
           

1.7 二叉樹的最大深度問題

需求:

給定一棵樹,請計算樹的最大深度(樹的根節點到最遠葉子結點的最長路徑上的結點數)

資料結構----二叉樹

上面這棵樹的最大深度為4。

實作:

我們在1.4中建立的樹上,添加如下的API求最大深度:

public int maxDepth():計算整個樹的最大深度

private int maxDepth(Node x):計算指定樹x的最大深度

實作步驟:

  1. 如果根結點為空,則最大深度為0;
  2. 計算左子樹的最大深度;
  3. 計算右子樹的最大深度;
  4. 目前樹的最大深度=左子樹的最大深度和右子樹的最大深度中的較大者+1

代碼:

// 擷取整個樹的最大深度
    public int maxDepth() {
       return maxDepth(root);
    }

    // 擷取指定樹x的最大深度
    public int maxDepth(Node x) {
        if (x == null) {
            return 0;
        }
        // 總的深度
        int max = 0;
        // 左子樹深度
        int maxL = 0;
        // 右子樹深度
        int maxR = 0;
        // 計算x左子樹的最大深度
        if (x.left != null) {
            maxL = maxDepth(x.left);
        }
        // 計算x右子樹的最大深度
        if (x.right != null) {
            maxR = maxDepth(x.right);
        }
        // 取二者較大的
        max = maxL > maxR ? maxL + 1 : maxR + 1;
        return max;
    }
           

繼續閱讀