算法笔记(三)-二叉查找树

1 定义

  • 一棵二叉查找树(BST)是一棵二叉树,其中每个结点都含有一个Comparable的键(以及相关的值)且每个结点的键都大于其左子树中的任意结点的键而小于右子树的任意结点的键。

2 基本实现

  • 一棵二叉查找树代表的是一组键(及其相应的值)的集合,而同一个集合可以用多棵不同的二叉查找树表示。但注意,将一棵二叉查找树的所以键投影到一条直线上,保证一个节点的左子树的键出现在其左边,右子树的键出现在其右边。
    不同二叉查找树表示同一个集合
  • 下面给出二叉查找树的实现代码,接下来将对这些方法进行解释:
package BinaryTree.BinarySearchTree;

public class BTS<Key extends Comparable<Key>, Value> {
    //二叉树根节点
    private Node root;

    private class Node {
        private Key key; //键
        private Value value;//值
        private Node left, right;//左节点和右节点
        private int N;//以该节点为根的子树中的节点总数

        //构造方法
        public Node(Key key, Value value, int N) {
            this.key = key;
            this.value = value;
            this.N = N;
        }

        //以该节点为根的子树中的节点总数
        public int size() {
            return size(root);
        }

        private int size(Node x) {
            if (x == null) {
                return 0;
            } else {
                return x.N;
            }
        }

        //查找某个值
        public Value get(Key key) {
            return get(root, key);
        }

        public Value get(Node x, Key key) {
            //如果树为空,则返回null
            if (x == null) {
                return null;
            }
            int cmp = key.compareTo(x.key);
            //如果相等,则返回该结点,小于就去左子树,大于就去右子树
            if(cmp == 0){
                return x.value;
            }else if(cmp<0){
                return get(x.left, key);
            }else{
                return get(x.right, key);
            }
        }

        //插入某个值
        public void put(Key key, Value value) {
            //查找key,找到更新它的值,找不到则插入
            root = put(root, key, value);
        }

        public Node put(Node x, Key key, Value value) {
            if (x == null) {
                return new Node(key, value, 1);
            }
            int cmp = key.compareTo(x.key);
            if (cmp < 0) {
                x.left = put(x.left, key, value);
            } else if (cmp > 0) {
                x.right = put(x.right, key, value);
            } else {
                x.value = value;
            }
            x.N = size(x.left) + size(x.right) + 1;
            return x;
        }

        public Key min() {
            return min(root).key;
        }

        public Node min(Node x) {
            if (x.left == null) return x;
            return min(x.left);
        }

        public Key floor(Key key) {
            Node x = floor(root,key);
            if(x == null) return null;
            return x.key;
        }

        public Node floor(Node x, Key key) {
            if (x == null) return null;
            int cmp = key.compareTo(x.key);
            if (cmp == 0) return x;
            if (cmp < 0) return floor(x.left, key);
            Node t = floor(x.right, key);
            if (t != null) return t;
            else return x;
        }
    }
}

2.1 查找

  • 查找只会获得两种结果,一是含有该节点,二是不含该节点返回Null.
  • 递归算法:①如果树是空的,则返回null②如果被查找的键和根节点键相同,则查找命中,返回该节点③否则递归地在适当的子树中进行查找,如果被查找的键小于该节点就选择左子树,否则选择右子树。上面代码中get()函数完全按照这种思想进行的
  • 代码运行流程实例如下:


    查找流程

2.2 插入

  • 插入同样会产生两种后果,一是该节点已经存在,则更新该结点的值(Value),二是该节点不存在,则生成一个节点插入。
  • 递归算法:①如果该树是空的,则返回一个含有该键值对的新结点②如果要插入的键已经存在且是当前结点,则修改其value值③如果被插入的键小于根节点,则在左子树中插入该键,否则在右子树中插入。
  • 注意:结点还有一个属性N(代码以该结点为根节点的子树的结点数量),在进行插入后需要修改该值。*将递归调用前的代码想象成沿着树向下走(比较键大小),那递归后调用的代码想象成沿着树向上爬(对于get()方法,这仅代表一系列的返回指令return,但对于put()方法,这意味着重置N值)
  • 上面代码中put()函数完全按照这种思想进行的
  • 运行流程示例如下:


    插入流程

2.3 最大键和最小键

  • 二叉查找树的根节点小于其右子树中的所有结点,大于其左子树中的所有结点。因策,最小键即最左下角的结点,最大键即最右下角的结点。
  • 代码实现如下:
        public Node min(Node x) {
            if (x.left == null) return x;
            return min(x.left);
        }
        public Node max(Node x) {
            if (x.right == null) return x;
            return min(x.right);
        }

2.4 向上取整和向下取整

  • 向下取整:即取得小于等于key的最大键
  • 向上取整:即取得大于等于key的最小键
  • 算法思想:如果给定的键等于该根节点,则返回根节点;如果给定的键小于根节点的键,则小于等于key的最大键floor(key)在其左子树中;如果给定的键大于根节点的键,则仅当其右子树存在小于等于key的键时返回该键,否则就返回当前根节点的键。下面的代码完全按照该思想进行编码
        public Node floor(Node x, Key key) {
            if (x == null) return null;
            int cmp = key.compareTo(x.key);
            if (cmp == 0){
                return x;
            }else if (cmp < 0){
                return floor(x.left, key);
            }else{
                //当找到小于key的键,要判断其右子树是否存在小于等于key的键,若有则返回那个节点,没有则返回当前节点
                Node t = floor(x.right, key);
                if (t != null) return t;
                else return x;
            }
        }
  • 代码运行流程如下:


    floor

2.5 select选择操作

  • 假设我们想找到排名为k的键(树中正好有k个小于它的键)。如果左子树中的结点t大于k,我们就继续在左子树中查找排名为k的键;如果t等于k,我们就返回根节点中的键;如果t小于k,就查找右子树中排名(k-t-1)的键。下面的代码完全按照该思想进行编码
        public Node select(Node x, int k) {
            if (x == null) {
                return null;
            }
            int t = size(x.left);
            if(t>k){
                return select(x.left,k);
            }else if(t<k){
                return select(x.right,k-t-1);
            }else{
                return x;
            }
        }
  • 运行流程如下:


    select

2.6 删除最大键和最小键

  • 删除最小键算法思想:只要不断深入左子树直至遇见一个空连接。
        public Node deleteMin(Node x) {
            if (x.left == null) return x.right;
            x.left = deleteMin(x.left);
            x.N = size(x.left) + size(x.right) + 1;
            return x;
        }

2.7 删除任意键

  • 将即将被删除的结点保存为t
  • 将x 指向它的后继结点min(t.right)
  • 将x的右链接指向deleteMin(t.right)
  • 将x的左连接设为t.left
        public Node delete(Node x, Key key) {
            if(x == null) return null;
            int cmp = key.compareTo(x.key);
            if(cmp<0){
                x.left = delete(x.left,key);
            }else if(cmp>0){
                x.right = delete(x.right,key);
            }else{
                if(x.right == null) return x.left;
                if(x.left == null) return x.right;
                Node t = x;
                x = min(t.right);
                x.right = deleteMin(t.right);
                x.left = t.left;
            }
            x.N = size(x.left) + size(x.right) +1;
            return x;
        }
  • 运行流程如下:


    运行流程

2.8 范围查找

  • 代码如下:
        public void key(Node x, Queue<Key> queue,Key lo,Key hi){
            if(x == null){
                return ;
            }
            int cmplo = lo.compareTo(x.key);
            int cmphi = hi.compareTo(x.key);
            if(cmplo<0){
                key(x.left,queue,lo,hi);
            }
            if(cmplo<=0&&cmphi>=0){
                queue.add(x.key);
            }
            if(cmphi>0){
                key(x.right,queue,lo,hi);
            }
        }

3 练习

3.1 插入练习

  • 题目:将 E A S Y Q U E S T I O N 作为键按顺序插入一棵初始为空的二叉查找树。
    @Test
    public void testPut(){
        BTS<Character,String> bts = new BTS<Character,String>();
        Character ch[] = {'E','A','S','Y','Q','E','S','T','I','O','N'};
        BTS.Node node = bts.new Node();
        for(char c:ch){
            node.put(c,"");
        }
        //先序遍历一下
        node.preOrder(bts.root);
    }
  • 共需要21次比较

3.2 插入顺序导致的最优与最劣二叉查找树结构

  • A X C S E R H 顺序插入将会导致一棵最坏情况的二叉查找树,其最后一个结点两个链接全空,其余结点都含有一个空结点。树变为了链表。
  • H C A E S R X便会产生最优的二叉查找树。


    插入顺序导致的不同

3.3 为二叉查找树添加查询树高度的方法

        public int height() {
            return height(root);
        }
        private int height(Node x) {
            if (x == null) return 0;
            return 1 + Math.max(height(x.left), height(x.right));
        }

3.4 方法测试

package BinaryTree.BinarySearchTree;


import sun.misc.Queue;

public class BTS<Key extends Comparable<Key>, Value> {
    //二叉树根节点
    public Node root;

    public class Node {
        private Key key; //键
        private Value value;//值
        private Node left, right;//左节点和右节点
        private int N;//以该节点为根的子树中的节点总数
        public Node(){

        }
        //构造方法
        public Node(Key key, Value value, int N) {
            this.key = key;
            this.value = value;
            this.N = N;
        }


        public int height() {
            return height(root);
        }
        private int height(Node x) {
            if (x == null) return 0;
            return 1 + Math.max(height(x.left), height(x.right));
        }
        //线序遍历
        public void preOrder(){
            preOrder(root);
        }
        //线序遍历
        private void preOrder(Node x){
            if(x == null) return;
            System.out.print(x.key);
            preOrder(x.left);
            preOrder(x.right);
        }
        //以该节点为根的子树中的节点总数
        public int size() {
            return size(root);
        }

        private int size(Node x) {
            if (x == null) {
                return 0;
            } else {
                return x.N;
            }
        }

        //查找某个值
        public Value get(Key key) {
            return get(root, key);
        }

        private Value get(Node x, Key key) {
            //如果树为空,则返回null
            if (x == null) {
                return null;
            }
            int cmp = key.compareTo(x.key);
            if (cmp == 0) {
                return x.value;
            } else if (cmp < 0) {
                return get(x.left, key);
            } else {
                return get(x.right, key);
            }
        }

        //插入某个值
        public void put(Key key, Value value) {
            //查找key,找到更新它的值,找不到则插入
            root = put(root, key, value);
        }

        private Node put(Node x, Key key, Value value) {
            if (x == null) {
                return new Node(key, value, 1);
            }
            int cmp = key.compareTo(x.key);
            if (cmp == 0) {
                x.value = value;
            } else if (cmp < 0) {
                x.left = put(x.left, key, value);
            } else {
                x.right = put(x.right, key, value);
            }
            x.N = size(x.left) + size(x.right) + 1;
            return x;
        }

        public Key min() {
            return min(root).key;
        }

        public Node min(Node x) {
            if (x.left == null) return x;
            return min(x.left);
        }

        public Key floor(Key key) {
            Node x = floor(root, key);
            if (x == null) return null;
            return x.key;
        }

        private Node floor(Node x, Key key) {
            if (x == null) return null;
            int cmp = key.compareTo(x.key);
            if (cmp == 0) {
                return x;
            } else if (cmp < 0) {
                return floor(x.left, key);
            } else {
                //当找到小于key的键,要判断其右子树是否存在小于等于key的键,若有则返回那个节点,没有则返回当前节点
                Node t = floor(x.right, key);
                if (t != null) return t;
                else return x;
            }
        }
        public Key select(int k){
            return select(root,k).key;
        }
        private Node select(Node x, int k) {
            if (x == null) {
                return null;
            }
            int t = size(x.left);
            if (t > k) {
                return select(x.left, k);
            } else if (t < k) {
                return select(x.right, k - t - 1);
            } else {
                return x;
            }
        }
        public void deleteMin(){
            root = deleteMin(root);
        }
        private Node deleteMin(Node x) {
            if (x.left == null) return x.right;
            x.left = deleteMin(x.left);
            x.N = size(x.left) + size(x.right) + 1;
            return x;
        }

        public void delete(Key key){
            root = delete(root,key);

        }
        private Node delete(Node x, Key key) {
            if(x == null) return null;
            int cmp = key.compareTo(x.key);
            if(cmp<0){
                x.left = delete(x.left,key);
            }else if(cmp>0){
                x.right = delete(x.right,key);
            }else{
                if(x.right == null) return x.left;
                if(x.left == null) return x.right;
                Node t = x;
                x = min(t.right);
                x.right = deleteMin(t.right);
                x.left = t.left;
            }
            x.N = size(x.left) + size(x.right) +1;
            return x;
        }
        public Queue<Key> keys(Key lo,Key hi){
            Queue<Key> queue = new Queue<Key>();
            keys(root,queue,lo,hi);
            return  queue;
        }

        private void keys(Node x, Queue<Key> queue,Key lo,Key hi){
            if(x == null){
                return ;
            }
            int cmplo = lo.compareTo(x.key);
            int cmphi = hi.compareTo(x.key);
            if(cmplo<0){
                keys(x.left,queue,lo,hi);
            }
            if(cmplo<=0&&cmphi>=0){
                queue.enqueue(x.key);
            }
            if(cmphi>0){
                keys(x.right,queue,lo,hi);
            }
        }
    }
}
package BinaryTree.BinarySearchTree;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import sun.misc.Queue;

import static org.junit.Assert.*;

public class BTSTest {
    BTS<Character, Integer> bts = new BTS<Character, Integer>();
    Character ch[] = {'E', 'A', 'S', 'Y', 'Q', 'E', 'S', 'T', 'I', 'O', 'N'};
    BTS.Node node = bts.new Node();

    @Before
    public void testBefore() {
        for (int i = 1; i <= ch.length; i++) {
            node.put(ch[i - 1], i);
        }
    }

    @Test
    public void testPut() {
        //先序遍历一下
        node.preOrder();
        System.out.println();
        //求树高度
        System.out.print(node.height());
    }

    @Test
    public void testGet() {
        //asn = 7
        System.out.println(node.get('S'));
    }
    @Test
    public void testFloor(){
        //找小于F的最大键,ans = E
        System.out.println(node.floor('F'));
    }
    @Test
    public void testSelect(){
        //选出比5个键大的那个键asn = Q
        System.out.println(node.select(5));
    }
    @Test
    public void testDeleteMin(){
        node.deleteMin();
        node.preOrder();
    }
    @Test
    public void testDelete(){
        node.delete('E');
        node.preOrder();
    }
    @Test
    public void testKeys() throws InterruptedException {
        Queue<Character> queue = node.keys('I','T');
        while (!queue.isEmpty()){
            System.out.print(queue.dequeue());
        }
    }
}
代码中生成的树

3.5 完美平衡

  • 实现一个和二分查找等价的二叉查找树。也就是说在这棵树中查找任意键所产生的比较序列和二分查找所产生的完全相同。
    @Test
    public void testPrefect(){
        Arrays.sort(ch);
        prefect(ch,0,ch.length-1);
        bts.preOrder();
    }
    private void prefect(Character []a,int lo, int hi){
        if(lo > hi) return ;
        int mid = lo + ((hi - lo)>>1);
        bts.put(a[mid],mid);
        prefect(a,lo,mid-1);
        prefect(a,mid +1 ,hi);
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容