数据结构笔记5
6 查找技术
6.1 概念
6.1.1 查找定义
-
假设{K0,K1,K2,…,Kn-1}是互不相同的关键码,有一个包含n条记录的集合C,形式是(k0, R0),(k1, R1),(k2, R2),…, (kn-1,Rn-1),其中Rj是与关键码kj相关联的信息,0<=j<=n-1。
-
给定某个关键码值K, 查找就是在C中定位记录(kj, Rj),使得kj=K。即查找,就是定位关键码值kj=K的记录的系统过程。
-
查找算法的结果
成功,就是至少找到一个关键码值为kj的记录,使得kj=K
不成功,就是不存在记录可以使kj=K
6.1.2 查找算法的分类
-
按查找之后,查找数据表是否改变分类
-
静态查找
就是查找算法执行之后,无论是否找到记录( kj,Rj )使得kj=K ,并不改变备查找的数据表。
-
静态查找
就是查找算法执行之后,无论是否找到记录( kj,Rj )使得kj=K ,并不改变备查找的数据表。
-
-
按记录组织形式分类
基于线性表的查找。如顺序查找、折半查找
根据关键码值直接访问。如用数组下标直接查找、散列查找
树索引查找方法。如二叉搜索树、B树等
基于属性的查找方法。如倒排表、倒排文件等
6.2 线性表的查找技术
6.2.1 折半查找
折半查找(Binary Search):当存储表上的数据关键字有序,那么每次可以缩小近乎一半的查找范围,直到查找成功,或不成功。
(05 13 19 21 37 56 64 75 80 88 92)
每次key与mid比较,大于mid指定的值,则移动low和mid;小于mid指定的值,则移动high和mid;直到low>high,则表示查找不成功。
-
折半算法——递归思想
假设有n个从小到大排序的元素,现在需要查找元素x。
在[1..n]范围内容,假设l=1,h=n,用x与a[n/2]比较;
如果x=a[n/2],则找到x,算法中止;
如果x<a[n/2],则在 [1..n/2-1](即 l=1,h=n/2-1)范围内继续搜索x,就是设n=n/2-1,重复第1步;
如果x>a[n/2],则在[n/2+1..n] (即 l=n/2+1,h=n)范围内继续搜索x,就是设只要在数组a的后半部分重复第1步搜索x。// 折半查找前提数据元素已排序,查找关键码为x的元素 int BinSearch(List list, KeyType x, int *position) { int low, mid, high; low=0; high=list->n-1; while(low<=high) { mid=(low+high)/2; if (list->element[mid].key == x) { *position=mid; return 1; }//查找成功 else if(list->element[mid].key > x) high=mid-1;//查前半部分 else low=mid+1;//查后半部分 } *position=low; return 0;//查找失败 }or
// 折半查找前提数组元素data已排序,查找x
int BinSearch(DataType data[], DataType x, int nArrayLen)
{ int nMid = nArrayLen / 2;
if ( data[nMid]==x )
return nMid; // 找到x
if (nMid == 0) return -1; // 找不到
if (x < data[nMid])
return BinSearch(data, x, nMid);//查前半部分
else
return BinSearch(data+nMid+1, x, nArrayLen – nMid-1);//查后半部分
}
- 折半查找的时间效率:

折半查找只能在顺序存储结构的线性表上实现
6.2.2 链表上实现的静态和动态查找
-
带头结点链表静态查找
-
带头结点链表动态查找
PNode Dyn_Search(LinkList llist, DataType x) { PNode p=llist, q, s;//q指向比较点,p指向q的前一点 q=p->next; while (q){//查找 if (q->data==x) return q;//找到 if (q->data>x) break; //没找到 p=q; q=q->next; //下移一位,这里表示要插在p,q之间 } s=(PNode)malloc(sizeof(struct Node));//插入新结点 s->data=x; s->next=p->next; p->next=s; return s;//链入链表 }
6.3 二叉树在数据动态查找中的应用
二叉搜索树/二叉排序树
6.3.1 定义
-
一棵二叉排序树是一棵二叉树,它可以为空。如果不为空,则它将满足
-
非空左子树的所有键值小于其根结点的键值(关键字)
-
非空右子树的所有键值大于其根结点的键值
-
左、右子树都是二叉排序树
-
6.3.2 二叉排序树查找算法
- 静态查找
从根结点开始比较,直到数值被找到,或没找到
int search (PBinSearchTree ptree, KeyType key,
PBinSearchNode *position) {
PBinSearchNode p,q;
p=*ptree; q=p;
while (p!=NULL) { q=p;
if (p->key==key) { *position=p; return 1; //找到 }
else if (p->key > key) p=p->Left; else p=p->Right; }
*position=q; return 0; //没找到
}
- 动态查找(!!!)
在二叉排序树上插入元素,一定是插在叶子结点上,第一个结点一定是根节点
int insert (PBinSearchTree ptree, keyType key){
PBinSearchNode p, position; //p插入点,position插入位置
if (search(ptree, key, &position)==1) return 1; //找到
p=(PBinSearchTree )malloc(sizeof(struct BinSearchNode));
if(p==NULL) { printf(“\Error\n”); return 0; }
p->key=key; p->Left=p->Right=NULL;
if (position==NULL) *ptree=p; //找到插入位置
else if (key<position->key) position->Left=p; //插入左子树
else position->Right=p; //插入右子树
return 1;
}
#include <stdio.h>
#include <stdlib.h>
// 1. 定义【关键字类型】 这里用int举例
typedef int KeyType;
// 2. 定义【二叉排序树节点结构】
// 每个节点包含:数据 + 左孩子指针 + 右孩子指针
struct BinSearchNode {
KeyType key; // 节点存储的数据(关键字)
struct BinSearchNode *Left; // 左孩子指针(存比我小的数)
struct BinSearchNode *Right;// 右孩子指针(存比我大的数)
};
// 3. 给节点指针、树根指针起别名,方便书写
// PBinSearchNode = 节点指针
// PBinSearchTree = 树根指针(二级指针,因为要修改根节点)
typedef struct BinSearchNode *PBinSearchNode;
typedef struct BinSearchNode **PBinSearchTree;
// ===================== 核心函数1:查找 =====================
// 功能:在树中查找key
// 参数:
// ptree 树根指针(二级指针)
// key 要查找的值
// position 传出参数:找到=目标节点,没找到=最后停留的节点(插入用)
// 返回值:1=找到 0=没找到
int search(PBinSearchTree ptree, KeyType key, PBinSearchNode *position) {
PBinSearchNode p, q;
p = *ptree; // p 从根节点开始遍历
q = p; // q 永远保存 p 的上一个节点(父节点)
// 循环遍历:p走到空就结束
while (p != NULL) {
q = p; // q 跟上 p,保存当前节点
// 情况1:找到了!
if (p->key == key) {
*position = p; // 把目标节点返回给调用者
return 1;
}
// 情况2:要找的值 < 当前节点 → 去左子树
else if (p->key > key) {
p = p->Left;
}
// 情况3:要找的值 > 当前节点 → 去右子树
else {
p = p->Right;
}
}
// 循环结束 p=NULL → 没找到
// position = q(最后停留的父节点,插入时用)
*position = q;
return 0;
}
// ===================== 核心函数2:插入 =====================
// 功能:向二叉排序树插入一个key
// 返回值:1=插入成功 0=失败/已存在
int insert(PBinSearchTree ptree, KeyType key) {
PBinSearchNode p, position;
// 第一步:先查找!
// 如果返回1 → 节点已存在,不插入,直接返回
if (search(ptree, key, &position) == 1) {
printf("关键字 %d 已存在,插入失败!\n", key);
return 1;
}
// 第二步:没找到 → 创建新节点
p = (PBinSearchNode)malloc(sizeof(struct BinSearchNode));
if (p == NULL) {
printf("内存分配失败!\n");
return 0;
}
// 初始化新节点
p->key = key;
p->Left = NULL;
p->Right = NULL;
// 第三步:把新节点挂到树上
// 情况1:position=NULL → 树是空树 → 新节点当根
if (position == NULL) {
*ptree = p;
}
// 情况2:key < 父节点 → 插左边
else if (key < position->key) {
position->Left = p;
}
// 情况3:key > 父节点 → 插右边
else {
position->Right = p;
}
printf("关键字 %d 插入成功!\n", key);
return 1;
}
// ===================== 辅助函数:中序遍历 =====================
// 二叉排序树【中序遍历】一定是 从小到大有序序列!
void inOrder(PBinSearchNode tree) {
if (tree != NULL) {
inOrder(tree->Left); // 遍历左子树
printf("%d ", tree->key); // 访问根节点
inOrder(tree->Right); // 遍历右子树
}
}
// ===================== 主函数:测试示例 =====================
int main() {
// 定义一棵空树(树根初始为NULL)
PBinSearchNode tree = NULL;
// 插入测试数据
insert(&tree, 50);
insert(&tree, 30);
insert(&tree, 80);
insert(&tree, 20);
insert(&tree, 40);
insert(&tree, 90);
insert(&tree, 50); // 重复值,插入失败
printf("\n中序遍历结果(从小到大有序):");
inOrder(tree);
printf("\n");
return 0;
}
连续调用实现创建:
int createSearchTree(PBinSearchTree ptree,
SeqDictionary *dic){
int i;
*ptree=NULL;
for (i=0; i<dic->n; i++)
if (!insert(ptree, dic->element[i].key))
return 0; //插入不成功
return 1; //插入成功
}
练习1:依次输入元素10,8,16,5,20,7,12,19,试生成一棵二叉排序树,画出建立的二叉排序树
练习2:现在对二叉排序树进行动态查找数据{36,15,24,13,9}
-
删除某个元素
int delete(PBinSearchTree ptree, KeyType key);
-
第1种情况,删除叶子结点
直接删除叶结点
-
第2种情况,删除单枝结点
直接删除结点,且单枝孩子树直接上升 -
第3种情况,删除双枝结点
找左子树上最大值(或者是右子树上最小值)替换删除结点值,将问题转为第2种情况
-

6.3.3 二叉排序树性能分析
对于有 n 个关键码的集合,其关键码有 n! 种不同排列,可构成不同二叉搜索树有
(棵)
-
二叉排序树的特点分析
它是一棵二叉树
关于结点的值有:左子树< 根< 右子树
定义是递归的: 左子树和右子树依然是二叉排序树 -
二叉排序树的动态查找算法
查找: 从根往下走O(h)
插入: 查找失败后O(1),总O(h)
删除: 分情况讨论, O(h)
退化:斜二叉树
6.4 平衡二叉树(判断)
6.4.1 定义
-
平衡二叉树(AVL树),或者是空树,或者是具有下列性质的非空二叉排序树
-
任一结点的左、右子树均为AVL树
-
任一结点的左、右子树高度差的绝对值不超过1
-
6.4.2 平衡因子(BF, Balance Factor)
-
某结点的左子树与右子树的高度差即为该结点的平衡因子
-
平衡二叉树上所有结点的平衡因子只可能是 -1,0 或 1
引入平衡二叉树的目的是为了提高查找效率, 使其平均查找长度为O(log2n)
6.4.3 平衡二叉树的构造
如果在一棵AVL树中插入一个新结点,就有可能造成失衡,此时必须重新调整树的结构,使之恢复平衡,称调整平衡过程为平衡旋转
平衡旋转可以归纳为四类(调平衡不考)
-
LL平衡旋转
若在A的左子树的左子树上插入结点,使A的平衡因子从1变为2,需要进行一次顺时针旋转。(以B为旋转轴)
-
RR平衡旋转
若在A的右子树的右子树上插入结点,使A的平衡因子从-1增加至-2,需要进行一次逆时针旋转。
(以B为旋转轴) -
LR平衡旋转
若在A的左子树的右子树上插入结点,使A的平衡因子从1变为2,需要先进行逆时针旋转,再顺时针旋转。 (以插入的结点C为旋转轴)
-
RL平衡旋转
若在A的右子树的左子树上插入结点,使A的平衡因子从-1变为-2,需要先进行顺时针旋转,再逆时针旋转。 (以插入的结点C为旋转轴)
6.3 散列查找
- 常见查找算法的分析
| 分类 | 算法 | 平均查找长度 |
|---|---|---|
| 静态查找表 | 顺序查找 | ![]() |
| 折半查找 | ![]() |
|
| 动态查找表 | 二叉排序树 | O(log(n)) |
| 二叉平衡树 | O(log(n)) |
以上讨论的查找表的各种结构的共同特点:
-
记录在表中的位置和它的关键字之间不存在一个确定的关系
-
查找的效率取决于和给定值进行比较的关键字个数
-
用这类方法表示的查找表,其平均查找长度都不为零
有没有ASL = 0的查找表?
6.3.1 散列表(哈希表)
实际问题求解
学校为招收的新生(大约500名左右)建立查找表,设关键字为学号,其值的范围为 xxxxxx00 ~ xxxxxx99 (前4位为年份,再2位是班级号)
下标为00 ~99 是顺序号,如果把它作为顺序表下标
则查找班级内某个学生过程可以简化:取给定值(学号)的后2位,不需要经过比较便可直接从顺序表中找到待查关键字
散列查找的概念
散列法,杂凑法,关键码-地址转换法
基本思想:根据一个关键码,用一个函数h(key)计算出该记录元素的存储地址,并尽量将记录存储到期望的地址
eg:有学号yyyyxzhh,yyyy入学年份,x专业,z班号,hh顺序号。设计学生信息存储顺序表下标函数为h(hh),试问20142213学生存储在班级顺序表第几位?
-
散列表(哈希表)的定义
根据设定的散列函数(也称哈希函数) H(key) 和所选中的处理冲突的方法,将一组关键字映象到一个有限的、地址连续的地址集 (区间) 上,并以关键字在地址集中的“象”作为相应记录在表中的存储位置,如此构造所得的查找表称之为散列表(Hash Table,即哈希表)
-
冲突与同义词
如果两个不相等的关键码key1和key2,用散列函数 f 计算得到相同的散列地址(即f(key1)=f(key2)),称为冲突(碰撞),称key1和key2为同义词
负载因子:又称装填因子

- 实现散列查找的方法
-
需在关键字与记录在表中的存储位置之间建立一个函数关系,以 h(key) 作为关键字为 key 的记录在表中的位置,通常称这个函数 h(key) 为哈希函数
-
必须设计一种解决冲突的方法,以保证散列算法得于实现
-
Hash函数的特点
(1) 哈希(Hash)函数是一个映象,即将关键字的集合映射到某个地址集合上,它的设置很灵活,只要这个地址集合的大小不超出允许范围即可
(2) 由于哈希函数是一个压缩映象,因此,在一般情况下,很容易产生“冲突”现象,即 key1 key2,而 h(key1) = h(key2)
(3) 很难找到一个不产生冲突的哈希函数。一般情况下,只能选择恰当的哈希函数,使冲突尽可能少地产生
哈希函数的构造方法
-
数字分析法
假设关键字集合中的每个关键字都是由 s 位数字组成 (u1, u2, …, us),分析全体关键字, 并从中提取分布均匀的若干位或它们的组合作为地址


此方法适合于
事先明确知道表中所有关键字每一位数值的分布情况,它完全依赖于关键字集合。如果换一个关键字集合,选择哪几位要重新决定
把超出地址位数的最高位删去, 仅保留最低的3位,作为可用的散列地址
-
折叠法
此方法把关键码自左到右分成位数相等的几部分, 每一部分的位数应与散列表地址位数相同, 只有最后一部分的位数可以短一些
把这些部分的数据叠加起来, 就可以得到具有该关键码的记录的哈希地址
有两种叠加方法-
移位叠加 — 把各部分的最后一位对齐相加
-
间界叠加 — 各部分不折断, 沿各部分的分界来回折叠, 然后对齐相加, 将相加的结果当做哈希地址
例 设给定的关键码为 key = 23938587841, 若存储空间限定3位,则划分结果为每段3位,上述关键码可划分为4段
239 385 878 41
此方法适合于 关键字的数字位数特别多,而且关键字中每一位上数字分布大致均匀时
-
-
平方取中法
以关键字的平方值的中间几位作为存储地址。求“关键字的平方值” 的目的是“扩大差别”,同时平方值的中间各位又能受到整个关键字中各位的影响,先计算构成关键码的标识符内码的平方, 再按表的大小取中间的若干位作为哈希地址

-
直接定址法
哈希函数为关键字的线性函数
H(key) = key 或者 H(key) = a key + b这类哈希函数是一对一的映射,一般不会产生冲突
此法仅适合于
地址集合的大小 = = 关键字集合的大小 -
除留余数法
设定哈希函数为 H(key) = key MOD p 其中,p≤m (表长)
一般情况下,若散列表表长为m,通常可以选p为小于或等于表长(最好接近m)的最大质数或不包含小于20质因子的合数。
-
随机数法
设定哈希函数为
H(key) = Random(key)
其中,Random 为伪随机函数
通常,此方法用于对长度不等的关键字构造哈希函数
-
非数字关键字的常见Hash函数
-
ASCII码加和法
-
移位法
-
实际造哈希表时,采用何种构造哈希函数的方法取决于建表的关键字集合的情况(包括关键字的范围和形态),总的原则是使产生冲突的可能性降到尽可能地小。
练习:一组长度为11的整型关键字为{11,21,12,34,43,45,54,65,67,78,89},通过哈希函数H(key)= key Mod 11映射到长度为11的哈希表中。画出哈希表,并放入数据,然后计算装填因子。
6.3.2 处理冲突的方法
“处理冲突” 的实际含义是为产生冲突的地址寻找下一个哈希地址
-
开放定址法
为产生冲突的地址 H(key) 求得一个地址序列
H0, H1, H2, …, Hs 1≤ s≤m-1
其中 H0 = H(key)
Hi = ( H(key) + di ) MOD m
i=1, 2, …, s对增量 di 有三种取法
-
线性探测再散列di = c i 最简单的情况 c=1
-
平方探测再散列 di = 12, -12, 22, -22, …,
-
随机探测再散列 di 是一组伪随机数列
-

练习1 :假设哈希函数为H(key)= k MOD 7,用线性探测法处理冲突。试在0~8的散列地址空间中对关键字序列(100,20,21,35,3,78,99,45)构造哈希表,并求等概率下查找成功时的平均查找长度 。
-
再哈希法
Hi=RHi(key) i=1,2,3,…,k
RHi均是不同的哈希函数,即在同义词产生地址冲突时计算另一个哈希函数地址,直到冲突不再发生。
-
分离链接法
将所有哈希地址相同的记录都链接在同一链表中。即所有的关键字为同义词的记录都在同一线性链表上。

练习2:设哈希函数为H(key)= k MOD 13,用分离链接法处理冲突。试在0~12的散列地址空间中对关键字序列(240,29,345,189,100,20,21,35,3,208,78,99,45,350)构造哈希表,并求等概率下查找成功时的平均查找长度 。
-
建立一个公共溢出区
基本思想:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素一律填入溢出表。
6.3.3 哈希表的存储结构
-
哈希表存储就是一个顺序存储的线性表
//结点结构的定义 typedef int KeyType; typedef int DataType; typedef struct { KeyType key; DataType value; }Element;//哈希表的定义
typedef struct {
Element *element;
int m;
}Hash, *pHash;
- 构建一个空的线性表
```c
pHash creatEmpty(int m){
pHash phd=(pHash)malloc(sizeof(pHash));
if (phd!=NULL){
phd->element=(Element *)mallc(sizeof(Element)*m);
if (phd->element){
phd->m=m;
for(int i=0; i<m; i++) phd->element[i].key=0; //初始化
return (phd); }
else free(phd); }
printf(“Out of space!\n”);
return NULL;
}
-
散列表的查找
//假设用线性探测法解决冲突 int HashSearch (Hash *phash, KeyType key, int *position) { int d, inc; d=h(key); //假设散列函数是h(key) for (inc=0; inc<phash->m; inc++) { if (phash->element[d].key==key) { //找到 *position=d; return1; } else if (phash->element[d].key==0) { *position=d; return 0;} //没有找到,确认插入位置 d=(d+1) % phash->m; //线性探测 } //for *position=-1; //没找到,也没确认插入位置,即溢出 return 0; } -
散列表数据的插入
int HashInsert(Hash *phash, Element ele){ int position; if (HashSearch( phash, ele.key, &position)==1)) printf(“查找成功!\n”); else if (position!=-1) phash->element[position]=ele; //插入结点 else return 0; //溢出 return 1; }
6.3.4 哈希表查找分析
-
查找过程
给定Key值,根据哈希函数求得哈希地址,若表中此位置的值和给定Key相等,则查找次数为1;否则说明产生冲突,根据处理冲突的方法找“下一个地址”,直到找到为止,记录下查找的次数


从查找过程得知,哈希表查找的平均查找长度实际上并不等于零
-
决定哈希表查找的ASL的因素
-
选用的哈希函数
-
选用的处理冲突的方法
-
哈希表饱和的程度,即装填因子α=n/m 值的大小(n—记录数,m—表的长度),装填因子越大,说明表越饱和,冲突的概率越高
-
一般情况下,可以认为选用的哈希函数是“均匀”的,则在讨论ASL时,可以不考虑它的因素。
因此,哈希表的ASL是处理冲突方法和装填因子的函数。

从以上结果可见,哈希表的平均查找长度是 的函数,而不是 n 的函数。
这说明,用哈希表构造查找表时,可以选择一个适当的装填(负载)因子 ,使得平均查找长度限定在某个范围内。



浙公网安备 33010602011771号