数据结构 查找

基础概念:

查找:
根据给定的值,在查找表中确定一个其关键字等于给定值的数据元素。
关键字:
用于标识一个数据元素的数据项的值。
主关键字:
可以唯一的表示一个记录的关键字。
平均查找长度ASL
ASL(Average Search Length)
image

线性表的查找

顺序查找

优点: 简单,对逻辑次序无要求,且不同储存结构均可使用。

缺点: ASL长,时间效率低

ASL: \((1+2+..+n)/n=(n+1)/2\)
方法:
就是for循环
改进:
增加哨兵:将查找关键字存入表头,从而免去查找过程中每次都要检测是否查找完成。

int Search_Seq(SSTable ST,KeyType key){
	ST.R[0].key=key;//设置监视哨
	for(i=S+T.length;ST.R[i].key!=key;---i);
	return i;
}

二分查找

前提:
只能是数组,不能是链表,并且数组有序
查找次数:
比较次数等于查找成功时位于的树的深度。
\(\lfloor log_2n \rfloor+1\)
ASL:
\(log_2 (n+1)-1\) (n>50)
image

实现:

int a[1000]; //已经从小到大排序
int find(int k, int L, int R)
{
    if (L > R)  return -1;//大于号
    int mid = (L + R) / 2;
    if (a[mid] == k)  return k;
    if (a[mid] > k)   return find(k, L, mid - 1);//是返回  ...mid-1
    if (a[mid] < k)   return find(k, mid + 1, R);
    return -1;
}

优点:
效率比顺序查找快
缺点:
只适用于有序表且限于顺序储存结构(对线性链表无效)

分块查找

分块查找(Blocking Search)又称索引顺序查找。它是一种性能介于顺序查找和二分查找之间的查找方法。
存储结构
二分查找表由"分块有序"的线性表和索引表组成。

  • (1)"分块有序"的线性表
     表R[1..n]均分为b块,前b-1块中结点个数为 ,第b块的结点数小于等于s;每一块中的关键字不一定有序但前一块中的最大关键字必须小于后一块中的最小关键字,即表是"分块有序"的。

  • (2)索引表
     抽取各块中的最大关键字及其起始位置构成一个索引表ID[l..b],即:
    IDi中存放第i块的最大关键字及该块在表R中的起始位置。由于表R是分块有序的,所以索引表是一个递增有序表。
    image

查找方式:

  • 首先查找索引表
    • 索引表是有序表,可采用二分查找或顺序查找,以确定待查的结点在哪一块。
  • 然后在已确定的块中进行顺序查找
    • 由于块内无序,只能用顺序查找。

优点:

  • 在表中插入或删除一个记录时,只要找到该记录所属的块,就在该块内进行插入和删除运算。
  • 因块内记录的存放是任意的,所以插入或删除比较容易,无须移动大量记录。
    缺点:
    需要增加一个辅助数组的存储空间
    将初始表分块排序的运算。
    适应情况:
    线性表既要快速查找,又经常动态变化。(链表和线性表都适用)

比较总结

image

树表的查找

动态查找表

优点:
当表插入,删除操作频繁时,用动态查找表(几种特殊的树)比较好。

二叉排序树

二叉查找树/二叉排序树/二叉搜索树。
(BSTree) (Binary Sort Tree)

定义:
或是一棵空树,
或是具有下列性质的二叉树:
  1) 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  2) 若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;
  3) 左、右子树也分别为二叉排序树;
  4) 没有键值相等的节点。

简单来说就是:左儿子小于父亲,右儿子大于父亲

特点:
二叉有序树,使用中序遍历,是有序的.

操作

建立:

struct node{
    int data;
    struct node *l, *r;
};

struct node *creat(struct node *root,int x){
        if(root == NULL)    // 如果 root 是空,表示当前是可以把这个点加进去的,就可以放进去了
        {
            root = new node;
            root -> data = x;
            root -> l = NULL;
            root -> r = NULL;
        }
        else {                      //如果不是,考虑两种情况
            if(x > root -> data)   //如果比当前的根节点大,去右子树找
               root -> r = creat(root -> r, x);
            else
               root -> l = creat(root -> l, x);  // 否则去左子树找
        }
        return root;   //别忘记了返回树根!
};

查找:

  // 查询元素(递归)
 Node find(Node T, int key) {
            if (!T || key==T->data) return T;
            else
            if (key < T->data)
                return find(T->left, key);
            else
                return find(T->right, key);
        }

删除结点:

删除要求:
删除后,仍然保持二叉排序树的性质。

概述:
由于中序遍历二叉排序树可以得到一个递增有序的序列。那么,在二叉排序树中删去一个结点相当于删去有序序列中的一个结点,因此我们的操作就是:

  • 将因删除结点而断开的二叉链表重新链接起来
  • 防止重新链接后树的高度增加

删除节点共有三种情况:

目标元素为叶子节点
叶子节点最容易删除,过程如下:
找到,然后删除....

目标元素只有左子树,或只有右子树
删除过程如下

  • 找到目标节点的父节点
  • 判断目标节点是父节点的左子树还是右子树
  • 将父节点的left/right指针设为目标节点不为空的子树

目标元素即有左子树,也有右子树
该情况删除操作最为复杂
补充两个概念:
前驱: 左子树的最大结点。
后继: 右子树的最小结点。
有两种方法:

  • 法1: 将结点前驱替换该结点,然后删除前驱结点。
  • 法2:将结点后继替换该结点,然后删除后继结点。

题目

7-8 中序遍历树并判断是否为二叉搜索树 (20 分)

树结构练习——排序二叉树的中序遍历

ASL:

含有n个结点的二叉排序树的平均查找长度和树的 形态有关。
最好情况: ASL=\(\lfloor log_2n \rfloor+1\)(形态比较平衡)

最坏情况: ASL=(n+1)/2 O(n) (单支树的形态)

因此为了提高效率,需要将树尽量平衡,
因此推出平衡二叉树的概念。

平衡二叉树/AVL树

目的:
让二叉树尽量平衡,提高查找效率。

定义:

  1. 可以是空树。
  2. 假如不是空树
    • 是每个节点的左右两个子树的高度差的绝对值不超过 1的二叉树 。
    • 任何一个结点的左子树与右子树都是平衡二叉树。
      性质:
      对于n个结点的AVL树,
      树的高度保持在:\(\lfloor log_2n \rfloor+1\)
      ASL也保持在:\(\lfloor log_2n \rfloor+1\)
      方法:
  • 增加平衡因子(BF):结点子树的高度 - 结点子树的高度。
  • 使每个结点的平衡因子的绝对值小于等于1。

查找操作
平衡二叉树的查找基本与二叉查找树相同。

插入操作
与二叉查找树不同:
插入时要随时保证插入后整棵二叉树是平衡的。
也就是说要实时调整不平衡树。

调整的基本方法是: 旋转 。

插入调整

需要平衡旋转的有4种情况:
image
在讨论前要先弄清楚,我们需要调节的树是哪棵:也就是:
最小不平衡子树的根结点是什么:
当进行插入操作时,
找到该需要插入结点的位置并插入后,从该结点起向上寻找(回溯),第一个不平衡的结点即平衡因子bf变为-2或2的结点。
调整的方式有四种:
image

虽然调整的步骤看似很麻烦,其实我们只需要记住调整原则即可:
保持二叉树的性质降低高度

如果是人工调整,四种方式都可以总结为四步:

  1. 找到最小不平衡子树和其节点.

  2. 从根节点出发,沿插入路径找三个节点。

  3. 调整这三个节点:找出中位数,让中位数作为根节点,其余两个一左一右

  4. 调节三个结点的子树:

    • 调整后的为左右子树的结点子树位置保持不变,
    • 如果还剩一个子树没有位置:人工插入位置。

下面具体讲四种调整方式:
~~不写了,没必要,贴下图示吧

原文

LL型调整
image

RR调整
image

LR调整
image
RL调整

image

删除调节好像不需要掌握,不写了。

其他树:

** B-树(深度了解)
就是
平衡的多路**查找树

B+树
(深度了解)
是B-树的一种变型
要求:

  1. 所有的叶子结点都是关键字
  2. 叶子结点要有序

散列表-哈希(HASH)函数

概述:

基本概念
基本思想:
记录的储存位置与关键字之间存在对应关系。
也就是以键-值对存储数据的结构,我们只要输入待查找的值即key,即可查找到其对应的值。

优点: 查找效率高
缺点: 空间效率低

我们很容易想到:
用数字数据举例
定义一个数组a[n],如果有该数据x,再a[x]=1.

但是稍加思索就会发现,上述思想是不可能在任意情况下都实现的:

  1. 不可能任意情况下都有单射的散列函数
    比如数据关键字为任意整数时,关键字-a与a该如何映射?   
  2. 散列表的大小也不可能总是保证大于所有可能的散列值。
    比如数据关键字为正整数,那么散列函数只需要令散列值等于数据关键字即可保证单射,但是如果数据总量为1000,而数据的可能最大值为10000000,难道我们创建一个大小为10000000的散列表吗?

也就是说,
我们实际实现散列表时,必须面对这两个问题:

  1. 构造尽可能“接近”单射的散列函数。(还要求函数简单,转化效率高)
  2. 找到处理冲突的好方案。

冲突:
不同的关键码映射到同一个散列地址。
同义词:
具有相同函数值的多个关键字。

总结一下构造一个号好的散列函数需要考虑:
①执行速度(即计算散列函数所需时间);
② 关键字的长度;
③ 散列表的大小;
④关键字的分布情况;
⑤ 查找频率。

散列函数构造方法

image
我们只需要知道:直接定址法和除留余数法

直接定址法:
H(key)=a*key+b
    a 和 b均为常数
image

数字分析法:
    分析关键字的各个位的构成,截取其中若干位作为散列函数值,尽可能使关键字具有大的敏感度。

平方取中法:
    这种方法是先求关键字的平方值,然后在平方值中取中间几位为散列函数的值。因为一个数平方后的中间几位和原数的每一位都相关,因此,使用随机分布的关键字得到的记录的存储位置也是随机的。

折叠法:
    将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为散列函数的值,称为折叠法。

除留余数法:
    Hash(key) = key % p
其中,p为不大于散列表表长m的整数

解决冲突

开放地址法

基本思想
有冲突时就去寻找下一个空的散列地址,只要散列表足够大, 空的散列地址总能找到,并将数据元素存入。
常用方法:
线性探测法: di为1,2,3...m-1线性序列
二次探测法 \(d_i为1^2,2^2,3^2...q^2\) 二次序列
伪随机探测法 di为随机数序列

链地址法

也称 拉链法
基本思想:
是将所有关键字为同义词的记录存储在同一个线性链表中。
实现:
m个散列地址就开设m个散列表,然后用数组将m个单链表的表头指针储存起来。

优点:

  • 非同义词不会冲突,
  • 链表空间动态申请,更适合表长不确定的情况。

性能分析

查找过程:

image

image
 a为装填因子= n/m 其中n=存入的元素个数,m=哈希表的长度

结论

  1. 平均性能优秀。
  2. 链地址法优于开地址法。
  3. 除留余数法做散列函数优于其他类型函数。
posted @ 2021-12-27 21:18  kingwzun  阅读(228)  评论(0编辑  收藏  举报