java實現(1)-二叉查找樹

引言

在這個模塊中,主要是用自己的代碼來實現一些底層的源碼。不像底層源碼那樣難理解,會用更加通俗的方式讓每個人都能看得懂,比如說不會在if中聲明變量和賦值,即使if只有一句話也不會省略花括號,不適用移位等等。在本篇博文中,會用自己的方式來實現二叉查找樹,同時把它轉變爲更加通用的泛型。筆者目前整理的一些blog針對面試都是超高頻出現的。大家可以點擊鏈接:http://blog.csdn.net/u012403290

技術點

1、泛型
在jdk1.5中引入,目標是爲了更好的支持代碼的重用。考慮這麼一種情況,在除去對象的類型之外,實現方法是相同的,那麼我們就可以用泛型實現。比如說排序當中,我們可以對int排序,也可以對double,也可以對String排序,甚至可以對我們自己寫的對象進行排序,排序的方式是相同的,都是拿兩者進行比較,大的放前面,小的放後面,這個時候就可以用泛型實現。比如說一個List對象,你可以是放Integer,也可以放String,這就是泛型。

2、樹
一棵樹是節點的集合,在不是空集的情況下,樹就是由節點以及0個或者多個非空的子集樹構成,這些子集樹的都來自總集合樹的一條有向的所連接。比如說下面就是一棵樹:
這裏寫圖片描述
A是B,C,D,E,F,G,H的父親,反之它們是A的兒子;同理,F是K,L的父親,K,L是F的兒子;B,C,D,E,F,G,H之間可以稱爲兄弟;甚至,可以稱A爲I,J,K,L的祖父,反之它們是A的孫子。其中如果某一個節點不再有兒子,那麼我們稱這個節點爲樹葉
那麼,我們可以知道,樹其實是由N個節點和N-1條邊組成,爲什麼是N-1條邊呢?因爲除卻總集合的根節點外,每個節點都會有一條邊與父節點相連,所以“-1”是總集合根節點沒有父節點造成的。
接着我們說說樹的深度和高,對於任意一個節點n,從根到這個n節點所有邊的和稱爲節點n的深度,比如說A的深度爲0,B的深度爲1,K的深度爲2,M的深度爲3。對於任意一個節點n,從這個節點到一片樹葉所有的邊長的和稱爲高。比如說A的高爲3,B的高爲0,F的高爲2,K的高爲1。

3、二叉查找樹
分爲兩部分來分爲,二叉樹是指其中任意一個節點都不能有多餘兩個兒子。查找的意思就是它建立在二叉樹的基礎上多了一個節點大小的規則。對於任意一個節點,它的左兒子肯定比它自身小,它的右兒子肯定比它自身大,如下圖就是一個二叉查找樹:
這裏寫圖片描述
今天,我們主要是來實現二叉查找樹的創建,插入,查找最大,查找最小和刪除等。同時在Integer實現的情況下改成更通用的泛型實現。

代碼實現查找二叉樹

package com.brickworkers;

/**
 * 
 * @author Brickworker
 * data:2017年4月20日下午2:21:13 
 * 關於類SearchBinaryTree.java的描述:一個二叉查找樹存儲Integer數據
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class SearchBinaryTree {

    //定義一個節點
    private static class BinaryNode{
        Integer data;//存儲數據
        BinaryNode left;//左孩子
        BinaryNode right;//右孩子

        public BinaryNode(Integer data) {//構造函數,生成一個沒有兒子的節點
            this(data, null, null);
        }

        public BinaryNode(Integer data, BinaryNode lt, BinaryNode rt) {//構造函數,生產一個有兩個兒子的節點(lt,rt不爲空)
            this.data = data;
            left = lt;
            right = rt;
        }
    }


    //定義一個根節點,當二叉查找樹初始化的時候必須要有一個根節點,哪怕是個空的根節點
    private BinaryNode root;

    //構造函數,構造一顆空的查找二叉樹
    public SearchBinaryTree() {
        root = null;
    }

    //判斷是否包含某個節點
    public boolean contains(Integer t){
        return contains(t, root);
    }

    //尋找最小的節點
    public Integer findMin(){
        if(root == null)//如果跟節點是空的,表示二叉查找樹是空的,不必尋找最小了
            throw new IllegalArgumentException("該樹是空的");
        else
            return findMin(root).data;
    }

    //尋找最大節點
    public Integer findMax() {
        if (root == null)
            throw new IllegalArgumentException("該樹是空的");
        else
            return findMax(root).data;
    }


    //插入節點
    public void insert(Integer t){
        root = insert(t, root);
    }


    //刪除節點
    public void remove(Integer t){
        root = remove(t, root);
    }




    //按順序打印樹
    public void printTree(){
        if(root == null)
            throw new IllegalArgumentException("該樹是空的");
        else
            printTree(root);
    }


    //返回true或者false
    // 搜索樹,查找是否包含某個節點
    private boolean contains(Integer t, BinaryNode node) {
        if (node == null) {// 如果搜索到null了還沒有找到,那麼就返回false
            return false;
        }
        // 如果要查找的數據比目前節點的小,根據二叉查找樹的大下原則,往左邊繼續遞歸查找
        if (t < node.data) {
            return contains(t, node.left);
        }
        // 如果要查找的數據比目前節點的大,根據二叉查找樹的大下原則,往右邊繼續遞歸查找
        else if (t > node.data) {
            return contains(t, node.right);
        } else{//兩者相等,說明找到了目標值
            return true;
        }
    }

    //返回一個節點
    //尋找最小節點
    //在二叉查找樹要尋找最小,根據二叉查找樹的性質,其實就是找樹的最左邊的節點,也就是最左邊的葉子
    private BinaryNode findMin(BinaryNode node) {
        // 如果這個節點不爲空,且沒有左兒子,就表示這個節點是最小的了
        if (node.left == null) {
            return node;
        } else {// 否則繼續遞歸尋找
            return findMin(node.left);
        }
    }


    //返回一個節點
    // 尋找最大節點
    //在二叉查找樹要尋找最大,根據二叉查找樹的性質,其實就是找樹的最右邊邊的節點,也就是最右邊的葉子
    private BinaryNode findMax(BinaryNode node) {
        // 如果這個節點不爲空,且沒有右兒子,就表示這個節點是最大的了
        if (node.right == null) {
            return node;
        } else {//否則繼續遞歸尋找
            return findMax(node.right);
        }
    }

    //返回一個節點,爲什麼要返回一個節點呢?因爲在我們新建一個節點的時候,必須要把它關聯到上一個節點的做兒子或者右兒子上。比如下面的node.right = insert(t, node.right);
    //插入節點
    //在二叉查找樹插入中,根據它的原則,比節點小的要往左邊插入,比節點大的要往右邊插入
    private BinaryNode insert(Integer t, BinaryNode node){
        //如果這個節點爲null,就表示已經找到當前要插入的位置,並用t創建一個新的節點返回
        if(node == null){
            return new BinaryNode(t);
        }
        //如果要插入的數據比當前節點大,那麼根據二叉查找樹的原則,需要往當前節點的右側開始查找
        if(t > node.data){
            node.right = insert(t, node.right);
        }
        //如果要插入的數據比當前節點小,那麼根據二叉查找樹的原則,需要往當前節點的左側開始查找
        else if(t < node.data){
            node.left = insert(t, node.left);
        }else{//當兩者相同的時候,禁止插入(當然,你也可以設計成能插入的,具體插入到左邊還是右邊自己定)
            throw new IllegalArgumentException("不允許插入已存在的數據,t:" +node.data);
        }
        return node;

    }   

    //返回一個節點
    //刪除節點
    //在二叉查找樹插入中,刪除節點要分爲3中情況來討論,①要刪除的節點本身是樹葉,那麼直接刪除。②如果要刪除的節點
    //存在一個兒子,那麼直接把它兒子頂替它的位置;③如果要刪除的節點有兩個兒子,那麼就需要把它左邊最大,或者右邊最小
    //的來頂替它的位置。因爲只有左邊最大和右邊最小放在它的位置才能符合二叉查找樹的規則。
    private BinaryNode remove(Integer t, BinaryNode node){
        //如果要刪除的節點比當前節點小,那麼根據二叉查找樹的規則,往左邊繼續查找
        if(t < node.data){
            node.left = remove(t, node.left);
        }
        //如果要刪除的節點比當前節點大,那麼根據二叉查找樹的規則,往右邊繼續查找
        else if(t > node.data)
            node.right = remove(t, node.right);
        //如果已經找到要刪除的節點,但是這個節點有兩個兒子
        else if(node.left != null && node.right != null){
            node.data = findMin(node.right).data;//找到右邊最小的頂替目前節點的位置
            node.right = remove(node.data, node.right);//然後繼續向右遍歷刪除用於頂替的最小的那個節點
        }else{//如果要刪除的節點沒有或者只有一個兒子
            node = (node.left != null)? node.left: node.right;
        }
        return node; 
    }

    //按順序打印樹
    private void printTree(BinaryNode node){
        //當前節點不爲空
        if(node != null){
            //優先遍歷左邊,從小到大輸出
            printTree(node.left);
            System.out.print(node.data + "  ");
            /遍歷完左邊再遍歷右邊,從小到大輸出
            printTree(node.right);
        }
    }

}

在上面的實現中,我寫了大量的註釋,希望對大家有所幫助,但是在這裏,我還是要把刪除再解釋一遍。在二叉查找樹的刪除分爲3中情況,以下面這種圖爲例子:
這裏寫圖片描述

不存在兒子,比如說4,7,9,13。那麼在刪除這些節點的時候可以直接刪除。
存在一個兒子,比如說5,刪除之後應該是這樣的:
這裏寫圖片描述

存在兩個兒子,比如說10,6,8。在上圖中以刪除6爲例子,刪除之後應該是這樣的:
這裏寫圖片描述

同理,如果用6的左邊最大的來替換6節點也是一樣的道理。

其他的,如果一句句看上面的代碼應該是能看懂的。上面這個二叉查找樹只能使用於Integer的存儲,那如果我要存儲String,甚至是自己定義的對象怎麼辦?接下來,我們一起研究一下,如何把它轉變成一個更通用的泛型對象。

把特殊二叉查找樹轉化成一般二叉查找樹

我們先進行上面特殊的二叉查找樹進行分析:
①有一個重要的靜態內部類BinaryNode, 它主要是用於存儲數據,所以在這裏我們就要用泛型來表示數據的類型,也就是說data應該是anytype的。
②二叉查找樹的原則是要根節點比它左節點大,比右節點小。所以每個節點之前必然存在着一個比較的行爲,在上面的例子中Integer可以用>或者< 來比較,那一般對象怎麼辦呢?在以往的博客中有介紹過Comparable接口,這個接口可以實現使用compareto來進行比較。

所以接下來我們對上面的特殊類進行轉化成泛型類:

package com.brickworkers;

/**
 * 
 * @author Brickworker
 * data:2017年4月20日下午2:21:13 
 * 關於類SearchBinaryTree.java的描述:一個二叉查找樹存儲Integer數據
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class SearchBinaryTree <T extends Comparable<? super T>>{//類型 T 必須實現 Comparable 接口,並且這個接口的類型是 T 或 T 的任一父類,這樣聲明後,T 的實例之間,T 的實例和它的父類的實例之間,可以相互比較大小 

    private static class BinaryNode<T>{//用T表示節點的數據存儲類型
        T data;//用T表示數據類型
        BinaryNode<T> left;//用T表示數據類型
        BinaryNode<T> right;//用T表示數據類型

        public BinaryNode(T data) {//用T表示數據類型
            this(data, null, null);
        }

        public BinaryNode(T data, BinaryNode<T> lt, BinaryNode<T> rt) {//用T表示數據類型
            this.data = data;
            left = lt;
            right = rt;
        }
    }


    //一個支持任意類型的根節點
    private BinaryNode<T> root;

    public SearchBinaryTree() {
        root = null;
    }

    public boolean contains(T t){//用T表示數據類型
        return contains(t, root);
    }

    public T findMin(){//直接返回T類型
        if(root == null)
            throw new IllegalArgumentException("該樹是空的");
        else
            return findMin(root).data;
    }

    public T findMax() {//直接返回T類型
        if (root == null)
            throw new IllegalArgumentException("該樹是空的");
        else
            return findMax(root).data;
    }


    public void insert(T t){//用T表示數據類型
        root = insert(t, root);
    }


    //刪除節點
    public void remove(T t){//用T表示數據類型
        root = remove(t, root);
    }




    //按順序打印樹
    public void printTree(){
        if(root == null)
            throw new IllegalArgumentException("該樹是空的");
        else
            printTree(root);
    }


    private boolean contains(T t, BinaryNode<T> node) {// 用T表示數據類型
        if (node == null) {
            return false;
        }

        // 不能再用> < 來對T 類型的數據進行比較了,需要用compareTo方法
        if (t.compareTo(node.data) < 0) {
            return contains(t, node.left);
        } else if (t.compareTo(node.data) > 0) {
            return contains(t, node.right);
        } else {
            return true;
        }

    }

    private BinaryNode<T> findMin(BinaryNode<T> node) {// 用T表示數據類型
        if (node.left == null) {
            return node;
        } else {
            return findMin(node.left);
        }
    }


    private BinaryNode<T> findMax(BinaryNode<T> node) {// 用T表示數據類型
        if (node.right == null) {
            return node;
        } else {
            return findMax(node.right);
        }
    }

    private BinaryNode<T> insert(T t, BinaryNode<T> node){// 用T表示數據類型
        if(node == null){
            return new BinaryNode<T>(t);
        }
        if(t.compareTo(node.data) > 0){
            node.right = insert(t, node.right);
        }
        else if(t.compareTo(node.data) < 0){
            node.left = insert(t, node.left);
        }else{
            throw new IllegalArgumentException("不允許插入已存在的數據,t:" +node.data);
        }
        return node;

    }   

    private BinaryNode<T> remove(T t, BinaryNode<T> node){
        if(t.compareTo(node.data) < 0){
            node.left = remove(t, node.left);
        }
        else if(t.compareTo(node.data) > 0)
            node.right = remove(t, node.right);
        else if(node.left != null && node.right != null){
            node.data = findMin(node.right).data;
            node.right = remove(node.data, node.right);
        }else{
            node = (node.left != null)? node.left: node.right;
        }
        return node; 
    }

    private void printTree(BinaryNode<T> node){
        if(node != null){
            printTree(node.left);
            System.out.print(node.data + "  ");
            printTree(node.right);
        }
    }

}

我們可以用上面的測試類,給二叉查找樹賦予一個類型,進行測試:

package com.brickworkers;
/**
 * 
 * @author Brickworker
 * Date:2017年4月20日下午2:33:19 
 * 關於類TreeTest.java的描述:二叉查找樹測試
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class TreeTest {

    public static void main(String[] args) {
        SearchBinaryTree<Integer> searchBinaryTree = new SearchBinaryTree<Integer>();//用<Integer>來表示數據類型
        //插入0~9的數據
        for (int i = 0; i < 10; i++) {
            searchBinaryTree.insert(i);
        }
        //打印情況
        searchBinaryTree.printTree();
        System.out.println();
        //插入測試
        searchBinaryTree.insert(11);
        searchBinaryTree.insert(12);
        searchBinaryTree.printTree();
        System.out.println();

        //插入10,看是否遵循順序
        searchBinaryTree.insert(10);
        searchBinaryTree.printTree();
        System.out.println();

        //判斷某節點是否存在
        System.out.println(searchBinaryTree.contains(3));
        System.out.println(searchBinaryTree.contains(15));      

        //查找最小
        System.out.println("最小的數據爲:" + searchBinaryTree.findMin());

        //查找最大
        System.out.println("最大的數據爲:" + searchBinaryTree.findMax());

        //刪除某個節點:
        searchBinaryTree.remove(3);
        searchBinaryTree.printTree();
        System.out.println();

        //重複插入節點
        searchBinaryTree.insert(2);

    }
//輸出結果:
//0  1  2  3  4  5  6  7  8  9  
//  0  1  2  3  4  5  6  7  8  9  11  12  
//  0  1  2  3  4  5  6  7  8  9  10  11  12  
//  最小的數據爲:0
//  最大的數據爲:12
//  0  1  2  4  5  6  7  8  9  10  11  12  
//  Exception in thread "main" java.lang.IllegalArgumentException: 不允許插入已存在的數據,t:2
}

發現結果和上面特殊情況是一樣的。這樣就是把特殊轉化成了一般。好了,關於查找二叉樹,就寫到這裏了,希望對大家有所幫助,或者有所感悟也行哈。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章