算法 第三章 查找
符号表
一种存储键值对的数据结构,支持: put
、get
操作。
无序链表中的顺序查找
实现:
|
|
有序数组的二分查找
代码实现:
|
|
二分查找:
递归
12345678public int rank(Key key, int lo, int hi) {if (hi < lo) return lo;int mid = lo + (hi - lo) / 2;int cmp = key.compareTo(keys[mid]);if (cmp < 0) retunr rank(key, lo, mid -1);else if (cmp > 0) {return rank(key, mid+1, hi);}else return mid;}迭代
1234567891011public int rank(Key key) {int lo = 0; hi = N - 1;while(lo <= hi) {int mid = lo + (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;}
二叉查找树 (BST)
定义:一棵二叉查找树(BST) 是一棵二叉树,其中每个结点都含有一个 Comparable
的键(以及相关联的值)且每个结点的键都大于其左子树中的任意结点的键而小于右子树的任意结点的键。
代码实现:
|
|
二叉树中的删除操作:在删除结点 X 后用它的后继结点填补它的位置。因为 x 有一个右子结点,因此它的后继结点就是其右子树的最小结点。
将指向即将被删除的结点的链接保存为 t;
将 x 指向它的后继结点
min(t.right)
;将 x 的右链接指向
deleteMin(t.right)
, 也就是在删除后所有结点仍然都大于 x.key 的子二叉查找树;将 x 的左链接(本为空)设为 t.left (其下所有的键都小于被删除的结点和他的后继结点);
实现:
1234567891011121314151617private 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.size = size(x.left) + size(x.right) + 1;return x;}二叉树的范围查找
1234567891011121314151617public Iterable<Key> keys(Key lo, Key hi) {if (lo == null) throw new IllegalArgumentException("first argument to keys() is null");if (hi == null) throw new IllegalArgumentException("second argument to keys() is null");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-3 树中的键都小于该结点,右链接指向的 2- 3树中的键都大于该结点。
- 3 - 结点, 含有两个键(及其对应的值)和三个链接,左链接指向的 2-3树中的键都小于该结点,中链接指向的 2-3 树中的键都位于该结点的两个键之间,右链接指向的 2-3 树中的键都大于该结点。
- 一棵完美平衡的 2-3 查找树中的所有空链接到根结点的距离都是相同的。
插入操作
- 向 2- 结点中插入新建
- 使该2-结点变为 3- 结点
- 向一棵只含有一个 3-结点的树中插入新建
- 创建一个 4- 结点,同时将 4- 结点分解为 2-3 树
- 向一个父结点为2-结点的 3- 结点中插入新建
- 将 3- 结点替换为 4- 结点,将 4 - 结点分解为两个 2- 结点并将中键移动到至父结点。
- 向一个父结点为 3- 结点的 3- 结点中插入新建
- 如果从插入结点到根结点的路径上全都是3- 结点,那么我们的根结点就是一个 4- 结点,我们将执行分解根结点,将树高加 1;
红黑树(红黑二叉查找树)
定义:
红链接:将两个2-结点连接起来构成一个3 - 结点;
黑链接:2 -3 树中的普通链接;
- 定义:
- 红链接均为左链接;
- 没有任何一个结点同时和两条红链接相连;
- 该树是完美黑色平衡的,即任意空链接到根结点的路径上的黑链接数量相同。
- 满足这样定义的红黑树和相应的 2-3 树是一一对应的。
- 优点:二叉查找树的简洁高效的查找算法和2-3树中高效的平衡插入算法 完美的结合。
结点表示:
|
|
旋转
旋转操作可以保持红黑树的两个重要性质:有序性和完美平衡性。
左旋转 : 如果右子结点是红色的而左子结点是黑色的,进行左旋转。
code 实现:
1234567891011private Node rotateLeft(Node h) {// assert (h != null) && isRed(h.right);Node x = h.right;h.right = x.left;x.left = h;x.color = x.left.color;x.left.color = RED;x.size = h.size;h.size = size(h.left) + size(h.right) + 1;return x;}
右旋转:如果左子结点是红色的且它的左子结点也是红色的,进行右旋转。
code prompt
123456789101112// make a left-leaning link lean to the rightprivate Node rotateRight(Node h) {// assert (h != null) && isRed(h.left);Node x = h.left;h.left = x.right;x.right = h;x.color = x.right.color;x.right.color = RED;x.size = h.size;h.size = size(h.left) + size(h.right) + 1;return x;}
插入
插入新建时,使用旋转操作可以保证2 -3树和红黑树的一一对应关系。
向单个 2- 结点插入新建
- 左插入,新增一个红色结点即可,将这个 2 - 结点变为一个 3- 结点;
- 右插入,将会产生一条红色的右链接,运用
rotateRight
方法将其旋转为红色左链接并修正根结点的链接。
向树底部的2- 结点插入新建,方法同上
向一颗双键树(一个 3- 结点)中插入新建
- 新键大于原树中的两个键
- 用红链接和新结点相连,父结点左右链接都是红链接,同时将红链接变为黑链接(颜色变换)。
- 新键小于原树中的两个键
- 用红链接和新结点相连,此时存在连续两条红链接,运用右旋转,旋转后变为红色右链接,运用颜色变换。
- 新键介于原树中的两个键之间
- 用红链接和新结点相连,运用左旋转方式,变为红色左链接,进一步运用右旋转方法,变为红色右链接,再利用颜色变换。
- 新键大于原树中的两个键
颜色变换:如果左右结点均为红色,进行颜色转换。
Code Prompt:
12345678910// flip the colors of a node and its two childrenprivate void flipColors(Node h) {// h must have opposite color of its two children// assert (h != null) && (h.left != null) && (h.right != null);// assert (!isRed(h) && isRed(h.left) && isRed(h.right))// || (isRed(h) && !isRed(h.left) && !isRed(h.right));h.color = !h.color;h.left.color = !h.left.color;h.right.color = !h.right.color;}根结点总是黑色
颜色变换会使根结点变为红色,红色的根结点说明根结点是一个 3- 结点的一部分,因此我们在每次插入后都会将根结点设为黑色,同时将树的黑链接高度加1;
向树底部的 3- 结点插入新建, 是以上方法的集合使用;
将红链接在树中向上传递
要在一个 3- 结点下插入新键, 先创建一个临时的 4- 结点, 将其分解并将红链接由中间键传递给它的父结点。重复上述步骤,就能将红链接在树中向上传递,直至遇到一个2 - 结点或者根结点。
代码实现:
|
|
删除
- 删除
- 删除最小键
红黑树的性质
所有基于红黑树的符号表实现都能保证操作的运行时间为对数级别。
一棵大小为 N 的红黑树的高度不会超过 2lgN
一棵大小为 N 的红黑树中,根结点到任意结点的平均路径长度为~1.00lgN。
散列表
散列函数
除留余数法
Java 中
hasCode()
的两种实现:数组的索引, 产生一个 0 到 M-1 (M=32) 的整数
123private int hash(Key x) {return ((x.hasCode) & 0x7fffffff ) % M;}自定义
hasCode()
方法12345678public int hashCode() {int hash = 1;hash = 31*hash + who.hashCode();hash = 31*hash + when.hashCode();hash = 31*hash + ((Double) amount).hashCode();return hash;// return Objects.hash(who, when, amount);}
基于拉链法的散列表
拉链法
定义:将大小为M 的数组中的每一个元素指向一条链表,链表中的每个结点都存储了散列值为该元素的索引的键值对。基本思想是选择足够大的 M,使得链表尽可能短以提高查找效率。
用 M 条链表来保存 N 个键,无论键在各个链表中如何分布,链表 的平均长度肯定是 N / M;
代码实现:
|
|
基于线性探测的散列表
开放地址散列表
定义:用大小为 M 的数组保存 N个键值对,其中 M> N 。依靠数组中的空位解决碰撞冲突。
核心思想:与其将内存用作链表,不如将它们作为在散列表的空元素。
线性探测法: 当碰撞发生时,直接检查散列表中的下一个位置。
|
|
删除操作
将要删除的元素位置至为空,同时将数组中被删除键的右侧的所有键重新插入散列表。