符号表实现算法
文章目录
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.00LgN−0.5)
-
一棵大小为N的红黑树中,根节点到任意结点的平均路径长度为 ( 1.00 L g N ) (1.00LgN) (1.00LgN),比初等二叉查找树降低40%左右
各种符号表实现的性能总结:
4,散列表
散列的查找算法分为两步:
- 首先用散列函数将被查找的键转化为数组的一个索引
- 处理碰撞冲突的过程,拉链法和线性探测法
一个数据类型实现一个优秀的散列方法需要满足三个条件:
- 一致性
- 高效性
- 均匀性,均匀地散列所有的键
将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,各种符号表实现的性能总结:
一般而言使用散列表,代码简单,常数级别的查找
二叉查找树的优点在于抽象结构简单,不需要实现散列函数
红黑树可以保证最坏情况下的性能且能够支持的操作更多,如排名,选择,排序和范围查找