【数据结构】第七章 查找

第七章 查找

7.1 基本概念

  1. 查找:在数据集合中寻找满足条件的数据元素
  2. 查找表:用于查找的数据结合称之为查找表
  3. 静态查找表:如果一个查找表只涉及到检索操作,而不涉及删除和插入操作,则为静态查找表。
  4. 关键字:数据元素中唯一标识该元素的某个数据项的值,比如学生的学号
  5. 平均查找长度ASL:指的是一次查找需要比较的关键字的次数, 算法性能的关键

7.2 顺序查找和折半查找

一、顺序查找

最直观的查找方法,其基本思想是从线性表的一端开始,逐个检查关键字是否满足给定条件。

在顺序查找中会使用哨兵。哨兵指的是将存储结构的第0个赋值为需要查询的值,然后从后往前查找,如果返回的位置为0,则证明没有所需的元素。使用哨兵可以减少判断语句从而提高效率,哨兵是一种用途广泛的概念,不只是用于顺序查找
A S L 成 功 = 1 n + 1 ASL_{成功}=\frac{1}{n+1} ASL=n+11 A S L 失 败 = n + 1 ASL_{失败}=n+1 ASL=n+1

ASL的数量级为O(n),

有序表的顺序查找

在有序表中,判断查找是否失败相对比较方便。如果发现第i个元素小于关键字,但是第i+1个元素却大于关键字,则可以认为表中不存在关键字,查找失败。
在这里插入图片描述

其中圆形结点为成功结点,方形结点为失败结点。一个成功节点的查找长度为自身所在的层数,一个失败节点的查找长度为其父节点所在的层数

A S L 失 败 = n 2 + n n + 1 ASL_{失败}=\frac{n}{2}+\frac{n}{n+1} ASL=2n+n+1n

根据被查概率概率降序排列
可以缩短查找成功时的ASL,但是查找失败时必须遍历一整个表,因此失败时ASL比有序表大

二、折半查找

折半查找又称为二分查找,只适用于有序的顺序表。

其特点是将顺序表划分为一颗区间二叉树。该算法只适用于顺序存储结构,不适用于链式存储结构,并且要求元素关键字有序排列。

平均时间复杂度为O(log2n)。但是折半查找的速度不一定比顺序查找快

查找判定树
二分查找的过程可以构造出一个查找判定树
在这里插入图片描述
当low和high之间有奇数个元素,则mid分割后,左右两部分元素相等;
当low和high之间有偶数个元素,则mid分割后,左半部分比右半部分少一个元素

因此,在判定树中,如果 m i d = ⌊ ( l o w + h i g h ) / 2 ⌋ mid=\lfloor(low+high)/2\rfloor mid=(low+high)/2,则右子树结点数-左子树结点数=0或者1;,如果 m i d = ⌈ ( l o w + h i g h ) / 2 ⌉ mid=\lceil(low+high)/2\rceil mid=(low+high)/2,则左子树结点数-右子树结点数=0或者1

折半查找判定树一定是平衡二叉树,并且只有最下面一层是不满的,因此元素个数为n的时候树高为 ⌈ l o g 2 ( n + 1 ) ⌉ \lceil log_2(n+1)\rceil log2(n+1)。n个元素的判定树的失败节点有n+1个

三、分块查找

分块查找又称为索引顺序查找,在408中较少考察代码

分块查找基本思想:将查找表划分为若干个字快,块内元素可以无序,但是块之间是有序的。再建立一个索引表,索引表中的每个元素含有各块的最大关键字和第一个元素的地址,索引表按关键字有序排列。

在这里插入图片描述

分块查找的过程分为两步:一个是在索引表中确定待查关键字所在的块,可以使用顺序或者折半查找索引表;定位好了所在块后再在块内寻找。

在使用折半查找查找索引表时,如果索引表中不包括关键字,那么折半查找索引表会最终停到low>high的位置。如果是一般的折半查找,此时意味着查找失败,而对于折半查找索引表来说,此时意味着需要查找的元素可能位于low指针指向的索引的分块中,需要进入该分块进行查找。
也就是说,如果索引表中不包括关键字,那么折半查找需要一直运行到low>high的时候才能确定位于哪个分块

查找效率分析(ASL)
需要能够具体说出在采用顺序/折半查找索引表时,查找到某个具体元素所需要的查找次数。一般是查找索引表的次数+查找分块内元素的次数

假设长度为n的查找表被均匀分成了b块,每块有s个元素,则平均长度为: A S L = L I + L S ASL=L_I+L_S ASL=LI+LS,其中如果采用顺序查找查索引表的话,查找索引长度 L I = b + 1 2 L_I=\frac{b+1}{2} LI=2b+1, 查找分块长度 L S = s + 1 2 L_S=\frac{s+1}{2} LS=2s+1。由于n=sb,因此将式子回代得: A S L = b + 1 2 + s + 1 2 = 1 2 s + 1 + n 2 s ASL=\frac{b+1}{2}+\frac{s+1}{2}=\frac{1}{2}s+1+\frac{n}{2s} ASL=2b+1+2s+1=21s+1+2sn
通过求导可知道,当 s = n s=\sqrt n s=n 的时候,ASL最小

折半查找的ASL是:
A S L = ⌈ l o g 2 ( b + 1 ) ⌉ + s + 1 2 ASL=\lceil log_2(b+1)\rceil+\frac{s+1}{2} ASL=log2(b+1)+2s+1

7.3 树形查找

一、二叉排序树(BST)

定义

左子树上的结点都小于根节点、所有右子树都大于根节点
使用中序遍历可以得到一个递增的有序序列

查找具体值的代码实现

从根节点开始,如果小于根节点则往左走,大于根节点则往右走,其路径是不会出现分叉的,也就是说,对BST的查找是不会出现回溯的

// 查找二叉排序树具体结点
BiTree *BST_Search(BiTree t, int v){
    while (t!=NULL){
        if (t.data>v){
            t=t.rchild;
        }else if (t.data < v){
            t = t.rchild;
        }else{
            return t;
        }
    }
    return t;
}

// 查找二叉排序树具体结点(递归)
BiTree *BST_Search1(BiTree t, int v){
    if (t==NULL || t.data==v){
        return t;
    }
    if (t.data < v){
        BST_Search1(t.rchild,v);
    }else{
        BST_Search1(t.lchild, v);
    }
}

非递归的空间复杂度为O(1),递归的空间复杂度为O(n)
在BST的查找中,如果生成的BST平衡,那么其最大查找次数最小,为 l o g 2 n + 1 log_2n +1 log2n+1;如果生成的BST及其不平衡,比如生成BST时的元素插入顺序恰好是递增或递减,那么最大查找次数最大,为n

插入

如果二叉树为空,则直接插入,否则,如果关键字k小于根节点,则插入左子树,如果关键字大于根节点,则插入右节点。新插入的结点一定是叶子结点。采用递归调用的方法空间复杂度为O(H),H为树高;采用非递归的空间复杂度为O(1)

// 二叉排序树的插入(递归)
int BST_insert(BiTree &t, int v){
    if (t==NULL){   //已经到了根节点
        t = (BiTree)malloc(sizeof (BiTree));
        t.data=v;
        t.lchild=t.rchild=NULL;
        return 1;
    }else if (k<t.data){
        BST_insert(t.lchild,v);
    }else if (k>t.data)
        t = BST_insert(t.rchild,v);
    else
        return 0
}
删除
  • 叶子结点直接删除
  • 删除的结点n只有左子树,那么让其子树的根节点替代他的位置,成为n的父节点的子树
  • 同样,删除的结点n只有右子树,那么让其子树的根节点替代他的位置,成为n的父节点的子树
  • 删除的结点n既有左子树又有右子树。那么:
    • 可以中序遍历右子树,其第一个节点p为右子树中最小的结点(也就是右子树中最左下的结点),让结点p替代结点n的位置
    • 可以遍历左子树,取得左子树中最小的结点p(也就是左子树中最右下的结点),让结点p替代结点n的位置
效率分析

查找长度:在查找运算中,需要对比关键字的次数反映了查找操作时间的复杂度。平均查找长度又称为ASL。需要会算二叉排序树的平均查找长度。查找失败所需要的平均长度也要会计算,一般只有查找到叶子结点仍未查找到目标值,则判定为查找失败

二叉排序树的ASL的优劣很大情况下取决于树的高度,因此树的高度越小,性能越优秀。而构造二叉树的输入序列有序会导致一个倾斜的单支树,此时BST高度最高,性能最差。如果构造出来的是一颗平衡二叉树,则性能最好。其最好查找长度为O(logn),最坏查找长度为O(n)

平衡二叉树

平衡二叉树AVL是二叉排序树的一种特殊形式,在AVL中,任意结点的左子树和右子树的差不能超过1。
平衡因子:节点的左子树高-右子树高。AVL中平衡因子只可能是0\1-1三种情况。

平衡二叉树的插入
平衡二叉树的插入会使得二叉树失去平衡。由于对于AVL的插入都是插入叶子结点(因为AVL是一种二叉排序树),因此可以通过调整最小不平衡子树来使得AVL恢复平衡,

想要找到最小不平衡子树需要利用平衡因子。从插入节点处开始向上回溯,找到第一个平衡因子不是0、1或-1的结点。则以该节点为根节点的树为最小不平衡树

在导致AVL失衡插入操作中,有如下四种情况:
在这里插入图片描述
LL平衡旋转(右单旋转)
先判断图中各个节点的大小关系:BL<B<BR<A<AR

在结点A的左孩子左子树上插入了新节点,导致以A为根的子树失去了平衡,需要进行一次向右旋转的操作。将A的最孩子B向右旋转代替A成文根节点,而A成为B的子节点,并且将B原来的右子树变成了A的左子树。旋转完成后依旧符合二叉排序树的性质
在这里插入图片描述

代码思路:
假设指针p指向B,指针f指向A,指针gf指向A的父节点

f->lchild=p->rchild;
p->rchild=f;
gf->lchild/rchild=p;

RR平衡旋转(左单旋转)
先判断图中各个节点的大小关系:AL<A<BL<B<BR

在结点A的右孩子右子树上插入了新节点,导致以A为根的子树失去了平衡,需要进行一次向左旋转的操作。将A的右孩子B向右旋转代替A成为根节点,而A成为B的子节点,并且将B原来的左子树变成了A的右子树旋转完成后依旧符合二叉排序树的性质
在这里插入图片描述

LR平衡旋转(先左旋后右旋 )
先判断图中结点的大小:BL<B<CL<C<CR<A<AR

在A的左孩子的右子树上插入了新节点,导致以A为根的子树失去了平衡,需要先对C进行左旋,代替B的根节点的位置,而C的左子树称为B的右子树。然后右旋转C,代替A的根节点的位置,而C的右子树变成A的右子树。旋转完成后依旧符合二叉排序树的性质
在这里插入图片描述

RL平衡旋转
在A的左孩子的右子树上插入新节点,导致以A为根的子树失去平衡,需要先对C进行右旋,代替B的根节点位置,再进行左旋,代替A的根节点位置

删除
查找效率

主要影响因素来自于树的高度,也就是查找操作的时间复杂度不可能超过O(h)

假设以nh表示深度为h的平衡树中含有最少的结点数。则有n0=0,n1=1,n2=2,并且nh=nh-1+nh-2+1。平衡二叉树的平均高度为O(log2n),因此的平均查找长度AVL为O(log2n)

二、B树

考试侧重于考察性质、效率等

B树是一种多路平衡查找树。他每个节点最多有n个子树,而每一个节点是一个n-1的列表,比如根节点为10,20,30的列表,那么第一个子节点指向的是值小于10的列表,第二个子节点指向的是值位于10到20的列表,以此类推。其子节点中的元素也是有序的。另外,除了根节点之外所有非叶节点至少有m/2棵子树。 根节点都是没有数据的空节点,是失败节点。
在这里插入图片描述

如果每个节点内关键字太少,导致树变高,要查更多层节点,效率会下降,因此在B树中规定除根节点外每个节点至少有 ⌈ n / 2 ⌉ − 1 \lceil n/2\rceil-1 n/21个元素和 ⌈ n / 2 ⌉ \lceil n/2\rceil n/2颗子树,以提高效率。另外,n叉查找树越平衡,其查找效率也越高,因此B树规定,对于任意一个节点其所有子树高度都相同。

性质:
在一棵m阶的B树中:

  1. 树中每个节点最多有m棵子树,最多有m-1个关键字
  2. 如果根节点不是终端节点,则至少还有两棵子树
  3. 除根节点外每个节点至少有 ⌈ n / 2 ⌉ \lceil n/2\rceil n/2个子树和 ⌈ n / 2 ⌉ − 1 \lceil n/2\rceil-1 n/21个关键字
  4. 所有叶节点都出现在同一层次上,为失败结点,不携带任何信息
  5. 对于任意一个节点其所有子树高度都相同。

计算B树高度不会包括失败节点
含有n个关键字的m阶B树的最大、最小高度是多少

  • 最小高度要求让每个节点尽可能的多关键字,也就是有m-1个关键字和m个分叉,则有 n ≤ ( m − 1 ) ( 1 + m + m 2 + m 3 + . . . + m h − 1 ) = m h − 1 n\leq (m-1)(1+m+m^2+m^3+...+m^{h-1})=m^h-1 n(m1)(1+m+m2+m3+...+mh1)=mh1因此 h ≤ l o g m ( n + 1 ) h\leq log_m(n+1) hlogm(n+1)
  • 最大高度要求各层尽可能少,也就是根节点只有2个分叉,其他节点只有 ⌈ n / 2 ⌉ \lceil n/2\rceil n/2个元素。因此至少有 2 ( ⌈ m / 2 ⌉ ) h − 1 2(\lceil m/2\rceil)^{h-1} 2(m/2)h1个结点, 对于n个关键字的B树必然有n+1个叶子结点,则 n + 1 ≥ 2 ( ⌈ m / 2 ⌉ ) h − 1 n+1\geq 2(\lceil m/2\rceil)^{h-1} n+12(m/2)h1,可推出 h ≤ l o g ⌈ m / 2 ⌉ n + 1 2 + 1 h\leq log_{\lceil m/2\rceil}\frac{n+1}{2}+1 hlogm/22n+1+1
  • 对于n个关键字的B树必然有n+1个叶子结点,这也会单独考
B树的插入

假设一个5叉B树,也就是每个节点有4个数据。如果插入新数据可能会使得当前节点内数据超过限制,此时需要进行节点的拆分,将一个节点拆分为一个父节点和两个相连的子节点。拆分会从中间的第 ⌈ n / 2 ⌉ \lceil n/2\rceil n/2个位置处拆分。
在这里插入图片描述
在这里插入图片描述

每次插入一定插入到最底层的节点,因为失败节点只能出现在最底层。在B树中,非根节点值必须有它对应的左右子树,不然会导致失败节点出现在上层,这违背了B树的定义。

在B树中非根节点中插入元素,并且导致其元素个数超出限制时,作以下变换
在这里插入图片描述
在这里插入图片描述

B树的删除

删除非终端节点

被删除关键字为非终端节点,则用直接前驱或者直接后继代替被删除的关键字。
直接前驱:当前关键字左侧指针指向的子树最右下的元素,也就是左子树中最大的节点
直接后继:当前关键字右侧指针指向的子树最左下的元素,也就是右子树中最小的节点

如下图中,删除80节点,那么可以选择其直接前驱77或者直接后继82来代替
在这里插入图片描述

也就是删除非终端节点的删除可以转化为对终端节点的删除

删除终端节点

只要删除后节点数依然不小于 ⌈ n / 2 ⌉ \lceil n/2\rceil n/2,则都可以直接进行。

如果删除后节点数小于 ⌈ n / 2 ⌉ \lceil n/2\rceil n/2

  • 如果它相邻的兄弟节点数仍多于n/2,则可以像兄弟借一个节点使用。一般是该节点的父节点借一个元素给该节点,而该节点的兄弟节点将其中一个元素移动到父节点中。如果是左孩子借元素给父节点,那么借出的结点是左孩子中最大的元素;如果是右孩子借元素给父节点,那么借出的结点是右孩子中最小的元素。
  • 如果它的兄弟节点数小于n/2了,则可以将该节点和兄弟节点,以及其父节点中的关键字进行合并。合并过程中,双亲节点的关键字个数会减1。如果其双亲节点是根节点而且关键字个数减少到0了,则可以直接删除根节点。

三、B+树

一个m阶b+树满足:

  • 每个分支节点最多有m棵子树
  • 非叶根节点至少有2颗子树,其他每个分支节点至少有m/2棵子树。非叶根节点指的是叶节点再往上的一个节点。其他每个分支节点至少有 ⌈ n / 2 ⌉ \lceil n/2\rceil n/2个子树
  • 节点的子树与关键字个数相等,这是B+树和B树的区别
  • 所有叶节点包含全部关键字以及指向相应记录的指针,叶节点中将关键字按大小排序,并且相邻叶节点按大小顺序相互连接
  • 所有分支节点中只包含了她的各个子节点的最大值

在这里插入图片描述

B+树查找

无论查找成功与否,都需要走到最下面一层的节点。因为关键字都在叶子结点,非叶节点只是索引

B+树也可以通过指针p进行顺序查找

B树和B+树的区别
  • B+树中n个关键字对应n个子树;B树中n个关键字对应n+1个子树
  • B+树中,叶子节点包含所有关键字,非叶子节点的关键字也会在叶子节点中出现;B树中关键字不重复
  • B+树中叶子节点包含信息,非叶子节点只起索引作用;B树中所有节点都包含信息。
  • m阶B树的根节点关键字数为 [ 1 , m − 1 ] [1,m-1] [1,m1],其他节点为 [ ⌈ m / 2 ⌉ − 1 , m − 1 ] [\lceil m/2\rceil-1, m-1] [m/21,m1];m阶B树的根节点关键字数为 [ 1 , m − 1 ] [1,m-1] [1,m1],其他节点为 [ ⌈ m / 2 ⌉ − 1 , m − 1 ] [\lceil m/2\rceil-1, m-1] [m/21,m1]

在B+树中,非叶子节点不含有关关键字对应纪录的存储地址,可以使用一个磁盘块包含更多关键字,使得 B+树阶更大,树高更矮,读磁盘数更少,查找更快。如今比如MySQL等关系型数据库不少都采用了B+树。

四、散列(Hash)查找

散列表又称为哈希表,是一种数据结构,特点是数据元素的关键字与其存储地址直接相关。数据元素的值通过哈希函数处理后,就可以得出该元素应该存放的地址。

如果不同的关键字通过散列函数映射到了同一个值,则称他们为同义词
通过散列函数确定的位置已经存放了其他元素,这种情况称之为冲突

拉链法

拉链法处理冲突是将所有同义词存储在一个链表中
在这里插入图片描述
查找值为21的关键字,应该是21%13=8,所以查找长度为0

拉链法效率

需要会算查找成功和查找失败的平均ASL
冲突次数越多,则ASL越大,效率越低。
当完全没有冲突时,散列查找时间复杂度可达O(1)
在这里插入图片描述
装填因子 α \alpha α=表中记录数/散列表长度。装填因子的大小会影响到散列表的效率,因为装填得越多,发生冲突的几率越大

常见散列函数

除留余数法:H(key)=key%p。一般来说,如果散列表表长为m,则取不大于m但最接近于或等于m的质数p。因为取质数发生冲突的概率更小。

直接定址法:H(key)=key或者H(key)=a*key+b。这种方法计算最简单并且没有冲突,适用于关键字分布基本连续的情况。比如说一个班的学生的学号范围是2022-2222,那么就将这些学号一一对应放入长度为200的散列表中

平方取中法:取关键字的平方值的中间几位作为散列地址。具体取多少位要按照实际情况而定。这种方法得到的散列地址和关键字每一位都有关系,因此散列地址分布较为均匀,适用于关键字的每位取值都不够均匀的情况

开放定址法

开放定址法是处理冲突的一个方法。这种方法允许可存放新表项的空闲地址既向他的同义词表项开放,也向他的非同义词表项开放。它不像拉链法一样有链表跟随。

1.线性探测法
发生冲突的时候,一个一个往后探测相邻的下一个单元是否为空,如果有空单元则放入冲突的关键字。

查找失败时,需要一直向后查找直到出现第一个空元素。
直接删除一个元素,可能会导致其他的节点查找失败,因此删除可能不能直接移除元素,可以新增一个状态位用于标识该元素的有效性。在任何开放定址法中(包括平方探测法、再散列法),都不能直接删除元素。

要会算查找成功的ASL和查找失败的ASL
在这里插入图片描述
在这里插入图片描述
线性探测法很容易导致同义词和非同义词混杂在一起,严重影响查找效率。在对某个同义词进行查找的时候,还可能要遍历数个非同义词,查找精度低下,纯纯的弱智方法

2.平方探测法
有n个元素按照哈希函数运算后都需要放置到位置i中,按照数列[02, 12, -12, 22, 22, …n2]进行偏移存放,也就是第0个元素放在i处,第一个放在i+1处,第二个放在i-1处,第三个放在i+4处,以此类推

平方探测法比线性探测法不容易产生堆积问题,并且存储更有规律。

散列表长度必须表示成4j+3的素数才能探测所有位置。

3.再散列法
多准备几个散列函数,使用第一个散列函数发生冲突时再使用第二个散列函数,直到不冲突为止。

7.5 总结

算法名称成功平均ASL失败ASL最坏ASL
顺序查找 n + 1 2 \frac{n+1}{2} 2n+1n+1
折半查找 l o g 2 n log_2n log2n
  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值