[数据结构] 七、查找

目录

一、查找的思维导图:

二、查找的基本概念、顺序查找法、折半查找法:

三、二叉排序树和平衡二叉树:

四、散列表


一、查找的思维导图:

二、查找的基本概念、顺序查找法、折半查找法:

1、查找的基本概念

(1)查找的定义:给定一个 k 值,在含有 n 个记录的表中找出关键字等于 k 的记录。若找到,则查找成功,返回该记录的信息或者该记录在表中的位置;否则查找失败,返回相关的指示信息。

(2)影响查找方法的选择的因素:

①  使用哪种数据结构来表示查找表(顺序表 or 链表),即查找表中的记录是按照何种方式组织的。

②  查找表中关键字的次序,即对无序集合查找还是对有序结合查找

(3)由于查找算法的基本操作是关键字的比较,并且关键字比较次数与待查找关键字有关,因此通常把查找过程中对关键字的比较次数(也就是平均查找长度)作为衡量一个查找算法效率优劣的标准,平均查找长度用 ASL 来表示,其定义为

                                                                           ASL = \sum _{i=1}^{n}pi * ci

其中,n 是查找表中记录的个数;pi 是查找第 i 个记录的概率,一般取 1/n; ci 是找到第 i 个记录所需要进行比较的次数,即查找长度。

2、顺序查找法

(1)顺序查找法是一种最简单的查找方法。

(2)顺序查找的基本思路是:从表的一端开始,顺序扫描线性表,依次将扫描到的关键字和给定值 k 比较,若当前扫描的关键字与 k 相等,则查找成功:若扫描结束后,仍未发现关键字等于 k 的记录,则查找失败。由以上可知,顺序查找法对于顺序表和链表都是适用的。对于顺序表,可以通过数组下标递增来顺序扫描数组中的各个元素;对于链表,则可通过表结点指针(假设为p)反复执行p = p->next;来扫描表中各个元素。

(3)顺序查找算法如下:

int Search(Sqlist L,int k){    //顺序表查找
    int i;
    for(i=0;i<L.length;i++){
        if(L.data[i] == k)
            return i;        //查找成功返回 i
    }
    return 0;        //查找失败返回 0
}

LNode* Search(LNode *C,int k){    //链表查找
    LNode *p = C->next;
    while(p!=NULL){
        if(p->data == k)
            break;        //查找成功返回 break,返回 p 指针
        p = p->next;
    }
    return p;
}

3、折半查找法

(1)折半查找要求线性表是有序的,即表中记录按关键字有序(假设是递增有序的)。

(2)折半查找的基本思路:设 R [low,…,hig] 是当前的查找区间,首先确定该区间的中间位置 mid =(low - thigh)/ 2;,然后将待查的 k 值与 R [mid] 比较,若相等,则查找成功,并返回该位置,否则需确定新的查找区间。若 R [mid] > k,则由表的有序性可知 R [mid,…,hig ]均大于 k,因此若表中存在关键字等于 k 的记录,则该记录必定是在 mid 左边的子表 R [low,…,mid-1] 中,故新的查找区间是左子表 R [low,…,mid-1]。类似地,若 R [mid] < k,则要查找的 k 必在 mid 的右子表 R [mid1,…,high] 中,即新的查找区间是右子表 R [mid1,…,high],递归地处理新区间,直到子区间的长度小于 1 时查找过程结束。

(3)折半查找的算法如下:

int Bsearch(int R[],int low,int high,int k){
    int mid;
    while(low < high){    //当字表长度大于等于 1 的时候进行循环
        mid = (low + high)/2;    //取当前表的中间位置
        if(R[mid] == k)        //找到后返回元素位置
            return mid;
        else if(R[mid] > k)    //去右字表中查找
            high = mid-1;
        else                   //去左字表中查找
            low = mid+1;
    }
    return 0;    //查找不成功,返回 0
}

(4)折半查找的过程可以用二叉树来表示。把当前查找区间中的中间位置上的记录作为数根,左子表和右字表中的记录分别作为根的左右子树,由此得到的二叉树称为描述折半查找的判定树。

(5)折半查找判定树的构建:序列 {1,2,3,4,5,6,7,8,9,10,11} 可以做出一棵判定树,图中叶子节点(方框)代表查找不成功的位置,折半查找的比较次数即为从根节点到待查找元素所经过的节点数,其比较次数最多的情况为一直走到叶子节点的情况。因此算法的时间复杂度可以用树的高度来表示。推广到一般情况,对于又 n 个记录的查找表,进行折半查找的时间复杂度为 log2n。折半查找的平均查找长度近似为 log2(n+1)-1。

三、二叉排序树和平衡二叉树:

1、二叉排序树:

(1)二叉排序树(BST)的定义:

二叉排序树或者是空树,或者是满足以下性质的二叉树:

①  若它的左子树不为空,则左子树上所有关键字的值均小于根关键字的值

②  若它的右子树不为空,则右子树上所有关键字的值均大于根关键字的值

③  左右子树又是一棵二叉排序树

说明:由二叉排序树的定义可以直到,如果输出二叉排序树的中序遍历序列,则这个序列是递增有序的。

(2)二叉排序树的存储结构:

二叉排序树采用二叉链表进行存储,其节点类型定义与一般的二叉树类似。

typedef struct BTNode{
    int key;    //关键字
    struct BTNode *lchild;
    struct BTNode *rchild;
}BTNode;

(3)二叉排序树的基本算法 - 查找关键字的算法

要找的关键字要么在左子树上,要么在右子树上,要么在根结点上。由二叉排序树的定义可以知道,根结点中的关键字将所有关键字分成了两部分即大于根结点中关键字的部分和小于根结点中关键字的部分可以将待查关键字先和根结点中的关键字比较,如果相等则查找成功;如果小于则到左子树中去查找,无须考虑右子树中的关键字;如果大于则到右子树中去查找,无须考虑左子树中的关键字。如果来到当前树的子树根,则重复上述过程;如果来到了结点的空指针域,则说明查找失败。代码如下:查找成功则返回关键字所在节点的指针,否则返回NULL;

BTNode* BSTSearch(BTNode* bt,int key){
    if(bt == NULL)
        return NULL;
    else{
        if(bt->key == key)
            return bt;    //等于根节点中的关键字,查找成功,返回关键字所在的节点指针
        else if(key < bt->key)    //小于根节点中的关键字时到左子树中查找
            return BSTSearch(BTNode* bt->lchild,int key);
        else                    //大于根节点中的关键字时到右子树中查找
            return BSTSearch(BTNode* bt->rchild,int key);
    }
}

(4)二叉排序树的基本算法 - 插入关键字的算法:

二叉排序树是一个查找表,插入一个关键字首先要找到插入位置。对于一个不存在于二叉排序树中的关键字,其查找不成功的位置即为该关键字的插入位置,如图所示。

因此只需对查找关键字的算法进行修改,在来到空指针的时候将关键字插入即可。在插入过程中如果待插入关键字已经存在,则返回0,代表插入不成功;如果待插入关键字不存在,则插入,并返回1,代表插入成功。算法实现代码如下:

int BSTInsert(BTNode *&bt,int key){    //指针bt要该边,用引用型
    if(bt == NULL){    //当前为空指针时说明找到插入位置,创建新节点进行插入
        bt=(BTNode*)malloc(sizeof(BTNode));    //创建新节点
        bt->lchild = bt->rchild = NULL;
        bt->key = key;    //将带插入关键字存入新节点内,插入成功,返回 1
        return 1;
    }
    else{
        if(key == bt->key)    //关键字已经存在树种,插入失败,返回 0
            return 0;
        else if(key < bt->key)
            return BSTInsert(bt->lchild,key);
        else
            return BSTInsert(bt->rchild,key);
    }
}

(5)二叉排序树的构造方法:

在二叉排序树的插入操作的基础上进行构造。建立一棵空树,然后将关键字逐个插入到空树中即可构造一棵二叉排序树,算法代码如下:假设关键字已经存入数组key[ ]中。

void CreateBST(BTNode *&bt,int key[],int n){
    int i;
    bt = NULL;    //将树清空
    for(i=0;i<n;i++)    
        BTSInsert(bt,key[i]);
}

(6)删除关键字的操作

当在二叉排序树中删除一个关键字时,不能把以该关键字所在的结点为根的子树都删除,而是只删除这一个结点并保持二叉排序树的特性。假设在二叉排序树上被删除结点为 p ,f 为其双亲结点,则删除结点 p 的过程分为以下 3 种情况。

1)p 结点为叶子结点。由于删除叶子结点后不会破坏二又排序树的特性,因此直接删除即可。

2)p 结点只有右子树而无左子树,或者只有左子树而无右子树。此时只需将 p 删掉,然后将 p 的子树直接连接在原来 p 与其双亲结点 f 相连的指针上即可。

3)p 结点既有左子树又有右子树:此时可以将这种情况转化为1)或2)中的情况,做法为:先沿 p 的左子树根结点的右指针一直往右走,直到来到其右子树的最右边的一个结点 r (也可以沿着 p 的右子树根结点的左指针一直往左走,直到来到其左子树的最左边的一个结点)。然后将 p 中的关键字用 r 中的关键字代替。最后判断,如果 r 是叶子结点,则按照 1)中的方法删除 r;如果 r 是非叶子结点,则按照 2)中的方法删除 r(此时的r不可能是有两个子树的结点)。

2、平衡二叉树

(1)平衡二叉树的概念:

平衡二叉树(balanced binary tree)又称AVL树(Adelson-Velskii and Landis)。

一棵平衡二叉树或者是空树,或者是具有下列性质的二叉排序树:

①  左子树与右子树的高度之差的绝对值小于等于 1;

②  左子树和右子树也是平衡的二叉排序树;

为了判断一棵二又排序树是否是平衡二叉树,引进了平衡因子的概念。平衡因子是针对树中的结点设的,一个结点的平衡因子为其左子树的高度减去右子树高度的差。对于平衡二又树,树中的所有结直的平衡因子的取值只能是-1、0、1三个值:

(2)平衡二叉树的建立:

建立平衡二又树的过程和建立二又排序树的过程基本一样,都是将关键字逐个插入空树中的过程。所不同的是,在建立平衡二叉树的过程中,每插入一个新的关键字都要进行检查,看是否新关键字的插入会使得原平衡二叉树失去平衡,即树中出现平衡因子绝对值大于 1 的结点。如果失去平衡则需要进行平衡调整。

(3)平衡调整:

假定向平衡二叉树中插入一个新结点后破坏了平衡二叉树的平衡性,则首先要找出插入新结点后失去平衡的最小子树,然后再调整这棵子树,使之成为平衡子树。值得注意的是,当失去平衡的最小子树被调整为平衡子树后,无须调整原有其他所有的不平衡子树,整个二叉排序树就会成为一棵平衡二叉树。所谓失去平衡的最小子树是以距离插入结点最近,且以平衡因子绝对值大于 1 的结点作为根的子树,又称为最小不平衡子树。

(4)平衡调整必须保持排序二叉树左小右大的性质,平衡调整有4种情况,分别为LL型、RR型、LR型和RL型。平衡调整的四种类型如下:调整原则:降低高度、保持二叉排序树的性质

四、散列表

1、散列表的概念

根据给定的关键字来计算出关键字在表中的地址,记录的存储位置与关键字之间存在对应关系。对应关系用 Hash 函数计算,在 Hash 表中,关键字和关键字的地址有确定的关系,这种关系用 Hash 函数的 H 来表示,比如:关键字为 key,则 H(key) 称为 Hash 地址,就是 key 在查找表中的地址。

2、散列表的建立方法以及冲突解决方法

(1)Hash表的建立方法:即根据给定的关键字 key 依照函数 H 来计算关键字 key 在表中的地址,并把 key 存在这个地址上。

(2)例题:关键字序列为 {7,4,1,14,100,30,5,9,20,134} ,设 Hash 函数为 H(key) = key Mod 13,建立表长为 13 Hash表。求解过程如下:

根据所求的关键字的地址,将关键字填入Hash表中,如下:

现在的Hash表中存在的问题是,多个关键字存入一个地址中,这种情况称为冲突,那怎么解决冲突:

得到新的Hash表,消除了冲突:

(3)在等概率下查找成功和不成功的平均查找长度 ASL1 和 ASL2 :

求ASL1,关键是求出对于查找每个关键字所对应的比较次数,如果没有冲突,只需要比较一次,如果发生冲突,则根据其冲突解决方法来计算出比较次数。

                                                   ASL1 = (1+2+1+2+2+1+2+1+2+8) / 10 = 11 / 5

求ASL2,关键是求出查找不成功情况下的比较次数。这里的比较次数是针对于表中每个可以通过Hash函数计算得到的地址来说的,对每个地址求出由这个地址开始的比较次数,然后求其平均数就是查找不成功时的平均查找长度。例如,查找关键字21,由Hash函数得到一个地址8,经过比较发现和地址8上的关键字不同,则根据冲突处理方法后移,一直移到地址12,发现为空位置,证明查找不成功,比较次数为5,即对于地址8,其对应的查找不成功的比较次数为5。地址比较次数如下:

                                                   ASL2 = (1+3+2+1+9+8+7+6+5+4+3+2+1) / 13 = 4

3、散列表的性能分析

(1)查找成功时的平均查找长度是指找到表中已有表项的平均比较次数,它是找到表中各个已有表项的平均比较次数。而查找不成功的平均查找长度是指在表中找不到待查的表项,但找到插入位置的平均比较次数。它是在表中所有可能散列到的地址上插入新元素时,为找到空位置而进行探查的平均次数。装填因子是关键字个数和表长度的比值。

(2)例题:用关键字序列 {1,9,12.11,25,35,17,29} 创建一个 Hash 表,装填因子 a 为 1/2,试确定表长 m,采用除留余数法构造 Hash 函数,采通链地址法来处理冲突,并计算查找成功与不成功时的平均查我长度ASL1;和 ASL2;

1)由装填因子a的定义知道,a=n/m,其中n为关键字个数,m为表长,因此可以求出m为16。

2)除留余数法的Hash函数构造公式为 H(key)= key Mod p,其中 p 为不大于表长的最大素数(如果 p 大于 m,则对 p取余数后有可能映射到表之外的地址,因此 p 必须不大于表长,与素数做取余运算后可以使得 Hash 地址尽可能地散落均匀。减少冲突,因此 p 取素数),因表长为16,所以 p 取 13,Hash 函数为H(key)= key Mod 13 

3)用 2)中的Hash函数并用链地址法处理冲突所建立的Hash过程如下:

4)对于ASL1根据关键字来计算,对于ASL2根据地址来计算:

ASL1 = (1+1+1+1+2+1+1+2)/8 = 1.25

ASL2 = (0+1+0+1+1+0+0+0+0+2+0+1+2)/13 = 0.62

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值