符号表实现算法

符号表实现算法

文章目录

《符号表实现算法》

1,二叉查找树

二叉查找树(BST)是一棵二叉树,其中每个结点都含有一个Comparable的键(以及关联的值),且每个结点的键都大于其左子树中的任意结点的键,而小于右子树的任意结点的键。
《符号表实现算法》

数据表示:

    private class Node {
        //键
        private Key key;

        //值
        private Value value;
	 
        //左右结点
        private Node left;
        private Node right;

        //以该结点为根节点的子树中的结点总数
        private int N;

        public Node(Key key, Value value , int N) {
            this.key = key;
            this.value = value;
            this.N = N;
        }
    }

查找实现:

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

    private Value get(Node node, Key key) {
        if (node == null) {
            return null;
        }

        int compare = key.compareTo(node.key);

        if (compare < 0) {
            get(node.left, key);
        } else if (compare > 0) {
            get(node.right, key);
        } else {
            return node.value;
        }
        return null;
    }

在二叉树中查找元素27:
《符号表实现算法》

插入实现:

    /** * 插入 */
    public Node put(Key key,Value value) {
        return put(root, key,value);
    }

    private Node put(Node node, Key key,Value value) {
        if(node==null){
            return new Node(key,value,1);
        }
        int compare = key.compareTo(node.key);

        if(compare<0){
            put(node.left,key,value);
        }else if(compare>0){
            put(node.right,key,value);
        }else {
            node.value=value;
        }
        node.N=size(node.left)+size(node.right)+1;

        return node;
    }

《符号表实现算法》

性能分析:

二叉查找树的算法的运行时间取决于树的形状,而树的形状又取决于键被插入的先后顺序。

《符号表实现算法》

假设键的分布是均匀随机的,或者说它们的插入顺序是随机的。对这个模型的分析而言,二叉查找树和快速排序是类似的。树的根节点就是快速排序中的第一个切分元素(左侧的键都比它小,右侧的键都比它大),而这对于所有的子树同样适用,这和快速排序中对子数组的递归排序完全对应。

结论:
在由N个随机键构造的二叉查找树中,查找命中、未命中、插入操作平均所需的比较次数为 2 l n N 2lnN 2lnN 约等于 1.39 L g n 1.39Lgn 1.39Lgn

说明二叉查找树中查找随机键的成本比二分查找高约39%,同时插入一个新键的成本却是对数级别的——这是二分查找的有序数组所不具备的。

在一棵二叉查找树中,所有操作在最坏情况下所需要的时间都和树的高度成正比。

《符号表实现算法》

2,平衡二叉树

《符号表实现算法》

完美平衡性:
一棵完美平衡的2-3查找树中的所有空链接到根节点的距离都是相同的。

使用2-3树的主要原因就在于它能够在插入后继续保持平衡性。

如下的局部变换不会影响树的全局有序性和平衡性,任意空链接到根节点的路径长度都是相等的。

插入操作:
《符号表实现算法》

《符号表实现算法》

《符号表实现算法》

3,红黑二叉树

定义:

红黑二叉树背后的基本思想:用标准的二叉查找树(完全由2-结点构成)和一些额外的信息(替换3-结点)来表示2-3树。
红链接将两个2-结点连接起来构成一个3-结点,黑链接则是2-3树种的普通链接。确切的说,我们将3-结点表示为由一条左斜的红色链接相连的两个2-结点。
这种表示法的优点是,我们无需修改就可以直接使用标准二叉查找树的get()方法。

另一种等价定义:

  • 红链接均为左链接
  • 没有任何一点结点同时和两条红链接相连
  • 该树是完美黑色平衡的,即任意空链接到根节点的路径上的黑链接数量相同

《符号表实现算法》

数据表示:

    private class Node {

        Key key;
        Value value;
        Node left;
        Node right;
        //这颗子树中的结点总数
        int N;
        //其父节点指向它的链接的颜色
        boolean color;

        public Node(Key key, Value value, int n, boolean color) {
            this.key = key;
            this.value = value;
            this.N = n;
            this.color = color;
        }
    }

旋转操作:

实现某些操作过程中可能出现红色右链接或者两条连续的红链接,在操作完成前这些情况需要通过旋转修复:将两个键中的较小者作为根节点变换为将较大者作为根节点。

旋转操作可以保证红黑树的两个重要性质:有序性和完美平衡性。

右旋:
《符号表实现算法》

左旋:
《符号表实现算法》

插入操作:

向单个2-结点插入新键:
《符号表实现算法》

向树底部的2-结点插入新键:
《符号表实现算法》

向一棵双键树(既一个3-结点)中插入新键:

分三种情况:

  • 新键大于原树中的两个键
  • 新键小于原树中的两个键
  • 新建介于原树中的两个键之间

《符号表实现算法》

颜色变化(Flipping colors):
除了将子结点的颜色由红变黑之外,同时需要将父结点的颜色由黑变红,这样就可以保证这项操作是局部变换,不会影响整棵树的黑色平衡性。

《符号表实现算法》

向树底部的3-结点插入新键:

《符号表实现算法》

总结:

  • 右结点是红色,左结点是黑色,左旋转
  • 左结点是红色,左左结点噎死红色,右旋转
  • 左右字结点均为红色,颜色变换

《符号表实现算法》

红黑树的插入算法实现:

    public void put(Key key, Value value) {
        root = put(root, key, value);
        root.color = BLACK;
    }

    /** * 插入 * 除了递归之后的三条判断颜色的if语句,插入操作的实现和二叉查找树中的插入实现完全相同 */
    public Node put(Node node, Key key, Value value) {
        if (node == null) {
            return new Node(key, value, 1, RED);
        }
        int compare = key.compareTo(node.key);

        if (compare < 0) {
            put(node.left, key, value);
        } else if (compare > 0) {
            put(node.right, key, value);
        } else {
            node.value = value;
        }

        //右红左黑 左旋
        if (isRed(node.right) && isBlack(node.left)) {
            node = rotateLeft(node);
        }

        //左红左左红
        if (isRed(node.left) && isRed(node.left.left)) {
            node = rotateRight(node);
        }

        //左红右红
        if (isRed(node.left) && isRed(node.right)) {
            flipColors(node);
        }

        node.N = size(node.left) + size(node.right) + 1;

        return node;
    }

红黑树的性质:

  • 所有基于红黑树的符号表实现都能保证操作的运行时间是对数级别(范围查找除外)。

  • 一棵大小为N的红黑树的高度不会超过 2 L g N 2LgN 2LgN,查找所需要的比较次数约为 ( 1.00 L g N − 0.5 ) (1.00LgN-0.5) (1.00LgN0.5)

  • 一棵大小为N的红黑树中,根节点到任意结点的平均路径长度为 ( 1.00 L g N ) (1.00LgN) (1.00LgN),比初等二叉查找树降低40%左右

各种符号表实现的性能总结:
《符号表实现算法》

4,散列表

散列的查找算法分为两步:

  1. 首先用散列函数将被查找的键转化为数组的一个索引
  2. 处理碰撞冲突的过程,拉链法和线性探测法

一个数据类型实现一个优秀的散列方法需要满足三个条件:

  • 一致性
  • 高效性
  • 均匀性,均匀地散列所有的键

将hashcode()的返回值转化为一个数组索引:
将符号位屏蔽,然后用除留余数法计算它除以M的余数,M取为素数

private int hash(Key x) {
	 return (x.hashCode() & 0x7fffffff) % M;
}

自定义的hashCode()方法:
《符号表实现算法》

基于拉链法的散列表:

将大小为M的数组中的每个元素指向一条链表,链表中的每个结点都存储了散列值为该元素的索引的键值对。

查找分两步:首先根据散列值找到对应的链表,然后沿着链表顺序查找相应的键。

《符号表实现算法》

《符号表实现算法》

HashMap的工作原理
HashMap基于hashing原理,当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。
当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。
HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。
HashMap在每个链表节点中储存键值对对象。
当两个不同的键对象的hashcode相同时会发生什么? 它们会储存在同一个bucket位置的链表中。键对象的equals()方法用来找到键值对。

基于线性探测法的散列表:

开放地址散列表:
用2个大小为M的数组保存N个键值对,其中M>N,需要依靠数组中的空位解决碰撞冲突

开发地址散列表中最简单的方法是线性探测法:
当碰撞发生时,我们直接检查数组中的下一个位置(索引+1),如果该位置为空,放置元素;如果不为空,索引继续+1;直到找到空位;如果到达数组末尾,折回数组开头。

基于线性探测的符号表的元素插入轨迹:
《符号表实现算法》

《符号表实现算法》

性能:当散列表快满的时候查找所需的探测次数是巨大的,但当使用率<1/2时探测的预计次数只在1.5到2.5之间

5,各种符号表实现的性能总结:

《符号表实现算法》

一般而言使用散列表,代码简单,常数级别的查找
二叉查找树的优点在于抽象结构简单,不需要实现散列函数
红黑树可以保证最坏情况下的性能且能够支持的操作更多,如排名,选择,排序和范围查找

点赞