数据结构笔记|查找表

234 阅读13分钟

一、静态查找表

(一)数组顺序表

1.普通顺序查找

#define max 100

typedef struct {
	int key;//关键字 
}ElemType;

typedef struct {
	int elem[max];
	int length;
}SqList;
SqList L;

//顺序查找 
int search(SqList L, int x) {
	int i;
	for(i = 0; i < L.length; i++)
		if(L.elem[i].key == x)
			return i+1;//找到了,返回是线性表的第几个元素 
	return 0;//没找到 
} 

2.哨兵顺序查找(提高速度)

int search1(SqList L, int x) {
	int k = L.length;
	L.elem[0].key = x;//把x放在下标为0的数组元素(哨兵),1~n是线性表元素
	//哨兵也可以放在表尾 
	while(x != L.elem[k].key)//从表尾开始比较 
		k--;
	return k;//若在哨兵处相等(k=0)说明没找到,否则找到 
} 

查找成功的平均查找长度:(查找成功的情况下,平均找一个数据元素需要的比较次数)ASL=(1+2+…+n)/n=(n+1)/2
查找一次的平均检索长度:(成功,失败)ASL=(n+1)/2+(1+2+…+n)/(2*n)=3(n+1)/4
说明:假设每个数据元素查找的概率相同,并且查找一次成功和失败的概率也相同

3.二分查找(数组)

//二分查找
//查找表是有序的顺序存储结构,链表不行 
int binaryS(SqList L, int x) {
	int low = 0, high = L.length-1, m;
	while(low <= high) {
		m = (low + high)/2;
		if(L.elem[m].key == x)
			return m+1;
		if(L.elem[m].key > x)
			high = m-1;
		else low = m+1; 
	}
	return 0;
} 

二分查找成功时的平均查找长度:ASLbn≈log2(n+1)-1
二分查找在查找失败时:所需比较的关键字个数不超过判定树的深度,在最坏情况下查找成功的比较次数也不超过判定树的深度。即为:log2(n) + 1 1715612857038.png 画二叉查找树的方法:创建二分查找树,比你学的方法快很多。查找长度为5的有_哔哩哔哩_bilibili二叉查找树也是排序树

(二)索引顺序表

1.定义:线性表分成若干块,每一块中的键值存储顺序是任意的,块与块之间按键值排序,即后一块中的所有记录的关键字的值均大于前一块中最大键值。
建立一个索引表,该表中的每一项对应于线性表中的一块,每一项由键域(存放相应块的最大键值)和链域(存放指向本块地一个结点的指针)组成。 image.png 2.查找步骤

  1. 首先对索引表采用二分查找或顺序查找方法查找,以确定待查记录所在的块
  2. 在线性表查找k,若其在线性表中存在,且位于第i块,那么一定有:第i-1块的最大键值<k≤第i块的最大键值
  3. 然后在相应的块中进行顺序查找

3.说明

  • 表长为n的线性表 ,整个表分为b块,前b-1块中的记录数为s=n/b,第b块中的记录数小于或等于s。
  • ASL=(b+1)/2+(s+1)/2
  • 当s = sqrt(n) ,ASL最小 sqrt(n) +1

二、动态查找表

(一)二叉排序树

1.定义:二叉排序树或者是一棵空树;或者是具有如下特性的二叉树:

  • 若它的左子树不空,则左子树上所有结点的值均小于根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于根结点的值;
  • 它的左、右子树也都分别是二叉排序树

2.判断:二叉排序树的中序遍历结果递增有序
3.检索

//存储结构为二叉链表
typedef struct NODE {
	int key;
	struct NODE *lc, *rc;
}BiNode, *BiTree; 

Bitree Search(BiNode *t, int x) {
	BiTree p;
	p = t;
	while(p != NULL) {
		if(x == p->key) return p;//若x等于根结点的关键字,则查找成功
		if(x < p->key p = p->lc;//小于则在左子树上找 
		else p = p->rc;//大于则在右子树找 
	}
	return p;//函数返回查找结果,没找到为空指针! 
}

该二叉树查找成功的平均比较次数=(每层的结点数×深度)/总结点数 //根节点深度为1
查找节点的平均时间复杂度:O(log(2,n)) 4.插入

  • 新插入结点一定是作为叶子结点添加上去的
  • 建立一棵二叉排序树则是逐个插入结点的过程
int Insert(Bitree &t, int x) {
	BiTree q, p, s;
	q = NULL;//q为p的双亲节点,根节点无双亲节点 
	p = t;//p为正在查看的节点,初始从根节点t开始 
	while(p != NULL) {
		if(x == p->key) return 0;//在当前二叉树中找到x,不用插入
		q = p;//更新双亲节点 
		if(x < p->key) p= p->lc;//如果x比当前节点小,则进入左子树
		else p = p->rc; //如果x比当前节点大,则进入右子树 
	}
	s = (BiTree)malloc(sizeof(Binode));//没找到x,做插入,申请节点空间
	s->key = x; s->lc = NULL; s->rc = NULL;//s存放x,设为叶节点
	if(q == NULL) t = s; //若原先的二叉树是一棵空树,新插入的x对应的节点s为插入后二叉树的根节点
	else if(x < q->key) q->lc = s;//若x小于其双亲节点,则作为其左孩子
	else q->rc = s;//若x大于其双亲节点,则作为其右孩子
	return 1; 
} 

5.删除
(1)定义:从二叉排序树中删除一个结点之后,使其仍能保持二叉排序树的特性
(2) 被删结点p为叶结点,将被删结点的双亲结点相应指针域改为空指针
(3) 若被删结点p只有右子树pr或只有左子树pl,就用pr或pl替换被删结点
(4) 若被删结点p既有左子树pl又有右子树pr ,可按中序遍历保持有序进行调整
1715615057893.png 1715615066431.png (5)删除P节点:用P的左子树pl上的最大值Smax替换P,再删S,或用P的右子树pr上的最小值Smin替换P,再删S。Smax和Smin的特征是至多有一个孩子 1715615118887.png 6.最佳二叉排序树

  1. n个数据元素按照不同的输入顺序构造的二叉排序树不同,其中平均查找性能最高的为最佳二叉排序树
  2. 按照二分查找判定树的建立过程建立的二叉排序树为最佳二叉排序树
  3. 二叉排序树的查找速度一般比顺序查找快,但如果是单枝树则一样快

(二)平衡二叉树(AVL树)

1.定义

空树,或者是具有下列性质的二叉树:

  1. 左、右子树都是平衡二叉树
  2. 左、右子树高度之差的绝对值不超过1
  3. 平衡二叉树每个结点的平衡因子的绝对值不超过1

2.增加节点——旋转技术

(1)在一棵AVL树中插入一个新结点,就有可能造成失衡,此时必须重新调整树的结构,使之恢复平衡。我们称调整平衡过程为平衡旋转
(2)平衡因子BF:结点的左子树深度-右子树深度(根节点深度为0,也可以数线,左2线右1线则BF为1,左1右2为-1)
(3)四种情况。
LL:x插在A的左孩子B的左子树上,A为离新插结点x最近的失去平衡的结点
操作:单向右旋
RR:x位于A的右孩子的右子树上->左旋 LR:x位于A的左孩子的右子树上->先左旋后右旋 RL:x位于A的右孩子的左子树上->先右旋后左旋

  • 对不平衡的最小子树操作
  • 旋转后子树根节点平衡因子为0
  • 旋转后子树深度不变故不影响全树,也
  • 不影响插入路径上所有祖先结点的平衡度

但是这个旋转的方法太复杂了,我们可以直接写出调整后的平衡树,这个方法讲得很好 一秒学会 平衡二叉树的调整,非标题党!不简单你打我! (考研数据结构)_哔哩哔哩_bilibili

3.查找性能

1.在平衡树上进行查找的过程和二叉排序树相同,因此,查找过程中和给定值进行比较的关键字的个数不超过平衡二叉树的深度。
2.含n个关键字的二叉平衡树可能达到的最大深度是多少? 1715666703081.png 1715666733884.png

三、B-树

1.定义

1715617608595.png 3阶的B-树又称为2-3树

2.查找

#define MAX 10
typedef struct BTNode {
	int keynum;//存放关键字个数n
	int key[MAX];//一维数组存放关键字k1,k2,...kn
	//int是keytype 
	struct BTNode *parent;
	struct BTNode *ptr[MAX];
	//一维数组存放n棵子树的地址A0,A1,A1...An
	Record *recptr[MAX]; 
}BTNode, *BTree; 

//存放查找结果 
typedef struct {
	BTNode *pt;
	int i;
	int tag;
}Result;

//在B-树中查找节点k 
Result SearchBTree(BTree T, int k) {
	BTNode *p = T, q = NULL;
	while(p != NULL) {
		//key[0]是哨兵项,共有keynum个元素,1~keynum存放元素 
		i = p->keynum;//先指在最后一个元素 
		p->key[0] = k;
		while(k < p->key[i])//在节点中找关键字 
			i--;
		if(k == p->key[i] && i > 0)
			return (p, i, 1);
		else {  //其实就是k > p->key[i]的情况 
			q = p; 
			p = p->ptr[i]; //在B-树中找节点 
		}
		return(q, i, 0);
	}
}

查找分析

  1. 在磁盘上读取结点信息的次数更耗时,即B-树的层次树是决定B-树查找效率的首要因素
  2. n个关键字的m阶B-树,最坏情况下达到多深呢? 根结点到关键字所在结点的路径上涉及的结点数不超过log m/2 ((n+1)/2) + 1
  3. 深度为h+1的m阶B-树至少含有多少个结点? 第h+1层为2*(m/2)^(h-1)个节点

3.插入

1715689277210.png 1715689302119.png

4.删除

1715689520066.png 合并调整操作: 1715689554856.png

(删除和插入、B+树都没看懂)

四、B+树

1.定义

B+树是应文件系统所需而产生的一种B-树的变型树。一棵m阶的B+树和m阶的B-树的差异在于:

  • 有n棵子树的结点中含有n个关键字;
  • 所有的叶子结点中包含了全部关键字 的信息,及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接。
  • 所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。 1715689773082.png

2.查找

和B-树的查找稍有不同:

  • 在B+树上,既可以进行缩小范围的查找,也可以进行顺序查找;
  • 在进行缩小范围的查找时,不管成功与否,都必须查到叶子结点才能结束;
  • 若在结点内查找时,给定值≤Ki ,则应继续在Ai 所指子树中进行查找。

3.插入

五、哈希表

(一)构造哈希函数的方法

1.直接定址法

哈希函数为关键字的线性函数

2.数字分析法

假设关键字集合中的每个关键字都是由 s 位数字组成 (u1, u2, …, us),分析关键字集中的全体, 并从中提取分布均匀的若干位或它们的组合作为地址。
此方法仅适合于:能预先估计出全体关键字的每一位上各种数字出现的频度

3.平方取中法

以关键字的平方值的中间几位作为存储地址。求“关键字的平方值” 的目的是“扩大差别” ,同时平方值的中间各位又能受到整个关键字中各位的影响。

4.折叠法

将关键字(自左到右,自右到左)分成位数相等的几部分,最后一部分位数可以短些,然后将这几部分叠加求和作为哈希地址。
有两种叠加方法:

  1. 移位法 ─ 将各部分的最后一位对齐相加。
  2. 间界叠加法 ─ 从一端向另一端沿各部分分界,来回折叠后,最后一位对齐相加。

此方法适合于: 关键字的数字位数相对于地址位数特别多。
例子: image.png

5.除留余数法

设定哈希函数为: h(key) = key MOD p,p≤m (m为哈希表表长) , p 应为不大于 m 的素数或是不含 20 以下的质因子

6.随机数法

设定哈希函数为: h(key) = Random(key),其中,Random 为伪随机函数.通常,此方法用于对长度不等的关键字构造哈希函数

(二)构造哈希函数的考虑因素

(1)计算哈希函数的时间
(2)关键字的长度
(3)哈希表的大小
(4)关键字的分布
(5)记录的查找频率

(三)处理冲突的方法

1.开放定址法

1715691053830.png 1715691246096.png 1715691268912.png di要具有完备性

  • 产生的 hi 均不相同,且所产生(m-1)个 hi值能覆盖哈希表中所有地址
  • 平方探测时的表长 m 必为形如 4j+3 的素数(如: 7, 11, 19, 23, … 等)
  • 随机探测时的 m 和 d
  • i没有公因子

2.再哈希法

  • 为产生冲突的地址 h(key) 求得一个地址序列:h1, h2, …, hk, 1≤ k≤m-1, 其中:
  • hi (key) =Rhi (key), i=1,2,…,k
  • Rhi 是不同的哈希函数
  • 不宜产生”聚集”(二次聚集:哈希地址不同争夺同一个后继哈希地址)
  • 计算费时

3.链地址法

1715691524094.png

4.建立一个公共溢出区

将发生冲突的数据元素顺序存放于一个公共的溢出区

(四)哈希表的查找

1.步骤

建表时哈希函数 为H(key)=key MOD p,处理冲突采用线性探测再散列,那么在已经建好的表长为m的哈希表中查找x的步骤如下:

  1. 计算h=x MOD p
  2. 若哈希表中,地址h处为空闲,则查找失败,返回;否则将x与哈希表中h处的数据元素比较,若相等,则查找成功,返回;否则转3
  3. 按照线性探测再散列的地址探查序列: h+1, h+2, …, m-1, 0, 1, …, h-1, 从h+1开始查看。若当前查看的位置为空闲,则查找失败,返回;
  4. 否则将x与当前查看位置的数据元素进行比较,相等则查找成功,返回;不相等,则按照地址探查序列继续查看下一位置。若整个地址探查序列查看一遍均没找到,则查找失败,返回

2.查找分析

哈希表查找的平均查找长度实际上并不等于零 决定哈希表查找的ASL的因素

  • 选用的哈希函数(视为均匀,计算ASL时不考虑)
  • 选用的处理冲突的方法
  • 哈希表饱和的程度,装载因子 α=n/m 值的大小(n—数据数,m—表的长度) 1715691978689.png 说明:
  • 哈希表的平均查找长度是 α 的函数,而不是 n 的函数
  • 这说明,用哈希表构造查找表时,可以选择一个适当的装填因子 α ,使得平均查找长度限定在某个范围内

(五)哈希表的删除

  1. 开放定址法 :保证删除后查找表的操作正常进行
  2. 链地址法:直接删除
  3. 再哈希法:保证删除后查找表的操作正常进行
  4. 建立一个公共溢出区:保证删除后查找表的操作正常进行

1.开放定址法

删除一个数据,为保证查找工作的正常进行不能真删——加删除标志 1715692182877.png

(六)哈希表构造静态查找表

对静态查找表,有时可以找到不发生冲突的哈希函数。即,此时的哈希表的 ASL=0,称此类哈希函数为理想的哈希函数