查找 与 树(未完成)

来源:互联网 发布:24芯三网合一网络箱 编辑:程序博客网 时间:2024/06/06 03:33
作者:disappearedgod
文章出处:http://blog.csdn.net/disappearedgod/article/details/24055547
时间:2014-4-18

前记:

根据Robert Sedgewick & Kevin Wayne的《算法》第四版来写的。

1 查找

1.1 符号表

符号表一词,是来描述一张抽象的表格:将信息(Key)存储在其中,然后按照指定的键和很多信息,因此实现一张搞笑的符号表也是一项很有挑战性的任务。
一曰字典,一曰索引。
定义:符号表是一种存储值对的书记结构,支持两种操作:
  • 插入(put),即将一组新的键值对存入表中;
  • 查找(get),即根据给定的键得到相应的值。

1.1.1 基础

符号表是一种典型的抽象数据类型:它代表着一组定义清晰的值以及相应的操作,使得我们能够将类型的实现和使用区分开来。
用一下应用程序编程接口(API)来精确地定义这些操作。

构建数据结构方法:
插入:1. 未命中查找;2.新节点挂在底部;3.平衡。

1.1.1.1 泛型
1.1.1.2 重复的键
1.1.1.3 空键
1.1.1.4 空值
1.1.1.5 删除操作
1.1.1.6 便捷方法

1.1.2 顺序查找


1.1.3 二分查找


1.2 二叉查找树

二叉树是将链表插入的灵活性和有序数组查找的高效性结合起来的符号表实现。具体来说,就是使用每个节点含有两个链接(链表中每个节点只有一个链接)。
二叉查找树是一种高效实现符号表的手段。
二叉查找树是一种高效的优先队列

1.2.1 定义及实现

在二叉树中,每个节点职能有一个父节点指向自己(根节点除外),而且每个节点都只有左右两个链接,分别指向自己的左子节点右子节点。尽管链接指向的是结点,但我们可以将每个链接看做指向了另一颗二叉树,而这棵树的根节点就是被指向的节点。因此我们可以将二叉树定义为一个空链接,或者是一个有左右两个链接的结点,每个链接都指向一棵(独立的)子二叉树。在二叉查找树中,每个节点还包含了一个键和一个值,键之间也有顺序之分以支持高效的查找。

1.2.1.1 定义

一棵二叉查找树(BST)是一棵二叉树,其中每个结点都含有一个Comparable的键(以及相关联的值)且每个节点的键都是大于其左子树中的任意节点的键而小于右子树的任意节点的键。


     

1.2.1.2 实现

LeetCode中对树的java实现版
public class TreeNode {     int val;     TreeNode left;     TreeNode right;     TreeNode(int x) { val = x; } }



1.2.1.2.1 数据表示

每个结点都含有一个键、一个值、一条左链接、一条右链接和一个节点计数器。
左链接指向一棵由小于该结点的所有键组成的二叉搜索树,右链接指向一棵由大于该结点的所有键组成的二叉搜索树。变量N给出了以该节点为根的子数的结点总数。
保证以下公式对二叉树的任意结点x总是成立:
size(x) = size(x.left) + size(x.right) + 1; 
如果将其映射到一条直线上,保证一个节点的左子树中的键出现在他的左边,右子树中的键出现在右边,那么则可以得到一条有序的键列。
二叉树有异构性:一棵二叉查找树代表了一组键(及其相应的值)的集合,而同一个集合可以用多棵不同的二叉查找树表示。

1.2.1.2.1.1 构造实现
基于二叉查找树的符号表
在符号表中查找一个键可能得到的结果
  • 命中:含有该键的节点存在于表中,我们的查找就命中了,然后返回相应的值。
  • 未命中:返回null
在二叉查找树中查找一个键的递归算法:
  • 如果树是空的,则查找未命中。
  • 如果被查找的键和根节点的键相等,查找命中。
  • 否则,就递归地在适当的子树中继续查找。
用一个get()递归算法来实现:第一个参数是一个结点(子树的根节点),第二个参数是被查找的键。


 public class BST<Key extends Comparable<Key>, Value> {    private Node root;             // root of BST    private class Node {        private Key key;           // sorted by key        private Value val;         // associated data        private Node left, right;  // left and right subtrees        private int N;             // number of nodes in subtree        public Node(Key key, Value val, int N) {            this.key = key;            this.val = val;            this.N = N;        }    }    // is the symbol table empty?    public boolean isEmpty() {        return size() == 0;    }    // return number of key-value pairs in BST    public int size() {        return size(root);    }    // return number of key-value pairs in BST rooted at x    private int size(Node x) {        if (x == null) return 0;        else return x.N;    }    public Value get(Key key)    public void put(Key key, Value val)}

1.2.1.2.2 查找

二叉查找树的查找方法(java 版本)
 // does there exist a key-value pair with given key?    public boolean contains(Key key) {        return get(key) != null;    }    // return value associated with the given key, or null if no such key exists    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;    }

1.2.1.2.3 插入

二叉查找树的插入(排序)方法
 public void put(Key key, Value val) {        if (val == null) { delete(key); return; }        root = put(root, key, val);        assert check();    }    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 = 1 + size(x.left) + size(x.right);        return x;    }





1.2.2 分析

使用二叉查找树的算法的运行时间取决于树的形状,而树的形状又取决于键被插入的先后顺序。(这也就是他的弱点)。
  • 最好情况,一棵含有N个结点的树是完全平衡的,每条空链接和根节点的距离都为~lgN。
  • 最坏情况,搜索路径上可能有N个结点。
  • 在一般情况下的形状和最好情况更接近。

在由N个随机键构造的二叉查找树中,查找命中平均所需的比较次数为~2lnN(约为1.39lgN)(查找命中的平均成本为(1+Cn/N))
在由N个随机键构造的二叉查找树中,插入操作和查找未命中平局您所需的比较次数为~2lnN(约为1.39lgN)

1.2.3 有序性相关的方法与删除

1.2.4 一种可比拟的列表-跳表

1.3 平衡查找树

tree 在计算机数据结构里是一种非常重要的东西,多数用于检索量比较大的地方,比如数据库和硬盘文件排列之类。树分很多种,最简单的当然是 BST (binary search tree),这个东西虽然简单,可是毛病很多,比如很容易出现 worse case,就是检索中最不愿意看到的线性检索(一棵只有右子树或者左子树的树);而且BST不是balanced的。所以,BST一般很少应用,前人发明了很多其他的树,比如AVLB-TREE2-3 tree, 2-3-4 tree 等等。而 2-3-4 tree 只是一个变种。

平衡查找树的起因:算法的最坏情况下,性能很差。这里找到一种二分查找树并能保证无论如何构造。
理想情况下我买希望能够保持二分查找树的平衡性。
  • 平衡二叉查找树:其一,保证树的深度是O(logN),如果二叉树足够平衡,则深度足够少。
  • 其二,(2-3树)之所以能够保证在最差的情况下的效率的原因在于其插入之后仍然能够保持平衡状态。
在一棵含有N个结点的树中,我们希望树高是~lgN,这样我们就能保证所有查找都在~lgN次比较内结束,就和二叉查找一样。不幸的是,在动态插入中保证树的完美平衡的代价太高了。
2-3查找树是稍微放松了完美平衡的要求并将学习一种能够保证符号表API中所有操作(范围查找除外)均能够在对数时间内完成的数据结构。
C++实现
class TreeNode{private:TreeItemType smallItem,largeItem;TreeNode *leftChildPtr,middleChildPtr,rightChildPtr;friend class TwoThreeTree;};


1.3.1 2-3查找树

1.3.1.1 定义及优势

1.3.1.1.1 定义

定义 一棵2-3查找树或为一棵空树,或由以下结点组成:
2-结点,含有一个键(及其对应的值)和两条链接,左链接指向的2-3树中的键都小于该结点,右链接指向的2-3树中的键都大于该结点。
3-结点,含有两个键(及其对应的值)和三条链接,左链接指向的2-3树中的键都小于该结点,左链接指向的2-3树中的键都小于该结点,中链接指向的2-3树中的键都位于该结点的两个键之间,右链接指向的2-3树中的键都待遇该结点。
将指向一棵空树的链接成为空链接。

1.3.1.1.2 优势

和标准二叉树有上向下生长不同,2-3树是由下向上生长的(堆排序恰巧也是符合这个思想的二叉树)。一个简单的构造例子如下:

2-3树的查找效率与树的高度是息息相关的。

  • 在最坏的情况下,也就是所有的节点都是2-node节点,查找效率为lgN
  • 在最好的情况下,所有的节点都是3-node节点,查找效率为log3N约等于0.631lgN
  • 1百万个节点的2-3树,树的高度为12-20之间,对于10亿个节点的2-3树,树的高度为18-30之间。



1.3.1.1.3 为什么要这样排列?

 因为它是balanced的!一棵高度为k的2-3 tree有2k – 1 到 3k – 1 之间的叶;一棵有elements 为n的2-3 tree 高度最小为 1+log3n, 最大为 1+log2n。它的检索时间为O(logn)。

伪代码如下:

LOOP until curr = nilIF ( data = curr.small OR data = curr.large )    Return currELSE IF ( data < curr.small )  curr = curr.leftELSE IF ( data > curr.small AND data < curr.large )  curr = curr.middleELSE  curr = curr.large
假设已处平衡状态,我们先看基本操作。

1.3.1.2 查找

2-3树的查找和二叉查找树类似,要确定一个树是否属于2-3树,我们首先和其跟节点进行比较,如果相等,则查找成功;否则根据比较的条件,在其左中右子树中递归查找,如果找到的节点为空,则未找到,否则返回。


  • 二叉树按照升序插入10个key会得到高度为9的一棵最差树,2-3Tree的高度为2.
  • 含有10亿个结点的一棵2-3树的高度仅在19-30之间。(也就是说,访问最多30个结点可查)

1.3.1.2.1 插入

插入:1. 未命中查找;2.新节点挂在底部;3.平衡。

对于2-3 tree,所有的插入(新值)都必须发生在leaf。所以,首先找到新值应该所在的leaf,然后根据这个leaf的情况做出判断:

 1.  该点只有一个元素。直接加入就可以了,判断是small还是large。

 2.  该点full(small和large都有值),其父结点不是full。那就split该结点,并把中间值推上去。

 3.  该点和其父结点都full。如果父结点full,表示父结点已经有3个子树。那就需要一直split 并一直往上推,直到发生以下情况:

         1)     父结点最多只有3个子树

         2)     父结点是根,创建一个新root,保存中间值。树的高度增加1

1.3.1.2.2 往一个2-node节点插入

往2-3树中插入元素和往二叉查找树中插入元素一样,首先要进行查找,然后将节点挂到未找到的节点上。2-3树之所以能够保证在最差的情况下的效率的原因在于其插入之后仍然能够保持平衡状态。如果查找后未找到的节点是一个2-node节点,那么很容易,我们只需要将新的元素放到这个2-node节点里面使其变成一个3-node节点即可。但是如果查找的节点结束于一个3-node节点,那么可能有点麻烦。


1.3.1.2.3 往一个3-node节点插入

往一个3-node节点插入一个新的节点可能会遇到很多种不同的情况,下面首先从一个最简单的只包含一个3-node节点的树开始讨论。
  • 新建插入,临时将新键存入该节点中,使之成为4-结点。
  • 3-key 4-link,为了容易转化为一棵由3个2-node组成的2-3 Tree


1.3.1.2.4 向节点是3-node,父节点是2-node中插入新键

需要在维持树的完美平衡的前提下为新建腾出空间。
分析:父节点既然是2-node,就要改成3-node,只有将新的4-node中的中值赋给2-node,作为3-node中后面的一个点。
  • 新建插入,临时将新键存入该节点中,使之成为4-结点。
  • 3-key 4-link,为了容易转化为一棵由3个2-node组成的2-3 Tree
  • 将上述的父节点(3.1.2.2中分解出的父节点)与其真正的父节点(2-node)合并。


1.3.1.2.5 节点是3-node,父节点也是3-node

当我们插入的节点是3-node的时候,我们将该节点拆分,中间元素提升至父节点,但是此时父节点是一个3-node节点,插入之后,父节点变成了4-node节点,然后继续将中间元素提升至其父节点,直至遇到一个父节点是2-node节点,然后将其变为3-node,不需要继续进行拆分。

1.3.1.2.6 分解根节点

当根节点到字节点都是3-node节点的时候,这是如果我们要在字节点插入新的元素的时候,会一直查分到跟节点,在最后一步的时候,跟节点变成了一个4-node节点,这个时候,就需要将跟节点查分为两个2-node节点,树的高度加1,这个操作过程如下:

1.3.1.2.5 汇总

1.3.1.2.5.1 局部变换
2-3 Tree插入算法的根本在于这些变换都是局部的:除了相关节点和链接之外不必修改或检查树的其他部分。换句话说,每次变换中,变更的连接数不会超过一个很小的常数。

1.3.1.2.5.2 全局性质
全局有序性和全局平衡性:任意空链接到根节点的路径长度都是相等的。

1.3.1.2.5.3 2-3树的实现

1.3.1.3 一些扩展

一个有趣的小例子:   2–3 Tree Java Applet
Reference:
  1. Gross, R. Hernández, J. C. Lázaro, R. Dormido, S. Ros (2001). Estructura de Datos y Algoritmos. Prentice Hall. ISBN 84-205-2980-X.
  2. Aho, Alfred V.; Hopcroft, John E.; Ullman, Jeffrey D. (1974). The Design and Analysis of Computer Algorithms. Addison-Wesley., p.145-147

1.3.1.3.1 2-3-4 树


1.3.1.3.2 Finger 树


1.3.1.3.3 2-3 堆


1.3.1.3.4 (a,b)-树



1.3.2 红黑二叉查找树

红黑二叉查找树的基本思想:用标准的二叉查找树(完全由2-结点构成)和一些额外的信息(替换3-结点)来表示2-3树。
  • 红链接:将2个2-Node链接起来构成一个3-Node
  • 黑链接:2-3树中普通的链接。
优点:无需修改可以直接使用标准二叉查找树的get()方法。

1.4 散列表


1.4.1 散列函数


1.4.2 各种散列表


1.4.3 数组与内存


1.5 应用

在计算机发展的早期,符号表帮助程序员从使用机器语言的数字地址金华到在汇编语言中使用符号名称;
在现代应用程序中,符号名称的含义能够通行于跨越全球的计算机网络。
快速查找算法曾经并继续在计算机领域中扮演者重要的角色。

符号表的现代应用包括科学数据的组织
  • 在基因组数据中需找分子标记或模式从而绘制全基因组图谱
  • 网络信息的组织,从搜索在线贸易到数字图书馆
  • 互联网基础架构的实现,例如包在网络节点中的路由、共享文件系统和流媒体
高效的查找算法确保了这些遗迹无数其他重要的应用程序成为可能。

1.5.1 符号表的选择

这里总结了之前中多个命题和性质得到的各种符号表算法的性能特点(散列表的最坏情况来自文献)。从表中可以知道,对于典型的应用程序应该在散列表和二叉查找树之间进行选择。


1.5.2 集合API


1.5.3 字典类用例


1.5.4 索引类用例


1.5.5 稀疏向量


1.6 来源

1. 《算法》第四版
2. 浅谈算法和数据结构: 八 平衡查找树之2-3树
3. GoogleCode:2-3Tree

1.7 后记

图片为《算法》第四版中图片。
更新列表:
2014-4-19:二叉搜索树
相关链接:
数据结构-树(N叉树、二叉树、二叉搜索树、平衡二叉树、字典树、红黑树、线段树)


0 0