C#算法之散列表

目录
  • 1.散列函数
    • 正整数
    • 浮点数
    • 字符串
    • 组合键
    • 将 HashCode() 的返回值转化为一个数组索引
    • 自定义的 HashCode
    • 软缓存
  • 2.基于拉链法的散列表
    • 散列表的大小
    • 删除操作
    • 有序性相关的操作
  • 3.基于线性探测法的散列表
    • 删除操作
    • 键簇
    • 线性探测法的性能分析
    • 调整数组大小
    • 拉链法
    • 均摊分析
  • 4.内存的使用

如果所有的键都是小整数,我们可以使用一个数组来实现无序的符号表,将键作为数组的索引而数组中键 i 处存储的就是它对应的值。散列表就是用来处理这种情况,它是简易方法的扩展并能够处理更加复杂的类型的键。我们需要用算术操作将键转换为数组的索引来访问数组中的键值对。

使用散列表的查找算法分为两步。第一步是用散列函数将被查找的键转换为数组的一个索引理想情况下,不同的键都能转化为不同的索引值。当然,这只是理想情况,所以我们需要面对两个或多个键都会散列到相同的索引值的情况。因此散列查找的第二步是一个处理碰撞冲突的过程。解决碰撞的方法:拉链法和线性探测法。

散列表是算法在时间和空间上作出权衡的经典例子。如果没有内存限制,我们可以直接将键直接作为(可能超大)数组的索引,那么所有查找操作只需访问一次即可。另一方面,如果没有时间限制,我们可以使用无序数组并进行顺序查找,这样就只需很少的内存。而散列表使用了适度的空间和时间并在两个极端之间找到了一种平衡。我们只需要调整散列算法的参数就可以在空间和时间之间作出取舍。

使用散列表可以实现在一般应用中(均摊后)常数级别的查找和插入操作的符号表。这使得它在很多情况下成为实现简单符号表的最佳选择。

1.散列函数

散列算法的第一个问题就是散列函数的计算,这个过程会将键转化为数组的索引。如果我们有一个能够保存 M 个键值对的数组,那么我们就需要一个能够将任意键转化为数组范围内的索引([0, M-1] 范围内的整数)的散列函数。我们要找的散列函数应该易于计算并且能够均匀分布所有的键,即对于任意键,0 到 M-1 之间的每个整数都有相等的可能性与之对应(与键无关)。要理解散列,就首先要思考如何去实现一个散列函数。

散列函数和键的类型有关系。严格地说,对于每种类型的键都需要一个与之对应的散列函数。如果键是一个数,比如社会保险号,我们就可以直接使用这个数;如果键是一个字符串,比如一个人的名字,我们就需要将这个字符串转化为一个数;如果键含有多个部分,比如邮件地址,我们需要用某种方法将这些部分结合起来。

假设我们有一个应用程序,其中的键是美国的社会保险号。诸如123-45-6789之类的社会保险号是分为三个字段的9位数字。第一个字段标识地理区域发出号码的位置(例如,第一个字段为035的号码来自罗德岛,而第一个字段为214的号码来自马里兰州),其他两个字段标识个人。有十亿个不同的社会保险号,但是假设我们的应用程序只需要处理几百个密钥,那么我们就可以使用大小为M = 1000的哈希表。实现哈希函数的一种可能方法是使用三个密钥中的数字。使用右侧字段中的三位数字可能比使用左侧字段中的三位数字更可取(因为客户在地理区域上可能分布不均),但是更好的方法是使用所有九位数字一个int值,然后考虑整数的哈希函数。

正整数

将整数散列最常用方法是除留余数法。我们选择大小为素数 M 的数组,对于任意正整数 k ,计算 k 除以 M 的余数。这个函数的计算简单并且能有效地将键散布在 0 到 M-1 的范围内。如果 M 不是素数,我们可能无法利用键中包含的所有信息,可能导致无法均匀地散列散列值。例如,如果键是十进制数而 M 为 10^k ,那么我们只能利用键的后 k 位。 但如果使用素数 97 ,散列值的分布显然会更好(一个离100更远的素数会更好)。

浮点数

如果键是介于0和1之间的实数,我们可以乘以M并四舍五入为最接近的整数以获得介于0和M-1之间的索引。尽管很直观,但是这种方法是有缺陷的,因为它给按键的最高有效位赋予了更大的权重。最低有效位不起作用。解决这种情况的一种方法是将键表示位二进制数然后再使用除留余数法。

字符串

除留余数法也可以处理较长的键,如字符串,我们只需将它们当作大整数即可:

int hash = 0;
for(int i = 0;i < s.Length;i++)
{
     hash = (R * hash + s.CharAt(i)) % M;
}

如果 R 比任何字符的值都大,这种计算相当于将字符串当作一个 N 位的 R 进制值,将它除以 M 并取余。一种叫做 Horner 方法的经典算法用 N 次乘法,加法和取余来计算一个字符串的散列值。只要 R 足够小(如 31),不造成溢出,那么结果就能落在 0 至 M-1 之内。

组合键

如果键类型具有多个整数字段,则通常可以按照刚才针对String值所述的方式将它们混合在一起。

将 HashCode() 的返回值转化为一个数组索引

由于我们的目标是数组索引,而不是32位整数,因此我们在实现中将 HashCode() 和除留余数法结合,以产生0到M-1之间的整数:

private int Hash(Key x)
{
     return (x.HashCode() & 0x7fffffff) % M;
}

这段代码会将符号位屏蔽(将一个 32 位整数变为一个 31 位非负整数),然后用除留余数法。在使用这样的代码时我们一般会将数组的大小 M 取为素数以充分利用原散列值的所有位。

自定义的 HashCode

自定义的 HashCode() 需要将键平均地散布为所有可能的 32 位整数。也就是说,对于任意对象 x ,调用 x.HashCode() 有均等的机会得到 2^32 个不同整数中的任意一个 32 位整数值。更简单的方法:对实例变量使用hashCode()方法将每个实例变量转换为32位int值,然后进行算术运算。

    public class Transaction
    {
        private string who;
        private string when;
        private double amount;

        public int HashCode()
        {
            int hash = 17;
            hash = 31 * hash + who.GetHashCode();
            hash = 31 * hash + when.GetHashCode();
            hash = 31 * hash + amount.GetHashCode();
            return hash;
        }
    }

系数的具体值(这里是 31)并不是很重要。

软缓存

如果散列值的计算很耗时,那么我们可以将每个键的散列值缓存起来。第一次调用 HashCode() 时,我们需要计算对象的散列值,但之后可以直接返回缓存。

总的来说,要为一个数据类型实现一个优秀的散列方法需要满足三个条件:

  • 一致性:等价的键必然产生相等的散列值;
  • 高效性:计算简便;
  • 均匀性:均匀地散列所有的键。

在有性能要求时应该谨慎使用散列,因为糟糕的散列函数经常是性能问题的罪魁祸首。保证均匀性的最好办法也许就是保证键的每一位都在散列值的计算中起到了相同的作用。实现散列函数最常见的错误也许就是忽略了键的高位。无论散列函数的实现是什么,当性能很重要时应该测试所使用的散列函数:

计算散列函数和比较两个键,哪个耗时更多?

你的散列函数能够将一组键均匀地散布在 0到 M-1之间吗?

用简单的实现测试这些问题能够预防未来的悲剧。

这些讨论的背后是我们在使用散列时作出一个重要的假设(均匀散列假设),我们使用的散列函数能够均匀并独立地将所有键散布于 0 到 M-1 之间。这个假设是一个我们实际上无法达到的理想模型,但它是我们实现散列函数时的指导思想。原因有两点:一是设计散列函数时尽量避免随意指定参数以防止大量的碰撞,这是我们的重要目标;二是它提示我们使用数学分析来预测散列算法的性能并在实验中进行验证。

2.基于拉链法的散列表

一个散列函数能够将键转化为数组索引。散列算法的第二步是碰撞处理,也就是处理两个或多个键的散列值相同的情况。一种直接的方法是将大小为 M 的数组中的每个元素指向一条链表,链表中的每个结点都存储了散列值为该元素的索引的键值对,这种方法称为拉链法。

这个方法的基本思想就是选择足够大的 M ,使得所有链表都尽可能短以保证高效的查找。查找分两步:首先根据散列值找到对应的链表,然后沿着链表顺序查找对应的键。

拉链法的一种简单实现方法是,为 M 个元素分别构建符号表来保存散列到这里的键,可以使用之前查找树的代码。

因为我们要用 M 条链表保存 N 个键,无论键在各个链表中额分布如何,链表的平均长度肯定是 N/M。

    public class SeparateChainingHashST<Key,Value>
    {
        private int N;//键值总对数
        private int M;//散列表的大小
        private SequentialSearchST<Key, Value>[] ST;//存放链表对象的数组

        public SeparateChainingHashST(int M)
        {
            this.M = M;
            ST = new SequentialSearchST<Key, Value>()[M];
            for (var i = 0; i < M; i++)
            {
                ST[i] = new SequentialSearchST<Key, Value>();
            }
        }

        private int Hash(Key key)
        {
            return (key.GetHashCode() & 0x7fffffff) % M;
        }

        public Value Get(Key key)
        {
            return ST[Hash(key)].Get(key);
        }

        public void Put(Key key, Value value)
        {
            ST[Hash(key)].Put(key,value);
        }
    }

当你能预知所需要的符号表的大小时,这段短小的方案能够得到不错的性能。一种更可靠的方案是动态调整数组的大小。

在一张含有 M 条链表和 N 个键的散列表中,未命中查找和插入操作所需的比较次数为 ~N/M。

散列表的大小

在实现基于拉链法的散列表时,我们的目标是选择适当的数组大小 M,既不会因为空链表而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。而拉链法的一个好处就是这并不是关键性的选择。如果存入的键多于预期,查找所需的时间只会比选择更大的数组稍长;如果少于预期,虽然空间浪费但查找会非常快。当内存不是很紧张时,可以选择一个足够大的 M,使得查找需要的时间变为常数;当内存紧张时,选择尽量大的 M 仍然能够将性能提高 M倍。另一种方法是动态调整数组的大小以保持短小的链表。

删除操作

要删除一个键值对,先用散列值找到含有该键的SequentialSearchST 对象,然后调用该对象的 Delete 方法。

有序性相关的操作

散列最主要的目的在于均匀地将键散布开来,因此在计算散列后键的顺序信息就丢失了。基于拉链法的散列表实现简单,在键的顺序不重要的应用中,他可能是最快的,也是使用最广泛的符号表实现。

3.基于线性探测法的散列表

实现散列表的另一种方式就是用大小为 M 的数组保存 N 个键值对,其中 M > N 。我们需要依靠数组中的空位解决碰撞冲突。基于这种策略的所有方法被统称为开放地址散列表。

开放地址散列表中最简单的方法叫做线性探测法:当发生碰撞时(当一个键的散列值已经被另一个不同的键占用),我们直接检查散列表的下一个位置(将索引值加一)。这样的线性探测可能会产生三种结果:

  • 命中:该位置的键和查找的键相同;
  • 未命中:键为空(该位置没有键);
  • 继续查找:该位置的键和被查找的键不同。

我们用散列函数找到键在数组中的索引,检查其中的键和被查找的键是否相同。如果不用则继续查找(将索引增大,到达数组结尾时折回数组的开头),直到找到该键或者遇到一个空元素。我们将检查一个数组位置是否含有被查找的键的操作称为探测。

开放地址类的散列表的核心思想是与其将内存用作链表,不如将它们作为在散列表的空元素,这些空元素可以作为查找结束的标志。我们在实现中使用了并行数组,一条保存键,一条保存值。

    public class LinerProbingHashST<Key,Value>
    {
        private int N;//符号表中键值对的总数
        private int M = 16;//线性探测表的大小
        private Key[] keys;//键
        private Value[] values;//值

        public LinerProbingHashST()
        {
            keys = new Key[M];
            values = new Value[M];
        }

        private int Hash(Key key)
        {
            return (key.GetHashCode() & 0x7ffffff) % M;
        }

        public void Put(Key key, Value value)
        {
            if (N >= M / 2)
                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 default(Value);
        }

        /// <summary>
        /// 调整数组大小
        /// </summary>
        /// <param name="v"></param>
        private void Resize(int v)
        {
            throw new NotImplementedException();
        }
    }

和拉链法一样,开放地址类的散列表的性能也依赖于α = N/M 的比值,但意义有所不同。我们将α 称为散列表的使用率。对于基于拉链法的散列表,α 是每条链表的长度,因此一般大于 1 ;对于基于线性探测的散列表,α 是表中已被占有的空间的比例,它是不可能大于 1 的。事实上,在LinerProbingHashST 中我们不允许α 达到1(散列表被占满),因为此时未命中的查找会导致无限循环。为了保证性能,会动态调整数组的大小来保证使用率 在 1/8 到 1/2 之间。

删除操作

如何从基于线性探测的散列表中删除一个键?如果直接将该键所在的位置设为 null 会使得在此位置之后的元素无法被查找。因此我们需要将簇中被删除的右侧的所有键重新插入列表。

        public void Delete(Key key)
        {
            if (!keys.Contains(key))
                return;

            int i = Hash(key);
            while (!key.Equals(keys[i]))
                i = (i + 1) % M;
            keys[i] = default(Key);
            values[i] = default(Value);

            i = (i + 1) % M;
            while (keys[i] != null)
            {
                Key keyToRedo = keys[i];
                Value valueToRedo = values[i];
                keys[i] = default(Key);
                values[i] = default(Value);
                N--;//重新插入
                Put(keyToRedo,valueToRedo);
                i = (i + 1) % M;
            }

            N--;
            if (N > 0 && N >= M / 8)
                Resize(M/2);
        }

键簇

线性探测的平均成本取决于元素再插入数组后聚集成的一组连续的条目,也叫键簇。显然,短小的键簇才能保证较高的效率。随着插入的键越来越多,这个要求很难满足,较长的键簇会越来越多。另外,基于均匀性假设,数组的每个位置都有相同的可能性被插入一个新键,长键簇更长的可能性比短键簇更大,因为新键的散列值无论落在簇中的任何位置都会使簇的长度加一。

线性探测法的性能分析

尽管最后的结果的形式相对简单,准确分析线性探测法的性能是非常有难度的。

在一张大小为 M 并含有 N =α M 个键的基于线性探测的散列表中,,基于均匀性假设,命中和未命中的查找所需的探测次数分别为: ~ 1/2 (1 + (1 / (1 - α )) ) 和~ 1/2 (1 + (1 / (1 -α) ^ 2) ) 。特别是当α 约为 1/2 时,查找命中所需的探测次数约为 3/2 ,未命中所需的约为 5/2 。当α 趋近于 1 时,这些估计值的精确度会下降,我们会保证散列表的使用率小于 1/2 。下面我们看看动态调整数组大小。

调整数组大小

private void Resize(int cap)
{
    LinearProbingHashST<Key,Value> t = new LinearProbingHashST<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;
}

动态数组可以为我们保证α 不大于 1/2 。

拉链法

我们可以使用相同的方法在拉链表中保持较短的链表(平均长度在 2 到 8 之间):当 N >= 8*M 时,Resize(2*M);当 N > 0 && N <= 2*M 时 ,Resize( M/2 )。

对于拉链法,如果能准确地估计用例所需的散列表的大小,调整数组的工作并不是必需的,只需根据查找耗时和 (1 + N/M)成正比来选取一个适当的 M 即可。而对于线性探测法,调整数组的大小是必需的,因为当用例插入的键值对数量超过预期时它的查找时间不仅会变长,还会在散列表被填满时进入无限循环。

均摊分析

理论上,当我们动态调整数组大小时,需要找出均摊成本的上限,因为使散列表长度加倍的插入操作需要大量的探测。

假设一张散列表能够自己调整数组大小,初始为空。基于均匀性假设,执行任意顺序的 t 次查找,插入和删除操作所需的时间和 t 成正比,所使用的内存量总是在表中键的总数的常数因子范围内。

4.内存的使用

我们希望将散列表的性能调整到最优,理解它的内存使用情况是非常重要的。我们可以通过估计引用使用数量来粗略计算所需的内存量:除了存储键和值所需的空间之外,我们实现的SeparateChainingHashST 保存了 M 个SequentialSearchST 对象和它们的引用。每个SequentialSearchST 对象需要 16 字节,它的每个引用需要 8 字节。另外还有 N 个 node 对象,每个需要 24 字节以及三个引用(key , value 和 next),比二叉查找树的每个结点还多需要一个引用。在使用动态调整数组大小来保证表的使用率在 1/8 到 1/2 之间的情况下,线性探测使用 4N 到 16N 个引用。对于原始数据类型,这些计算又有所不同。可以看出,根据内存使用来选择散列表的实现并不容易。

方法 N 个元素所需的内存(引用类型)
基于拉链法的散列表 ~ 48N + 32M
基于线性探测的散列表 在 ~32N 和 ~128N 之间
各种二叉查找树 ~ 56N

还有很多关于实现散列表的算法,大多数改进都能降低 时间 - 空间的曲线:在查找耗时相同的情况下使用更少的空间,或使在使用相同空间的情况下进行更快的查找。其他方法包括提供更好的性能保证,如最坏情况下的查找成本;改进散列函数的设计等。

拉链法和线性探测的详细比较取决于实现的细节和用例对空间和时间的要求。即使基于性能考虑,选择拉链法而非线性探测法也不一定是合理的。在实践中,两种方法的性能差别主要是因为拉链法为每个键值对都分配了一小块内存而线性探测则为整张表使用了两个很大的数组。对于非常大的散列表,这些做法对内存管理系统的要求也很不同。

基于均匀性假设,期望散列表能支持和数组大小无关的常数级别的查找和插入操作是可能的。对于任意的符号表实现,这个期望都是理论上的最优性能。但散列表并非包治百病,因为:

  • 每种类型的键都需要一个优秀的散列函数;
  • 性能保证来自于散列函数的质量;
  • 散列函数的计算可能复杂而且昂贵;
  • 难以支持有序性相关的符号表操作。

到此这篇关于C#算法之散列表的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • C#实现快速排序算法

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

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

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

  • C#实现二叉查找树

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

  • C#实现希尔排序

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

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

  • C#算法之散列表

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

  • Java数据结构之散列表详解

    目录 介绍 1 散列表概述 1.1 散列表概述 1.2 散列冲突(hash collision) 2 散列函数的选择 2.1 散列函数的要求 2.2 散列函数构造方法 3 散列冲突的解决 3.1 分离链接法 3.2 开放定址法 3.3 再散列法 4 散列表的简单实现 4.1 测试 介绍 本文详细介绍了散列表的概念.散列函数的选择.散列冲突的解决办法,并且最后提供了一种散列表的Java代码实现. 数组的特点是寻址容易,插入和删除困难:而链表的特点是寻址困难,插入和删除容易.而对于tree结构,它们

  • C语言实现散列表(哈希Hash表)实例详解

    C语言实现散列表(哈希Hash表) 实例代码: //散列表查找算法(Hash) #include <stdio.h> #include <stdlib.h> #define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 #define SUCCESS 1 #define UNSUCCESS 0 #define HASHSIZE 7 #define NULLKEY -32768 typedef int Status; type

  • Java数据结构之散列表(动力节点Java学院整理)

    基本概念 散列表(Hash table,也叫哈希表),是根据关键字(key value)而直接进行访问的数据结构. 说的具体点就是它通过吧key值映射到表中的一个位置来访问记录,从而加快查找的速度. 实现key值映射的函数就叫做散列函数 存放记录的数组就就叫做散列表 实现散列表的过程通常就称为散列(hashing),也就是常说的hash 散列 这里的散列的概念不仅限于数据结构了,在计算机科学领域中,散列-哈希是一种对信息的处理方法,通过某种特定的函数/算法(散列函数/hash()方法)将要检索的

  • 散列表的原理与Java实现方法详解

    本文实例讲述了散列表的原理与Java实现方法.分享给大家供大家参考,具体如下: 概述 符号表是一种用于存储键值对(key-value pair)的数据结构,我们平常经常使用的数组也可以看做是一个特殊的符号表,数组中的"键"即为数组索引,值为相应的数组元素.也就是说,当符号表中所有的键都是较小的整数时,我们可以使用数组来实现符号表,将数组的索引作为键,而索引处的数组元素即为键对应的值,但是这一表示仅限于所有的键都是比较小的整数时,否则可能会使用一个非常大的数组.散列表是对以上策略的一种&

  • JS散列表碰撞处理、开链法、HashTable散列示例

    本文实例讲述了JS散列表碰撞处理.开链法.HashTable散列.分享给大家供大家参考,具体如下: /** * 散列表碰撞处理.开链法.HashTable散列. * 将数组里的元素位置,也设置为数组,当两个数据的散列在同一个位置时, * 就可以放在这个位置的二维数组里,解决了散列函数的碰撞处理问题 */ function HashTable() { this.table = new Array(137); this.betterHash = betterHash;//散列函数 this.show

  • java面试散列表及树所对应容器类及HashMap冲突解决全面分析

    目录 性能分析 HashMap 产生冲突原因及解决方法 HashMap 解决冲突方法 jdk7 与 jdk8 中HashMap的区别 发生冲突 扩容 使用建议 散列表 Hashmap.hashtable.concurrentHashMap.hashset: 树: treemap.treeset.hashset treeset 继承自 treemap,hashset 继承自 hashmap : 性能分析 Map 是 Java 中的接口,Map.Entry 是 Map 的一个内部接口 Map 提供了

  • java教程散列表和树所对应容器类及HashMap解决冲突学习

    目录 java中散列表.树所对应的的容器类 jdk7与jdk8中HashMap的区别 HashMap如何解决冲突 HashMap的工作原理 java中散列表.树所对应的的容器类 散列表:hashmap,hashtable,concurrentHashmap 树:hashset,treemap,treeset jdk7与jdk8中HashMap的区别 jdk7中hashMap采用数组+链表,如果过多的节点在hash时发生碰撞,如果要查找其中一个节点,需要O(n)的查找时间. jdk7中hashMa

  • C语言写一个散列表

    目录 一.快速理解散列表 二.散列函数 三.防撞 一.快速理解散列表 散列表,就是下标可以为字母的数组. 假设现有一个数组int a[100],想查找其中第40个元素,则直接输入a[40]就可以了,时间复杂度为O ( 1 ) O(1)O(1). 问题在于,当下标不是数字,而是一个字符串的时候,可能需要一个超大的空间才能将所有下标妥善地存放在特定的位置.例如,若以大小写字母作为下标索引,那么一位就需要预留52个空间,10位就需要52的10次方 这么大的空间,根本没有设备可以满足. 好在,52的10

  • 散列算法与散列码(实例讲解)

    一.引入 /** * Description:新建一个类作为map的key */ public class Groundhog { protected int number; public Groundhog(){ } public Groundhog(int number) { this.number = number; } @Override public String toString() { return "Groundhog{" + "number=" +

随机推荐