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

 更新时间:2022年4月15日 17:44  点击:323 作者:Ruby_Lu

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

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

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 次随机插入后)

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

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

原文出处:https://www.cnblogs.com/afei-24/p/13514164.html

[!--infotagslink--]

相关文章

  • C#实现简单的登录界面

    我们在使用C#做项目的时候,基本上都需要制作登录界面,那么今天我们就来一步步看看,如果简单的实现登录界面呢,本文给出2个例子,由简入难,希望大家能够喜欢。...2020-06-25
  • 浅谈C# 字段和属性

    这篇文章主要介绍了C# 字段和属性的的相关资料,文中示例代码非常详细,供大家参考和学习,感兴趣的朋友可以了解下...2020-11-03
  • C#中截取字符串的的基本方法详解

    这篇文章主要介绍了C#中截取字符串的的基本方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-11-03
  • C#实现简单的Http请求实例

    这篇文章主要介绍了C#实现简单的Http请求的方法,以实例形式较为详细的分析了C#实现Http请求的具体方法,需要的朋友可以参考下...2020-06-25
  • C#连接SQL数据库和查询数据功能的操作技巧

    本文给大家分享C#连接SQL数据库和查询数据功能的操作技巧,本文通过图文并茂的形式给大家介绍的非常详细,需要的朋友参考下吧...2021-05-17
  • C#中new的几种用法详解

    本文主要介绍了C#中new的几种用法,具有很好的参考价值,下面跟着小编一起来看下吧...2020-06-25
  • 使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序)

    这篇文章主要介绍了使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25
  • C#开发Windows窗体应用程序的简单操作步骤

    这篇文章主要介绍了C#开发Windows窗体应用程序的简单操作步骤,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-04-12
  • C#从数据库读取图片并保存的两种方法

    这篇文章主要介绍了C#从数据库读取图片并保存的方法,帮助大家更好的理解和使用c#,感兴趣的朋友可以了解下...2021-01-16
  • C#几种排序算法

    作者:Sabine 【导读】本文介绍了C#的四种排序算法:冒泡排序、选择排序、插入排序和希尔排序  冒泡排序 using System; namespace BubbleSorter { public class Bubb...2020-06-25
  • C#和JavaScript实现交互的方法

    最近做一个小项目不可避免的需要前端脚本与后台进行交互。由于是在asp.net中实现,故问题演化成asp.net中jiavascript与后台c#如何进行交互。...2020-06-25
  • C++调用C#的DLL程序实现方法

    本文通过例子,讲述了C++调用C#的DLL程序的方法,作出了以下总结,下面就让我们一起来学习吧。...2020-06-25
  • 轻松学习C#的基础入门

    轻松学习C#的基础入门,了解C#最基本的知识点,C#是一种简洁的,类型安全的一种完全面向对象的开发语言,是Microsoft专门基于.NET Framework平台开发的而量身定做的高级程序设计语言,需要的朋友可以参考下...2020-06-25
  • C#变量命名规则小结

    本文主要介绍了C#变量命名规则小结,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-09-09
  • C#绘制曲线图的方法

    这篇文章主要介绍了C#绘制曲线图的方法,以完整实例形式较为详细的分析了C#进行曲线绘制的具体步骤与相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25
  • C# 中如何取绝对值函数

    本文主要介绍了C# 中取绝对值的函数。具有很好的参考价值。下面跟着小编一起来看下吧...2020-06-25
  • c#自带缓存使用方法 c#移除清理缓存

    这篇文章主要介绍了c#自带缓存使用方法,包括获取数据缓存、设置数据缓存、移除指定数据缓存等方法,需要的朋友可以参考下...2020-06-25
  • c#中(&&,||)与(&,|)的区别详解

    这篇文章主要介绍了c#中(&&,||)与(&,|)的区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-06-25
  • C#学习笔记- 随机函数Random()的用法详解

    下面小编就为大家带来一篇C#学习笔记- 随机函数Random()的用法详解。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25
  • 经典实例讲解C#递归算法

    这篇文章主要用实例讲解C#递归算法的概念以及用法,文中代码非常详细,帮助大家更好的参考和学习,感兴趣的朋友可以了解下...2020-06-25