數據結構分析之二叉樹

概述

在分析樹形結構以前,先看一下樹形結構在整個數據結構中的位置java

數據結構
數據結構

固然,沒有圖,如今尚未足夠的水平去分析圖這種太複雜的數據結構,表數據結構包含線性表跟鏈表,也就是常常說的數組,棧,隊列等,在前面的Java容器類框架中已經分析過了,上面任意一種數據結構在java中都有對應的實現,好比說ArrayList對應數組,LinkedList對應雙向鏈表,HashMap對應了散列表,JDK1.8以後的HashMap採用的是紅黑樹實現,下面單獨把二叉樹抽取出來:node

二叉樹
二叉樹

因爲樹的基本概念基本上你們都有所瞭解,如今重點介紹一下二叉樹算法

正文

二叉樹

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

二叉樹
二叉樹

分類

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

對於徹底二叉樹,若某個節點數爲i,左子節點位2i+1,右子節點爲2i+2。bash

實現

下面用代碼實現一個二叉樹數據結構

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

遍歷

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

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

構造徹底二叉樹
dom

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

}複製代碼

生成徹底二叉樹post

//數組
 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 );
    }複製代碼

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

相關文章
相關標籤/搜索