實現二叉搜尋樹的新增,查詢和刪除(JAVA)

南菇嶺發表於2020-11-23

1.二叉搜尋樹

1.1 概念

二叉搜尋樹又稱為二叉排序樹,它是一顆空樹或者具有以下性質的樹:

  1. 若它的左子樹不為空,則左子樹上所有節點的值都小於根節點的值
  2. 若它的右子樹不為空,則右子樹上所有節點的值都大於根節點的值
  3. 它的左右子樹也分別為二叉搜尋樹
    在這裡插入圖片描述

例如對上圖二叉樹採用中序遍歷,得到的結果為:{0,1,2,3,4,5,6,7,8,9}

即一顆二叉搜尋樹的中序遍歷一定是有序的,且是從小到大進行排列

2.二叉搜尋樹的操作

建立節點,這裡使用靜態內部類來建立

程式碼示例:

calss BinarySearchTree {
	static class Node {
		public int val;
		public Node left;
		public Node right;
		public Node(int val) {
			this.val = val;
		}
	}
	//定義根節點為外部類的成員變數
	Node root = null;
}

2.1 插入

定義插入的值為val,則插入節點要考慮兩種情況:

  1. 樹為空樹,即root == null,直接插入即可
    在這裡插入圖片描述

  2. 樹不為空樹,則先比較root的值和val大小關係,如果小於插入值,則說明去root的右邊尋找;如果大於val,則說明去root的左邊尋找;等於說明找到了,拼接即可

程式碼示例:

//插入節點
public void put(int val) {
	Node node = new Node(val);
	//第一次插入,root == null
	if(root == null) {
		root = node;
		return;
	}
	//不是第一次插入 root != null
	Node pre = null;//用來指向cur的父親節點
	Node cur = root;
	while (cur != null) {
		//去右邊找
		if (cur.val < val) {
			pre = cur;//記錄下此時cur
			cur = pre.right;//cur向右節點移動
		}else if (cur.val > val) {//該去左邊找
			pre = cur;
			cur = pre.left;
		}else {//已經有相等的值,則不做任何處理,直接退出
			return;
		}
	}
	//此時cur = null,pre = cur的根節點
	//判斷當前val和pre的val大小,決定插入左邊還是右邊
	if (pre.val < val) {//插入到右節點
		pre.right = node;
	}else {//插入到左節點
		pre.left = node;
	}
}

2.2 查詢

在這裡插入圖片描述
查詢的大體思路跟插入差不多,也是從根節點的值開始比較,大的就往右邊找,小的往左邊找,直到找到或者找不到,也就是說迴圈退出會有兩種結果,一種是找到節點並返回,一種是沒找到返回null

程式碼示例:

public Node get(int val) {
	Node cur = root;
	while (cur != null) {
		//找到該節點
		if (cur.val == val) {
			return cur;
		}else if (cur.val < val) {//去右邊找
			cur = cur.right;
		}else {//去左邊找
			cur = cur.left;
		}
	}
	//說明沒找到
	return null;
}

2.3 刪除(難點)

對於二叉搜尋樹來說,插入和查詢都很方便,因為這兩個操作只有插入會用到父親節點,而父親節點自上而下遍歷很容易尋找;但是對於刪除操作來說,我們要找到當前需要刪除節點的下一個節點,並且讓這個節點覆蓋掉需要刪除的節點,這樣才能完成刪除操作,當然這是大體上的思路

設當前節點為cur,當前節點的父親節點為pre,我們需要考慮一下3種情況

  1. cur.left == null
    ① cur == root,則 root = cur.right
    ② cur == pre.left,則 pre.left = cur.right
    ③ cur == pre.right,則 pre.right = cur.right
  2. cur.right == null
    ① cur == root,則 root = cur.left
    ② cur == pre.left,則 pre.left = cur.left
    ③ cur == pre.right,則 pre.right = cur.left
  3. cur.left != null && cur.right == null
    ① 找到cur的下一個節點 minCur
    ② cur = minCur 覆蓋掉cur
    ③ 再覆蓋掉之前的minCur,因此還需要定義一個minCur的根節點minPre

程式碼示例:

public boolean remove(int val) {
        Node cur =  root;//當前結點
        Node pre = null;//當前節點的根節點
        //先找到要刪除的節點
        while (cur != null) {
            if (cur.val == val) {
                break;
            }else if (cur.val < val) {
                pre = cur;
                cur = pre.right;
            }else {
                pre = cur;
                cur = pre.left;
            }
        }
        //判斷是否找到
        //cur為null,說明沒有找到
        if (cur == null) return false;
        //說明找到了cur,開始判斷cur的位置
        if (cur.left == null) {
            if (cur == root) {//是根節點
                root = cur.right;//替換
            }else {//不是根節點
                if (cur == pre.left) {
                    pre.left = cur.right;
                }else {
                    pre.right = cur.right;
                }
            }
        }else if (cur.right == null) {
            if (cur == root) {
                root = cur.left;
            }else {
                if (cur == pre.left) {
                    pre.left = cur.left;
                }else {
                    pre.right = cur.left;
                }
            }
        }else {//cur的左右節點都不為null
           	Node minPre = cur;//指向cur下一節點的根節點
            Node minCur = cur.right;//cur的下一個節點
            while (minCur.left != null) {
                minPre = minCur;
                minCur = minPre.left;
            }
            cur.val = minCur.val;//先覆蓋掉原來節點
            //判斷下一節點在根節點的哪一邊
            if (minCur == minPre.left) {
                minPre.left = minCur.right;//再覆蓋掉原來的下一節點
            }else {
                minPre.right = minCur.right;
            }
        }
        return true;
    }

2.4 演示

將剛剛的程式碼加上中序遍歷整合到一起,然後給定一組測試資料來測試程式碼

程式碼示例:

public class HomeWork {
    public static void main(String[] args) {
        int[] arr = {7,5,45,9,26,41,2,6};
        BinarySearchTree binarySearchTree = new BinarySearchTree();
        for (int i = 0; i < arr.length; i++) {
            binarySearchTree.put(arr[i]);
        }
        binarySearchTree.inOrder(binarySearchTree.root);
        System.out.println();
        binarySearchTree.preOrder(binarySearchTree.root);
        System.out.println();
        try {
            System.out.println(binarySearchTree.get(45).val);
        }catch (NullPointerException e) {
            e.printStackTrace();
            System.out.println("沒有該節點");
        }
        System.out.println(binarySearchTree.remove(45));
        binarySearchTree.inOrder(binarySearchTree.root);
    }
}
class BinarySearchTree {
    static class Node {
        public int val;
        public Node left;
        public Node right;

        public Node(int val) {
            this.val = val;
        }
    }
    Node root = null;
    public void put(int val) {
        Node node = new Node(val);
        //第一次插入,root == null
        if(root == null) {
            root = node;
            return;
        }
        //不是第一次插入 root != null
        Node pre = null;//用來指向cur的父親節點
        Node cur = root;
        while (cur != null) {
            //去右邊找
            if (cur.val < val) {
                pre = cur;//記錄下此時cur
                cur = pre.right;//cur向右節點移動
            }else if (cur.val > val) {//該去左邊找
                pre = cur;
                cur = pre.left;
            }else {//已經有相等的值,則不做任何處理,直接退出
                return;
            }
        }
        //此時cur = null,pre = cur的根節點
        //判斷當前val和pre的val大小,決定插入左邊還是右邊
        if (pre.val < val) {//插入到右節點
            pre.right = node;
        }else {//插入到左節點
            pre.left = node;
        }
    }
    public Node get(int val) {
        Node cur = root;
        while (cur != null) {
            //找到該節點
            if (cur.val == val) {
                return cur;
            }else if (cur.val < val) {//去右邊找
                cur = cur.right;
            }else {//去左邊找
                cur = cur.left;
            }
        }
        //說明沒找到
        return null;
    }
    //刪除
    public boolean remove(int val) {
        Node cur =  root;//當前結點
        Node pre = null;//當前節點的根節點
        //先找到要刪除的節點
        while (cur != null) {
            if (cur.val == val) {
                break;
            }else if (cur.val < val) {
                pre = cur;
                cur = pre.right;
            }else {
                pre = cur;
                cur = pre.left;
            }
        }
        //判斷是否找到
        //cur為null,說明沒有找到
        if (cur == null) return false;
        //說明找到了cur,開始判斷cur的位置
        if (cur.left == null) {
            if (cur == root) {//是根節點
                root = cur.right;//替換
            }else {//不是根節點
                if (cur == pre.left) {
                    pre.left = cur.right;
                }else {
                    pre.right = cur.right;
                }
            }
        }else if (cur.right == null) {
            if (cur == root) {
                root = cur.left;
            }else {
                if (cur == pre.left) {
                    pre.left = cur.left;
                }else {
                    pre.right = cur.left;
                }
            }
        }else {//cur的左右節點都不為null
            Node minPre = cur;//指向cur下一節點的根節點
            Node minCur = cur.right;//cur的下一個節點
            while (minCur.left != null) {
                minPre = minCur;
                minCur = minPre.left;
            }
            cur.val = minCur.val;//先覆蓋掉原來節點
            //判斷下一節點在根節點的哪一邊
            if (minCur == minPre.left) {
                minPre.left = minCur.right;//再覆蓋掉原來的下一節點
            }else {
                minPre.right = minCur.right;
            }
        }
        return true;
    }
    //中序遍歷
    public void inOrder(Node root) {
        if (root == null) return;
        inOrder(root.left);
        System.out.print(root.val + " ");
        inOrder(root.right);
    }
    //前序遍歷
    public void preOrder(Node root) {
        if (root == null) return;
        System.out.print(root.val + " ");
        preOrder(root.left);
        preOrder(root.right);
    }
}

結果演示

2 5 6 7 9 26 41 45 
7 5 2 6 45 9 26 41 
45
true
2 5 6 7 9 26 41 

以上就是二叉搜尋樹的插入,查詢和刪除,此部落格最想讓大家掌握的是二叉搜尋樹的刪除操作,希望各位小夥伴都能多多練習!

2.5 效能分析

插入和刪除操作都必須先查詢,查詢效率代表了二叉搜尋樹中各個操作的效能

對有n個結點的二叉搜尋樹,若每個元素查詢的概率相等,則二叉搜尋樹平均查詢長度是結點在二叉搜尋樹的深度的函式,即結點越深,則比較次數越多

但對於同一個關鍵碼集合,如果各關鍵碼插入的次序不同,可能得到不同結構的二叉搜尋樹:

在這裡插入圖片描述
最優情況下,二叉搜尋樹為完全二叉樹,其平均比較次數為:logN

最差情況下,二叉搜尋樹退化為單支樹,其平均比較次數為:2/N

問題:如果退化成單支樹,二叉搜尋樹的效能就失去了。那能否進行改進,不論按照什麼次序插入關鍵碼,都可以使二叉搜尋樹的效能最佳?關於這個改進我將再寫一篇部落格來講解

相關文章