数据结构——第八章(查找)

查找算法

1. 查找的基本概念

  • 查找定义:在数据集合中寻找满足某种条件的数据元素的过程
  • 查找表:同一种类型元素构成的集合
  • 关键字:数据元素中某个可以唯一标识该元素的数据项

2. 查找的分类

2.1 静态查找

  • 查询每个“特定的”数据元素是否在查找表中
  • 读取每个“特定的”数据元素和各种属性

使用线性查找结构来组织数据,这样可以使用顺序查找折半查找等高效查找

2.2 动态查找

  • 查找时插入数据
  • 查找时删除数据

考虑二叉排序树和散列表结构

3. 顺序查找

3.1 基本顺序查找 (O(2n))

  • 从前往后:每次都要判断是否越界
//顺序查找,a为数组,n为要查找的数组个数,key为要查找的关键字
int Seq_search(int a[], int n, int key)
{
    int i;
    for (i = 0; i < n;i++)
    {
        if (a[i] == key)
            return i;
    }
    return 0;
}

3.2 优化后的顺序查找(O(n))

  • 从后往前:不需要判断是否越界,性能更优
int Seq_search(int a[], int n, int key)
{
    int i = n;
    a[0] = key;    //设置哨兵
    while(a[i] != key){    //如果不是要找的元素
        i--;         //从后往前找
    }
    return i;        //返回0表示没有找到,返回其它数字代表找到
}

4. 有序表查找

有序表查找要求,数据是有序的,是排序好的,我们只需要进行查找

4.1 折半查找与顺序查找动图

4.2 折半查找代码( O l o g n )

int Binary_Search(int a[], int n, int key)
{
    int low, high, mid;
    low = 0;
    high = n - 1;
    //[low,high]
    while (low<=high)
    {
        mid = (low + high) / 2;   //二分查找
        if (a[mid] < key)
            low = mid + 1;        //[mid+1,high]
        else if (a[mid]>key)
            high = mid - 1;       //[low,mid-1]
        else
            return mid;
    }
    return -1;
}

对于折半查找,为什么一定要,而不是1/4或者其他?
比如:我们查字典Apple,我们会先从中间查找,还是有一定目的的向前找。

4.3 插值查找 O l o g n

对折半查找到的优化,将查找点的选择改为自适应选择
对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。
其余部分不变,只对mid值进行优化

4.3.1 二分查找mid值

  • m i d = l o w + h i g h 2 = l o w + 1 2 h i g h - l o w

4.3.2 插值查找mid值

  • ln i d = l o w + k e y - a l o w a h i g h - a lo w

4.3.3 插值查找代码

int Insert_Search(int a[ ], int n, int key)
{
    int low, high, mid;
    low = 0;
    high = n - 1;
    while (low <= high)
    {
        mid = low + (key - a[low]) / (a[high] - a[low])*(high - low);   //修改mid值后的插值查找
        if (a[mid] < key)
            low = mid + 1;
        else if (a[mid]>key)
            high = mid - 1;
        else
            return mid;
    }
    return -1;
}

4.4 斐波那契查找(黄金比例:0.618:1)

对二分法进行改造,mid值为黄金分割点

  • 斐波那契数列:后面的数等于前面两个数的和 F i = F i - 1 + F i - 2

  • 如果与给定关键字相同,则查找成功,返回在表中的位置;

  • 如果给定关键字大,向右查找并减小2个斐波那契区间;

  • 如果给定关键字小,向左查找并减小1个斐波那契区间;

  • 重复过程,直到找到关键字(成功)或区间为空集(失败)。

mid值为黄金分割点
数据结构——第八章(查找)_第1张图片

5. 无序查找

5.1 线性索引查找

索引是为了加快查找速度而设计的一种数据结构。索引就是把一个关键字与他对应的记录相关联的过程

5.2 稠密索引

  • 概念:稠密索引文件的每个记录都有一个索引项,记录在数据区存放是任意的,但索引是按序的,这种索引称为稠密索引。
  • 稠密索引是指在线性索引中,将数据集中的每个记录对应一个索引项。

5.3 分块索引

5.3.1 概念:分块有序是把数据集的记录分成了若干块,并且这些块需要满足两个条件:块内无序,块外有序

5.3.2 分块索引的索引项结构分为三个数据项:

a: 最大关键码–存储每一块中的最大关键字。
b: 存储每一块中记录的个数以便于循环时使用。
c: 用于指向块首数据元素的指针,便于开始对这一块中记录进行遍历。

5.3.3 查找步骤:

  • 确定待查找值在哪个块(折半查找)
  • 在确定的块中查找待查找的值(顺序查找)

5.4 倒序索引

  • 概念:其中记录号表存储具有相同次关键字的所有记录的记录号(可以是指向记录的指针或者是该记录的主关键字)。

由属性来确定记录位置,查找块维护困难

6. 二叉排序树(二叉搜索树)

左小右大

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点值
  • 若它的右子树不空,则右子树上所有结点的值均小于它的根结点值
  • 根的左、右子树也分别为二叉排序树(递归)

因为数据集是有序存放,查找的方法可以使用折半,插值,斐波那契等,但是因为有序,在插入和删除操作上的效率并不高
这时我们就需要一种动态查找方法,既可以高效实现查找,又可以使得插入和删除效率不错,这时我们可以考虑二叉排序树

6.1 二叉排序树的查找(递归)

/*
BiTree T     我们要搜索的二叉树
ElemType key 我们要搜索的关键字
BiTree F     记录下我们的当前搜索子树的双亲结点
BiTree* P    查找成功,指针p指向该数据元素节点,并返回true 、 查找失败,指针p指向查找路径上访问的最后一个节点,并返回Flase
*/
Status SearchBST(BiTree T, ElemType key, BiTree F, BiTree* P)
{
    if (!T)
    {
        *P = F;        //若是未找到则返回父节点位置
        return FALSE;
    }
    else
    {
        if (T->data == key)
        {
            *P = T;    //若是找到则P返回该结点位置
            return TRUE;
        }
        else if (T->data < key)    //查找值大于节点值
            return SearchBST(T->rchild, key, T, P);
        else                       //查找值小于节点值
            return SearchBST(T->lchild, key, T, P);    //下一个节点,查找值,双亲节点,查找到的结果
    }
}

6.2 二叉排序树的插入

Status InsertBST(BiTree* T, int key)
{
    BiTree P,s;
    if (!T)
        return ERROR;
    if (!SearchBST(*T, key, NULL, &P))
    {
        //没有查找到有重复数据,获取到了应该插入的位置
        s = (BiTree)malloc(sizeof(BiTNode));
        s->data = key;
        s->lchild = s->rchild = NULL;
        
        if (!P)    //空树直接插入
            *T = s;                            //p指针为查找时的最接近的叶子节点
        else if (key < P->data)                //如果该节点比p节点还小则插入到p节点的左边
            P->lchild = s;
        else
            P->rchild = s;                     //如果该节点比p节点大则插入到p节点的右边
        return OK;
    }
    else
        return ERROR;        //树中已经有了相同节点,不再插入
}

6.3 二叉排序树的删除

6.3.1 删除的节点是叶子节点,,直接删除(判断左右指针是否为空)

6.3.2 删除的节点只有左子树,直接让该节点的双亲节点指向左孩子

6.3.3 删除的节点只有右子树,直接让该节点的双亲节点指向右孩子

6.3.4 删除的节点既有左孩子也有右孩子

二叉排序树通过中序遍历可以变成一个有序的序列
方法:找到待删除节点的直接前驱或者直接后继,用该节点来替换待删除节点,再删除该节点
也就是选择与待删除节点最接近的值,左子树中最大的,或者右子树中最小的

  • 删除红色节点,选取右孩子中最小的绿色节点代替它的位置,并删掉绿色节点
Status Delete(BiTree* T)
{
    BiTree q,f;               //q为双亲节点,s每一次迭代用到的节点
    if (!*T)
        return ERROR;
    if (!(*T)->lchild)        //若是左子树不存在,我们只需要接到右子树
    {
        q = *T;               //将当前节点赋值给p节点,便于删除free
        *T = (*T)->rchild;    //将当前节点的右孩子赋值给当前节点
        free(q);              //释放删除节点
    }
    else if (!(*T)->rchild)   //若右子树不存在,接入左子树
    {
        q = *T;              //将当前节点赋值给p节点,便于删除free
        *T = (*T)->lchild;   //将当前节点的左孩子赋值给当前节点
        free(q);
    }
    else  //两边都存在,这里选择右子树最小(右子树中最左边的)作为接入点
    {
        f = *T;    //f作为q的双亲结点     (做了一点点修改)
        q = (*T)->rchild;
        while (q)
        {
            f = q;
            q = q->lchild;    //找到右子树最小,注意其可能存在右子树,我们要进行保存,接入其父节点
        }
        //将找到的右子树最小节点的数据与待删除的数据进行替换,然后记录该点将其删除
        (*T)->data = q->data;
        if (f != (*T))
            f->lchild = q->rchild;
        else
            f->rchild = q->rchild;    //当右子树是一个右斜树时(寻找时没有找到右子树左端的节点)画图
        free(q);                      //释放已经替换过的节点
    }
    return TRUE;
}

Status DeleteBST(BiTree* T, int key)
{
    if (!*T)
        return ERROR;
    else
    {
        if ((*T)->data == key)    //找到了,开始删除
        {
            Delete(T);            //删除该结点,由于要分情况讨论,所以另外写一个函数
        }
        else if ((*T)->data < key)
            DeleteBST(&(*T)->rchild, key);
        else
            DeleteBST(&(*T)->lchild, key);
    }
}

7. 平衡二叉树(AVL树)

  • 一种特殊的二叉排序树,空树或任一结点左右子树高度差的绝对值不超过1,即|BF|<=1(平衡因子<=1)
  • 最小不平衡子树:距离插入结点最近的,且平衡因子绝对值大于1的结点为根的子树

构建平衡二叉树对的基本思想:在构建二叉排序树的过程中,每当插入一个结点时,先检查是否因插入而破坏了树的平衡性,若是,则找出最小不平衡子树。在保存二叉排序树的前提下,调整最小不平衡子树中各个结点,进行相应的旋转,使之成为新的平衡子树。

7.1 平衡因子BF>0(LL型:正则右旋)

7.2 平衡因子BF<0(RR型:负则左旋)

7.3 两个平衡因子符号不统一的,先统一符号,再进行旋转(LR/RL型:先局部转为LL、RR再进行调整)

一个节点的右子树的最左边的孩子可以放在这个节点左子树的最右边的孩子的位置(不满足二叉树性质时稍作调整)

7.4 平衡二叉树结构

  • 在二叉树的基础上加入了平衡因子bf
typedef struct _BiTNode
{
    ElemType data;
    int bf;
    struct _BiTNode* lchild, *rchild;
}BiTNode,*BiTree;

代码有空再写

8. 多路查找树

所有元素之间存在某种特定的排序关系
每个节点的孩子树可以多于两个,且每一个节点处可以存储多个元素

8.1 2-3树

  • 概念:每一个节点具有两个孩子或者三个孩子,顺序与二叉排序树类似
  • 2节点要么没有孩子,要么必须有两个孩子。3节点要么没有孩子,要么必须有三个孩子
  • 所有的叶子节点都在同一层次上
    数据结构——第八章(查找)_第2张图片

8.2 2-3树的插入

1.对于空树插入一个2节点即可

2.插入到一个2节点的叶子节点上:直接插入到叶子上
数据结构——第八章(查找)_第3张图片

3.向一个父节点为2节点的3节点插入数据:临时创建4节点,并将中间的值移至父节点中,再将剩余节点分裂为2个节点
数据结构——第八章(查找)_第4张图片

4.向一个父节点为3节点的3节点插入数据:继续向上寻找2节点,找到后重复3步骤
数据结构——第八章(查找)_第5张图片

5.父节点到根节点均是3节点:拆分节点增加深度
数据结构——第八章(查找)_第6张图片

8.3 2-3树的删除

1.删除元素位于3节点的叶子节点上:直接删除元素即可
数据结构——第八章(查找)_第7张图片
2.所删除元素位于2节点上,此节点双亲也是2节点,且拥有一个3节点的右孩子
数据结构——第八章(查找)_第8张图片
3.所删除元素位于2节点上,此节点双亲也是2节点,且拥有一个2节点的右孩子
数据结构——第八章(查找)_第9张图片
4.当删除节点是满二叉树时
数据结构——第八章(查找)_第10张图片

8.4 2-3-4树

  • 概念:在2-3树的基础上多了一个四节点
  • {7,1,2,5,6,9,8,4,3}构造2-3-4树
    数据结构——第八章(查找)_第11张图片

8.5 B树

  • 概念:平衡的多路查找树,节点最大的孩子数目称为B树的阶,2-3树是3阶B树,2-3-4树是4阶B树

8.6 B树的属性

  • 如果根节点不是叶节点,则其至少有两颗子树
  • 所有的叶子都属于同一层次,这表明B树是平衡的。平衡性其实正是B树名字的来源,B表示的正是单词Balanced
  • 每一个分支节点包含下列信息:n、A0、K1、A1、k2(n为元素个数,k为关键字ki
  • 在B树中,每个结点(非根内部结点)关键字个数n的范围是⌈m/2⌉ -1≤n≤m-1(根结点:1≤n≤m-1)。

比如说要查找7,首先从外存读取得到根节点3,5,8三个元素,发现7不在,但是5、8之间,因此就通过A2再读取外存的6,7节点找到结束

8.7 B+树

B+树是常用于数据库和操作系统的文件系统中的一种用于查找的数据结构

8.7.1 m阶的B+树与m阶的B树的主要差异在于:

1)
在B+树中,具有n个关键字的结点只含有n棵子树,即每个关键字对应一棵子树
在B树中,具有n个关键字的结点含有(n+1)棵子树。
2)
在B+树中,每个结点(非根内部结点)关键字个数n的范围是 ⌈m/2⌉≤n≤m(根结点1≤n≤m)
在B树中,每个结点(非根内部结点)关键字个数n的范围是⌈m/2⌉ -1≤n≤m-1(根结点:1≤n≤m-1)。
3)
在B+树中,叶结点包含了全部关键字,即在非叶结点中出现的关键字也会出现在叶结点中
在B树中,叶结点包含的关键字和其他结点包含的关键字是不重复的。
4)在B+树中,叶结点包含信息,所有非叶结点仅起到索引作用,非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。

8.7.2 B+树特点

  • 每一个父节点的元素都出现在子节点中,是子节点的最大或最小元素
  • 没一个叶子节点都带有指向下一个节点的指针
  • 叶子节点带有卫星数据,其余节点仅仅是索引没有数据关联

8.7.3 B+树的查询优点

  • 单一节点存储更多的元素,使得查询的IO次数更少。(中间没有卫星数据)
  • 所有查询都要查找到叶子节点,查询性能稳定。
  • 所有叶子节点形成有序链表,便于范围查询。

在数据库的聚集索引(Clustered Index)中,叶子节点直接包含卫星数据。在非聚集索引(NonClustered Index)中,叶子节点带有指向卫星数据的指针。

9. 散列表(哈希表)

  • 散列表:根据给定的关键字来计算出关键字在表中的地址的数据结构。也就是说,散列表建立了关键字和存储地址之间的一种直接映射关系
  • 散列函数:一个把查找表中的关键字映射成该关键字对应的地址的函数,记为Hash(key)=Addr
  • 散列函数可能会把两个或两个以上的不同关键字映射到同一地址,称这种情况为“冲突”

9.1 散列表查找步骤

  • 在存储时,通过散列函数计算记录的散列地址,并按此散列地址存储该记录;
  • 在查找时,通过同样的散列函数计算记录的散列地址,按此散列地址访问该记录

适合一对一查找,不适合范围行查找

9.2 散列函数的构造方法

计算简单,散列地址分布均匀

  • 直接定值法:直接取关键字的某个线性函数值为散列地址,散列函数为H(key)=a×key+b。式中,a和b是常数
  • 数字分析法:适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布比较均匀,就可以考虑这个方法
  • 平方取中法:取关键字的平方值的中间几位作为散列地址,适合于不知道关键字的分布,而位数又不是很大的情况
  • 折叠法:将关键字分割成位数相同的几部分(最后一部分的位数可以短一些),然后取这几部分的叠加和作为散列地址
  • 随机数法:用随机数作为散列地址,适用于关键字长度不相等的情况下
  • 除留余数法(最常用):假定散列表表长为m,取一个不大于m但最接近或等于m的质数p,利用以下公式把关键字转换成散列地址。散列函数为H(key)=key % p,除留余数法的关键是选好p,使得每一个关键字通过该函数转换后等概率地映射到散列空间上的任一地址,从而尽可能减少冲突的可能性

9.3 处理散列冲突的方法(附一个链接:散列冲突)

9.3.1 线性探测法

  • f(key) = [f(key)+di] mod 11(mod为求余运算)
  • 求余数,然后放到对应的位置上,如果位置上有数据元素了,那么就向后移动,移动到没有数据元素的位置上,然后占坑

查找成功与查找不成功的平均长度 散列地址、关键字、比较次数参考例子

9.3.2 二次探测法(di取平方)

  • 增加平方运算的目的是为了不让关键字都聚集在某一块区域,使用正负更可以双向进行探测(避免线性探索法的堆积现象)

9.3.3 随机探测法(di取随机数)

  • 在冲突时,对于位移量di采用随机函数计算得到

9.3.4 再散列函数法(fi(key)=Ri(key))

  • 我们事先准备多个散列函数,当第一个失效,我们就选择下一个去测试
  • 使得关键字不聚集,但是增加了计算的时间

9.3.5 链地址法

  • 产生冲突不换地方,将关键字的同义词记录存储在一个单链表中,称之为同义词子表,在散列表中只存储所有同义词子表的头指针。

9.3.6 公共溢出区法(易理解,常用)

  • 将产生冲突的数据带走,为所有冲突的关键字建立一个公共的溢出区来存放
  • 冲突较少的情况下,性能强

9.4 散列表的查找过程

1.类似于构造散列表,给定一个关键字Key。
2.先根据散列函数计算出其散列地址。然后检查散列地址位置有没有关键字。

  • 如果没有,表明该关键字不存在,返回查找失败。

  • 如果有,则检查该记录是否等于关键字。

  • 如果等于关键字,返回查找成功。

  • 如果不等于,则按照给定的冲突处理办法来计算下一个散列地址,再用该地址去执行上述过程。

你可能感兴趣的:(数据结构,二叉树,算法,二分查找)