数据结构(二):散列表、二叉搜索树

《算法导论》笔记

1 散列表(hash table)

散列表是实现字典操作的一种有效数据结构,查找一个元素的时间最坏情况是O(n),而在合理假设下,查找性能都是极好的,平均时间可达O(1)
散列表是普通数组的推广,寻址可以类比数组的O(1)时间内访问任意位置

散列表是根据关键字计算相应下标,通过链接(chaining)方法解决冲突(collision),冲突就是多个关键字映射到数组的同一个下标

直接寻址表:使用数组表示动态集合,数组每个位置称为槽(slot),把对象存放在槽中

使用散列函数根据关键字k计算出槽的位置,将全域U映射到散列表的槽中,其中散列表的槽占用内存远远小于全域U,但是存在两个关键字同时映射到同一个槽中,从而引起冲突

散列函数实现映射,需要满足给定输入始终产生相同的结果,冲突解决方法包括:链接法和开放寻址法

链接法

链接法是把散列到同一槽中的所有元素放到一个链表中
插入元素x如果为了防止重复存储需要进行遍历操作,花费额外复杂度

删除元素x(这里x是指向对象的指针,而这个对象存放的是关键字key和链表指针),如果对于采用单向链接,需要遍历链表,找到元素x指向对象的前驱元素的next属性让它指向元素x的后继元素,如此操作需要额外时间;如果对于采用双向链接,通过元素x就可以知道其指向对象中的prev和next指针,从而以O(1)时间让prev对象的next指向元素x的next对象即可,即删除操作变得十分简单(详见https://blog.csdn.net/yuanbohx/article/details/6664855)

note:上述说法存在漏洞,就是对于单向链表,可以将元素x的next对象的key赋值到元素x指向的对象,删除next对象即可,当然如果元素x指向对象是链表最后的节点则直接删除即可。这种做法实际上不是释放元素x对应内存,但也能以O(1)时间实现删除操作

链接法散列分析

装载因子(load factor):一个散列表具有m个槽位,能存放n个元素,则n/m为装载因子,装载因子可以大于、等于或小于1

如果散列表所有关键字都映射到同一个槽中,则散列表等同于一个链表,其查找性能为O(n)

散列方法的平均性能依赖于选取的散列函数,目标是将所有关键字能够均匀映射到槽位上

对于简单均匀散列,采用链接法解决冲突,一次查找的平均时间为O(1+α),其中O(1)是散列函数计算时间,α是槽中链表长度,O(α)是查找时间

因此,散列表的槽数要与所需要存放的元素个数成正比,即n=O(m),一个数量级,则查找的平均操作能达到常数时间,插入和删除的最坏情况也是常数时间

散列函数

好的散列函数应近似满足简单均匀散列假设,将关键字均匀映射到槽位

使用启发式方法来构造性能好的散列函数

  • 映射到自然数集
    多数散列函数都假定关键字的全域是自然数集,例如字符串可以根据ASCII字符进行转换,然后进行相应的数值计算进行转化

  • 除法散列法
    通过取k除以m的余数来确定槽位,m不能是2的幂,不然会导致余数实际上就是关键字的最后几位数字
    h ( k ) = k m o d m h(k) = k mod m h(k)=kmodm

  • 乘法散列法
    将关键字k乘以常数A,然后提取小数部分,再用m乘以这个值,再向下取整

h ( k ) = f l o o r ( m ( k A m o d 1 ) ) h(k) = floor(m(kA mod 1)) h(k)=floor(m(kAmod1))

乘法散列法对m的选择不敏感
A的取值近似于 ( ( 5 ) − 1 ) / 2 (\sqrt(5)-1)/2 (( 5)1)/2是比较理想的值

  • 全域散列法

随机选取散列函数

开放寻址法

开放寻址法的装载因子不超过1,不采样链表,而是将所有元素都放在散列表中

有的元素都存放在散列表里,每个表项或包含动态集合的一个元素或者NIL。当查找某个元素时,要系统的检查所有表项,直到找到所有的元素或者最终查明元素不在表中。为了使用开放寻址法插入一个元素,需要连续的检查散列表,或称为探查(probe),直到找到一个空槽来放置待插入的关键字为止。检查的顺序不一定是0,1,2…m的顺序序列,而是依赖于待插入的关键字。

二叉搜索树

基本情况

二叉搜树的基本操作花费时间与树的高度成正比,最坏运行时间为O(lg n),但如果构建的树及其不平衡,甚至退化为链表,则最坏情况可能是O(n)

二叉搜索树,每个节点包含key和指针p、left和right,分别指向父节点、左孩子和有孩子,重要性质是左子树的节点关键字不大于其当前节点的关键字,右子树的关键字不小于其当前节点的关键字

二叉搜索树可以采用简单的递归进行遍历,包含中序遍历、先序遍历和后续遍历,具体实现可查看https://blog.csdn.net/baidu_35231778/article/details/118785115,
遍历过程都是线性时间

查询二叉搜索树

利用二叉搜索树性质,可以方便查找某一关键字所对应的节点:

TreeNode search(node, k){
    if (k == node->key){
        return node;
    }
    else if (k < node->key){
        return search(node->left, k);
    }
    else{
        return search(node->right, k);
    }
}

或者使用while循环

TreeNode search(node, k){
    while (node != nullptr and k != node->key){
        if (k < node->key){
            node = node -> left;
        }
        else{
            node = node -> right;
        }
    }
    return node;
}

查询最小值和最大值就比较容易了,最小值对应的是一直沿着left孩子指针搜寻,直到遇到NULL,前一个节点即为关键字最小的节点,最大值搜寻也类似,只不过是沿着righ孩子指针搜寻

有了关键字,可以通过中序遍历来查询其子节点

这些查询的操作都与树的高度有关,也就是在O(h)时间内完成

插入和删除

插入节点,就是根据节点的关键字从树跟开始查询,如果小于当前树节点的关键字,则继续查询其左节点,否则查询右节点,直至遇到null节点,就是插入节点的位置,而其父节点也即是插入节点的父节点

void insert(Tree T, TreeNode z){
    TreeNode y;
    TreeNode x = T.root;
    while (x != nullptr){
        y = x; // 记录父节点
        if (z.key < x.key){
            x = x.left;
        }
        else{
            x = x.right;
        }
    }
    z.p = y;
    if (y == nullptr){
        T.root = z; // tree T was empty
    }
    else if (z.key < y.key){
        y.left = z;
    }
    else{
        y.right = z;
    }
}

删除操作就比较复杂了,删除节点z分成三种情况

  • z没有子节点
    则直接删除,修改其父节点的指针指向null即可
  • z只有一个孩子节点
    则将这个孩子节点提升到z的位置,并修改其父节点的指针指向这个新的节点
  • 如果z有两个孩子
    则从z的右子树中找z的后继y,并让y占据z的位置,这个y需没有左节点,而其父节点可以直接指向y的右节点

插入和删除操作的时间复杂度也是O(h)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值