目录
1. 查找的基本概念
根据给定的值,在查找表中确定一个其关键字等于给定值的数据元素。
主关键字:
次关键字:
查找表分类
静态查找表(仅查询
动态查找表(有删除和插入
2. 线性表的查找
顺序查找:从表中最后一个记录开始,逐个进行记录的关键字和给定值的比较
平均查找长度ASL:
顺序查找的平均查找长度为(n+1)/2
折半查找的平均查找长度为 ,时间复杂度为
折半查找判定树
已知一个顺序存储的有序表为(15,26,34,39,45,56,58,63,74,76)
折半查找法的平均查找长度(成功/失败)_折半查找平均查找长度计算-CSDN博客
1.首先拿到有序表
2.根据有序表进行画树
步骤1.先取中间结点作为树的根,通过计算mid = (i + j)/ 2 = 6(向下取整)i = 1, j = 12
步骤2.然后左边剩余的1-5当中继续重复步骤一得到6的左儿子3,紧接着从右边剩余的7-12当中得到数字9作为6的右儿子
步骤3.重复上述操作,得到一个完整的树
3.紧接着将结点补齐
用方框补齐,代表查找失败,叶子结点也需要进行处理
4.写出层数
查找的次数=每一层的层数 x 每一层的结点个数,上图即是1x1 + 2x2 + 3x4 + 4x5 = 37,则查找成功的平均查找长度为37/12,注意:当计算查找失败的平均长度时,层数需要依次减1,即原先第四层变为第三层,然后进行查找失败的次数的计算,即是3x3 + 4x10 = 49(以上算式中“x”前面的为层数,后面为该层的结点数),则对应的查找失败的平均查找长度为49/13
综上答案为A,D
动态查找表
在查找的过程中动态生成
3. 树表的查找
3.1 二叉排序树
3.1.1 定义
1. 就是若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
2. 若它的右子树不空,则右子树上所有节点的值均大于其根节点的值。
3. 其左右子树本身又各是一棵二叉排序树
左小,中中,右大
3.1.2 存储结构
typedef int DataType;
typedef struct BST_Node {
DataType data;
struct BST_Node *lchild, *rchild;
}BST_T, *BST_P;
void CreateBST(BST_P *T, int a[], int n)
{
int i;
for (i = 0; i < n; i++)
{
Insert_BST(T, a[i]);
}
}
3.1.3 二叉排序树的查找
要在二叉树中找出查找最大最小元素是极简单的事情,从根节点一直往左走,直到无路可走就可得到最小值;从根节点一直往右走,直到无路可走,就可以得到最大值。
查找最小关键字:
BST_P SearchMin(BST_P root)
{
if (root == NULL)
return NULL;
if (root->lchild == NULL)
return root;
else //一直往左孩子找,直到没有左孩子的结点
return SearchMin(root->lchild);
}
查找最大关键字:
BST_P SearchMax(BST_P root)
{
if (root == NULL)
return NULL;
if (root->rchild == NULL)
return root;
else //一直往右孩子找,直到没有右孩子的结点
return SearchMax(root->rchild);
}
递归版查找(找到返回关键字的结点指针,没找到返回NULL):
BST_P Search_BST(BST_P root, DataType key)
{
if (root == NULL)
return NULL;
if (key > root->data) //查找右子树
return Search_BST(root->rchild, key);
else if (key < root->data) //查找左子树
return Search_BST(root->lchild, key);
else
return root;
}
非递归版查找:
BST_P Search_BST(BST_P root, DataType key)
{
BST_P p = root;
while (p)
{
if (p->data == key) return p;
p = (key < p->data) ? p->lchild : p->rchild;
}
return NULL;
}
3.1.4 二叉排序树的插入
插入新元素时,可以从根节点开始,遇键值较大者就向左,遇键值较小者就向右,一直到末端,就是插入点。
void Insert_BST(BST_P *root, DataType data)
{
//初始化插入节点
BST_P p = (BST_P)malloc(sizeof(struct BST_Node));
if (!p) return;
p->data = data;
p->lchild = p->rchild = NULL;
//空树时,直接作为根节点
if (*root == NULL)
{
*root = p;
return;
}
//是否存在,已存在则返回,不插入
if (Search_BST(root, data) != NULL) return;
//进行插入,首先找到要插入的位置的父节点
BST_P tnode = NULL, troot = *root;
while (troot)
{
tnode = troot;
troot = (data < troot->data) ? troot->lchild : troot->rchild;
}
if (data < tnode->data)
tnode->lchild = p;
else
tnode->rchild = p;
}
从空树出发经历一系列的查找插入操作后,可以生成一棵二叉排序树
关键字的输入顺序不同,建立的二叉排序树就不同
3.1.5 二叉排序树删除
对于二叉排序树中的节点A,对它的删除分为两种情况:
1、如果A只有一个子节点,就直接将A的子节点连至A的父节点上,并将A删除;
2、如果A有两个子节点,我们就以左子树内的最大节点取代A。
删除节点代码:
void Delete ( BiTree & 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;}
p->data =s->data;
if(q!=p) q->rchild = s->lchild;
else q->lchild =s-> lchild;
delete s;
}
return TRUE;
}
3.1.6 二叉排序树的查找分析
不同插入次序的序列构成的二叉排序树,形态不同,查找效率差别也很大。
最坏情况:单支的树,相当于顺序查找,ASL=(n+1)/2 O(n)
最好情况:二叉排序树的深度同折半查找的判定树 , ASL = ,树的深度为 ,O(logn)
3.2 平衡二叉树(AVL
在AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树。
3.2.1 为什么要有平衡二叉树
二叉搜索树一定程度上可以提高搜索效率,但是当原序列有序时,例如序列 A = {1,2,3,4,5,6},构造二叉搜索树如图 1.1。依据此序列构造的二叉搜索树为右斜树,同时二叉树退化成单链表,搜索效率降低为 O(n)。
在此二叉搜索树中查找元素 6 需要查找 6 次。
二叉搜索树的查找效率取决于树的高度,因此保持树的高度最小,即可保证树的查找效率。同样的序列 A,将其改为图 1.2 的方式存储,查找元素 6 时只需比较 3 次,查找效率提升一倍。
可以看出当节点数目一定,保持树的左右两端保持平衡,树的查找效率最高。
这种左右子树的高度相差不超过 1 的树为平衡二叉树。
3.2.2 平衡二叉树的定义
1. 可以是空树。
2. 假如不是空树,任何一个结点的左子树与右子树都是平衡二叉树,并且高度之差的绝对值不超过 1。
ps:完全二叉树必为平衡树,平衡树不一定是完全二叉树
平衡因子
定义:某节点的左子树与右子树的高度(深度)差即为该节点的平衡因子(BF,Balance Factor),平衡二叉树中不存在平衡因子大于 1 的节点。在一棵平衡二叉树中,节点的平衡因子只能取 0 、1 或者 -1 ,分别对应着左右子树等高,左子树比较高,右子树比较高。
结点的平衡因子=结点左子树的高度-结点右子树的高度
如果在一棵AVL树中插入一个新结点后造成失衡,必须立刻重新调整树的结构,使之恢复平衡
调整的基本思想:
1. 在构造二叉排序树的过程中,每插入一个结点,首先检查是否因插入而破坏了树的平衡性
2. 若失衡,则找出最小的不平衡子树,在保持二叉排序树特性的前提下,调整最小不平衡子树使之成为新的平衡子树
最小失衡子树:在新插入的结点向上查找,以第一个平衡因子的绝对值超过 1 的结点为根的子树称为最小不平衡子树。
也就是说,一棵失衡的树,是有可能有多棵子树同时失衡的。而这个时候,我们只要调整最小的不平衡子树,就能够将不平衡的树调整为平衡的树。
3个结点
平衡二叉树的失衡调整主要是通过旋转最小失衡子树来实现的。根据旋转的方向有两种处理方式,左旋 与 右旋 。
旋转的目的就是减少高度,通过降低整棵树的高度来平衡。哪边的树高,就把那边的树向上旋转。
平衡二叉排序树的构造
在插入过程中,采用平衡旋转技术
例题1
设有关键码序列{5 ,4 , 2, 8, 6, 9}构造平衡树
例题2
设有关键码序列{16 ,3 ,7 ,11 ,9 ,26 ,18 ,14 ,15 }构造平衡树
3.3 B-树
是一种平衡的多分树
3.3.1 m阶的B-树的结构定义:
1. 树的每个结点至多有m棵子树
2. 若根结点不是叶子结点,则至少有两棵子树
3. 除根结点之外的所有非终端结点至少有m/2 棵子树
4. 一个包含 个关键字的结点有 +1 个孩子;
5. 所有叶子结点都出现在同一层次,不含任何信息
6. 一个结点中的所有关键字升序排列,两个关键字 1 和 2 之间的孩子结点的所有关键字 key 在 (1,2) 的范围之内。
3.3.2 B-树的性质:
1. 树高平衡,所有叶结点都在同一层
2. 关键字没有重复,父结点中的关键字是其子结点的分界
3. B-树把值接近的相关记录放在同一个磁盘页中,从而利用了访问局部性原理
4. B-树保证树至少有一定比例的结点是满的
3.3.3 B-树的查找
查找的时间取决于两个因素:
1. 给定关键字所在结点的层次
2. 结点中关键字的个数
3.3.4 B-树的插入
步骤
1.找到最底层,插入
2. 若溢出,则结点分裂,中间关键字同新指针插入父结点
3. 若父结点也溢出,则继续分裂
4. 分裂的过程可能传达到根结点(则树升高一层)
3.3.5 B-树的中序遍历
B-树的中序遍历与二叉树的中序遍历也很相似,我们从最左边的孩子结点开始,递归地打印最左边的孩子结点,然后对剩余的孩子和关键字重复相同的过程。最后,递归打印最右边的孩子.
对于这个图的中序遍历结果为:
3.3.6 B-树的删除
B-树的删除操作相比于插入操作更为复杂,如果仅仅只是删除叶子结点中的关键字,也非常简单,但是如果删除的是内部节点的,就不得不对结点的孩子进行重新排列。
与 B-树的插入操作类似,我们必须确保删除操作不违背 B-树的特性。
1. 删除的关键字不在叶结点层时
-先把此关键字与它在B树里的后继对换位置,然后再删除该关键字
2. 删除的关键字在叶结点层时
-删除后关键字的个数不小于[m/2]-1
`直接删除
-删除后关键字的个数小于[m/2]-1
`若兄弟结点关键字不等于[m/2]-1,从兄弟结点借(通过父结点
·若兄弟结点关键字等于[m/2]-1,合并
3.4 B+树
B+树是B-树的一种变形,是在叶子结点存储信息的树
定义:
1. 根结点至少有两棵子树,最多有m棵子树
2. 每个结点(除根外),至少有[m/2]棵子树,最多有m棵子树
3. 有n棵子树的结点包含n个关键字
4. 所有叶子结点在同一层,并包含了所有关键字,按关键字从小到大顺序链接
5. 所有非终端结点可以作为叶结点的索引,结点中仅包含其子树中最大(或最小)的关键字
3.5 红黑树
一种高效的自平衡(不是绝对平衡)二叉排序树
性质:
1.结点时红色或者黑色的
2. 根结点时黑色的
3. 叶子结点都为黑色,且都为空
4. 红色结点的父节点和子节点都为黑色(不存在两个连续的红色结点
5. 从任一结点到叶子结点的所有路径都包含相同数量的黑色结点
4. 哈希表的查找
参考图文并茂详解数据结构之哈希表 - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/144296454
哈希表也叫散列表,哈希表是一种数据结构,它提供了快速的插入操作和查找操作,无论哈希表总中有多少条数据,插入和查找的时间复杂度都是为O(1),因为哈希表的查找速度非常快,所以在很多程序中都有使用哈希表,例如拼音检查器。
基本思想:
在记录的存储地址和它的关键字之间建立一个确定的对应关系
对应关系--哈希函数(散列
冲突:两个不同的记录需要放到同一个存储位置
4.1 哈希函数
·哈希函数是一种映像
设计哈希函数一般应遵循以下原则:
1. 计算简单。散列函数不应该有很大的计算量,否则会降低查找效率
2. 函数值即散列地址分布均匀。函数值要尽量均匀散布在地址空间,这样才能保证存储空间的有效利用并减少冲突
4.2 哈希函数的构造方法
4.2.1 直接定址法
取关键字或关键字的某个线性函数值为哈希地址:H(key) = key 或 H(key) = a·key + b
其中a和b为常数,这种哈希函数叫做自身函数。
注意:由于直接定址所得地址集合和关键字集合的大小相同。因此,对于不同的关键字不会发生冲突。但实际中能使用这种哈希函数的情况很少。
适用情况:事先知道关键码,关键码集合不是很大而且连续性较好
4.2.2 数字分析法
根据关键码在各个位上的分布情况,选取分布比较均匀的若干位组成散列地址
如果关键字是位数较多的数字(比如手机号),且这些数字部分存在相同规律
则可以采用抽取剩余不同规律部分作为散列地址
比如手机号前三位是接入号,中间四位是 HLR 识别号,只有后四位才是真正的用户号
也就是说,如果手机号作为关键字,那么极有可能前 7 位是相同的
此时我们选择后四位作为散列地址就是不错的选择
同时,对于抽取出来的数字,还可以再进行反转
右环位移,左环位移等操作
目的就是为了提供一个能够尽量合理地将关键字分配到散列表的各个位置的散列函数
适用情况:
处理关键字位数比较大的情况
能预先估计出全部关键码的每一位上各种数字出现的频度,不同的关键码集合需要重新分析
4.2.3 平方取中法
取关键字平方后的中间几位为哈希地址。(适用于不知道全部关键字的情况,事先不知道关键码分布且关键码的位数不是很大
通过平方扩大差别,另外中间几位与乘数的每一位相关,由此产生的散列地址较为均匀。这是一种较常用的构造哈希函数的方法。
将一组关键字(0100,0110,1010,1001,0111)
平方后得(0010000,0012100,1020100,1002001,0012321)
若取表长为1000,则可取中间的三位数作为散列地址集:(100,121,201,020,123)
4.2.4 折叠法
4.2.5 除留余数法
取关键字被数p除后所得余数为哈希地址:H(key) = key mod p (p ≤ m)。
注意:这是一种最简单,也最常用的构造哈希函数的方法,并且不要求事先知道关键码的分布。它不仅可以对关键字直接取模(mod),也可在折迭、平方取中等运算之后取模。值得注意的是,在使用除留余数法时,对p的选择很重要。一般情况下可以选p为小于或等于表长(最好接近表长)的质数或不包含小于20的质因素的合数
4.3 哈希冲突
哈希冲突是不可避免的,我们常用解决哈希冲突的方法有两种「开放地址法」和「链表法」
4.3.1 开放地址法
在开放地址法中,若数据不能直接存放在哈希函数计算出来的数组下标时,就需要寻找其他位置来存放。在开放地址法中有三种方式来寻找其他的位置,分别是「线性探测」、「二次探测」、「再哈希法」
用开放定地址法处理冲突得到的散列表 叫 闭散列表
% m
(1) 线性探测再散列
(2) , 二次探测再散列
(3) d = 伪随机数序列,称伪随机探测再散列
在线性探测哈希表中,数据的插入是线性的查找空白单元,例如我们将数88经过哈希函数后得到的数组下标是16,但是在数组下标为16的地方已经存在元素,那么就找17,17还存在元素就找18,一直往下找,直到找到空白地方存放元素。我们来看下面这张图
线性探测法的优点:
只要哈希表欸有被填满,保证能找到一个空地址单元存放有冲突的元素
4.3.2 双哈希
双哈希是为了消除原始聚集和二次聚集问题,不管是线性探测还是二次探测,每次的探测步长都是固定的。双哈希是除了第一个哈希函数外再增加一个哈希函数用来根据关键字生成探测步长,这样即使第一个哈希函数映射到了数组的同一下标,但是探测步长不一样,这样就能够解决聚集的问题。
双哈希的方法,不易产生“聚集”,但增加了计算的时间
第二个哈希函数必须具备如下特点
- 和第一个哈希函数不一样
- 不能输出为0,因为步长为0,每次探测都是指向同一个位置,将进入死循环,经过试验得出
stepSize = constant-(key%constant);
形式的哈希函数效果非常好,constant
是一个质数并且小于数组容量
我们将上面的添加改变成双哈希探测,示意图如下:
双哈希的哈希表写起来来线性探测差不多,就是把探测步长通过「关键字」来生成
为什么双哈希需要哈希表的容量是一个质数?
假设我们哈希表的容量为15,某个「关键字」经过双哈希函数后得到的数组下标为0,步长为5。那么这个探测过程是0,5,10,0,5,10,一直只会尝试这三个位置,永远找不到空白位置来存放,最终会导致崩溃。
如果我们哈希表的大小为13,某个「关键字」经过双哈希函数后得到的数组下标为0,步长为5。那么这个探测过程是0,5,10,2,7,12,4,9,1,6,11,3。会查找到哈希表中的每一个位置。
使用开放地址法,不管使用那种策略都会有各种问题,开放地址法不怎么使用,在开放地址法中使用较多的是双哈希策略。
4.3.4 拉链法
用拉链法处理冲突得到的散列表 叫 开散列表
开放地址法中,通过在哈希表中再寻找一个空位解决冲突的问题,还有一种更加常用的办法是使用「拉链法」来解决哈希冲突。「拉链法」相对简单很多,「拉链法」是每个数组对应一条链表。当某项关键字通过哈希后落到哈希表中的某个位置,把该条数据添加到链表中,其他同样映射到这个位置的数据项也只需要添加到链表中,并不需要在原始数组中寻找空位来存储。下图是「拉链法」的示意图。
「拉链法」解决哈希冲突代码比较简单,但是代码比较多,因为需要维护一个链表的操作,我们这里采用有序链表,有序链表不能加快成功的查找,但是可以减少不成功的查找时间,因为只要有一项比查找值大,就说明没有我们需要查找的值,删除时间跟查找时间一样,有序链表能够缩短删除时间。但是有序链表增加了插入时间,我们需要在有序链表中找到正确的插入位置。
哈希表:线性探测法和链地址法求查找成功与不成功的平均查找长度_链地址法查找失败的平均查找长度怎么算-CSDN博客
4.4 哈希表的效率
在哈希表中执行插入和搜索操作都可以达到O(1)的时间复杂度,在没有哈希冲突的情况下,只需要使用一次哈希函数就可以插入一个新数据项或者查找到一个已经存在的数据项。
如果发生哈希冲突,插入和查找的时间跟探测长度成正比关系,探测长度取决于装载因子,装载因子是用来表示空位的多少
装载因子:散列表中关键字个数和散列表长度之比
参考图解:什么是B树?(心中有 B 树,做人要虚心)一文读懂B-树 - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/146252512