字典和集合,哈希表

概述

数据的存储和访问是计算机最基本的功能,本文主要讨论字典检索,基于关键码的数据存储和检索。字典就是基于关键码的数据存储与检索的数据结构。在一些专业书籍或编程语言里,字典也被称为查找表,映射或者关联表等。下面将讨论基于顺序表、二叉树和其他树形结构等的字典实现。

静态字典:在建立之后,这种字典的内容和结构不再发生变化,主要操作只有检索。无论如何,创建工作只需要做一次,而检索是在字典的整个生命周期中反复进行的操作。
动态字典:在初始创建后,这种字典的内容将一直处于动态变化之中。除了检索之外,最重要的基本操作还包括数据项的插入和删除等。不仅需要考虑检索的效率,还必须考虑插入和删除操作的效率,需要在多种相互影响中作出权衡。

注意:各种字典都不应该允许修改字典关联中的关键码,因为关键码用于确定其所在项在字典里的存储位置,以支持高校检索。如果允许修改关键码,就可能破坏字典数据结构的完整性,导致后续检索操作失败。

字典线性表实现

将关键码和值的关联作为元素顺序存入线性表,形成关联的序列,可以作为字典的一种实现技术。在python里可以用list实现,也可用二元的tuple。由于没有任何限制,插入新关联可以用append实现,删除可以在定位后用list的pop操作实现。

优缺点:
1. 数据结构和算法都很简单,检索、删除等操作中只需要比较关键码相同或不同即可,适用于任意关键码类型,不要求关键码集合存在某种顺序关系
2. 平均检索效率低,需要线性时间,表长度n较大时,检索很耗时
3. 删除操作的效率也比较低,不适合频繁变动的字典。

有序线性表和二分法检索

若关键码取自一个有序集合,例如字符串的字典序,我们就可以按照关键码的大小顺序排列字典里的项。在数据按序排列时,可以采用二分法实现快速检索。
二叉树可以表示二分法的检索过程,称为二分法检索过程的判定书。树中节点所标的数为数据项的关键码。

算法分析:在向表中插入数据时,必须维持关键码有序,找到正确位置后,通过逐项后移数据的方式腾出空位后插入新数据项。线性时间是无法避免的。且需要连续的存储快,不适合实现很大的动态字典。

散列和散列表

具体方法
1 选定一个整数的下标范围,建议一个包括相应元素位置范围的顺序表
2 选定一个从实际关键码集合到上述下标范围的适当映射h:在需要存入关键码key的数据时,将其存入第h(key)个位置;在遇到以key为关键码检索时,直接去找第h(key)个位置的元素。其中的h称为散列函数或哈希函数、

散列思想:散列的思想是在信息和计算领域中逐渐发展起来的,其应用远远超出了数据存储和检索的范围。所谓散列,是以某种精心设计的方式,从一段可能很长的数据中生成一段很短的信息串,例如简单的整数或是字符串,最终都是二进制串,例如文件的完整性检查、网页中安全性检查、安全协议等等。

在通常情况下,散列函数h是从一个大集合到一个小集合的映射,显然不可能是单射,必然会出现不同的关键码被h对应到同一个下标位置的情况,称为冲突(collision)。冲突是散列表使用中必然会出现的情况。我们定义负载因子

α=/ α = 哈 希 表 中 实 际 数 据 项 数 / 哈 希 表 的 基 本 存 储 区 能 容 纳 的 元 素 个 数

负载因子越大,出现冲突的概率越大。因此基于散列技术实现字典时,必须解决两个问题:散列函数的设计与冲突消解机制。
散列函数
1. 散列函数应该能把关键码映射到值域index中尽可能大的部分。显然,若扩大函数值的范围,出现冲突的可能性下降。如果某个下标不是散列函数的可能值,这个位置就可能无法用到。应尽量避免这种情况。
2. 不同关键码的散列值应在index里均匀分布,有可能减少冲突。当然实际情况还与真实数据里不同关键码出现的分布有关。
3. 函数的计算比较简单

用于整数关键码的散列方法
数字分析法、折叠法、中平方法。散列函数的映射关系越乱越好,越不清晰越好

常用散列函数
除余法:适用于整数关键码。用key除以某个不大于散列表长度m的整数p。为了存储管理方便,人们经常将m取为2的某个幂值,此时p可以取小于m的最大素数。
基数转换法:适用于整数或字符串关键码。取一个正整数r,把关键码看作基数为r的数,将其转化为十进制或十二进制。通常取r为素数以减少规律性。这时的关键码取值可能不合适,可以考虑用除余法或是折叠法,或删除几位数字等方法,将其归入所需下标的范围。

冲突的消解:内消解技术与外消解技术
内消解的基本方法称为开地址法,其基本思想是,在准备插入数据并发现冲突时,设法在基本存储区里为需要插入的数据项另行安排一个位置。为此需要设计一种系统的并且易于计算的位置安排方式,称为探查方式。
外消解的一种技术是设置一个溢出存储区。当插入关键码的散列位置没有数据时就直接插入,发生冲入时将相应数据和关键码一起存入溢出区。数据在溢出区里顺序排列。对应的检索和删除操作也是先找到散列位置,如果那里有数据单关键码不匹配,就转到溢出区顺序检索,直到找到要找的关键码,或确定相应数据不存在。当随着溢出区中数据的增长,字典的性能将趋向于线性。
接下来讨论桶散列,拉链法。在桶散列技术里,散列表的每个元素只是一个引用域,引用这一个保存实际数据的存储桶。在拉链法中一个存储桶就是一个链接的节点表。具有相同散列值的数据项都保存在这个散列值对应的链表里。桶散列结构可用于实现大型字典,用于组织大量的数据,包括外存文件等。

散列表的性质
1. 扩大存储区,用空间换时间。无论采用哪种消解技术,随着元素的增加,散列表的负载因子增大,出现冲突的可能性也会同样增大。存储区扩大后,需要相应调整散列函数,尽可能利用增加的存储单元,例如可以应用除余法。然后,需要把字典里已有的数据项重新散列到新存储区。可见,扩大存储要付出重新分配存储区和再散列装入数据项的代价。对于散列表,只需要简单的扩大存储,就能从概率上提高字典的操作效率,明天的用空间换时间
2. 负载因子和操作效率。在采用内部消解技术时,负载因子 0.75 ≤ 0.75 时,散列表的平均检索长度接近于常数。如果采用桶散列技术,负载因子就是桶的平均大小,采用拉链法时为链表的平均长度。这种技术可以容忍任意大的负载因子,但随着负载因子变大,检索时间也趋于线性。

高效的字典实现技术也基于一些假设:
1 实际存入字典的数据的散列函数值均匀分布
2 字典散列表的负载因为不太高,试验证明应在0.7以下

可能技术和实用情况
1 给用户提供检查负载因子和主动扩大散列表存储区的操作。这样,用户可以在一段效率要求高的计算之前,根据需要首先设定组头大的存储
2 对于开地址散列表,记录或检查被删除项的量或比例,在一定情况下自动整理。最简单的方法是另外分配一块存储区,把散列表里的有效数据项重新散列到新区。

二叉排序树

二叉排序树是一种存储数据的二叉树,把字典的数据存储和查询功能融合在二叉树的结构里,有可能得到较高的检索效率。采用链接式的实现方式,数据项的插入、删除操作都比较灵活方便。基本思想:

  • 在二叉树的节点里存储字典中的信息
  • 为二叉树安排好一种字典数据项的存储方式,使字典查询等操作可以利用二叉树的平均高于远远小于树中节点个数的性质,使检索能沿着树中路径进行,从而获得较高的检索效率。

二叉树排序可以用于实现关键码有序的字典,树中数据的存储和实用都利用了数据或是关键码的序。接下来介绍二叉排序树的定义:(递归结构)

  • 根节点保存着一个数据项,及其关键码
  • 如果左子树不空,则左子树的所有节点保存的关键码的值均小于根节点保存的值
  • 若右子树不空,则右子树的所有节点保存的关键码的值均大于根节点保存的值
  • 非空的左子树或右子树也是二叉排序树

根据其存储方式,如果对二叉排序树做中序遍历,得到的讲师一个按关键码值上升排序的序列。若树中存在重复的关键码,虽然关键码相同的数据项的前后顺序不能确定,但这些项必定位于中序遍历序列中的相邻位置。同一数据集合对应的二叉排序数不唯一。

性质:一颗节点中存储着关键码的二叉树是二叉排序树,当且仅当通过中序遍历这颗二叉树得到的关键码序列是一个递增序列。

    def bt_search(btree, key):
    bt = btree
        entry  = bt.data
        if key < entry.key:
            bt = bt.left
        elif key > entry.key:
            bt = bt.right
        else:
            return entry.key
    return None

根据被检索关键码与当前节点关键码的比较情况,决定向左走还是向右走。遇到要检索的关键码时成功结束,否则返回None。

二叉排序字典类

现在考虑基于二叉排序树的思想定义一个字典类,并分析和实现类中的主要算法。这个类的借口应该参考前面给出的字典抽象数据类型,其基础元素是前面定义的二叉树节点类和关联类,检索方法是上述检索方法的简单修改。并考虑数据项的插入和删除操作,一般要修改树的结构。
在进行插入操作时,如果遇到与检索关键码相同的数据项,我们考虑用新值代替旧值,保证字典中不会出现关键码重复的项。基本算法如下:

  • 如果二叉树为空,则直接建立一个包含新关键码和关联值的树根节点
  • 否则搜索新节点的插入位置。
class DictBinTree:
    def __init__(self):
        self.__root = None

    def is_empty(self):
        return self.__root is None

    def search(self, key):
        bt = self.__root
        while bt is not None:
            entry = bt.data
            if key < entry.key:
                bt = bt.left
            elif key > entry.key:
                bt = bt.right
            else:
                return entry.value
        return None

    def insert(self, key, value):
        bt = self.__root
        if bt is None:
            self.__root = BinTNode(Assoc(key, value))
            return 
        while True:
            entry = bt.data
            if key < entry.key:
                if bt.left is None:
                    bt.left = BinTNode(Assoc(key, value))
                    return
                bt = bt.left
            elif key > entry.key:
                if bt.right is None:
                    bt.right = BinTNode(Assoc(key, value))
                    return 
                bt = bt.right
            else:
                bt.data.value = value
                return 

    #中序遍历
    def values(self):
        t, s = self.__root, SStack()
        while t is not None or not s.is_empty():
            while t is not None:
                s.push(t)
                t = t.left
            t = s,pop()
            yield t.data.value
            t = t.right
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值