数据结构——深入探析查找问题

查找

  • 前言
  • 查找
    • 关于查找的概念
    • 对查找表的操作
    • 查找表的分类
    • 查找表中的关键字
    • 如何进行查找
    • 静态查找表
      • 顺序表的查找
      • 有序表的查找
        • 折半查找
      • 索引顺序表的查找
    • 动态查找表
      • 二叉排序树(二叉查找表)
      • 二叉排序树的查找算法
      • 二叉排序树的插入算法
      • 二叉排序树的删除算法
      • 查找性能分析
      • 平衡二叉树(AVL树)
    • 哈希表
      • 哈希表的查找
      • 哈希表的定义
      • 构造哈希函数的方法
        • 1、直接定址法
        • 2、数字分析法
        • 3、平方取中法
        • 4、折叠法
        • 5、除留余数法
        • 6、随机数法
      • 处理冲突的方法
      • 哈希表的查找
      • 哈希表查找分析

前言

今天我们学习的是数据结构数据运算中的查找运算
数据结构——深入探析查找问题_第1张图片

查找

关于查找的概念

  • 查找表是由同一类型的数据元素(或记录)构成的集合
  • 由于集合中的数据元素之间存在着松散的关系,所以查找表是一种应用灵便的结构
  • 根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素或记录。如果查找表中存在这样的一个记录则称查找成功,查找结果给出整个记录的信息或指示该记录在查找表中的位置;否则称为查找不成功,查找结果给出空记录或空指针

对查找表的操作

  1. 查询某个特定的数据元素是否在查找表中
  2. 检索某个特定的的数据元素的各种属性
  3. 在查找表中插入一个数据元素
  4. 按查找表中删除一个数据元素

查找表的分类

查找表可以分为以下两种:

  1. 静态查找表
    仅仅只是作为查询和检索
  2. 动态查找表
    有时在查询之后,还会将查询结构为不在查找表中的数据元素插入到查找表中;或者从查找表中删除查询结果为在查找表中的数据元素

查找表中的关键字

关键字是数据元素(或记录)中的某个数据项的值,用来标记一个数据元素(或记录)

  • 如果这个关键字可以唯一识别一个记录,称之为主关键字
  • 如果此关键字能够识别若干个记录,则称之为次关键字

如何进行查找

查找的方法取决于查找表的结构

由于查找表中的数据元素之间不存在明显的组织规律,因此不便于查找

为了提高查找的效率,需要在查找表中的元素之间人为的附加某种确定的关系也就是说使用另外一种结构来表示查找表。

对于静态查找表:
根据表元素的特点可以采用顺序查找,折半查找

对于动态查找表:
通常将序列构造为一棵二叉树,一般是均衡二叉树(AVL树),这样常见的查找操作可以转为对二叉树的操作

特殊的,在存储位置和关键字之间建立一个确定的对应关系——需要用到哈希函数和哈希查找

静态查找表

静态查找表的顺序存储结构:

typedef struct {
    ElemType *elem;
        //数据元素村粗空间基址 
        //建表时按实际长度分配 0号单元留空
    int length;     //表的长度
}SSTable;


数据元素类型的定义:

typedef struct {
	keyType key;	//关键字域
	…………			//其他属性域
}ElemType;

顺序表的查找

以顺序表或线性表表示静态查找表
顺序表的查找过程:
数据结构——深入探析查找问题_第2张图片

int location(SqList L,ElemType e,Status(*compare)(ElemType,ElemType)) {
    k = l;
    p = L.elem;
    while(k<=L.length && !(*compare)(*++p.c))
        k++;
    if(k<=L.length)
        retutn k;
    else
        return 0;
}   //location

算法改进:
数据结构——深入探析查找问题_第3张图片

int Search_Seq(SSTable ST,KeyType key) {
    //顺序表ST中顺序表查找其关键字等于key的数据元素
    //若找到则函数值为该元素在表中的位置 否则为0
    ST.elem[0].key = key;   //哨兵
    for(i=ST.length; ST.elem[i].key!=key;--i);
            //从后向前找
            //找不到时i=0
    return i;
}

分析顺序查找的时间性能:
定义:查找算法在查找成功时的平均查找长度(Average Search Length)

为确定记录在查找表中的位置,需要和给定值进行比较的关键字个数的期望值:

数据结构——深入探析查找问题_第4张图片
对于顺序表而言:
Ci= n-i+1

ASL = nP1+(n-1)P2+… +2Pn-1+Pn

在等概率查找的情况下,数据结构——深入探析查找问题_第5张图片

顺序表查找的平均查找长度:
数据结构——深入探析查找问题_第6张图片
在不等概率的情况下,ASL在Pn ≥ Pn-1 ≥ ꞏꞏꞏ ≥ P2 ≥ P1 时取最小值。

总结:

若查找概率无法事先测定,则查找 过程采取的改进办法是,在每次查找之后,将刚刚查找到的记录直接移至表尾 的位置上。

有序表的查找

虽然顺序查找表的查找算法简单,但是平均查找长度大,特别不适合于表长较大的查找表

如果以有序表表示静态查找表,则查找过程可以基于折半查找。

折半查找

  • 查找过程:每次将待查记录所在区间缩小一半
  • 适用条件:采用顺序存储结构的有序表
  • 算法实现 :
    • 设low、high和mid分别指向待查元素所在区间的下界、 上界和中点,k为给定值
    • 初始时,令low=1,high=n,mid=【(low+high)/2】(【】表示向下取整)
    • 让k与mid指向的记录比较
    • 若k==r[mid].key,查找成功
    • 若k
    • 若k>r[mid].key,则low=mid+1
    • 重复上述操作,直至low>high时,查找失败
int Search_Bin(SSTable ST,KeyType key) {
    low = 1; high = ST.length;      //置区间初值
    while(low<=high) {
        mid = (low+high)/2;
        if(EQ(key,ST.elem[mid].key))
            return mid;     //找到待查元素
        else if(LT((key , ST.elem[mid].key))
            high = mid -1;       // 继续在前半区间进行查找 
        else
            low = mid + 1; // 继续在后半区间进行查找

    }
    return0;                 // 顺序表中不存在待查元素 
}

分析折半查找的平均查找长度:

数据结构——深入探析查找问题_第7张图片
判定树:描述查找过程的二叉树

有n个结点的判定树的深度为【log2n】+1

折半查找法在查找过程中进行的比较次数 最多不超过其判定树的深度

一般情况下,表长为n 的折半查找 的判定树的深度和含有n 个结点的完全 二叉树的深度相同

数据结构——深入探析查找问题_第8张图片

索引顺序表的查找

数据结构——深入探析查找问题_第9张图片

索引顺序表 = 索引 + 顺序表
一般情况下,索引是一个有序表

索引顺序表的查找过程(也叫分块查找特点:块间有序,块内无序)

  1. 由索引确定记录所在区间
  2. 在顺序表的某个区间内进行查找

可见,索引顺序查找的过程也是一个 “缩小区间”的查找过程。

算法实现:

  1. 用数组存放待查记录,每个数据 元素至少含有关键字域
  2. 建立索引表,每个索引表结点包 括两个域:大关键字、指向本块 第一个结点的指针
    数据结构——深入探析查找问题_第10张图片

分块查找的过程:

  1. 对索引表使用折半查找法(因为索引表是有 序表)
  2. 确定了待查关键字所在的子表后,在子表内 采用顺序查找法(因为各子表内部是无序表)

索引顺序查找的平均查找长度= 查找“索引”的平均查找长度 + 查找“顺序表”的平均查找长度

分块查找性能分析:
数据结构——深入探析查找问题_第11张图片

分块查找优缺点:
优点:插入和删除比较容易,无需进行大量移动。
缺点:要增加一个索引表的存储空间并对初始索引 表进行排序运算
使用情况:如果线性表既要快速查找又经常动态变 化,则可采用分块查找

三个典型的静态查找方法的比较:
数据结构——深入探析查找问题_第12张图片

动态查找表

数据结构——深入探析查找问题_第13张图片

从这几种查找表的特性可以看出:

  1. 从查找性能看,最好情况能达(logn),此 时要求表有序

  2. 从插入和删除的性能看,最好 情况能达(1),此时要求存储结构是链表。

二叉排序树(二叉查找表)

定义:
二叉排序树或者是一棵空树;或者是具有如 下特性的二叉树:

  1. 若它的左子树不空,则左子树上所有结 点的值均小于根结点的值
  2. 若它的右子树不空,则右子树上所有结 点的值均大于根结点的值
  3. 它的左、右子树也都分别是二叉 排序树

通常取它的左、右子树也都分别是二叉 排序树

typedef struct BiTNode {    //结点结构
    TElemType data;
    struct BiTNode *lchild,*rchild; //左右孩子指针
}BiTNode,*BiTree;

二叉排序树的查找算法

若二叉排序树为空,则查找不成功;否则

  1. 若给定值等于根结点的关键字,则查 找成功
  2. 若给定值小于根结点的关键字,则继 续在左子树上进行查找
  3. 若给定值大于根结点的关键字,则继 续在右子树上进行查找

算法:

在根指针T 所指二叉排序树中递归地查找其 关键字等于key 的数据元素,若查找成功, 则返回指针p 指向该数据元素的结点,并返回 函数值为TRUE;否则表明查找不成功,返回 指针p 指向查找路径上访问的最后一个结点, 并返回函数值为FALSE, 指针f 指向当前访问 的结点的双亲,其初始调用值为NULL

Status SearchBST(BiTree T,KeyType key,BiTree f,BiTree &p) {
    if(!T) {
        p = f;
        return FALSE;   //查找不成功
    }
    else if(EQ(key,T->data.key)) {
        p = T;
        return TRUE;       
    }   //查找成功
    else if (LT(key,T->data.key))
        SearchBST (T->lchild, key, T, p );  // 在左子树中继续查找 
    else
         SearchBST (T->rchild, key, T, p ); // 在右子树中继续查找 
}

二叉排序树的插入算法

二叉排序树的操作-生成
从空树出发,经过一系列的查找、插入操作之后, 可生成一棵二叉排序树

不同插入次序的序列生成不同形态的二叉排序树

  • 根据动态查找表的定义,“插入”操 作在查找不成功时才进行
  • 若二叉排序树为空树,则新插入的结 点为新的根结点;否则,新插入的结 点必为一个新的叶子结点,其插入位 置由查找过程得到
Status InsertBST(BiTree &T,ElemType e) {
    // 当二叉排序树中不存在关键字等于e.key 的 
    // 数据元素时,插入元素值为e 的结点,并返 
    // 回TRUE; 否则,不进行插入并返回FALSE 
    if(!SearchBST ( T, e.key, NULL, p )) 
    { 
        s = (BiTree)malloc(sizeof(BiTNode)); // 为新结点分配空间
        s->data = e;  
        s->lchild = s->rchild = NULL; 
        if( !p )  T = s;     // 插入s 为新的根结点 
        else  if( LT(e.key, p->data.key) ) 
            p->lchild = s;// 插入*s 为*p 的左孩子
       else
            p->rchild = s;     // 插入*s 为*p 的右孩子  
       return TRUE;     // 插入成功    
    }
    else 
        return FALSE
}

二叉排序树的删除算法

和插入相反,删除在查找成功之后进行,并且要 求在删除二叉排序树上某个结点之后,仍然保持 二叉排序树的特性。

可以分为三种情况:

  • 被删除的结点是叶子:将其双亲结点中对应指针域的值改为空
  • 被删除的结点只有左子树或者只有右子树:将其双亲结点的相应指针域的值改为指向被删除节点的左子树或右子树
  • 被删除的结点既有左子树,也有右子树 :以其前驱替代,然后再删除该前驱结点

二叉排序树的删除操作——删除:

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

删除叶结点,只需将其双亲结点指向它的指 针清零,再释放它即可

被删结点缺右子树,可以拿它的左子女结点 顶替它的位置,再释放它

被删结点缺左子树,可以拿它的右子女结点 顶替它的位置,再释放它

被删结点左、右子树都存在,可以在它的右 子树中寻找中序下的第一个结点(关键码小), 用它的值填补到被删结点中,再来处理这个结 点的删除问题

算法描述:

Status DeleteBST(BiTree &T,KeyType key) {
    // 若二叉排序树T 中存在其关键字等于key 的 
    // 数据元素,则删除该数据元素结点,并返回 
    // 函数值TRUE,否则返回函数值FALSE
    if(!T) reture FALSE;
        //不存在关键字等于key的数据元素
    else {
        if(EQ(key,T->data.key)) {
            Delete(T); return TRUE;
        else if(LT(key,T->data.key))
            DeleteBST(T->lchild,key); //继续在左子树中进行查找 
        else 
            DeleteBST(T->rchild,key); // 继续在右子树中进行查找
        }
    } 
}

// 这里并没有调用前面的SearchBST函 数判断结点是否已存在,
// 而是边查找 边确定,这样可以巧妙地利用递归中 的参数T->lchild,T->rchild和父结 点建立了联系,
// 在后面的Delete函数 中,删除的是以p为根的二叉排序树 上的p结点。


//其中的删除操作过程如下所描述:
void Delete(BiTree &p) {
    //从二叉排序树中删除结点p, 
    // 并重接它的左子树或右子树 
    if(!p->rchild) {
// 右子树为空树则只需重接它的左子树 
        q = p;
        p = p->lchild;
        free(q);
    }
    else if(!p->lchild) {
// 左子树为空树只需重接它的右子树
        q= p;
        p = p->rchild;
        free(q);
    }
    else {
        // 左右子树均不空
        q = p;
        s= p->lchild;
        while(s->rchild) {
            q= s;
            s = s->rchild;  // s 指向被删结点的前驱

        }
        p->data = s->data;
        if(q!=p) 
            q->rchild = s->lchild;////p->lchild有右子树时, //重接*q的右子树 
        else q->lchild = s->lchild;// 重接*q的左子树
        free(s);
    }
}

查找性能分析

对于每一棵特定的二叉排序树,均可按照平 均查找长度的定义来求它的ASL 值,显然, 由值相同的n 个关键字,构造所得的不同 形态的各棵二叉排序树的平均查找长度的 值不同,甚至可能差别很大

平均查找长度和二叉树的形态有关:

  • 最好:log2n(形态匀称,与二分查找的判定树相似)
  • 最坏: (n+1)/2(单支树)

下面讨论平均情况:
不失一般性,假设长度为n的序列中有k 个关键字小于第一个关键字,则必有n-k-1 个关键字大于第一个关键字,由它构造的二叉 排序树的平均查找长度是n和k 的函数

 P(n, k)      ( 0 <=k <=n-1 ) 。 

假设n 个关键字可能出现的n! 种排列的 可能性相同,则含n 个关键字的二叉排序 树的平均查找长度:
在这里插入图片描述
在等概率查找的情况下,

在这里插入图片描述

数据结构——深入探析查找问题_第14张图片
如何提高二叉排序树的查找效率? 尽量让二叉树的形状均衡

平衡二叉树(AVL树)

定义:

或者是一棵空树,或者是具有下 列性质的二叉树:它的左子树和右子树 都是平衡二叉树,且左子树和右子树的 深度之差的绝对值不超过1. 若将二叉树上结点的平衡因子(BF)定义 为该结点的左子树的深度减去它的右子 树的深度,则平衡二叉树上所有结点的 平衡因子只可能是-1,0,1

  • 任一结点的平衡因子只能取:-1、0 或1;如果 树中任意一个结点的平衡因子的绝对值大于1, 则这棵二叉树就失去平衡,不再是 AVL 树
  • 对于一棵有 n 个结点的 AVL 树,其高度保持在 O(log2 n )数量级, ASL 也保持在O(log2 n )量级

如果在一棵AVL树中插入一个新结点,就有可能造 成失衡,此时必须重新调整树的结构,使之恢复 平衡。我们称调整平衡过程为平衡旋转。

数据结构——深入探析查找问题_第15张图片
1、LL平衡旋转:
若在A的左子树的左子树上插入 结点,使A的平衡因子从1增加至 2,需要进行一次顺时针旋转。 (以B为旋转轴)

2、RR平衡旋转:
若在A的右子树的右子树上插入 结点,使A的平衡因子从-1增加 至-2,需要进行一次逆时针旋转。 (以B为旋转轴)

3、LR平衡旋转:
若在A的左子树的右子树上插入 结点,使A的平衡因子从1增加至 2,需要先进行逆时针旋转,再 顺时针旋转。 (以插入的结点C为旋转轴)

4、RL平衡旋转:

若在A的右子树的左子树上插入结 点,使A的平衡因子从-1增加至-2, 需要先进行顺时针旋转,再逆时 针旋转。 (以插入的结点C为旋转轴)

哈希表

对于频繁使用 的查找表:

希望ASL=0,只有一个办法:预先知道所查关键字在表 中的位置,即,要求:记录在表中位置和 其关键字之间存在一种确定的关系。

哈希表的查找

基本思想:
记录的存储位置与关键字之间存在 对应关系,Loc(i)=H(keyi)
数据结构——深入探析查找问题_第16张图片
优点:
查找速度极快O(1),查找效率与元素个数n无关

哈希表的定义

根据设定的哈希函数H(key) 和所选中 的处理冲突的方法,将一组关键字映 象到一个有限的、地址连续的地址集 (区间) 上,并以关键字在地址集中的 “象”作为相应记录在表中的存储位 置,如此构造所得的查找表称之为 “哈希表”。

构造哈希函数的方法

1、直接定址法

  • 哈希函数为关键字的线性函数 :H(key) = key 或者H(key) = a x key + b

此法仅适合于: 地址集合的大小= = 关键字集合的大小

2、数字分析法

假设关键字集合中的每个关键字都是由s 位数字组成(u1, u2, …, us),分析关键字集 中的全体,并从中提取分布均匀的若干位 或它们的组合作为地址。

此方法仅适合于: 能预先估计出全体关键字的每一位上各种数 字出现的频度。

3、平方取中法

以关键字的平方值的中间几位作为存 储地址。求“关键字的平方值”的目 的是“扩大差别”,同时平方值的中 间各位又能受到整个关键字中各位的 影响

此方法适合于: 关键字中的每一位都有某些数字重复 出现频度很高的现象。

4、折叠法

将关键字分割成若干部分,然后取它们的 叠加和为哈希地址。有两种叠加处理的方 法:移位叠加和间界叠加

此方法适合于: 关键字的数字位数特别多。

5、除留余数法

设定哈希函数为: H(key) = key MOD p
其中,p≤m (表长) 并且p 应为不大于m 的素数 或是 不含20 以下的质因子的合数

6、随机数法

设定哈希函数为: H(key) = Random(key)
其中,Random 为伪随机函数
通常,此方法用于对长度不等的关键字构造 哈希函数。

实际造表时,采用何种构造哈希函数的
方法取决于建表的关键字集合的情况 (包括关键字的范围和形态),总的原则 是使产生冲突的可能性降到尽可能地
小。

处理冲突的方法

“处理冲突”的实际含义是: 为产生冲突的地址寻找下一个哈希地址。

1.开放定址法
2.再哈希法
3.链地址法
4.建立一个公共溢出区

1、开放定址法
为产生冲突的地址H(key) 求得一个地址序列:
H0, H1, H2, …, Hs 1≤ s≤m-1

其中:H0 = H(key)
Hi = ( H(key) + di ) MOD m i=1, 2, …, s
di为增量序列,m为哈希表表长

1)线性探测再散列 di= 1,2,…,m-1 
2)二次探测(平方探测)再散列 di= 12, -12, 22, -22,,k2,-k2(k≤m/2) 
3) 随机探测再散列 di是一组伪随机数列

2、再哈希法
Hi=RHi(key) i=1,2,…,k RHi均是不同的哈希函数,即在同义词产生 地址冲突时计算另一个哈希函数地址,直 到冲突不再发生。
3、链地址法
将所有哈希地址相同的记录 都链接在同一链表中

•非同义词不会冲突,无“聚集”现象
•链表上结点空间动态申请,更适合于表长不确 定的情况
4、建立一个公共溢出区

假设哈希函数的值域为[0…m-1],则设向 量HashTable[0…m-1]为基本表,每个 分量存储一个记录,另设向量 OverTable[0…v]为溢出表。所有关键字 和基本表中关键字为同义词的记录, 不管它们由哈希函数得到的哈希地址 为多少,一旦发生冲突,都填入溢出 表。

哈希表的查找

查找过程和造表过程一致。假设采用开放定址处理 冲突,则查找过程为:
对于给定值K,计算哈希地址i = H(K)
若r[i] = NULL 则查找不成功
若r[i].key = K 则查找成功
否则“求下一地址Hi” ,直至 r[Hi] = NULL (查找不成功) 或r[Hi].key = K (查找成功) 为止

//---开放定址哈希表的存储结构--
int hashsize[] = {997,...};  //哈希表容量递增表 
typedef struct {
    ElemType *elem;
    int count;      //当前元素个数
    int sizeindex;  // hashsize[sizeindex]为当前容量 

}HashTable;

#define SUCCESS 1
#define UNSUCCESS 0
#define DUPLICATE -1

Status SearchHash(HashTable H,KeyType K,int &p,int &c) {
    // 在开放定址哈希表H中查找关键码为K的记录 
    p = Hash(K);  //求得哈希地址
    while(H.elem[p].key! = NULLKEY && !EQ(K,H.elem[p].key))\
        collision(p,++c);       //求得下一探查地址p
    if(EQ(K,H.elem[p].key)) return SUCCESS;
            //查找成功 返回待查数据元素位置p
    else return UNSUCCESS;      //查找不成功
}

Status InsertHash(HashTable &H.ElemType e) {
    c = 0;
    if(SearchHash(H,e.key,p,c) == SUCCESS)
        return DUPLICATE;
            //表中已有与e 有相同关键字的元素 
    else if(c<hashsize[H.sizeindex]/2) {
        // 冲突次数c 未达到上限,(阀值c 可调) 
        H./elem[p] = e;
        ++H.count;
        return OK;
    }// 查找不成功时,返回p为插入位置
    else RecreateHashTable(H); // 重建哈希表 
}

哈希表查找分析

从查找过程得知,哈希表查找的平均查找长度实际上并不等 于零。

决定哈希表查找的ASL的因素:

  1. 选用的哈希函数;
  2. 选用的处理冲突的方法;
  3. 哈希表饱和的程度,装载因子α=n/m 值 的大小(n—记录数,m—表的长度)

一般情况下,可以认为选用的哈希函数是 “均匀”的,则在讨论ASL时,可以不考虑 它的因素。 因此,哈希表的ASL是处理冲突方法和装载 因子的函数。

哈希表查找效率分析:

使用平均查找长度ASL来衡量查找算法,ASL取决于 :
数据结构——深入探析查找问题_第17张图片
在这里插入图片描述
装载因子越大,表中记录数越多,说明表装得越 满,发生冲突的可能性就越大,查找时比 较次数就越多

ASL与装填因子有关!既不是严格的O(1),也不是O(n)
可以证明,查找成功时有以下结果:
数据结构——深入探析查找问题_第18张图片
从以上结果可见:

  • 哈希表的平均查找长度是的函数,而不是n的 函数
  • 这说明,用哈希表构造查找表时,可以选择一个 适当的装填因子,使得平均查找长度限定在某 个范围内

你可能感兴趣的:(数据结构与算法)