資料結構分析之二叉樹

wustor發表於2017-11-06

概述

在分析樹形結構之前,先看一下樹形結構在整個資料結構中的位置

資料結構
資料結構

當然,沒有圖,現在還沒有足夠的水平去分析圖這種太複雜的資料結構,表資料結構包含線性表跟連結串列,也就是經常說的陣列,棧,佇列等,在前面的Java容器類框架中已經分析過了,上面任意一種資料結構在java中都有對應的實現,比如說ArrayList對應陣列,LinkedList對應雙向連結串列,HashMap對應了雜湊表,JDK1.8之後的HashMap採用的是紅黑樹實現,下面單獨把二叉樹抽取出來:

二叉樹
二叉樹

由於樹的基本概念基本上大家都有所瞭解,現在重點介紹一下二叉樹

正文

二叉樹

二叉樹(binary tree)是一棵樹,其中每個節點都不能有多於兩個的兒子。

二叉樹
二叉樹

分類

  • 滿二叉樹:若設二叉樹的高度為h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數,第h層有葉子結點,並且葉子結點都是從左到右依次排布,這就是完全二叉樹。
  • 完全二叉樹:除了葉結點外每一個結點都有左右子葉且葉子結點都處在最底層的二叉樹。
  • 平衡二叉樹:稱為AVL樹(區別於AVL演算法),它是一棵二叉排序樹,且具有以下性質:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。

對於完全二叉樹,若某個節點數為i,左子節點位2i+1,右子節點為2i+2。

實現

下面用程式碼實現一個二叉樹

public class BinaryNode {
    Object data;
    BinaryNode left;
    BinaryNode right;
}複製程式碼

遍歷

二叉樹的遍歷有兩種:按照節點遍歷與層次遍歷

節點遍歷
  • 前序遍歷:遍歷到一個節點後,即刻輸出該節點的值,並繼續遍歷其左右子樹(根左右)。
  • 中序遍歷:遍歷一個節點後,將其暫存,遍歷完左子樹後,再輸出該節點的值,然後遍歷右子樹(左根右)。
  • 後序遍歷:遍歷到一個節點後,將其暫存,遍歷完左右子樹後,再輸出該節點的值(左右根)。

構造完全二叉樹

enter image description here
enter image description here

就以上圖的二叉樹為例,首先用程式碼構造一棵完全二叉樹
節點類TreeNode

public class TreeNode {
    private int value;
    private TreeNode leftChild;
    private TreeNode rightChild;

    public TreeNode(int value) {
        super();
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

    public TreeNode getLeftChild() {
        return leftChild;
    }

    public void setLeftChild(TreeNode leftChild) {
        this.leftChild = leftChild;
    }

    public TreeNode getRightChild() {
        return rightChild;
    }

    public void setRightChild(TreeNode rightChild) {
        this.rightChild = rightChild;
    }

}複製程式碼

生成完全二叉樹

 //陣列
 private int[] saveValue = {0,1, 2, 3, 4, 5, 6};
  private ArrayList<TreeNode> list = new ArrayList<>();
   public TreeNode createTree() {
   //將所有的節點儲存進一個List集合裡面
        for (int i = 0; i < saveValue.length; i++) {
            TreeNode treeNode = new TreeNode(saveValue[i]);
            list.add(treeNode);
        }
    //根據完全二叉樹的性質,i節點的左子樹是2*i+1,右節點字數是2*i+2
         for (int i = 0; i < list.size() / 2; i++) {
            TreeNode treeNode = list.get(i);
            //判斷左子樹是否越界
            if (i * 2 + 1 < list.size()) {
                TreeNode leftTreeNode = list.get(i * 2 + 1);
                treeNode.setLeftChild(leftTreeNode);
            }
            //判斷右子樹是否越界
            if (i * 2 + 2 < list.size()) {
                TreeNode rightTreeNode = list.get(i * 2 + 2);
                treeNode.setRightChild(rightTreeNode);
            }
        }
        return list.get(0);
    }複製程式碼

前序遍歷

   //前序遍歷
    public static void preOrder(TreeNode root) {
        if (root == null)
            return;
        System.out.print(root.value + " ");
        preOrder(root.left);
        preOrder(root.right);
    }複製程式碼

遍歷結果: 0 1 3 4 2 5 6

中序遍歷

   //中序遍歷
    public static void midOrder(TreeNode root) {
        if (root == null)
            return;
        midOrder(root.left);
        System.out.print(root.value + " ");
        midOrder(root.right);
    }複製程式碼

遍歷結果:3 1 4 0 5 2 6

後序遍歷

  //後序遍歷
    public static void postOrder(TreeNode root) {
        if (root == null)
            return;
        postOrder(root.left);
        postOrder(root.right);
        System.out.print(root.value + " ");
    }複製程式碼

遍歷結果:3 4 1 5 6 2 0
上述三種方式都是採用遞迴的方式進行遍歷的,當然也可以迭代,感覺迭代比較麻煩,遞迴程式碼比較簡潔,仔細觀察可以發現,實際上三種遍歷方式都是一樣的,知識列印value的順序不一樣而已,是一種比較巧妙的方式。

層次遍歷

二叉樹的層次遍歷可以分為深度優先遍歷跟廣度優先遍歷

  • 深度優先遍歷:實際上就是上面的前序、中序和後序遍歷,也就是儘可能去遍歷二叉樹的深度。
  • 廣度優先遍歷:實際上就是一層一層的遍歷,按照層次輸出二叉樹的各個節點。

深度優先遍歷

由於上面的前序、中序和後續遍歷都是採用的遞迴的方式進行遍歷,下面就以前序遍歷為例,採用非遞迴的方式進行遍歷

步驟

  • 若二叉樹為空,返回:
  • 用一個stack來儲存二叉樹
  • 然後遍歷節點的時候先讓右子樹入棧,再讓左子樹入棧,這樣右子樹就會比左子樹後出棧,從而實現先序遍歷

    //2.前序遍歷(迭代)
      public static void preorderTraversal(TreeNode root) {  
          if(root == null){  
              return;  
          }  
          Stack<TreeNode> stack = new Stack<TreeNode>();  
          stack.push(root);  
          while( !stack.isEmpty() ){  
              TreeNode cur = stack.pop();     // 出棧棧頂元素  
              System.out.print(cur.data + " ");  
              //先壓右子樹入棧
              if(cur.right != null){  
                  stack.push(cur.right);  
              }  
              //再壓左子樹入棧
              if(cur.left != null){  
                  stack.push(cur.left);  
              }  
          }  
      }複製程式碼

    遍歷結果:0 1 3 4 2 5 6

廣度優先遍歷

所謂廣度優先遍歷就是一層一層的遍歷,所以只需要按照每層的左右順序拿到二叉樹的節點,再依次輸出就OK了

步驟

  • 用一個LinkedList保留所有的根節點
  • 依次輸出即可
    //分層遍歷
      public static void levelOrder(TreeNode root) {
          if (root == null) {
              return;
          }
          LinkedList<TreeNode> queue = new LinkedList<>();
          queue.push(root);
          while (!queue.isEmpty()) {
              //列印linkedList的第一次元素
              TreeNode cur = queue.removeFirst();
              System.out.print(cur.value + " ");
              //依次新增每個節點的左節點
              if (cur.left != null) {
                  queue.add(cur.left);
              }
              //依次新增每個節點的右節點
              if (cur.right != null) {
                  queue.add(cur.right);
              }
          }
      }複製程式碼
    遍歷結果:0 1 2 3 4 5 6

二叉堆

二叉堆是一棵完全二叉樹或者是近似完全二叉樹,同時二叉堆還滿足堆的特性:父節點的鍵值總是保持固定的序關係於任何一個子節點的鍵值,且每個節點的左子樹和右子樹都是一個二叉堆。

分類
  • 最大堆:父節點的鍵值總是大於或等於任何一個子節點的鍵值
  • 最小堆:父節點的鍵值總是小於或等於任何一個子節點的鍵值
    二叉堆的分類
    二叉堆的分類

由於二叉堆 的根節點總是存放著最大或者最小元素,所以經常被用來構造優先佇列(Priority Queue),當你需要找到佇列中最高優先順序或者最低優先順序的元素時,使用堆結構可以幫助你快速的定位元素。

性質
  • 結構性質:堆是一棵被完全填滿的二叉樹,有可能的例外是在底層,底層上的元素從左到右填入。這樣的樹稱為完全二叉樹。
    一棵完全二叉樹
    一棵完全二叉樹
實現

二叉堆可以用陣列實現也可以用連結串列實現,觀察上述的完全二叉樹可以發現,是比較有規律的,所以完全可以使用一個陣列而不需要使用鏈。下面用陣列來表示上圖所對應的堆結。

完全二叉樹的陣列實現
完全二叉樹的陣列實現

對於陣列中任意位置i的元素,其左兒子在位置2i上,右兒子在左兒子後的單元(2i+1)中,它的父親則在位置[i/2上面]

public class MaxHeap<Item extends Comparable> {

    protected Item[] data;
    protected int count;
    protected int capacity;
    // 建構函式, 構造一個空堆, 可容納capacity個元素
    public MaxHeap(int capacity) {
        data = (Item[]) new Comparable[capacity + 1];
        count = 0;
        this.capacity = capacity;
    }

    // 返回堆中的元素個數
    public int size() {
        return count;
    }

    // 返回一個布林值, 表示堆中是否為空
    public boolean isEmpty() {
        return count == 0;
    }

    // 像最大堆中插入一個新的元素 item
    public void insert(Item item) {
        assert count + 1 <= capacity;
        data[count + 1] = item;
        count++;
        shiftUp(count);
    }

    // 交換堆中索引為i和j的兩個元素
    private void swap(int i, int j) {
        Item t = data[i];
        data[i] = data[j];
        data[j] = t;
    }
    //調整堆中的元素,使其成為一個最大堆
    private void shiftUp(int k) {
    // k/2表示k節點的父節點
        while (k > 1 && data[k / 2].compareTo(data[k]) < 0) {
        //子節點跟父節點進行比較,如果父節點小於子節點則進行交換
            swap(k, k / 2);
            k /= 2;
        }
    }

}複製程式碼
構造一個二叉堆
  // 測試 MaxHeap
    public static void main(String[] args) {
        MaxHeap<Integer> maxHeap = new MaxHeap<>(100);
        int N = 50; // 堆中元素個數
        int M = 100; // 堆中元素取值範圍[0, M)
        for (int i = 0; i < N; i++)
            maxHeap.insert((int) (Math.random() * M));
        System.out.println(maxHeap.size());
    }複製程式碼
insert操作
    public void insert(Item item) {
        //從第1個元素開始賦值
        assert count + 1 <= capacity;
        data[count + 1] = item;
        count++;
        //上慮操作
        shiftUp(count);
    }複製程式碼
上慮操作

先將新插入的元素加到陣列尾部,然後跟父節點進行比較,如果比父節點大就進行交換

    private void shiftUp(int k) {
    // k/2表示k節點的父節點
        while (k > 1 && data[k / 2].compareTo(data[k]) < 0) {
        //子節點跟父節點進行比較,如果父節點小於子節點則進行交換
            swap(k, k / 2);
            k /= 2;
        }
    }複製程式碼
獲取最大元素
public Integer extractMax(){
        assert count > 0;
        Integer ret = data[1];
        swap( 1 , count );
        count --;
        //下慮操作
        shiftDown(1);
        return ret;
    }複製程式碼
下慮操作

先將根節點跟最後一個元素進行交換,然後將count減1,然後根節點再跟左子樹與右子樹之間的最大值進行比較

 private void shiftDown(int k){
        while( 2*k <= count ){
            int j = 2*k; // 在此輪迴圈中,data[k]和data[j]交換位置
            if( j+1 <= count && data[j+1].compareTo(data[j]) > 0 )
                j ++;
            // data[j] 是 data[2*k]和data[2*k+1]中的最大值
            if( data[k].compareTo(data[j]) >= 0 ) 
            //當前節點比根節點大,中斷迴圈
            break;
            //交換節點跟子樹
            swap(k, j);
            k = j;
        }
    }複製程式碼

二叉查詢樹

二叉查詢樹:對於樹中的每個節點X,它的左子樹中所有項的值小於X中的項,而它的右子樹中的所有項的值大於X中的項。

兩棵二叉樹(只有左邊的樹是查詢樹)
兩棵二叉樹(只有左邊的樹是查詢樹)

實現
public class BST<Key extends Comparable<Key>, Value> {
    private class Node {
        private Key key;
        private Value value;
        private Node left, right;
        public Node(Key key, Value value) {
            this.key = key;
            this.value = value;
            left = right = null;
        }
        public Node(Node node){
            this.key = node.key;
            this.value = node.value;
            this.left = node.left;
            this.right = node.right;
        }
    }
   // 建構函式, 預設構造一棵空二分搜尋樹
    public BST() {
        root = null;
        count = 0;
    }

    // 返回二分搜尋樹的節點個數
    public int size() {
        return count;
    }
    // 返回二分搜尋樹是否為空
    public boolean isEmpty() {
        return count == 0;
    }



    }複製程式碼
contains方法
    // 呼叫contains (root,key)

    public boolean contain(Key key){
        return contain(root, key);
    }
  // 檢視以node為根的二分搜尋樹中是否包含鍵值為key的節點, 使用遞迴演算法
    private boolean contain(Node node, Key key){
        if( node == null )
            return false;
            //根節點的key跟查詢的key相同,直接返回true
        if( key.compareTo(node.key) == 0 )
            return true;
            //key小的話遍歷左子樹
        else if( key.compareTo(node.key) < 0 )
            return contain( node.left , key );
            //key大的話遍歷右子樹
        else // key > node->key
            return contain( node.right , key );
    }複製程式碼
insert方法
     // 向二分搜尋樹中插入一個新的(key, value)資料對
    public void insert(Key key, Value value){
        root = insert(root, key, value);
    }
    // 向以node為根的二分搜尋樹中, 插入節點(key, value), 使用遞迴演算法
    // 返回插入新節點後的二分搜尋樹的根
    private Node insert(Node node, Key key, Value value){
        //空樹直接返回
        if( node == null ){
            count ++;
            return new Node(key, value);
        }
        //如果需要插入的key跟當前的key相同,則將值替換成最新的值
        if( key.compareTo(node.key) == 0 )
            node.value = value;
         //如果需要插入的key小於當前的key,從左子樹進行插入
        else if( key.compareTo(node.key) < 0 )
            node.left = insert( node.left , key, value);
        else    
        //如果需要插入的key大於當前的key,從右子樹進行插入
            node.right = insert( node.right, key, value);
        return node;
    }複製程式碼
finMin方法
    // 尋找二分搜尋樹的最小的鍵值
    public Key minimum(){
        assert count != 0;
        Node minNode = minimum( root );
        return minNode.key;
    }

    // 返回以node為根的二分搜尋樹的最小鍵值所在的節點
    private Node minimum(Node node){
        //遞迴遍歷左子樹
        if( node.left == null )
            return node;
        return minimum(node.left);
    }複製程式碼
finMax方法
 // 尋找二分搜尋樹的最大的鍵值
    public Key maximum(){
        assert count != 0;
        Node maxNode = maximum(root);
        return maxNode.key;
    }
    // 返回以node為根的二分搜尋樹的最大鍵值所在的節點
    private Node maximum(Node node){
    //遞迴遍歷右子樹
        if( node.right == null )
            return node;
        return maximum(node.right);
    }複製程式碼
search方法
  // 在二分搜尋樹中搜尋鍵key所對應的值。如果這個值不存在, 則返回null
    public Value search(Key key){
        return search( root , key );
    }

    // 在以node為根的二分搜尋樹中查詢key所對應的value, 遞迴演算法
    // 若value不存在, 則返回NULL
    private Value search(Node node, Key key){
        if( node == null )
            return null;
        if( key.compareTo(node.key) == 0 )
            return node.value;
        else if( key.compareTo(node.key) < 0 )
            return search( node.left , key );
        else // key > node->key
            return search( node.right, key );
    }複製程式碼

上面分析了三種比較特殊的樹形結構,二叉樹,二叉堆以及二叉搜尋樹,尤其是二叉堆跟二叉搜尋樹,尤其是二叉堆主要用於優先佇列,二叉搜尋樹主要用來查詢。

相關文章