C#使用符号表实现查找算法

高效检索海量信息(经典查找算法)是现代信息世界的基础设施。我们使用符号表描述一张抽象的表格,将信息(值)存储在其中,然后按照指定的键来搜索并获取这些信息。键和值的具体意义取决于不同的应用。符号表中可能会保存很多键和很多信息,因此实现一张高效的符号表是很重要的任务。

符号表有时被称为字典,有时被称为索引。

1.符号表

符号表是一种存储键值对的数据结构,支持两种操作:插入(put),即将一组新的键值对存入表中;查找(get),即根据给定的键得到相应的值。符号表最主要的目的就是将一个健和一个值联系起来。

构造符号表的方法有很多,它们不光能够高效地插入和查找,还可以进行其他几种方便的操作。要实现符号表,首先要定义其背后的数据结构,并指明创建并操作这种数据结构以实现插入,查找等操作所需的算法。

API

    public interface ISymbolTables<Key,Value> where  Key : IComparable
    {
        int CompareCount { get; set; }
        /// <summary>
        /// 将键值对存入表中(若值未空则将键key从表中删除)
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        void Put(Key key, Value value);

        /// <summary>
        /// 获取键 key 对应的值(若键不存在则返回 null)
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        Value Get(Key key);

        /// <summary>
        /// 从表中删去键 key
        /// </summary>
        /// <param name="key"></param>
        void Delete(Key key);

        /// <summary>
        /// 键 key 是否在表中存在
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        bool Contains(Key key);

        /// <summary>
        /// 表是否未空
        /// </summary>
        /// <returns></returns>
        bool IsEmpty();

        /// <summary>
        /// 表中的键值对数量
        /// </summary>
        /// <returns></returns>
        int Size();

        /// <summary>
        /// 表中所有键的集合
        /// </summary>
        /// <returns></returns>
        IEnumerable<Key> Keys();

        /// <summary>
        /// 最小的键
        /// </summary>
        /// <returns></returns>
        Key Min();
        /// <summary>
        /// 最大的键
        /// </summary>
        /// <returns></returns>
        Key Max();
        /// <summary>
        /// 小于等于 key 的键
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        Key Floor(Key key);
        /// <summary>
        /// 大于等于 key 的键
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        Key Ceilling(Key key);
        /// <summary>
        ///小于 key 的键的数量(key 的排名)
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        int Rank(Key key);
        /// <summary>
        ///  排名为 k 的键
        /// </summary>
        /// <param name="k"></param>
        /// <returns></returns>
        Key Select(int k);
        /// <summary>
        /// 删除最小的键
        /// </summary>
        void DeleteMin();
        /// <summary>
        /// 删除最大的键
        /// </summary>
        void DeleteMax();
        /// <summary>
        /// [lo ... hi]之间的键的数量
        /// </summary>
        /// <param name="lo"></param>
        /// <param name="hi"></param>
        /// <returns></returns>
        int Size(Key lo,Key hi);
        /// <summary>
        /// [lo ... hi]之间的键
        /// </summary>
        /// <param name="lo"></param>
        /// <param name="hi"></param>
        /// <returns></returns>
        IEnumerable<Key> Keys(Key lo, Key hi);
    }

基本实现:

    /// <summary>
    /// 符号表基类
    /// </summary>
    /// <typeparam name="Key"></typeparam>
    /// <typeparam name="Value"></typeparam>
    public class BaseSymbolTables<Key, Value>: ISymbolTables<Key, Value>
        where Key : IComparable
    {
        public int CompareCount { get; set; }
        /// <summary>
        /// 将键值对存入表中(若值未空则将键key从表中删除)
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public virtual void Put(Key key, Value value)
        {
        }

        /// <summary>
        /// 获取键 key 对应的值(若键不存在则返回 null)
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public virtual Value Get(Key key)
        {
            return default(Value);
        }

        /// <summary>
        /// 从表中删去键 key
        /// </summary>
        /// <param name="key"></param>
        public void Delete(Key key)
        {

        }

        /// <summary>
        /// 键 key 是否在表中存在
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public bool Contains(Key key)
        {
            return false;
        }

        /// <summary>
        /// 表是否未空
        /// </summary>
        /// <returns></returns>
        public bool IsEmpty()
        {
            return Size()==0;
        }

        /// <summary>
        /// 表中的键值对数量
        /// </summary>
        /// <returns></returns>
        public virtual int Size()
        {
            return 0;
        }

        /// <summary>
        /// 表中所有键的集合
        /// </summary>
        /// <returns></returns>
        public virtual IEnumerable<Key> Keys()
        {
            return new List<Key>();
        }

        /// <summary>
        /// 最小的键
        /// </summary>
        /// <returns></returns>
        public virtual Key Min()
        {
            return default(Key);
        }
        /// <summary>
        /// 最大的键
        /// </summary>
        /// <returns></returns>
        public virtual Key Max()
        {
            return default(Key);
        }
        /// <summary>
        /// 小于等于 key 的键
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public virtual Key Floor(Key key)
        {
            return default(Key);
        }
        /// <summary>
        /// 大于等于 key 的键
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public virtual Key Ceilling(Key key)
        {
            return default(Key);
        }
        /// <summary>
        ///小于 key 的键的数量(key 的排名)
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public virtual int Rank(Key key)
        {
            return 0;
        }
        /// <summary>
        ///  排名为 k 的键
        /// </summary>
        /// <param name="k"></param>
        /// <returns></returns>
        public virtual Key Select(int k)
        {
            return default(Key);
        }
        /// <summary>
        /// 删除最小的键
        /// </summary>
        public virtual void DeleteMin()
        {

        }
        /// <summary>
        /// 删除最大的键
        /// </summary>
        public virtual void DeleteMax()
        {

        }
        /// <summary>
        /// [lo ... hi]之间的键的数量
        /// </summary>
        /// <param name="lo"></param>
        /// <param name="hi"></param>
        /// <returns></returns>
        public virtual int Size(Key lo, Key hi)
        {
            return 0;
        }
        /// <summary>
        /// [lo ... hi]之间的键
        /// </summary>
        /// <param name="lo"></param>
        /// <param name="hi"></param>
        /// <returns></returns>
        public virtual IEnumerable<Key> Keys(Key lo, Key hi)
        {
            return new List<Key>();
        }
    }

2.有序符号表

典型的应用程序中,键都是IComparable 对象,因此可以使用 a.CompareTo( b ) 来比较 a 和 b 两个键。符号表保持键的有序性,可以扩展它的API,根据键的相对位置定义更多实用操作。例如,最大和最小的键。

排名(Rank 方法)和选择 (Select 方法)

检查一个新的键是否插入合适位置的基本操作是排名(Rank,找出小于指定键的键的数量)和选择(Select,找出排名为 k 的键)。对于 0 到 Size()-1 的所有 i 都有 i == Rank( Select(i) ),且所有的键都满足 key == Select( Rank( key ) ) 。

键的等价性

IComparable 类型中 CompareTo 和 Equals 方法是一致的,但为了避免任何潜在的二义性,这里只是用a.CompareTo( b ) == 0 来判断 a 和 b 是否相等。

成本模型

查找的成本模型:在符号表的实现中,将比较的次数作为成本模型。在内循环不进行比较的情况下,使用数组的访问次数。

符号表实现的重点在于其中使用的数据结构和 Get() , Put() 方法。

3.无序链表中的顺序查找

符号表中使用的数据结构的一个简单选择是链表,每个结点存储一个键值对。

Get 方法的实现即为遍历链表,用Equals 方法比较需要查找的键和每个结点中键。如果匹配就返回相应的值,否则返回 null。Put 方法的实现也是遍历,如果匹配就更新相应的值,否则就用给定的键值对创建一个新的结点并将其插入链表的开头。这种方法称为顺序查找。

    /// <summary>
    /// 顺序查找
    /// </summary>
    /// <typeparam name="Key"></typeparam>
    /// <typeparam name="Value"></typeparam>
    public  class SequentialSearchST<Key,Value>:BaseSymbolTables<Key, Value>
        where Key : IComparable
    {

        private Node First;
        private class Node
        {
            public Key key;
            public Value value;
            public Node next;
            public Node(Key _key,Value _value,Node _next)
            {
                key = _key;
                value = _value;
                next = _next;
            }
        }

        public override Value Get(Key key)
        {
            for (Node x = First; x != null; x = x.next)
            {
                if (key.Equals(x.key))
                    return x.value;
            }
            return default(Value);
        }

        public override void Put(Key key, Value value)
        {
            for (Node x = First; x != null; x = x.next)
            {
                CompareCount++;
                if (key.Equals(x.key))
                {
                    x.value = value;
                    return;
                }
            }

            First = new Node(key,value,First);
        }
    }

这里我们使用一个字符串数组来测试上面的算法,键是数组中的值,值是插入的索引:

string[] strs = new string[] { "S", "E", "A", "R", "C", "H", "E", "X", "A", "M", "P", "L", "E" };

下面是顺序查找的索引用例的轨迹:

分析符号表算法比排序算法更困难,因为不同的用例所进行的操作序列各不相同。常见的情形是虽然查找和插入的使用模式是不可预测的,但它们的使用肯定不是随机的。因此我们主要研究最坏情况下的性能。我们使用命中表示一次成功的查找,未命中表示一次失败的查找。

在含有 N 对键值的基于链表的符号表中,未命中的查找和插入操作都需要 N 次比较。命中的查找在最坏情况下需要 N 次比较。特别地,向一个空表中插入 N 个不同的键需要 ~ N^2 /2 次比较。

查找一个已经存在的键并不需要线性级别的时间。一种度量方法是查找表中的每个键,并将总时间除以 N 。在查找表中每个键的可能性都相同的情况下,这个结果就是一次查找平均所需的比较次数。称它为随机命中。由上面的算法可以得到平均比较次数为 ~N/2: 查找第一个键需要比较一次,查找第二个键需要比较两次 ...... 平均比较次数为(1+2+3.....+N)/ N = (N+1)/2。

这证明基于链表的实现以及顺序查找是低效的。

4.有序数组中的二分查找

有序符号表API的实现使用的数据结构是一对平行的数组,一个存储键一个存储值。下面的代码保证数组中IComparable 类型的键有序,然后使用数组的索引来高效地实现 Get 和其他操作。

下面算法的核心是 Rank 方法(它使用二分查找的算法),它返回表中小于给定键的键的数量。对于 Get 方法,只要给定的键存在于表中就返回,否则返回空。

Put 方法,只要给定的键存在于表中,Rank 方法就能够精确告诉我们它的为并去更新,以及当键不存在时我们也能直到将键存储到什么位置。插入键值对时,将更大的键和值向后移一格,并将给定的键值对分别插入到数组。

    public class BinarySearchST<Key,Value> : BaseSymbolTables<Key, Value>
        where Key : IComparable
    {
        private Key[] keys;
        private Value[] vals;
        private int N;

        public BinarySearchST(int capacity)
        {
            keys = new Key[capacity];
            vals = new Value[capacity];
        }

        public override int Size()
        {
            return N;
        }

        public override Value Get(Key key)
        {
            if (IsEmpty())
                return default(Value);

            int i = Rank(key);
            if (i < N && keys[i].CompareTo(key) == 0)
                return vals[i];
            else
                return default(Value);
        }

        public override int Rank(Key key)
        {
            int lo = 0, hi = N - 1;
            while (lo <= hi)
            {
                int mid = lo + (hi-lo) / 2;
                CompareCount++;
                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 override void Put(Key key, Value value)
        {
            int i = Rank(key);
            if (i < N && keys[i].CompareTo(key) == 0)
            {
                vals[i] = value;
                return;
            }

            for (int j = N; j > i; j--)
            {
                keys[j] = keys[j-1];
                vals[j] = vals[j-1];
            }

            keys[i] = key;
            vals[i] = value;
            N++;
        }

        public override Key Min()
        {
            return keys[0];
        }

        public override Key Max()
        {
            return keys[N-1];
        }

        public override Key Select(int k)
        {
            return keys[k];
        }

        public override Key Ceilling(Key key)
        {
            int i = Rank(key);
            return keys[i];
        }

        public override IEnumerable<Key> Keys()
        {
            return keys;
        }
    }

下面是该算法的用例移动轨迹:

对二分查找的分析

Rank 方法的递归实现使用了二分查找,二分查找比顺序查找快很多。在 N 个键的有序数组中进行二分查找最多需要 (lgN + 1)次比较。

尽管该算法能够保证查找所需的时间是对数级别的,但 Put 方法还是太慢,需要移动数组。对于随机数组,构造一个基于有序数组的符号表所需访问数组的次数是数组长度的平方级别。

向大小为 N 的有序数组中插入一个新的键值对在最坏情况下需要访问 ~2N 次数组,因此向一个空符号表中插入 N 个元素在最坏情况下需要访问 ~N^2 次数组。

对于一个静态表(不允许插入)来说,将其在初始化时就排序是值得的。下面是符号表简单实现的总结:

算法
最坏情况下的成本

(N 次插入后)


平均情况下的成本

(N 次随机插入后)

是否支持有序性相关的操作
查找 插入 查找 插入
顺序查找(无序链表) N N N/2 N
二分查找(有序数组) lgN 2N lgN N

到此这篇关于C#使用符号表实现查找算法的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • C#算法之散列表

    目录 1.散列函数 正整数 浮点数 字符串 组合键 将 HashCode() 的返回值转化为一个数组索引 自定义的 HashCode 软缓存 2.基于拉链法的散列表 散列表的大小 删除操作 有序性相关的操作 3.基于线性探测法的散列表 删除操作 键簇 线性探测法的性能分析 调整数组大小 拉链法 均摊分析 4.内存的使用 如果所有的键都是小整数,我们可以使用一个数组来实现无序的符号表,将键作为数组的索引而数组中键 i 处存储的就是它对应的值.散列表就是用来处理这种情况,它是简易方法的扩展并能够处理

  • C#实现优先队列和堆排序

    目录 优先队列 1.API 2.初级实现 3.堆的定义 二叉堆表示法 4.堆的算法 上浮(由下至上的堆的有序化) 下沉(由上至下的堆的有序化) 改进 堆排序 1.堆的构造 2.下沉排序 先下沉后上浮 优先队列 许多应用程序都需要处理有序的元素,但不一定要求它们全部有序,或是不一定要一次就将它们排序.很多情况下是收集一些元素,处理当前键值最大的元素,然后再收集更多的元素,再处理当前键值最大的元素.这种情况下,需要的数据结构支持两种操作:删除最大的元素和插入元素.这种数据结构类型叫优先队列. 这里,

  • C#实现快速排序算法

    快速排序是应用最广泛的排序算法,流行的原因是它实现简单,适用于各种不同情况的输入数据且在一般情况下比其他排序都快得多. 快速排序是原地排序(只需要一个很小的辅助栈),将长度为 N 的数组排序所需的时间和 N lg N 成正比. 1.算法 快速排序也是一种分治的排序算法.它将一个数组分成两个子数组,将两部分独立地排序. 快速排序和归并排序是互补:归并排序是将数组分成两个子数组分别排序,并将有序数组归并,这样数组就是有序的了:而快速排序将数组通过切分变成部分有序数组,然后拆成成两个子数组,当两个子数

  • C#实现平衡查找树

    目录 1. 2-3查找树 1.查找 2.向 2- 结点中插入新键 3.向一棵只含有一个 3- 结点的树中插入新键 4.向一个父结点为 2- 结点的 3- 结点中插入新键 5.向一个父结点为 3- 结点的 3- 结点插入新键 6.分解根结点 7.局部变换 8.全局性质 2.红黑二叉查找树 1.定义 2.一一对应 3.颜色表示 4.旋转 5.在旋转后重置父结点的链接 6.向单个 2- 结点中插入新键 7.向树底部的 2- 结点插入新键 8.向一棵双键树(即一个 3- 结点)中插入新键 9.颜色变换

  • C#实现希尔排序

    对于大规模乱序的数组,插入排序很慢,因为它只会交换相邻的元素,因此元素只能一点一点地从数组地一段移动到另一端.希尔排序改进了插入排序,交换不相邻地元素以对数组地局部进行排序,最终用插入排序将局部有序的数组排序. 希尔排序的思想是使数组中任意间隔为 h 的元素都是有序的.这样的数组成为 h 有序数组.换句话说,一个 h 有序数组就是 h 个相互独立的有序数组组合在一起的一个数组. 在进行排序时,刚开始 h 很大,就能将元素移动到很远的地方,为实现更小的 h 有序创造方便.h 递减到 1 时,相当于

  • C#实现二叉查找树

    目录 1.实现API 1.数据结构 2.查找 3.插入 4.分析 有序性相关的方法和删除操作 1.最大键和最小键 2.向上取整和向下取整 3.选择操作 4.排名 5.删除最大键和删除最小键 6.删除操作 7.范围查找 8.性能分析 对于符号表,要支持高效的插入操作,就需要一种链式结构.但单链表无法使用二分查找,因为二分查找的高效来自于能够快速通过索引取得任何子数组的中间元素,链表只能遍历(详细描述).为了将二分查找的效率和链表的灵活性结合,需要更复杂的数据结构:二叉查找树.具体来说,就是使用每个

  • C#使用符号表实现查找算法

    高效检索海量信息(经典查找算法)是现代信息世界的基础设施.我们使用符号表描述一张抽象的表格,将信息(值)存储在其中,然后按照指定的键来搜索并获取这些信息.键和值的具体意义取决于不同的应用.符号表中可能会保存很多键和很多信息,因此实现一张高效的符号表是很重要的任务. 符号表有时被称为字典,有时被称为索引. 1.符号表 符号表是一种存储键值对的数据结构,支持两种操作:插入(put),即将一组新的键值对存入表中:查找(get),即根据给定的键得到相应的值.符号表最主要的目的就是将一个健和一个值联系起来

  • PHP哈希表实现算法原理解析

    在PHP内核中,其中一个很重要的数据结构就是HashTable.我们常用的数组,在内核中就是用HashTable来实现.那么,PHP的HashTable是怎么实现的呢?最近在看HashTable的数据结构,但是算法书籍里面没有具体的实现算法,刚好最近也在阅读PHP的源码,于是参考PHP的HashTable的实现,自己实现了一个简易版的HashTable,总结了一些心得,下面给大家分享一下. HashTable的介绍 哈希表是实现字典操作的一种有效数据结构. 定义 简单地说,HashTable(哈

  • PHP常用的排序和查找算法

    本文汇总了常见的php排序算法和查找,在进行算法设计的时候有不错的借鉴价值.现分享给大家供参考之用.具体如下: <?php /** * PHP最常用的四个排序方法及二种查找方法 * 下面的排序方法全部都通过测试 * auther : soulence * date : 2015/06/20 */ //PHP冒泡排序法 function bubbleSort(&$arr){ //这是一个中间变量 $temp=0; //我们要把数组,从小到大排序 //外层循环 $flag=false;//这个优

  • WinForm通过操作注册表实现限制软件使用次数的方法

    本文实例讲述了WinForm通过操作注册表实现限制软件使用次数的方法.分享给大家供大家参考,具体如下: 1.创建注册表文件: 打开记事本,输入一些内容: REGEDIT4 [HKEY_CURRENT_USER/Software/MyRegDataApp] "UseTime"="10" 保存为"RegData.reg" 2.创建winform项目 引用名称空间 using Microsoft.Win32 ; 在Form中激活load事件,并添加代码

  • C语言基于哈希表实现通讯录

    本文为大家分享了C语言基于哈希表实现通讯录的具体代码,供大家参考,具体内容如下 1.需求分析 本演示程序用C语言编写,完成哈希表的生成,电话号码的插入.以及查找等功能. (1)按提示输入相应的联系人的相关资料: (2)以相应的输出形式输出所存储的的联系人的资料: (3)程序可以达到建立.添加.查找.打印的功能: (4)程序可以判断用户输入的非法数据并引导正确的输入. 2.概要设计 存储电话号码的记录时,若在存储位置和其关键字之间建立某种确定的对应关系使得每个关键字和存储结构中一个唯一的存储位置相

  • python 哈希表实现简单python字典代码实例

    这篇文章主要介绍了python 哈希表实现简单python字典代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 class Array(object): def __init__(self, size = 32, init = None): self._size = size self._items = [init] * size def __getitem__(self, index): return self._items[index

  • C++顺序表实现图书管理系统

    本文为大家分享了C++顺序表实现图书管理系统的具体代码,供大家参考,具体内容如下 图书信息表包括以下10项常用的基本操作:图书信息表的创建和输出.排序.修改.逆序存储.最贵图书的查找.最爱图书的查找.最佳位置图书的查找.新图书的入库.旧图书的出库.图书去重. 代码: #include<iostream> #include<iomanip> #include<string> using namespace std; //函数结果状态代码 #define OK 1 #def

  • Java顺序表实现图书管理系统

    本文实例为大家分享了Java顺序表实现图书管理系统的具体代码,供大家参考,具体内容如下 一.简介 实现此项目的目的是巩固并理解前面的知识点:类,抽象类,封装,继承,多态,接口等 二.核心需求 管理端   查阅书籍   增加书籍   删除书籍   打印书籍列表   退出系统 用户端   查询书籍   借阅书籍   归还书籍   打印书籍列表   退出系统 三.类的设计 1. 创建图书类 图书类中包含图书的名称,价格,类型,作者和是否被借出等信息,并生成构造方法,Getter()和Setter()方

  • JavaScript数据结构之二叉树的查找算法示例

    本文实例讲述了JavaScript数据结构之二叉树的查找算法.分享给大家供大家参考,具体如下: 前面文章介绍了二叉树的遍历,现在谈谈在二叉树中进行查找.对二叉查找树来说,一般有以下三类查找:最大值,最小值和给定值. 查找最小值就是遍历左子树,直到找到最后一个结点,这是因为在二叉查找树中较小的值总是在左子节点上的. 代码如下: function getMin(){//查找最小值 var current=this.root;//指向根节点 while(current.left!=null){ cur

  • Java实现的快速查找算法示例

    本文实例讲述了Java实现的快速查找算法.分享给大家供大家参考,具体如下: 快速查找算法,可以根据想要找的是第几个大的数,每次循环都能固定下来一个数在数组完整排完序之后的位置,每次循环都能定一个数的位置,如果当前固定的数的位置和用户要找的第几个数匹配,则就直接返回.例如我要找第二大的数,如果循环一次固定的数的下标是1,那就是当前需要找的数. 代码如下: // 快速查找算法 public static int quickSelect(int[] arr, int selectIndex) { in

随机推荐