1. 基于符号表/索引表的查找算法
符号表目的就是将一个键和一个值联系起来,是一种存储键值对的数据结构。
1.1 基于二叉查找树实现的符号表/索引表
二叉查找树是将二分查找的效率和链表的灵活性结合起来,但依赖结点(键)的分布足够随机。具体是使每个结点含有两个链接的二叉查找树,每个结点只能有一个父结点(除了根节点),而且每个结点都只有左右两个链接,分别指向左子结点和右子结点。特点是左子结点小于根结点,根结点小于右子结点。
public class BST<Key extends Comparable<Key>, Value> { 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; N = n; } } private Node root;//二叉查找树的根结点 public int size() { return size(root); } public 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) { //在以x为根结点的柿子树中查找并返回key对应的值,找不到为null 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 value) { //查找key,找到则更新,否则创建一个新的结点 put(root, key, value); } private Node put(Node x, Key key, Value value) { //如果key存在于已x为根结点的子树中则更新它的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.val = value; } x.N = size(x.left) + size(x.right) + 1; return x; } } 1.1.1 最大键和最小键以及向上取整和向下取整 //最小键 public Key min(){ return min(root).key; } private Node min(Node x){ if (x.left==null){ return x; } return min(x.left); } //小于等于key的最大键 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; } } 键的排名和排名k的键 //返回排名为k的键 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(root, key); } private int rank(Node x, Key key) { //返回以x为根结点的子树中小于x.keyd的键的数量 if (x == null) { return 0; } int cmp = key.compareTo(x.key); if (cmp < 0) { return rank(x.left, key); }else if (cmp>0){ return 1+size(x.left)+rank(x.right,key); }else { return size(x.left); } } 删除最小结点和删除某一结点//删除最小键操作 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) { /** * 在删除结点x后用它的后继结点(即x的右子树中的最小结点)填充它的位置,这样可以保证树的有序性 * ①将删除接结点的链接保存在t * ②将x指向它的后继结点min(t.right) * ③将x的左链接设为t.left * ④将x的右链接指向deleteMiN(t.right) */ 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; }1.2 平衡二叉查找树
由于二叉查找树在最坏情况下的性能还是不能接受,我们需要保证二叉查找树的平衡性,才能得到解决且时间复杂度在对logN数级别。
1.2.1 2-3平衡查找树
2-3平衡查找树引入3-结点,它含有2个键和3条链接。特点:2-结点,左链接指向的键都小于2-结点的键,右链接指向的键都大于2-结点的键。3-节点,左链接指向的键都小于3-结点的2个键,中链接指向的键位于3-结点的2个键之间,右链接指向的键都大于3-结点的2个键。
1. 向2-结点中插入新键:
只要把2-结点替换为一个3-结点。
2. 向3-结点中插入新键:
把3-结点变为4-结点,再将4-结点分解为二叉树,高度加1。
3. 向一个父结点为2-结点的3-结点中插入新键:
把3-结点变为4-结点,再将4-结点分解为二叉树,然后将二叉树的第二大结点加入
到2-结点的父结点,高度加1。
4. 向一个父结点为3-结点的3-结点中插入新键(6种情况):
把3-结点变为4-结点,将4-结点分解为二叉树,再将二叉树的第二大结点加入到3-
结点的父结点,使之变为4-结点,再将父结点分为两个2-结点并将第二大键加入到
父结点的父结点,高度加1。
注:4-结点的分解一次局部变换,不影响树的有序性和平衡性。
5. 小结:
2-3平衡查找树虽然再最坏情况下,也可以是logN对数级别的时间复杂度,但是实现
起来相当复杂且产生很大的额外开销,所以就引出了红黑平衡二叉查找树来解决。
1.2.2 红黑平衡查找树
基于2-3平衡查找树的思想,红黑平衡查找树对其进行优化。一个是替换3-结点,引出红链接和黑链接。
红链接将两个2-结点连接起来构成一个3-结点。
黑链接则是2-3平衡查找树的普通链接。
时间复杂度除了范围查找外(所需额外的时间和返回的键的数量成正比),其他操作都是logN对数级别。
一:红黑树的定义:
1. 红链接均为左链接
2. 没有任何一个结点同时和两条红链接相连。
3. 任意叶子结点到根节点的路径上的黑链接数量相同。
4. 将红链接相连的结点合并,得到就是一颗2-3树。
二:红黑树的实现过程:
每次插入的新结点,它的父结点指向它的链接变为红链接【注:指向根结点的链接一定得为黑链接)】,然后再经过旋转变化【注:如果一个结点的两个子结点都为红黑(红链接),那么需要将子结点都变为黑色并将它的父结点变为红色(指向它的链接变为红链接)】,再将根结点设为黑色,如果根结点由红变为黑,那么树的高度加1,最终得到完美红黑树。
1. 向单个2-结点中插入新键和向树底部的2-结点插入一个新键:
2. 向一个3-结点中插入新键(3中情况):
3. 向树底部的3-结点中插入新键:
4. 小结:
①如果右子结点是红色的而左自结点是黑色的,进行左旋转。
②如果左子结点是红色的且它的左子结点也是红色,进行右旋转。
③如果左右子结点都为红色,进行黑色转换,父结点变为红色。
5. 代码实现:
public class RedBlackBST<Key extends Comparable<Key>, Value> { private final static boolean RED = true; private final static boolean BLACK = false; private Node root; private class Node { private boolean color;//由其父结点指向它的链接的颜色 private Key key;//键 private Value value;//值 private RedBlackBST.Node left, right;//指向子树的链接 private int N;//结点总数 public Node(Key key, Value val, int n, boolean color) { this.key = key; this.value = val; N = n; this.color = color; } } 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; } public int size(Node x) { if (x == null) { return 0; } else { return x.N; } } //颜色转换(两个子链接由red变为black,指向它的链接变为red) private void flipColors(Node h) { h.color = RED; h.left.color = BLACK; h.right.color = BLACK; } //插入新键 public void put(Key key, Value value) { //查找ket。找到则更新其值,否则创建一个新的结点 root = put(root, key, value); } private Node put(Node h, Key key, Value value) { if (h == null) { return new Node(key, value, 1, RED); } int cmp = key.compareTo(h.key); if (cmp < 0) { h.left = put(h.left, key, value); } else if (cmp > 0) { h.right = put(h.right, key, value); } else { h.value = value; } if (isRed(h.right) && !isRed(h.left)) { h = rotateLeft(h); } if (isRed(h.left) && !isRed(h.left.left)) { h = rotateRight(h); } if (isRed(h.right) && isRed(h.left)) { flipColors(h); } h.N = size(h.left) + size(h.right) + 1; return h; } //删除最小值 public void deleteMin() { if (!isRed(root.left) && !isRed(root.right)) { root.color = RED; } root = deleteMin(root); if (root != null) { root.color = BLACK; } } private Node deleteMin(Node h) { if (h.left == null) { return null; } if (!isRed(h.left) && !isRed(h.left.left)) { h = moveRedLeft(h); } h.left = deleteMin(h.left); return balance(h); } private Node moveRedLeft(Node h) { //假设结点h为红色,h.left和h.left.left都是黑色 //将h.left或者h.left的子节点之一变为红 flipColors(h); if (isRed(h.right.left)) { h.right = rotateRight(h.right); h = rotateLeft(h); flipColors(h); } return h; } private Node balance(Node h) { if (isRed(h.right)) { h = rotateLeft(h); } if (isRed(h.left) && isRed(h.left.left)) { h = rotateRight(h); } if (isRed(h.left) && isRed(h.right)) { flipColors(h); } return h; } //删除最大值 public void deleteMax() { if (!isRed(root.left) && !isRed(root.right)) { root.color = RED; } root = deleteMax(root); if (root == null) { root.color = BLACK; } } private Node deleteMax(Node node) { if (node.right == null) { return null; } if (!isRed(node.right) && !isRed(node.right.left)) { node = moveRedRight(node); } node.right = deleteMax(node.right); return balance(node); } //查找key的值 public Value search(Key key) { Node node = search(root, key); return node == null ? null : node.value; } private Node search(Node node, Key key) { if (node == null || node.key == key) { return node; } int cmp = key.compareTo(node.key); if (cmp < 0) { return search(node.left, key); } else { return search(node.right, key); } } private Node moveRedRight(Node h) { //假设结点h为红色,h.left和h.right.left都是黑色 //将h.right或者h.right的子节点之一变为红色 flipColors(h); if (isRed(h.left.left)) { h = rotateRight(h); flipColors(h); } return h; } //删除key操作 public void delete(Key key) { if (root == null) { return; } root = deleteNode(root, key); root.color = BLACK; } private Node deleteNode(Node node, Key key) { if (node == null) { return null; } int cmp = key.compareTo(node.key); if (cmp < 0) { node.left = deleteNode(node.left, key); } else if (cmp > 0) { node.right = deleteNode(node.right, key); } else { if (node.left == null) { return node.right; } else if (node.right == null) { return node.left; } node.key = (Key) minValue(node.right); node.right = deleteNode(node.right, node.key); } if (isRed(node.right) && !isRed(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); } return node; } private Key minValue(Node node) { Key minValue = node.key; while (node.left != null) { minValue = (Key) node.left.key; node = node.left; } return minValue; } }1.2.3 多向平衡二叉树(B-树)
B-树是一颗由键的副本组成的树,每个副本都关联着一条链接。使得索引和符号表分开。更适合大规模的数据存储、高访问速度或随机访问的场景,后被B+树(适合范围查询和有序遍历的场景)优化。
//B-树集合的实现 public class BTreeSET<Key extends Comparable<Key>{ public class Page<Key>{ //创建并打开一个页 public Page(boolean bottom){} //关闭页 public void close(){} //将键查放入外部的页中 public void add(Key key){} //打开p,向这个内部页中插入一个条目并将p和q中的最小键相关联 public void add(Page page){} //这是一个外部页吗 public boolean isExternal(){ return true; } //键key在页中吗 public boolean contains(Key key){ return true; } //可能含有键key的子树 public Page next(Key key){ return new Page(true); } //页是否已经溢出 public boolean isFull(){ return false; } //将较大的中间键移动到一个新的页中 public Page split(){ return new Page(true); } //页中所有键 public Iterable<Key> keys(){ return new Iterable<Key>() { @Override public Iterator<Key> iterator() { return null; } }; } } private Page root = new Page(true); public BTreeSET(Key sentinel){ add(sentinel); } public void add(Key key){ add(root,key); if (root.isFull()){ Page lefthalf =root; Page righthalf=root.split(); root = new Page(false); root.add(lefthalf); root.add(righthalf); } } public void add(Page h,Key key){ if (h.isExternal()) { h.add(key); return; } Page next = h.next(key); add(next,key); if (next.isFull()){ h.add(next.split()); } next.close(); } public boolean contains(Key key){ return contains(root,key); } private boolean contains(Page h,Key key){ if (h.isExternal()){ return h.contains(key); } return contains(h.next(key),key); } } B-树在查找时使用Page数据类型来将键和可能含有该键的子树相关联,并通过检测键的溢出和分裂结点的方法完成了插入操作。1.3 基于散列表实现的符号表/索引表
如果所有的键都是小整数,我们就可以使用数组来实现无序的符号表,将键作为索引,这样就可以快速访问到键的值,这样就是我们要基于散列表来实现符号表。
①用散列函数将键转化为数组的一个索引。
②处理不同键有相同的索引及处理碰撞冲突。【使用拉链法和线性探测法来解决】
使用散列表可以在时间复杂度为常数级别的查找和插入操作。
要实现一个优秀的散列方法需要满足三个条件:
①一致性---等价的键必然产生相等的散列值。
②高效性---计算简便。
③均匀性---均匀地散列所有的键。
java通常直接使用对象的hashcode()函数来实现散列即可。
1.3.1 基于拉链法的散列表
拉链法来解决碰撞冲突,即将大小为M的数组中的每个元素指向一条链表,链表中的每个结点都存储了该元素的键值对。在键的顺序不重要的应用中,它可能是最快的。
//基于拉链法的散列表 public class ChainingHashST<Key, Value> { private int N;//键值对总数 private int M;//散列表大小 private ChainingHashST<Key, Value>[] st;//存放链表对象的数组 public ChainingHashST() { this(997); } public ChainingHashST(int M) { //创建M条链表,默认创建997条 this.M = M; st = new ChainingHashST[M]; for (int i = 0; i < M; i++) { st[i] = new ChainingHashST(); } } private int hash(Key key){ return (key.hashCode() & 0x9fffffff) % M; } private Value get(Key key){ return st[hash(key)].get(key); } public void put(Key key,Value value){ st[hash(key)].put(key,value); } }1.3.2 基于线性探测法的散列表(开发地址散列表)
线性探测法是用大小为M的数组保存N个键值对【M>N】,需要依靠数组中的空位解决碰撞冲突。但碰撞发生时,直接检查散列表中的下一位置(将索引值加1),直到下一位置不发生碰撞位置。使用并行数组来实现,一条保存键,一条保存值。
//基于线性探测法的散列表(开放寻找法) public class LinearHashST<Key, Value> { private int N;//符号表中键值对总数 private int M;//线性探测表的大小 private Key[] keys;//键 private Value[] values;//值 public LinearHashST() { keys = (Key[]) new Object[M]; values = (Value[]) new Object[M]; } public LinearHashST(int cap) { keys = (Key[]) new Object[cap]; values = (Value[]) new Object[cap]; } private int hash(Key key) { return (key.hashCode() & 0x7fffffff) % M; } public void put(Key key, Value value) { if (N >= M / 2) { //扩大线性探测表M=2M; resize(2 * M); } int i; for (i = hash(key); keys[i] != null; i = (i + 1) % M) { if (keys[i].equals(key)) { values[i] = value; return; } } keys[i] = key; values[i] = value; N++; } public Value get(Key key) { for (int i = hash(key); keys[i] != null; i = (i + 1) % M) { if (keys[i].equals(key)) { return values[i]; } } return null; } public void delete(Key key) { //删除键的同时,需要该键的右侧的所有键重新插入散列表 if (!contain(key)) { return; } int i = hash(key); while (!key.equals(keys[i])) { i = (i + 1) % M; } keys[i] = null; values[i] = null; i = (i + 1) % M; while (keys[i] != null) { Key keyToRedo = keys[i]; Value valueToRedo = values[i]; keys[i] = null; values[i] = null; N--; put(keyToRedo, valueToRedo); i = (i + 1) % M; } N--; //散列表的性能依赖a=N/M即散列表的使用率, // 对于拉链法。使用率>1,对于线性探测法不大于1,最好在1/8到1/2 if (N > 0 && N == M / 8) { //缩小线性探测表的大小M = M/2; resize(M / 2); } } private boolean contain(Key key) { for (Key key1 : keys) { if (key1.equals(key)) { return true; } } return false; } //扩大或者缩小数组 private void resize(int cap) { LinearHashST<Key, Value> t = new LinearHashST<Key, Value>(cap); for (int i = 0; i < M; i++) { if (keys[i] != null) { t.put(keys[i], values[i]); } } keys = t.keys; values = t.values; M = t.M; } }2. 查找算法总结