第九章 查找

  • 说明:由于本章节涉及到的代码比较繁琐,个人能力和精力有限,所以直接记一些必须掌握的概念性的东西,便于理解各种结构的操作以及算法实现思路.

一、基础知识点

1.基本概念

  • 查找:给定一个k值,在含有n个记录的表中找出关键字等于k的记录。查找成功,返回记录信息或者记录在表中的位置;查找失败,返回相关指示信息。
  • 考研中可以将结构体中的关键字域和其他域简化,关键字本身就是记录的全部。
  • 查找方法的选取:

    1.使用哪种数据结构来表示查找表,即查找表中的记录是按照何种方式组织的;
    2.查找表中关键字的次序,即对无序集合查找还是对有序集合查找;

  • 重点:关键字的比较次数与待查找关键字有关,用平均比较次数(平均查找长度)ASL作为衡量一个查找算法效率优劣的标准,与之前所说的算法时间复杂度f(n)类似。

2.顺序查找法

思路:从表的一端开始,顺序扫描线性表,依次扫描到的关键字和给定值k比较

//数组a[]有n个元素,没有次序,数组从下标1开始存储,写出查找元素k的算法
int Search(int a[],int n,int k)
{
    int i;
    for(i=1;i<=n;++i)
    {
        if(a[i]==k)return i;
    }
    return 0;
}
//查找成功----ASL分析:ASL=(1/n)*n*(1+n)/2=(n+1)/2,时间复杂度O(n);
//查找失败----ASL分析:ASL=n,时间复杂度O(n);

3.折半查找法

思路:
1.要求线性表有序,设R[low, … ,high]是当前查找区间,mid = ( low / high ) / 2;
2.将待查找k与R[mid]进行比较,相等则查找成功,返回mid,失败则确定新查找区间;
3.R[mid] > k,则high = mid - 1;若R[mid]

//数组从下标1开始存储
int HalfSearch(int R[],int low,int high,int k)
{
    int mid;
    while(low<=high)
    {
        mid=(low+high)/2;
        if(R[mid]==k)return mid;
        else if(R[mid]>k)high=mid-1;
        else low=mid+1;
    }
    return 0;
}
  • 折半查找过程可以用二叉树表示,当前查找区间的中间位置上的记录作为树根,左右字表就是根的左右子树,叶子结点即为查找不成功的位置,有此得到的二叉树称为描述折半查找的判定树
  • 树的即为查找的次数,其中折半查找的时间复杂度O(n) = log2(n),平均查找长度近似为log2(n+1)-1;
  • 如果没有特殊说明,对空指针的比较次数不计算在ASL内。

4.分块查找

  • 数据结构:
    1.分块查找将线性表分块,块内元素存储顺序任意,但块与块之间按照关键字大小有序排列,前一块的最大关键字小于当前块的最小关键字;
    2.而对于顺序表,需要额外建立一个索引表,表中每一项对应线性表的每一块,每个索引项都是由键值分量和链值分量组成,键值分量存放最大关键字,链值分量存放指向本块中第一个元素和最后一个元素的指针
//索引表定义
typedef struct
{
    int key;               
    int low,high;    //记录块内第一个和最后一个元素位置
}indexElem;
indexElem index[maxSize];   //定义索引表

算法描述:
首先确定待查找元素的块,采用二分法查找;块内元素较少,直接用顺序查找;
平均查找长度 = 二分法查找平均长度 + 顺序查找平均查找长度;

二、二叉排序树与平衡二叉树

1.二叉排序树

  • BST定义:与折半查找中描述的二叉树相同
  • 如果对BST采用中序遍历输出,则输出的序列是递增的
  • BTN结点定义:
    typedef struct BTNode
    {
       int key;
       struct BTNode *lchild;
       struct BTNode *rchild;
    }BTNode;

基本算法:

//1.查找关键字算法
//与这折半查找的二叉树类比,很简单
BTNode *BSTSearch(BTNode *bt,int key)
{
    if(bt==NULL)return NULL;
    else
    {
        if(bt->key==key)return bt;
        else if(bt->key>key)return BSTSearch(bt->lchild,key);
        else if(bt->key<key)return BSTSearch(bt->rchild,key);
    }
}

//2.插入关键字的算法
//注意BST是一个查找表,对于一个不存在于二叉排序树中的关键字,查找不成功的位置即为需要将关键字插入的位置
int BSTInsert(BTNode *&bt,int key)//由于二叉树需要改变,所以用引用,绪论里说过
{
    if(bt==NULL)//空指针即为找到关键字插入位置
    {
        bt=(BTNode*)malloc(sizeof(BTNode));
        bt->lchild=bt->rchild=NULL;
        bt->key=key;
        return 1;   
    }
    else
    {
        if(key==bt->key)return 0;//关键字存在二叉树中,插入1失败
        else if(key<bt->key)return BSTInsert(bt->lchild,key);
        else if(key>bt->key)return BSTInsert(bt->rchild,key);
    }
}

//3.二叉排序树的构造算法
//建立一棵空树,直接逐个插入即可
void CreatBST(BTNode *&bt, int key[], int n)
{
    int i;
    bt=NULL;
    for(i=0;i<n;++i)BSTInsert(bt,key[i]);
}

//4.删除关键字操作
/*
1.p结点为叶子结点
2.p结点只有右子树或者只有左子树
3.p结点有左右子树,为保证二叉排序树的成立条件(输出的中序遍历有序)
遍历p左子树的右指针,直到到达最右边的结点r(或者遍历p右子树的左指针,直到到达最左边的结点)
p的关键字用r的关键字(相当于删除p),然后处理多出来的r,删除方式就按1,2情况处理。
*/
//具体算法严版书上有P230页

//5.判断一棵二叉树是否为二叉排序树(结点值为int型)
//思路:利用二叉排序树BST的中序遍历为递增序列的性质,对该二叉树进行中序遍历即可
int predt=INF;//INF为已知常量,小于任何树中结点值,predt始终记录当前结点的前驱结点的值
int judgeBST(BTNode *bt)
{
    int b1,b2;
    if(bt==NULL)return1;//空BST
    else
    {
        b1=judgeBST(bt->lhild);
        if(b1==0||predt > bt->date)return 0;
        predt=bt->date;
        b2=judgeBST(bt->rchild);
        return b2; 
    }   
}

2.平衡二叉树

  • AVL树,其左右子树都是AVL树,且左右子树高度之差绝对值不超过1;
  • AVL树是特殊的BST树;
  • 平衡因子:AVL树的左子树的高度减去右子树的高度,取值1,-1,0;
  • AVL树的建立:与BST相同,但要加入一个判断,即新插入的关键字是否会使原平衡的二叉树失去平衡,若失去平衡,就需要进行平衡调整;
  • AVL平衡调整:
    1.新插入的结点破坏了平衡,则找出插入新结点后失去平衡的最小子树,然后调整这棵小树,使之成为平衡树;
    2.失去平衡的最小子树:以距离插入结点最近,且以平衡因子绝对值大于1的结点作为根的子树,又称为最小不平衡子树;
    3.RL、LL、LR、RR是对不平衡状态的描述,如LL:新插入的结点落在最小不平衡子树根结点的左L孩子的左L子树上;
    4.LL调整:右单旋转调整,RR调整:左单旋转调整,RL调整:先右后左双旋转调整,LR:先左后右双旋转调整。

三、B-树和B+树

1.基本概念

  • B-树的阶:所有结点孩子个数的最大值m,m>=3;
  • 熟悉掌握B-树的例图;
  • B-树满足的要求:
    1. 每个结点最多有m个分支,最少分支数的结点为根结点且不是叶子结点,则至少有两个分支;非根非叶结点结点至少有m/2[向上取整]个分支.
    2. 有n(k<=n<=m)个分支的结点有n-1个关键字,按递增顺序排列。k=2(根结点)或m/2[向上取整](非根结点).
    3. 结点内的关键字互不相等且按从小到大排列.
    4. 叶结点处于同一层,可以用空指针表示,是查找失败到达的位置.
    5. 其中n为关键字个数,ki(1<=i<=n)为该结点的关键字且满足ki < ki+1;
      pi(0<=i<=n)为该结点的孩子结点指针且满足pi(1<=i<=n-1)所指结点上的关键字大于ki且小于ki+1,p0所指结点上的关键字小于k1,pn所指结点的关键字大于kn.
      每个结点的结构为:
nk1k2kn
p0p1p2pn

2.基本操作

  • 1.B-树关键字的查找:

    • B-树关键字查找其实就是BST的扩展,BST是二路查找,B-树是多路查找;
    • 由于B-树结点内关键字有序,可以通过折半查找提高效率;
    • 查找步骤:
      1. 先与根结点比较key=k[i],则查找成功
      2. key<k[1],则去p[0]所指示的子树进行查找;
      3. key>k[n],则到p[n]所指子树进行查找;
      4. key[i]<key<k[i+1]则沿着p[i]所指子树查找;
      5. 如果最后遇到空指针,则查找失败.
  • 2.B-树关键字的插入和删除:

    • 类比BST,B-的创建过程也是将关键字逐个插入树中的过程;
    • 插入操作步骤:
      1. 求结点中关键字个数范围:B-树的阶为m,则结点中关键字个数范围为m/2[向上取整]-1~m-1;
      2. 关键字的插入:在B-树的查找过程中,遇到空指针(终端结点),则查找不成功,同时也是插入的位置,B-树新关键字的插入总是落在终端结点上;
      3. 需要注意插入后导致阶数不符合条件,则需要进行拆分调整;
      4. 注意:插入操作只会使B-树逐渐变高而不会改变叶子结点在同一层的特性;
    • 删除结点步骤:
      1. 先找到关键字的位置;
      2. 判断关键字的是否少于关键字个数,如果少了,就需要向兄弟借关键字,或者从其孩子结点进行关键字交换或者进行结点合并;
      3. 与当前结点的孩子结点进行关键字交换的操作可以保证删除操作总是发生在终端结点上;
      4. 注意:采用不同的合并方法将产生不同的B-树。
    • 重点掌握拆分操作和合并操作以及引起的连锁反应。

3.B+树

  • B+树与B-树的差别:
    1. 在B+树中,具有n个关键字的结点含有n个分支,在B-树中,具有n个关键字的结点含有n+1个分支。
    2. B+树结点关键字个数取值范围m/2(向上取整)<=n<=m,根结点取值范围为2<=n<=m;B-树关键字取值范围m/2(向上取整)-1<=n<=m,根结点取值范围1<=n<=m。
    3. B+树中叶子结点包含信息,包含了全部关键字,并且叶子结点引出的指针指向记录(这里的记录与关键字不同)。
    4. B+树所有非叶子结点只是起到索引的作用,即结点中每个索引只含有对于树的最大关键字和指向该孩子的指针(联想之前的分块查找中的索引表);B-树,每个关键字对应一个记录的存储地址。
    5. B+树上有一个指针指向关键字最小的叶子结点,所有叶子结点链接成一个线性链表;B-树没有。

四、散列表

1.基本概念

  • Hash表:根据给定关键字来计算出关键字在表中的地址
  • 对于其他表,关键字的地址与关键字之间不存在确定的关系,Hash表中关键字与关键字的地址是有确定关键字的。
  • Hash函数,H(key)为key在查找表中的地址。

2.Hash表建立以及冲突解决

  • 根据Hash函数求得的Hash表中会出现多个关键字共用同一个地址的情况,称之为冲突,即当key1!=key2,H(key1)=H(key2),发生冲突,key1和key2是Hash函数H的同义词,这时需要进行相关处理;
  • 冲突解决方法:对i属于1~m-1,从冲突地址开始,Hi(key)=(H(key)+i)Mod m,直到冲突结束,m为表长;
  • Hash表查找关键字key的过程:
    1. Hash(key)为空,查找失败;
    2. key = Hsah(key)地址上的关键字,查找成功;
    3. key != Hash(key),根据冲突解决方法到下一个地址查找,直到相同为止,查找成功;
    4. 如果3中冲突查找遇到空指针,则查找失败。
  • 关于ALS(等概率查找):
    1. 查找成功:针对对每个关键字进行其比较次数,求其平均数;
    2. 查找失败:针对每个地址,求出由该地址开始需要进行的比较次数。
    3. 计算查找不成功的平均查找长度是根据Hash函数可以映射到的地址个数而不是表内所有地址,eg:H(key)=key Mod 10,其映射地址就为0~9;H(key)=key Mod 13,其映射地址就为0~12

3.常用Hash函数构造方法

  • 1.直接定址法:
    H(key) = key || H(key) = a*key+b
  • 2.数字分析法:
    关键字是r进制的,并且Hash表中出现的关键字范围事先知道,选取关键字的若干位组成Hash地址。选取原则是使得到的Hash地址尽可能减少冲突(所选数的位上的数字随机)。
  • 3.平均取中法:
    取关键字中间几位作为Hash表的地址,通常将关键字进行平方后取中间几位作为地址,取的位长与表长相关。
  • 4.除留余数法(重点)
    就是上述的H(key) = key mod p p<=m

4.常用的Hash冲突处理方法

  • Hash表不能完全避免发生冲突,必须进行冲突处理。
  • 1.开放定址法:
    以发生冲突的Hash地址为自变量,通过某种冲突解决函数得到一个新的空闲的Hash地址。
    1)线性探查法:就是上述的Hi(key)=(H(key)+i)Mod m,不过容易产生堆积问题,设第一个同义词占用单元d,这些连续的同义词就会占用d+1,d+2…单元,此时随后每一个d+1,d+2单元的映射都会发生冲突。
    2)平方探查法:>设发生冲突的地址为d,平方探查法得到新的序列地址d+1^2,d-1^2,d+2^2,d-2^2…这种方法可以减少堆积问题,缺点是不能探查Hash表上所有单元,但至少能探查到一半的单元
    3)还有伪随机序列和双Hash函数法(H(H(k)))。
  • 2.链地址法:
    即把所有同义词连接起来的方法。Hash存放的不再是记录本身,而是相应同义词单链表的表头指针。

5.散列表的性能分析

  • ASL1:找到表中已有表项的平均比较次数;
  • ASL2:表中所有可能散列到的地址上插入新元素时,为找到空位置而进行探查的平均次数;
  • 装填因子a:关键字个数与表长度的比值;
解决冲突的方法查找成功时查找不成功时
线性查找法[1+1/(1-a)]/2[1+1/(1-a)^2]/2
平方探查法-(1/a)ln(1-a)1/(1-a)
链地址法1+a/2a+e^a≈a

特别注意链地址法的ASL2求法

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值