第三章 查找
符号表
符号表最主要的目的就是将一个键和一个值联系起来。
简单的API
public class ST<Key,Value>
ST()
void put(Key key,Value value)
Value get(Key key)
void delete(Key key) put(key,null)//默认实现
boolean contains(Key key) return get(key)!=null;//默认实现
boolean isEmpty() return size()!=0;//默认实现
int size()
Iterable<Key> keys()
每个键对应着一个键,当向表中存入的键值对和表中已有的键冲突时,新的值会替代旧的值;键不能为空;删除操作有两种:延时删除,将键对应的值置为空,然后在某个时候删去所有值为空的键,或是即时删除。
有序符号表
就是要求键实现了Comparable接口的符号表
public class ST<Key extends Comparable<key>,Value>
ST()
void put(Key key,Value val)
Value get(Key key)
void delete(Key key)
boolean contains(Key key)
boolean isEmpty()
int size()
Key min()
Key max()
Key floor(Key key) //找到小于等于键的最大键
Key ceiling(Key key) //找到大于等于键的最小键
int rank(Key key)
Key select(int k)
void deleteMin()
void deleteMax()
int size(Key lo,Key hi)
Iterable<Key> keys(Key lo,Key hi)
Iterable<Key> keys()
//便捷方法
void deleteMin() delete(min())
void deleteMax() delete(max())
int size(Key lo,Key hi) if(hi.compareTo(lo)<0) return 0;
else if(contains(hi)) return rank(hi)-rank(lo)+1;
else return rank(hi)-rank(lo);
Iterable<key> keys() return keys(min(),max());
检查一个新的键是否插入合适位置的基本操作是排名和选择,就是在有序符号表中要保证i==rank(select(i))
和key==select(rank(key))
。
无序链表中的顺序查找
get方法是遍历链表,查找对应的键。put方法也是遍历链表,用equals方法比较需被查找的方法。
//顺序查找(基于无序链表)
public class SequeSearST<Key,Value>{
private Node first;
private class Node{
Key key;
Value val;
Node next;
public Node(Key key,Value val,Node next){
this.key = key; this.val = val; this.next = next;
}
}
public Value get(Key key){
for(Node x=first;x!=null;x=x.next){
if(key.equals(x.key)) return val;
}
return null;
}
public void put(Key key,Value val){
for(Node x = first;x!=null;x=x.next){
if(key.equals(x.key)){x.val = val;return;
}
first = new Node(key,val,first);
}
}
有序数组中的二分查找
使用到一对的平行数组,一个存储键一个存值。核心是rank方法,只要给定的键存在于表中,就可以找到对应的位置,以及键不存在于表中是将键存储到表的何处。
public class BinSearST<Key extends Comparable<key>,Value>{
private Key[] keys;
private Value[] vals;
private int N;
public BinSearST(int cap){
keys = (Key[]) new Comparable[cap];
vals= (Value[]) new Object[cap];
}
public int size(){
return N;
}
public Value get(Key key){
if(isEmpty()) return null;
int i = rank(key);
if(i<N&&keys[i].compareTo(key)==0) return vals[i];
else return null;
}
public void put(Key key,Value val){
int i = rank(key);
if(i<N&&keys[i].compareTo(key)==0){
vals[i]=val;return;
}
for(int j=N;j>i;j--){
keys[j]=keys[j-1];vals[j]=vals[j-1];
}
keys[i]=key;vals[i]=val;
}
public int rank(Key key){
int lo=0,hi=N-1;
while(lo<=hi){
int mid = (hi+lo)/2;
int cmp = key.compareTo(keys[mid]);
if(cmp<0) hi=mid-1;
else if(cmp>0) lo=mid+1;
else return mid;
}
return lo;
}
public Key min(){
return keys[0];
}
public Key max(){
return keys[N-1];
}
public Key select(int k){
return keys[k];
}
public Key ceiling(Key key){
int i=rank(key);
return keys[i];
}
public Iterable<Key> keys(Key lo, Key hi){
Queue<Key> q = new Queue<Key>();
for(int i = rank(lo);i<rank(hi);i++){
q.enqueue(keys[i]);
}
if(contains(hi)) q.enqueue(keys[rank(hi)]);
return q;
}
}
rank方法的思路是基于二分查找法,如果表中存在该键,rank返回该键的位置,如果不存在,返回表中小于它的键数量。
二叉查找树
使用的数据结构由结点组成。每个结点只能有一个父节点,而且每个结点只有左右两个结点。
基于二叉查找树的符号表
public class BST<Key extends Comparable<Key>,Value>{
private Node root;
private class Node{
private Key key;
private Value val;
private Node left,right;
private int N; //以该结点为根的子树中的结点总数
public Node(Key key,Value val, int N){
this.key = key;this.val=val;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);
}
private Value get(Node x,Key key){
if(x==null) return null;
int cmp=key.compareTo(x.key);
if(cmp<0) return get(x.left,key);//左边递归
else if(cmp>0) return get(x.right,key);//右边递归
else return x.val;//找到了
}
public void put(Key key,Value val){
root=put(root,key,val);
}
private Node put(Node x,Key key,Value val){
if(x==null) return new Node(key,val,1);
int cmp = key.compareTo(x.key);
if(cmp<0) x.left = put(x.left,key,val);
else if(cmp>0) x.right=put(x.right,key,val);
else x.val = val;
x.N=size(x.left)+size(x.right)+1;
return x;
}
}
最大键和最小键
最小键是查找左子树,最大键是存在右子树。
向下取整和向上取整
如果给定键小于二叉查找树的根节点的键,那floor(小于等于key的最大键)一定在左子树;如果大于key,那么只有当根节点的右子树存在floor时,根节点就不是floor。同理可得ceiling(大于等于key的最小键)。
max,min,floor,ceiling的实现
public Key min(){
return min(root).key;
}
private 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;
if(cmp<0) return floor(x.left,key);
Node t = floor(x.right,key);
if(t!=null) return t;
else return x;
}
ceiling方法和max方法与上面的实现相同,只是将right和left相互调换。
排名
rank方法中,如果给定的键和根节点相同,返回左子树的结点总数t,如果给定键小于根节点,返回该键在左子树的排名,如果给定的键大于根节点,返回t+1加上其在右子树上的排名
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 int rank(Key key){
return rank(key,root);
}
private int rank(Key key){
if(x==null) return 0;
int cmp = key.compareTo(x.key);
if(cmp<0) return rank(key,x.left);
else if(cmp>0) return 1+size(x.left)+rank(x.right);
else return size(x.left);
}
删除操作
从删除最小键开始,要不断深入树的左子树,直到找到左子树为空的结点,然后将这个结点的父节点的left置为这个结点的right。此时已没有任何连接指向该节点,它会被垃圾回收器回收。删除最大键同理。
删除一个任意结点,四个步骤:首先,查找对应结点,并保存为t
;接着x
指向以t
为根节点的min(t.right)
;再是将x的右链接指向deleteMin(t.right)
;将x的左连接设为t的左链接。
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(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;
}
范围查找
需要一个遍历二叉查找树的基本方法,叫做中序遍历。在给定范围下,将所有在范围内的键加入一个队列Queue并跳过那些不可能还有所查找键的子树。
public Iterable<Key> keys(){
return keys(min(),max());
}
public Iterable<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);
}
平衡查找树
标准的二叉树中的结点称为2-结点(含有一个键和两条链接),3-结点含有两个键和三条链接。一颗完美平衡2-3查找树所有空链接到根节点的距离应该都是相同的。
查找
将给定键进行查找,如果指向的是空链接,查找未命中。
向2-结点插入新键
先进行插入位置确定,如果未命中结束与2-结点,只需要将这个2-结点转换为3-结点。
向只有一个3-结点的树中插入新键
先从最简单的情况开始,向只有一个3-结点的插入新键。先临时将新键存入3-结点,创建了4-结点(3个键,4条链接),4-结点就可以转换为一颗由3个2-结点组成的二叉树,其中根节点为中键,小键在中键的左边,大键在右边。
向一个父结点为2-结点的3-结点插入新键
先找到插入位置,然后构建4-结点,接下来是分解转为3个2-结点组成的树,将根节点插入2-结点,转为3-结点。
向一个父结点为3-结点的3-结点插入新键
找到位置,然后构建4-结点,接下来是拆分为二叉树,插入中键到父结点,由于父结点也为3-结点,同理继续,直到父结点为2-结点。
全局性质
局部变化不会影响树的全局性和平衡性:任意空连接到根节点的路径长度都是相等。和标准的二叉树不同的,2-3树是由下向上的。
红黑二叉查找树
替换3-结点
红黑二叉查找树的基本思想是用标准的二叉查找树和一些额外的信息(替换3-结点)来表示2-3树。红链接将两个2-结点链接起来构成一个3-结点,黑链接则是2-3树中的普通链接。
需要满足的条件为:红链接为左连接;没有任何一个结点同时和两条红链接相连;是完美黑色平衡的,即任意空连接到根结点的黑链接数量相同。
颜色表示
指向它的链接是红色的,color变量为true,黑链接的话,为false。
private static final boolean RED =true;
private static final boolean BLACK = false;
private class Node {
Key key;
Value val;
Node left,right;
int N;
boolean color;
Node(Key key,Value val,int N,boolean color){
this.key = key;this.val = val;this.N=N;this
}
}
旋转
左旋转是指一条右红链接转换为左红链接,旋转操作会返回一条链接,右旋转同理。旋转操作可以保持红黑树的两个重要性质:有序性和完美平衡性。
插入
向单个2-结点插入新键
如果新键小于老键,只需添加一个红色结点即可;如果大于,新增红色结点产生右红色链接,然后就左旋转。
向一个3-结点插入新键
当新键大于原树中的两个键,被连接到3-结点的右链接。此时树为平衡,是由三个2-结点组成的树。
当新键处于中间值时,会链接到小键的右链接,此时需要对小键进行左旋转。然后对新键进行右旋转。
当新键小于两个键时,直接对中间键使用右旋转。
上述操作都能得到一个二叉树。
根结点总是黑色的。
将红链接向上传递
2-3树中的插入算法需要分解3-结点,将中间键插入父结点,直到遇到2-结点,或者根节点。
在沿着插入点到移动的终点,顺序完成:如果右子节点为红而左为黑,左旋转;如果左节点为红且其的左子结点也为红色,进行右旋转;如果左右都为红色,进行颜色变化
插入代码
public class RedBlackBStT<Key extends Comparable<Key>,Value>{
private boolean isRed(Node x){
if(x==null) return false;
return x.color==RED;
}
private Node rotateLeft(Node h){
Node x = h.right;
h.right=x.left;
x.left=h;
x.color=h.color;
h.color=RED;
x.N=h.N;
h.N=1+size(h.left)+size(h.right);
return x;
}
private Node rotateRight(Node h){
Node x = h.left;
h.left = x.right;
x.right = h;
x.color = h.color;
h.color = RED;
x.N=h.N;
h.N=1+size(h.left)+size(h.right);
return x;
}
private void flipColors(Node h){
h.color = RED;
h.left.color=BLACK;
h.right.color=BLACK;
}
public void put(Key key,Value val){
root=put(root,key,val);
root.color=BLACK;
}
private Node put(Node h,Key key,Value val){
if(h==null) return new Node(key,val,1,RED);
int cmp = key.comparableTO(h.key);
if(cmp<0) h.left=put(h.left,key,val);
else if(cmp>0) h.right=put(h.right,key,val);
else h.val=val;
if(isRed(h.right)&&!isRed(h.left)) h=rotateLeft(h);
if(isRed(h.left)&&isRed(h.left.left)) h=rotateRight(h);
if(isRed(h.left)&&isRed(h.right)) flipColors(h);
h.N=1+size(h.right)+size(h.left);
return h;
}
}
删除操作
删除最小键
从树底的3-结点删除键是简单的,但删除2-结点会留下一个空结点,会破坏树的完美平衡性。为了保证不会删除一个2-结点,确保当前结点不是2-结点。在沿着左连接向下的过程中,保证以下情况:如果当前节点不是2-结点,完成;如果当前结点的左子节点是2-结点而它的兄弟结点不是2-结点,将兄弟结点的一个键移到左子节点;如果当前的左子结点和它的兄弟结点都是2-结点,将左子结点,父结点中的最小键和左子节点最近的兄弟节点合并为一个4-结点。
散列表
使用散列查找算法分为两步:第一步是用散列函数将被查找的键转化为数组的一个索引。第二步是处理散列碰撞问题,有两种方法:拉链法和线性探测法。
基于拉链法的散列表
直接将大小为M的数组指向一条链表,链表每个结点存储了散列值为该元素的索引的键值对。由于要用M条链表保持N个键,无论键在各个链表分布如何,链表的平均长度是N/M。
public class SepChainHashST<Key,Value>{
private int N;
private int M;
private SeqSearST<Key,Value>[] st;
public SepChainHashST(){
this(997);
}
public SeqChainHasST(){
this.M=M;
st=(SeqSearST<Key,Value>[]) new SeqSearST[M];
for(int i=0;i<M;i++)
st[i] = new SeqSearST();
}
private int hash(Key key){
return (key.hashCode()&0x7fffffff)%M;
}
public Value get(Key key){
return (Value) st[hash(key)].get(key);
}
public void put(Key key,Value val){
st[hash(key)].put(key,val);
}
}
基于线性探测法的散列表
线性探测法思想是:当碰撞发生时,直接检查散列表中的下一个位置,有三种结果:命中,该位置的键和被查找的键相同;为命中,键为空;继续查找,该位置的键和被查找的键。
在代码中,使用并行数组,一条保持键,一条保持值。
public class LinearProHashST<Key,Value>{
private int N;
private int M=16;
private Key[] keys;
private Value[] vals;
public LinearProHashST(){
keys = (Key[]) new Object[M];
vals=(Value[]) new Object[M];
}
private int hash(Key key){
return (key.hashCode()&0x7fffffff)%M;
}
public void put(Key key,Value val){
if(N>=M/2) resize(2*M);
int i;
for(i=hash(key);key[i]!=null;i=(i+1)%M){
if(keys[i].equals(key)) {vals[i]=val;return;}
}
keys[i]=key;
vals[i]=val;
N++;
}
public Value get(Key key){
for(int i = hash(key);keys[i]!=null;i=(i+1)%M){
if(keys[i].equals(key)) return vals[i];
}
return null;
}
public void delete(Key key){
if(!contains(key)) return;
int i= hash(key);
while(!key.equals(keys[i])) i=(i+1)%M;
keys[i] =null;
vals[i]=null;
i=(i+1)%M;
while(keys[i]!=null){
Key keyToRedo = keys[i];
Value valToRedo = vals[i];
keys[i]=null;
vals[i]=null;
N--;
put(keyToRedo,valToRedo);
i=(i+1)%M;
}
N--;
if(N>0&&N==M/8) resize(M/2);
}
}