第九章 查找 £9.1 概述 £9.2 静态查找表 £9.3 动态查找表 £9.4 哈希表 £9.1.1 查找表 £9.2.1 概述

Slides:



Advertisements
Similar presentations
1 、谁能说说什么是因数? 在整数范围内( 0 除外),如果甲数 能被乙数整除,我们就说甲数是乙数的 倍数,乙数是甲数的因数。 如: 12÷4=3 4 就是 12 的因数 2 、回顾一下,我们认识的自然数可以分 成几类? 3 、其实自然数还有一种新的分类方法, 你知道吗?这就是我们今天这节课的学.
Advertisements

第 9 章 查找.
主讲:计算机工程学院 李兰 答疑地点:主教学楼B区213
第九章 查找.
主讲:计算机工程学院 李兰 答疑地点:主教学楼B区213
数据结构课程的内容.
数据结构作业及答案 (C语言版).
第九章. 查找 (Chapter 9. Searching)
第9章 查找 9.1 基本概念和术语 9.2 静态查找表 9.3 动态查找表 9.4 哈希表 9.5 小结 顺序查找
第8章 查找 在英汉字典中查找某个英文单词的中文解释;在新华字典中查找某个汉字的读音、含义;在对数表、平方根表中查找某个数的对数、平方根;邮递员送信件要按收件人的地址确定位置等等。可以说查找是为了得到某个信息而常常进行的工作。 计算机、计算机网络使信息查询更快捷、方便、准确。要从计算机、计算机网络中查找特定的信息,就需要在计算机中存储包含该特定信息的表。如要从计算机中查找英文单词的中文解释,就需要存储类似英汉字典这样的信息表,以及对该表进行的查找操作。本章将讨论的问题即是“信息的存储和查找”。
第八章 查找.
第8章 查找 8.1 静态查找表 8.2 动态查找表 8.3 哈希表.
第十章 搜索 静态搜索结构 动态搜索结构 散列 可扩充散列.
第九章 查找.
第8章 查找 数据结构(C++描述).
第7章 查找 丽水学院工学院.
Chapter 7 Search.
第三章 多维随机变量及其分布 §2 边缘分布 边缘分布函数 边缘分布律 边缘概率密度.
第九章查找.
教 师:曾晓东 电 话: E_mail: 计算机软件技术基础 教 师:曾晓东 电 话: E_mail:
强连通分量 无向图 1、任意两顶点连通称该图为连通图 2、否则将其中的极大连通子图称为连通分量 A D C B E 有向图
第八章 查找表 8.1概述 8.2静态查找表 8.3动态查找表 8.4哈希表及其查找.
辅导课程六.
第九章 查找 2018/12/9.
第八章 查找表 8.1概述 8.2静态查找表 8.3动态查找表 8.4哈希表及其查找.
第八章 查找表 8.1静态查找表 8.2动态查找表 8.3哈希表及其查找.
本章重点难点 重点:顺序查找、二分查找、二叉排序树查找以及散列表查找的基本思想和算法实现。
第7章 查找 北京师范大学 教育技术学院 杨开城.
第九章 查找(Search) 9.1 基本概念 9.2 静态查找表 9.3 动态查找表 9.4 Hash表.
何谓查找表 ? 查找表是由同一类型的数据元素(或记录)构成的集合。
第7章 查找 本章中介绍下列主要内容: 静态查找表及查找算法:顺序查找、折半查找 动态查找表及查找算法:二叉排序树 哈希表及查找算法.
§2 求导法则 2.1 求导数的四则运算法则 下面分三部分加以证明, 并同时给出相应的推论和例题 .
第11讲 树和二叉树(二).
图内容回顾 图 Dijkstra算法 最短路径 Floyd算法 拓扑排序 邻接矩阵 有向图的应用 DAG图 关键路径 邻 接 表 存储结构
Ch.9 查找 §9.1 基本概念 查找和排序是两个重要的运算
Ch.9 查找 查找和排序是两个重要的运算 §9.1 基本概念
数据结构 第9章 查找 什么是查找?? 静态查找 动态查找 哈希表 数据结构.
第九章 查找表 9.1静态查找表 9.2动态查找表 9.3哈希表及其查找.
第九章 查找 2019/2/16.
顺序表的插入.
无向树和根树.
计算.
简单介绍 用C++实现简单的模板数据结构 ArrayList(数组, 类似std::vector)
§ B-树 1.概念 B-树是一种完全平衡的多阶查找树,主要是作为磁盘文件的索引组织,用于外部查找。
顺序表的删除.
概 率 统 计 主讲教师 叶宏 山东大学数学院.
线 性 代 数 厦门大学线性代数教学组 2019年4月24日6时8分 / 45.
9.1 基本概念 9.2 线性表的查找 9.3 树表的查找 9.4 哈希表的查找 9.5 各种查找方法的比较
5.2 常用统计分布 一、常见分布 二、概率分布的分位数 三、小结.
顺序查找.
Ch.9 查找 §9.1 基本概念 查找和排序是两个重要的运算
第 四 讲 线性表(二).
§6.7 子空间的直和 一、直和的定义 二、直和的判定 三、多个子空间的直和.
1.2 子集、补集、全集习题课.
第4课时 绝对值.
多层循环 Private Sub Command1_Click() Dim i As Integer, j As Integer
分数再认识三 真假带分数的练习课.
第15讲 特征值与特征向量的性质 主要内容:特征值与特征向量的性质.
#define RBTREE_RED 0 #define RBTREE_BLACK 1 typedef int dataType; typedef struct treeNode{ dataType key; int color; //red:0.
实验目的:掌握数据的顺序存储结构及它们在计算机中的操作。 实验内容:
第8章 查找 预备知识 8.1 静态查找表 8.2 动态查找表 8.3 哈希表.
第9章 查找 静态查找表 二叉排序树 平衡二叉树(AVL树) 小结 B树 哈希表.
基于列存储的RDF数据管理 朱敏
1 School of Computing and Information Engineering.
插入排序的正确性证明 以及各种改进方法.
第七章 搜索结构 东南大学计算机学院 方效林 本课件借鉴了清华大学殷人昆老师 和哈尔滨工业大学张岩老师的课件.
第9章 查找 9.1 查找的基本概念 9.2 线性表的查找 9.3 树表的查找 9.4 哈希表查找.
本节内容 1. 平衡二叉树的定义 2.平衡化旋转 3.平衡二叉排序树的插入操作 昆山爱达人信息技术有限公司
第二章 线性表 东南大学计算机学院 方效林 本课件借鉴了清华大学殷人昆老师 和哈尔滨工业大学张岩老师的课件.
§2 自由代数 定义19.7:设X是集合,G是一个T-代数,为X到G的函数,若对每个T-代数A和X到A的函数,都存在唯一的G到A的同态映射,使得=,则称G(更严格的说是(G,))是生成集X上的自由T-代数。X中的元素称为生成元。 A变, 变 变, 也变 对给定的 和A,是唯一的.
Presentation transcript:

第九章 查找 £9.1 概述 £9.2 静态查找表 £9.3 动态查找表 £9.4 哈希表 £9.1.1 查找表 £9.2.1 概述 第九章 查找 £9.1 概述 £9.2 静态查找表 £9.1.1 查找表 £9.2.1 概述 £9.1.2 相关术语 £9.2.2 顺序表的查找 £9.1.3 类型说明 £9.2.3 有序表的查找 £9.2.4 索引顺序表的查找 £9.3 动态查找表 £9.4 哈希表 £9.3.1 概述 £9.4.1 定义 £9.3.2 二叉排序树和平衡二叉树 £9.4.2 哈希函数的构造 £9.3.3 B-树和B+树 £9.4.3 处理冲突的方法 £9.4.4 哈希表的查找及其分析

£9.1 概述 £9.1.1 查找表 查找表(Search Table):是由同一类型的数据元素(或记录)构 成的集合。 静态查找表(Static Search Table):只对查找表作“查找”操作的 一类查找表。 动态查找表(Dynamic Search Table):对查找表不仅作“查找”操 作,在查找过程中还同时插入查找表中不存在的数据元素,或者从查找 表中删除已存在的某个数据元素的一类查找表。 对查找表经常进行的操作通常有: (1)查询某个“特定的”数据元素是否在查找表中; (2)检索某个“特定的”数据元素的各种属性; (3)在查找表中插入一个数据元素; (4)从查找表中删去某个数据元素。

£9.1.2 相关术语 关键字(Key):是数据元素(或记录)中某个数据项的值,用 它可以标识(识别)一个数据元素(或记录)。 主关键字(Primary Key):可以唯一的标识一个建立的关键字。 次关键字(Secondary Key):用以识别若干记录的关键字。 查找(Searching):根据给定的某个值,在查找表中确定一个其关键字等于给定值的记录或数据元素。 查找成功:查找表中存在关键字等于给定值的记录。此时查找的结果为给出整个记录的信息,或指示该记录在查找表中的位置。 查找失败:查找表中不存在关键字等于给定值的记录。此时查找 的结果可给出一个“空”记录或“空”指针。

£9.1.3 类型说明 以下是在本章以后各节的讨论中涉及的关键字类型和数据元素类型 的统一说明: 典型的关键字类型说明: typedef float KeyType; //实型 typedef int KeyType; //整型 typedef char* KeyType; //字符串型 数据元素类型说明: typedef struct { KeyType key; //关键字域 … //其他域 } ElemType; 对两个关键字比较的宏定义: //-----对数值型关键字 #define EQ(a, b) ((a) = = (b)) #define LT(a, b) ((a) < (b)) #define LQ(a, b) ((a) < = (b)) … //-----对字符串型关键字 #define EQ(a, b) (!strcmp ((a) , (b))) #define LT(a, b) (strcmp ((a) , (b)) < 0) #define LQ(a, b) (strcmp ((a) , (b)) < = 0) …

£9.2 静态查找表 £9.2.1 概述 (1)静态查找表的抽象数据类型定义: ADT StaticSearchTable { 数据对象D:D是具有相同特性的数据元素的集合。各个数据元 素均含有类型相同,可唯一标识数据元素的关键字。 数据关系R:数据元素同属一个集合。 基本操作P: Create (&ST, n); 操作结果:构造一个含n个数据元素的静态查找表ST。 Destroy (&ST); 初始条件:静态查找表ST存在。 操作结果:销毁表ST。 Search (ST, key); 初始条件:静态查找表ST存在,key为和关键字类型相同的给定值。 操作结果:若ST中存在其关键字等于key的数据元素,则函数值为 该元素的值或在表中的位置,否则为“空”。 Traverse (ST, Visit ( )); 初始条件:静态查找表ST存在,Visit是对元素操作的应用函数。 操作结果:按某种次序对ST的每个元素调用函数visit ( )一次且仅 一次。一旦visit ( )失败,则操作失败。 } ADT StaticSearchTable

(2)查找操作的性能分析 衡量查找算法好坏的依据:其关键字和给定值进行过比较的记录个 数的平均值。 平均查找长度(Average Search Length):为确定记录在查找表中 的位置,需和给定值进行比较的关键字个数的期望值,称为查找算法在 查找成功时的平均查找长度。 对于含有n个记录的表,查找成功时的平均查找长度为: (9-1) 其中:Pi为查找表中查找第i个记录的概率,且 ;Ci为找到表中其关 键字与给定值相等的第i个记录时,和给定值已进行过比较的关键字个数。 查找算法的平均查找长度= 查找成功时的平均查找长度 + 查找不成功时的平均查找长度

£9.2.2 顺序表的查找 (1)顺序存储结构的类型定义 typedef struct { ElemType *elem; //数据元素存储空间基址,建表时按 //实际长度分配,0号单元留空 int length; //表长度 } SSTable (2)顺序查找的实现 顺序查找(Sequential Search)的查找过程为:从表中最后一个记录开 始,逐个进行记录的关键字和给定值的比较,若某个记录的关键字和给定值 比较相等,则查找成功,找到所查记录;反之,若直至第一个记录,其关键 字和给定值比较都不等,则表明表中没有所查记录,查找不成功。 算法9.1如下: int Search_Seq (SSTable ST, KeyType key) { //在顺序表ST中顺序查找其关键字等于key的数据元素。 //若找到,则函数值为该元素在表中的位置,否则为0。 ST.elem[0].key = key; //“哨兵” for (i = ST.length; !EQ(ST.elem[i].key, key); ――i); //从后往前找 return i; //找不到时i为0 } // Search_Seq

(3)性能分析 ①只考虑查找成功时的情况 在顺序查找的过程中,式(9-1)中Ci取决于所查记录在表中的位置。 假设n=ST.length,则顺序查找的平均长度为: ASL = nP1 + (n-1)P2 + … + 2Pn-1 + Pn 假设查找每个记录的概率相等,即 Pi=1/n。则在等概率情况下顺序查 找的平均查找长度为: ②考虑查找成功和查找不成功时的情况 顺序查找中,不论给定值key为何值,查找不成功时和给定值进行比较的关 键字个数均为n+1。 假设查找成功与不成功的可能性相同,对每个记录的查找概率也相等,则 Pi=1/(2n),此时顺序查找的平均查找长度为:

缺点:平均查找长度较大,特别是当n很大时,查找效率较低。 (4)顺序查找算法的特点 优点:算法简单且适应面广。它对表的结构无任何要求,无论 记录是否按关键字有序均可应用。 缺点:平均查找长度较大,特别是当n很大时,查找效率较低。

£9.2.3 有序表的查找 (1)有序表查找的实现 折半查找(Binary Search)的查找过程:先选定待查记录所在范 围(区间),然后逐步缩小范围直到找到或找不到该记录为止。 算法9.2如下: int Search_Bin (SSTable ST, KeyType key) { //在有序表ST中折半查找其关键字等于key的数据元素。 //若找到,则函数值为该元素在表中的位置,否则为0。 low = 1; //置区间初值 high = ST.length; //low和high分别指示待查元素所在范围的下界和上界 while (low <= high) { mid = (low + high) / 2; //mid指示区间的中间位置 if (EQ(key, ST.elem[mid].key)) //找到待查元素 return mid; else if (LT(key, ST.elem[mid].key)) //继续在前半区间进行查找 high = mid-1; else low = mid + 1; //继续在后半区间进行查找 } // while return 0; } // Search_Bin

(2)例子 例如:已知如下11个数据元素的有序表(关键字即为数据元素的值): (05,13,19,21,37,56,64,75,80,88,92) 现要查找关键字为21和85的数据元素。 ①给定值key=21的查找过程: 05 13 19 21 37 56 64 75 80 88 92 high low mid ST.elem[mid].key > key, 令high = mid-1, mid = 3; 05 13 19 21 37 56 64 75 80 88 92 low mid high ST.elem[mid].key < key, 令low = mid + 1, mid = 4; 05 13 19 21 37 56 64 75 80 88 92 low mid high ST.elem[mid].key = key。查找成功。

②给定值key=85的查找过程: 05 13 19 21 37 56 64 75 80 88 92 high low mid ST.elem[mid].key < key, 令low = mid + 1, mid = 9; 05 13 19 21 37 56 64 75 80 88 92 low mid high ST.elem[mid].key < key, 令low = mid + 1, mid = 10; 05 13 19 21 37 56 64 75 80 88 92 low mid high ST.elem[mid].key > key, 令high = mid-1, mid = 9; 05 13 19 21 37 56 64 75 80 88 92 low high 因为下界low >上界high,说明表中没有关键字等于key的元素,查找不成功。

(3)性能分析 ①查找算法的判定树 判定树:描述查找过程的二叉树。 判定树中圆形结点为内部结点;方形结点为外部结点(由内部结点的 空指针域链接)。树中每个圆形结点标识表中一个记录,结点中的值为该 记录在表中的位置。方形结点中的值v为:第i结点的值< v <第i+1结点的 值;若i结点为第1个结点则v <第1个结点的值;若i结点为最后一个结点则 v >最后一个结点的值。 多不超过树的深度,而具有n个结点的判定树的深度为 ,所以, 显然,折半查找法在查找成功(或不成功)时进行比较的关键字个数最 折半查找法在查找成功时和给定值进行比较的关键字个数至多为 。

例如,上述11个元素的表的例子。查找21的过程如红线所示,查找 85的过程如蓝线所示。 6 3 9 1 4 7 10 2 5 8 11 -1 3-4 6-7 9-10 1-2 2-3 4-5 5-6 7-8 8-9 10-11 11- 图9.1 判定树和查找21(红线),85(蓝线)的过程

②折半查找的平均查找长度 假定有序表的长度n=2h-1(反之,h=log2(n+1)),则描述折半 查找的判定树是深度为h的曼二叉树。假设表中每个记录的查找概率相 等(Pi = 1/n),则查找成功时折半查找的平均查找长度: 对任意的n,当n较大(n>50)时,可有下列近似结果: (4)折半查找算法的特点 特点:只适用于有序表,且限于顺序存储结构(对线性链表无法进行 折半查找)。 (5)斐波那契查找 斐波那契序列:F0 = 0, F1= 1, Fi = Fi-1+ Fi-2, i≥2。 算法思想:假设开始时表中记录个数比某个斐波那契数小,即n=Fu-1, 然后将给定值key和ST.elem[Fu-1].key进行比较,若相等,则查找成功;若 key < ST.elem[Fu-1].key,则继续在自ST.elem[1]至ST.elem[Fu-1]的子表中进行 查找,否则继续在自ST.elem[Fu-1+1]至ST.elem[Fu-1]的子表中进行查找, 后一子表的长度为Fu-2-1。

特点:①斐波那契查找的平均性能比折半查找好,但最坏情况下的性能 却比折半查找差。 ②分割时只需进行加、减运算。 (6)插值查找 插值查找时根据给定值key来确定进行比较的关键字ST.elem[i].key 的查找方法。 ST.elem[h]分别为有序表中具有最小关键字和最大关键字的记录。显然, 这种插值查找只适于关键字均匀分布的表,在这种情况下,对表长较大的顺序 表,其平均性能比折半查找好。 令 ,其中ST.elem[l]和

£9.2.4 索引顺序表的查找 (1)分块查找 分块查找又称索引顺序查找,这是顺序查找的一种改进方法。在此 查找法中,除表本身以外,尚需建立一个“索引表”。如下例所示。 例如,图9.2所示为一个表及其索引表,表中含有18个记录,可分成 3个子表(R1, R2, …, R6)、(R7, R8, …, R12)和(R13, R14, …, R18)。对每一个子 表(或称块)建立索引项。索引表按关键字有序,则表或者有序或者分块 有序。 22 12 13 8 9 20 33 42 44 38 24 48 60 58 74 49 86 53 索引表 最大关键字 起始地址 22 48 86 1 7 13 图9.2 表及其索引表 索引项包括: 关键字项:其值为该子表内的最大关键字。 指针项:指示该子表的第一个记录在表中的位置。

分块有序:即后一个子表中的所有记录的关键字均大于前一个子表中 的最大关键字。 分块查找的算法思想: ①确定待查记录所在的块(子表); ②在块中顺序查找 (2)性能分析 分块查找的平均查找长度为: ASLbs=Lb+Lw 其中:Lb为查找索引表确定所在块的平均查找长度,Lw为在块中查找元素的 平均查找长度。 ;又假定表中每个记录的查找概率相等,则每 一般情况下,为进行分块查找,可以将长度为n的表均匀地分成b块,每 块含有s个记录,即 块查找的概率为1/b,块中每个记录的查找概率为1/s。则分块查找的平均长 度为: ①用顺序查找确定所在块 ②用折半查找确定所在块

£9.3 动态查找表 £9.3.1 概述 (1)动态查找表的抽象数据类型定义: ADT DynamicSearchTable { 数据对象D:D是具有相同特性的数据元素的集合。各个数据元素均含有 类型相同,可唯一标识数据元素的关键字。 数据关系R:数据元素同属一个集合。 基本操作P: InitDSTable (&DT); 操作结果:构造一个空的动态查找表DT。 DestroyDSTable (&DT); 初始条件:动态查找表DT存在。 操作结果:销毁动态查找表DT。 SearchDSTable (DT, key); 初始条件:动态查找表DT存在,key为和关键字类型相同的给定值。 操作结果:若DT中存在其关键字等于key的数据元素,则函数值为 该元素的值或在表中的位置,否则为“空”。 InsertDSTable (&DT, e); 初始条件:动态查找表DT存在,e为待插入的数据元素。 操作结果:若DT中不存在其关键字等于e.key的数据元素,则插入 e到DT。

DeleteDSTable (&DT, key); TraverseDSTable (DT, Visit ( )); 初始条件:动态查找表DT存在,Visit是对元素操作的应用函数。 操作结果:按某种次序对DT的每个元素调用函数visit ( )一次且仅一 次。一旦visit ( )失败,则操作失败。 } ADT DynamicSearchTable (2)动态查找表的特点 特点:表结构本身是在查找过程中动态生成的,即对于给定值key, 若表中存在其关键字等于key的记录,则查找成功返回,否则插入关键字 等于key的记录。

£9.3.2 二叉排序树和平衡二叉树 (1)二叉排序树 ① 定义 二叉排序树(Binary Sort Tree):它或者是一棵空树;或者是具有 下列性质的二叉树: 1.若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 2.若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 3.它的左、右子树也分别为二叉排序树。 ② 图形表示 45 12 53 3 37 100 24 78 61 90 CAO ZHAO DING CHEN LI DU WANG XIA MA (b) (a) 图9.3 二叉排序树示例

③二叉排序树的查找 算法思想:二叉排序树又称二叉查找树,当二叉排序树不空时,首 先将给定值和根结点的关键字比较,若相等,则查找成功,否则将依据 给定值和根结点的关键字之间的大小关系,分别在左子树或右子树上继 续进行查找。 算法实现:取二叉链表作为二叉排序树的存储结构。 算法9.3(a)如下: BiTree SearchBST (BiTree T, KeyType key) { //在根指针T所指二叉排序树中递归地查找某关键字等于key的数据元素, //若查找成功,则返回指向该数据元素结点的指针,否则返回空指针。 if ((!T) | | EQ(key, T->data.key)) //查找结束 return (T); else if LT(key, T->data.key) //在左子树中继续查找 return (SearchBST (T->lchild, key)); else return (SearchBST (T->rchild, key)); //在右子树中继续查找 } // SearchBST

④二叉排序树的插入和删除 1.插入算法 i.算法思想:根据二叉排序树的特点,易知,新插入的结点一定是 一个新添加的叶子结点,并且是查找不成功时查找路径上访问的最后一个 结点的左孩子或右孩子。 ii.算法实现:为了在查找不成功时返回新结点的插入位置,将上述 算法算法9.3(a)修改为9.3(b)。插入算法如算法9.4所示。 算法9.3(b)如下: Status SearchBST (BiTree T, KeyType key, BiTree f, BiTree &p) { //在根指针T所指二叉排序树中递归地查找某关键字等于key的数据元素,若 //查找成功,则指针p指向该数据元素结点,并返回TRUE,否则指针p指向 //查找路径上访问的最后一个结点并返回FALSE,指针f指向T的双亲,其初 //始调用值为NULL。 if (!T) { p = f; return FALSE;} //查找不成功 else if EQ(key, T->data.key) { p = T; return TRUE;} //查找成功 else if LT(key, T->data.key) //在左子树中继续查找 return (SearchBST (T->lchild, key, T, p)); else return (SearchBST (T->rchild, key, T, p)); //在右子树中继续查找 } // SearchBST

算法9.4如下: Status InsertBST (BiTree T, ElemType e) { //当二叉排序树T中不存在关键字等于e.key的数据元素时,插入e并 //返回TRUE,否则返回FALSE。 if (!SearchBST (T, e.key, NULL, p)) { //查找不成功 s = (BiTree) malloc (sizeof (BiTree)); s->data = e; s->lchild = s->rchild = NULL; if (!p) T = s; //被插结点*s为新的根结点 else if LT(e.key, p->data.key) //被插结点*s为左孩子 p->lchild = s; else p->rchild = s; //被插结点*s为右孩子 return TRUE; } else return FALSE; //树中已有关键字相同的结点 } // InsertBST

iii.例子 例如,从空树出发,经过一系列的查找插入操作之后,可生成一棵二叉树。设查找的关键字序列为{45, 24, 53, 45, 12, 24, 90},则删除的二叉排序树如图9.4所示。 45 24 45 24 53 45 24 53 12 45 24 53 12 90 45 (a) (b) (c) (d) (e) (f) 图9.4 二叉排序树的构造过程

2.删除算法 i.算法思想 假设在二叉排序树上被删结点为*p(指向结点的指针为p),其双亲 结点为*f(结点指针为f),且不失一般性,可设*p是*f的左孩子(如图 9.5(a)所示)。 a.若*p结点为叶子结点,即PL和PR均为空树。由于删去叶子结点不破坏 整棵树的结构,则只需修改其双亲结点的指针即可。 b.若*p结点只有左子树PL后者只有右子树PR,此时只要令PL或PR直接成 为其双亲结点*f的左子树即可。 c.若*p结点的左子树和右子树均不空。从图9.5(b)可知,在删去*p结点之 前,中序遍历该二叉树得到的序列为{…CLC…QLQSLSPPRF…},在删去*p 之后,为保持其他元素直接的相对位置不变,可以有两种做法:其一是令*p 的左子树为*f的左子树,而*p的右子树为*s的右子树,如图9.5(c)所示;其二 是令*p的直接前驱(或直接后继)替代*p,然后再从二叉排序树中删去它的 直接前驱(或直接后继)。如图9.5(d)所示,当以直接前驱*s替代*p时,由于 *s只有左子树SL,则在删去*s之后,只要令SL为*s的双亲*q的右子树即可。

c f p F P C Q S PR s q CL QL SL f p F P PL PR (a) 以*f为根的子树 (b) 删除*p之前 返回 算法

c f p F S C Q PR q CL QL SL f c F C PR S CL SL (c) 删除*p之后,以PR作为*s的右子树的情形 (d) 删除*p之后,以*s 代替*p的情形 返回 算法 图9.5 在二叉排序树中删除*p

ii.算法实现 在二叉排序树上删除一个结点的算法如算法9.5所示,其中由前述3 中情况综合所得的删除操作如算法9.6所示。 算法9.5如下: Status DeleteBST (BiTree &T, KeyType key) { //若二叉排序树T中存在关键字等于key的数据元素时, //则删除该数据元素结点,并返回TRUE,否则返回FALSE。 if (!T) return FALSE; //不存在关键字等于key的数据元素 else { if (EQ(key, T->data.key)) { //找到关键字等于key的数据元素 return Delete(T); } else if LT(key, T->data.key) return (Delete BST (T->lchild, key)); else return (Delete BST (T->rchild, key)); } // DeleteBST

算法9.6如下: Status Delete (BiTree &p) { //从二叉排序树中删除结点p,并重接它的左或右子树 if (!p->rchild) { //右子树空则只需要接它的左子树 q = p; p = p->lchild; free (q); } else if (!p->lchild) { //只要重接它的右子树 p = p->rchild;

else { //左右子树均不空 q = p; s = p->lchild; while (s->rchild) { //转左然后向右到尽头 q = s; s = s->rchild; } p->data = s->data; //s指向被删除结点的前驱 if (q != p) q->rchild = s->lchild; //重接*q的右子树 else q->lchild = s->lchild; //重接*q的左子树 delete s; } // else return TRUE; } // Delete

⑤二叉排序树的查找分析 假设在含有n(n≥1)个关键字的序列中,i个关键字小于第一个关键字,n-i-1个关键字大于第一个关键字,则由此构造而得的二叉排序树在n个记录的查找概率相等的情况下,其平均查找长度为: (9-2) 其中,P(i)为含有i个结点的二叉排序树的平均查找长度,则P(i)+1为查找 左子树中每个关键字时所用比较次数的平均值,P(n-i-1)+1为查找右子 树中每个关键字时所用比较次数的平均值。 又,假设表中n个关键字的排列是“随机”的,即任一个关键字在序列中将是第1个,或第2个,……,或第n个的概率相同,则可对(9-2)式从i等于0至n-1取平均值: 容易看出上式括弧中的第一项和第二项对称。又,i=0时iP(i)=0,则上式 可改写为: n≥2 (9-3) 显然,P(0)=0, P(1)=1。

由式(9-3)可推得: 又 由此可得 (9-4) 即 由递推公式(9-4)和初始条件P(1)=1可推得: 则当n≥2时: (9-5)

平衡二叉树(Balanced Binary Tree 或Height-Balanced Tree)又称 (2)平衡二叉树 ①定义 平衡二叉树(Balanced Binary Tree 或Height-Balanced Tree)又称 AVL树。它或者是一棵空树,或者是具有下列性质的二叉树:它的左子 树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不 超过1。 平衡因子BF(Balance Factor):二叉树上结点的左子树的深度减去它的右子树的深度的值。 由平衡二叉树的定义易知,平衡二叉树上所有结点的平衡因子只可能 是-1、0和1。 ②图形表示 例如,图9.6(a)所示为两棵平衡二叉树,而图 9.6(b)为两棵不平衡二叉树,结点中的值为该结点 的平衡因子 1 -1 1 (a) 平衡二叉树

-1 -2 1 2 -1 1 (b) 不平衡二叉树 图9.6 平衡与不平衡二叉树及结点的平衡因子 ③C语言描述 typedef struct BSTNode { ElemType data; int bf; //结点的平衡因子 struct BSTNode *lchild, *rchild; //左、右孩子指针 } BSTNode, * BSTree;

④平衡调整 假设由于在二叉排序树上插入结点而失去平衡的最小子树根结点的 指针为a(即a是离插入结点最近,且平衡因子绝对值超过1的祖先结点), 则失去平衡后进行调整的规律可归纳为下列4种情况: 1.单向右旋平衡处理: 由于在*a的左子树根结点的左子树上插入结点,*a的平衡因子由1增 至2,致使以*a为根的子树失去平衡,则需进行一次向右的顺时针旋转操 作。如图9.6(a)所示。 2.单向左旋平衡处理: 由于在*a的右子树根结点的右子树上插入结点,*a的平衡因子由-1 变为-2,致使以*a为根的子树失去平衡,则需进行一次向左的顺逆针旋 转操作。如图9.6(c)所示。 3.双向旋转(先左后右)平衡处理: 由于在*a的左子树根结点的右子树上插入结点,*a的平衡因子由1增 至2,致使以*a为根的子树失去平衡,则需进行两次旋转(先左旋后右旋) 操作。如图9.6(b)所示。 4.双向旋转(先右后左)平衡处理: 由于在*a的右子树根结点的左子树上插入结点,*a的平衡因子由-1变 为-2,致使以*a为根的子树失去平衡,则需进行两次旋转(先右旋后左旋) 操作。如图9.6(d)所示。

(a) LL型 (b) LR型 返回 算法 LL 插入结点 LR 插入结点 2 A 1 B AR h-1 BL BR AR LL AR h-1 h BL h-1 BR (a) LL型 BL CL CR AR LR 2 A -1 B 1 C AR h-1 h BL h-1 CL h-2 CR 插入结点 (b) LR型 返回 算法

(c) RR型 返回 算法 (d) RL型 图9.6 二叉排序树的平衡旋转图例 RR 插入结点 RL 插入结点 AL BL BR -2 A -1 B RR AL h-1 BL h-1 BR h (c) RR型 RL -2 A 1 B C -1 BR h-1 h AL h-1 CL h-2 CR 插入结点 AL CL CR BR 返回 算法 (d) RL型 图9.6 二叉排序树的平衡旋转图例

⑤插入算法 算法思想: 在平衡二叉排序树BBST上插入一个新的数据元素e的递归算法可描述如下: 1.若BBST为空树,则插入一个数据元素为e的新结点作为BBST的根结 点,树的深度增1; 2.若e的关键字和BBST的根结点的关键字相等,则不进行插入; 3.若e的关键字小于BBST的根结点的关键字,而且在BBST的左子树中 不存在和e有相同关键字的结点,则将e插入在BBST的左子树上,并 且当插入之后的左子树深度增加(+1)时,分别就下列不同情况处 理之: i.BBST的根结点的平衡因子为-1(右子树的深度大于左子树的深度): 则将根结点的平衡因子更改为0,BBST的深度不变; ii.BBST的根结点的平衡因子为0(左、右子树的深度相等):则将根结 点的平衡因子更改为1,BBST的深度增1; iii.BBST的根结点的平衡因子为1(左子树的深度大于右子树的深度): a.若BBST的左子树根结点的平衡因子为1,则需进行单向右旋平衡处理, 并且在右旋处理之后,将根结点和其右子树根结点的平衡因子更改为0, 树的深度不变;b. 若BBST的左子树根结点的平衡因子为-1,则需进行 先向左、后向右的双向旋转平衡处理,并且在旋转处理之后,修改根结 点和其左、右子树根结点的平衡因子,树的深度不变。

4.若e的关键字大于BBST的根结点的关键字,而且在BBST的右子树中不 存在和e有相同关键字的结点,则将e插入在BBST的右子树上,并且当 插入之后的右子树深度增加(+1)时,分别就不同情况处理之。其处 理操作和上述3.中描述相对称。 算法实现: 算法9.7如下: void R_Rotate (BSTree &p) { //对以*p为根的二叉排序树作右旋处理,处理之后p指向新的树根结点, //即旋转处理之前的左子树的根结点 lc = p->lchild; //lc指向的*p的左子树根结点 p->lchild = lc->rchild; //lc的右子树挂接为*p的左子树 lc->rchild = p; p = lc; //p指向新的根结点 } // R_Rotate 算法9.8如下: void L_Rotate (BSTree &p) { //对以*p为根的二叉排序树作左旋处理,处理之后p指向新的树根结点, //即旋转处理之前的右子树的根结点 rc = p->rchild; //rc指向的*p的右子树根结点 p->rchild = rc->lchild; //rc的左子树挂接为*p的右子树 rc->lchild = p; p = rc; //p指向新的根结点 } // L_Rotate

算法9.9如下: void LeftBalance (BSTree &T) { //对以指针T所指结点为根的二叉树作左平衡旋转处理,本算法 //结束时,指针T指向新的根结点 lc = T->lchild; //lc指向*T的左子树根结点 switch (lc->bf) { //检查*T的左子树的平衡度,并作相应平衡处理 case LH: //新结点插入在*T的左孩子的左子树上,要作单右旋处理 T->bf = lc->bf = EH; R_Rotate (T); break; case RH: //新结点插入在*T的左孩子的右子树上,要作双旋处理 rd = lc->rchild; //rd指向*T的左孩子的右子树根 switch (rd->bf) { //修改*T及其左孩子的平衡因子 case LH: T->bf = RH; lc->bf = EH; case EH:

case RH: T->bf = EH; lc->bf = LH; break; } // switch (rd->bf) rd->bf = EH; L_Rotate (T->lchild); //对*T的左子树作左旋平衡处理 R_Rotate (T); //对*T作右旋平衡处理 } // switch (lc->bf) } // LeftBalance 算法9.10如下: #define LH +1 //左高 #define EH 0 //等高 #define RH -1 //右高 Status InsertAVL (BSTree &T, ElemType e, Boolean &taller) { //若在平衡的二叉排序树T中不存在和e有相同关键字的结点,则插入 //一个数据元素为e的新结点,并返回1,否则返回0。若因插入而使二 //叉排序树失去平衡,则作平衡旋转处理,布尔变量taller反映T长高 //与否。 if (!T) { //插入新结点,树“长高”,置taller为TRUE T = (BSTree) malloc (sizeof (BSTNode)); T->data = e; T->lchild = T->rchild = NULL; T->bf = EH;

taller = TRUE; } else { if (EQ(e.key, T->data.key)) //树中已存在和e有相同关键字的 {taller = FALSE; return 0;} //结点则不再插入 if (LT(e.key, T->data.key)) { //应继续在*T的左子树中进行搜索 if (!InsertAVL(T->lchild, e, taller)) //未插入 return 0; if (taller) //已插入到*T的左子树中且左子树“长高” switch (T->bf) { //检查*T的平衡度 case LH: //原本左子树比右子树高,需要作左平衡处理 LeftBalance (T); taller = FALSE; break; case EH: //原本左右子树等高,现因左子树增高而使树增高 T->bf = LH; case RH: //原本右子树比左子树高,现左、右子树等高 T->bf = EH; } // switch (T->bf) } // if

else { //应继续在*T的右子树中进行搜索 if (!InsertAVL(T->rchild, e, taller)) //未插入 return 0; if (taller) //已插入到*T的右子树中且右子树“长高” switch (T->bf) { //检查*T的平衡度 case LH: //原本右子树比左子树高,现左、右子树等高 T->bf = EH; taller = FALSE; break; case EH: //原本左右子树等高,现因右子树增高而使树增高 T->bf = RH; taller = TRUE; case RH: //原本右子树比左子树高,需要作右平衡处理 RightBalance (T); } // switch (T->bf) } // else return 1; } // InsertAVL

⑥平衡树查找的分析 在平衡树上进行查找的过程和排序树相同,由此,在查找过程中 和给定值进行比较的关键字个数不超过树的深度。 含有n个结点的平衡树的最大深度为 。 由此,在平衡树上进行查找的时间复杂度为O(logn)。

£9.3.3 B-树和B+树 (1)B-树 ① B-树的定义 B-树是一种平衡的多路查找树。一棵m阶的B-树,或为空树,或 2.若根结点不是叶子结点,则至少有两棵子树; 3.除根之外的所有非终端结点至少有 棵子树; 4.所有的非终端结点中包含下列信息数据: (n, A0, K1, A1, K2, A2, … , Kn, An) 其中:Ki(i = 1, … , n)为关键字,且Ki < Ki+1(i = 1, … , n-1); Ai(i = 1, … , n)为指向子树根结点的指针,且指针Ai-1所指子树中 所有结点的关键字均小于Ki(i = 1, … , n),An所指子树中所有结 点的关键字均大于Kn, 为关键字的个数(或 n+1为子树个数)。 5.所有的叶子结点都出现在同一层次上,并且不带信息(可以看作是 外部结点或查找失败的结点,实际上这些结点不存在,指向这些结 点的指针为空)。

t a b c f e g d h 1 35 1 11 2 43 78 1 18 1 27 1 39 1 99 3 47 53 64 F F ②图形表示 图9.7 一棵4阶的B-树 ③C语言说明 #define m 3 // B-树的阶,暂设为3 typedef struct BTNode { int keynum; //结点中关键字个数,即结点的大小 struct BTNode *parent; //指向双亲结点 KeyType key[m + 1]; //关键字向量,0号单元未用 struct BTNode *ptr[m + 1]; //子树指针向量 Record *recptr[ m + 1]; //记录指针向量,0号单元未用 } BTNode, *BTree;;

typedef struct { BTNode *pt; //指向找到的结点 int i; //1..m,在结点中的关键字序号 int tag; //1:查找成功,0:查找失败 } Result; //B-树的查找结果类型 ④ B-树的查找 1.算法思想:在B-树上进行查找的过程是一个顺时针查找结点和在结点 的关键字中进行查找交叉进行的过程。 2.算法实现: 算法9.11如下: Result SearchBTree (BTree T, KeyType K) { //在m阶B-树T上查找关键字K,返回结果(pt, i , tag)。若查找成功, //则特征值tag=1,指针pt所指结点中第i个关键字等于K;否则 //特征值tag=0,等于K的关键字应插入在指针pt所指结点中第i和 //第i+1个关键字之间 p = T; //初始化,p指向待查结点,q指向p的双亲 q = NULL; found = FALSE; i = 0;

while (p && !found) { i = Search (p, K); //在p->key[1..keynum]中查找, //i使得: p->key[i] <= K < p->key[i + 1] if (i > 0 && p->key[i] = = K) found = TRUE; //找到待查关键字 else { q = p; p = p->ptr[i]; } if (found) return (p, i, 1); //查找成功 else return (q, i, 0); //查找不成功,返回K的插入位置信息 } // SearchBTree

3.查找分析: 从算法9.10可见,在B-树上进行查找包含两种基本操作: a.在B-树中找结点(操作在磁盘上进行); b.在结点中找关键字(操作在内存中进行)。 显然,在磁盘上进行一次查找比在内存中进行一次查找耗费时间多 得多,因此,在磁盘上进行查找的次数、即待查关键字所在结点在B-树 上的层次数,是决定B-树查找效率的首要因素。 根据B-树的定义,第一层至少有1个结点;第二层至少有2个结点;由 棵子树,则第三层至少有 2( )个结点;……;依次类推,第l+1层至少有2( )l-1个结点。 于除根之外的每个非终端结点至少有 而l+1层的结点为叶子结点。若m阶B-树中具有N个关键字,则叶子结点即查找不成功的结点为N+1,由此有: 推得: 这就是说,在含有N个关键字的B-树上进行查找时,从根结点到关键字所 在结点的路径上涉及的结点数不超过

⑤B-树的插入 1.算法思想:由于B-树结点中的关键字个数必须 ,因此, 每次插入一个关键字不是在树中添加一个叶子结点,而是首先在最低层的某个非终端结点中添加一个关键字,若该结点的关键字个数不超过 m-1,则插入完成,否则要产生结点的“分裂”。 一般情况下,结点可如下实现“分裂”: 假设*p结点中已有m-1个关键字,当插入一个关键字之后,结点中含有信息为: m, A0, (K1, A1), … , (Km, Am) 且其中 Ki < Ki+1 1≤i < m 此时可将*p结点分裂为*p和*p’两个结点,其中*p结点中含有信息为: *p’结点中含有信息: 而关键字 和指针*p’一起插入到*p的双亲结点中。

Status InsertBTree (BTree &T, KeyType K, BTree q, int i) { 2.算法实现 算法9.12如下: Status InsertBTree (BTree &T, KeyType K, BTree q, int i) { //在m阶B-树T上结点*q的key[i]与key[i+1]之间插入关键字K。 //若引起结点过大,则沿双亲链进行必要的结点分裂调整, //使T仍是m阶B-树。 x = K; ap = NULL; finished = FALSE; while (q && !finished) { Insert (q, i, x, ap); //将x和ap分别插入到q->key[i+1]和q->ptr[i+1] if (q->keynum < m) finished = TRUE; //插入完成 else { //分裂结点*q s = ; split (q, s, ap); x = q->key[s]; //将q->key[s+1..m], q->ptr[s..m] //和q->recptr[s+1..m]移入新结点*ap q = q->parent; if (q) i = Search(q, x); //在双亲结点*q中查找x 的插入位置 } // else } // while q和i是由查找函数SearchBTree 返回的信息而得。

if (!finished) NewRoot (T, q, x, ap); //T是空树(参数q初值为NULL)或者根结点 //已分裂为结点*q和*ap生成含信息(T, x, ap) //的新的根结点*T,原T和ap为子树指针。 return OK; } // InsertBTree 3.例子 例如,图9.8(a)所示为3阶B-树(图中略去F结点,即叶子结点),假设需一 次插入关键字30,26,85和7。 bt a 45 b 24 c 3 12 d 37 f 50 h 100 g 61 70 e 53 90 (a) 一棵2-3树 返回 图9.9例

bt a 45 b 24 c 3 12 d f 50 h 100 g 61 70 e 53 90 30 37 (b) bt a 45 b 24 c 3 12 d f 50 h 100 g 61 70 e 53 90 26 30 37 (c)

bt a 45 b 24 30 c 3 12 d f 50 h 100 g 61 70 e 53 90 37 26 d’ (d) bt a 45 b 24 30 c 3 12 d f 50 h 100 g 6170 85 e 53 90 37 26 d’ (e)

bt a 45 b 24 30 c 3 12 d f 50 h 100 g 61 e 53 70 90 37 26 d’ 85 g’ (f) bt a 45 70 b 24 30 c 3 12 d f 50 h 100 g 61 e 53 37 26 d’ 85 g’ e’ 90 (g)

bt a 45 70 b 7 24 30 c 3 d f 50 h 100 g 61 e 53 37 26 d’ 85 g’ e’ 90 12 c’ (h) bt a 24 45 70 b 7 c 3 d f 50 h 100 g 61 e 53 26 d’ 85 g’ e’ 90 12 c’ 30 b’ 37 (i)

bt a 70 b 7 c 3 d f 50 h 100 g 61 e 53 26 d’ 85 g’ e’ 90 12 c’ 30 b’ m a’ 45 24 37 (j) 图9.8 在B-树中进行插入(省略叶子结点) (a) 一棵2-3树; (b) 插入30之后; (c)、(d) 插入26之后; (e)~(g) 插入85之后; (h)~(j) 插入7之后;

⑥B-树的删除 1.算法思想: 在B-树中删除一个关键字,则首先应找到该关键字所在结点,并 从中删除之。 i.若所删关键字为非终端结点中的Ki,则可以指针Ai所指子树中的最 小关键字Y替代Ki,然后在相应的结点中删去Y。 ii.若所删关键字在最下层非终端结点中。有下列3种情况: a.被删关键字所在结点中的关键字数目不小于 ,则只需从该结点 中删去该关键字Ki和相应指针Ai,树的其他部分不变。 b.被删关键字所在结点中的关键字数目等于 -1,而与该结点相邻 的右兄弟(或左兄弟)结点中的关键字数目大于 -1,则需将其 兄弟结点中的最小(或最大)的关键字上移至双亲结点中,而将双亲 结点中小于(或大于)且紧靠该上移关键字的关键字下移至被删关键 字所在结点中。 c.被删关键字所在结点和其相邻的兄弟结点中的关键字数目均等于 -1。假设该结点有右兄弟,且其右兄弟结点地址由双亲结 点中的指针Ai所指,则在删去关键字之后,它所在结点中剩余的关 键字和指针,加上双亲结点中的关键字Ki一起,合并到Ai所指兄弟 结点中(若没有右兄弟,则合并至左兄弟结点中)。如果因此使双 亲结点中的关键字数目小于 -1,则一次类推作相应处理。

2.例子 bt a 45 b 24 c 3 d 37 f 50 h 100 g 61 70 e 53 90 (a) bt 45 a b 24 c 3 d 37 f 53 h 100 g 70 e 61 90 (b) 说 明

bt 45 a b 24 c 3 d 37 h 100 g 61 70 e 90 (c) e c 3 24 h 100 g 61 70 45 90 bt (d) 说 明 图9.9 在B-树中删除关键字的情形

i.从图9.8(a)所示B-树中删去关键字12,删除后的B-树如图9.9(a) 所示。 说明: i.从图9.8(a)所示B-树中删去关键字12,删除后的B-树如图9.9(a) 所示。 ii.从图9.9(a)中删去50,需将其右兄弟结点中的61上移至*e结点 中,而将*e结点中的53移至*f,从而使*f和*g中关键字数目均 不小于 -1,而双亲结点中的关键字数目不变,如图9.9(b) iii.从图9.9(b)所示B-树中删去53,则应删去*f结点,并将*f中的 剩余信息(指针“空”)和双亲*e结点中的61一起合并到右兄弟 结点*g中,删除后的树如图9.9(c)所示。 iv.在图9.9(c)的B-树中删去关键字37之后,双亲b结点中剩余信息 (“指针c”)应和其双亲*a结点中关键字45一起合并至右兄弟结点 *e中,删除后的B-树如图9.9(d)所示。 (2)B+树 ① 定义 B+树是应文件系统所需而出的一种B-树的变型树。一棵m阶的B+树和 m阶的B-树的差异在于: 有n棵子树的结点中含有n个关键字。

2.所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录 的指针,且叶子结点本身依关键字的大小自小而大顺序链接。 3.所有的非终端结点可以看成是索引部分,结点中仅含其子树(根结点) 中的最大(或最小)关键字。 通常在B+树上有两个头指针,一个指向根 结点,一个指向关键字最小的叶子结点。 root sqt 59 97 15 44 59 72 97 10 15 21 37 44 51 59 63 72 85 91 97 ②图形表示 图9.10 一棵3阶的B+树

③B+树的查找 对B+树可以进行两种查找运算: 1.从最小关键字起顺序查找; 2.从根结点开始,进行随机查找。 在查找时,若非终端结点上的剧组机等于给定值,并不终止,而是 继续向下直到叶子结点。因此,在B+树中,不管查找成功与否,每次 查找都是走了一条从根到叶子结点的路径。其余同B-树的查找类似。 ④B+树的插入 B+树的插入仅在叶子结点上进行,当结点中的关键字个数大于m时要分 和 。并且,它们的 裂成两个结点,它们所含关键字的个数分别为 双亲结点中应同时包含这两个结点中的最大关键字。其余同B-树的插入类似。 ⑤ B+树的删除 B+树的删除也仅在叶子结点进行,当叶子结点中的最大关键字被删除时, 其在非终端结点中的值可以作为一个“分界关键字”存在。若因删除而使结点中 时,其和兄弟结点的合并过程亦和B-树类似。 关键字的个数少于

£9.4 哈希表 £9.4.1 定义 哈希(Hash)函数:在记录的存储位置和它的关键字之间建立的一个 确定的对应关系f,使每个关键字和结构中一个唯一的存储位置相对应。 称这个对应关系f为哈希函数。 均匀的(Uniform)哈希函数:对于关键字集合中的任一个关键字, 经哈希函数映像到地址集合中任何一个地址的概率是相等的,则称此类哈 希函数为均匀的哈希函数。 哈希表:根据设定的哈希函数H(key)和处理冲突的方法将一组关键字映像 到一个有限的连续的地址集(区间)上,并以关键字在地址集中的“像”作为记 录在表中的存储位置,这种表便称为哈希表,这一映像过程称为哈希造表或散 列,所得存储位置称哈希地址或散列地址。 冲突(collision):不同的关键字可能得到同一哈希地址的现象。 同义词(synonym):在一个哈希函数中,具有相同函数值的关键字,互 称为同义词。

例如,假设建立一张全国30个地区的各民族人口统计表,每个地区 为一个记录,记录的各数据项为: 编号 地区名 总人口 汉族 回族 显然,可以用一个一维数组C(1:30)来存放这张表,其中C[i]是编号为i的 地区的人口情况。编号i便为记录的关键字,由它唯一确定记录的存储位 置C[i]。假设北京市的编号为1,则若要查看北京市的各民族人口,只要 取出C[1]的记录即可。若把这个数组看成是哈希表,则哈希函数f(key)=key。 又,若取关键字中第一个字母在字母表中的序号作为哈希函数。如, BEIJING的哈希函数值为字母“B”在字母表中的序号等于02。关键字 HEBEI和HENAN不等,但f(HEBEI)=f(HENAN),这种现象即为冲突。 HEBEI和HENAN相对于该哈希函数称为同义词。

£9.4.2 哈希函数的构造 构造哈希函数要考虑的元素,通常有: 1.计算哈希函数所需时间(包括硬件指令的因素); 2.关键字的长度; 3.哈希表的大小; 4.关键字的分布情况; 5.记录的查找频率。 直接定址所得地址集合和关键字集合的大小相同。对于不同的关键字不会发生冲突。 (1)直接定址法 1.定义 取关键字或关键字的某个线性函数值为哈希地址。即: H(key) = key 或H(key) = a * key + b 其中a和b为常数(这种哈希函数叫做自身函数)。 2.例子 例一,有一个从1岁到100岁的人口数字统计表。 地址 01 02 03 … 25 26 27 … 100 年龄 1 2 3 … 25 26 27 … … 人数 300 2000 5000 … 1050 … … … … 表9.1 直接定址哈希函数例子一 其中,年龄作为关键字,哈希函数取关键字自身。若要询问25岁的人有 多少,则只要查表的第25项即可。

其中,关键字是年份,哈希函数取关键字加一常数:H(key) = key + (-1948)。 例二:有一个解放后出生的人口调查表。 地址 01 02 03 … 22 … 年份 1949 1950 1951 … 1970 … 人数 … … … … 15000 … 表9.2 直接定址哈希函数例子二 其中,关键字是年份,哈希函数取关键字加一常数:H(key) = key + (-1948)。 若要查1970年出生的人,则只要查第(1970-1948)=22项即可。 (2)数字分析法 主要思想:假设关键字是以r为基的数(如:以10为基的十进制数),并且哈 希表中可能出现的关键字都是事先知道的,则可取关键字的若干位组成哈希地址。 例如,有80个记录,其关键字为8位十进制数。假设哈希表的表长为10010,则 可取两位十进制数组成哈希地址。这两位的取法,原则是使得到的哈希地址尽量避 免产生冲突。假设这80个关键字中的一部分如下所列:

8 1 3 4 6 5 3 2 8 1 3 7 2 2 4 8 1 3 8 7 4 2 2 8 1 3 0 1 3 6 7 8 1 3 2 2 8 1 7 8 1 3 3 8 9 6 7 8 1 3 5 4 1 5 7 8 1 3 6 8 5 3 7 8 1 4 1 9 3 5 5 对关键字全体的分析:第①②位都是“8 1”,第③位只可能取1、2、3或4, 第⑧位只可能取2、5或7,因此这4位都不可取。由于中间4位可看成是近乎随 机的,因此可取其中任意两位,或取其中两位与另两位的叠加求和后舍去进位 作为哈希地址。 (3)平方取中法 1.定义 取关键字平方后的中间几位为哈希地址。取的位数由表长决定。

例如,为BASIC源程序中的标识符建立一个哈希表。假设BASIC语言 中允许的标识符为一个字母,或一个字母和一个数字。在计算机内可用两 2.例子 例如,为BASIC源程序中的标识符建立一个哈希表。假设BASIC语言 中允许的标识符为一个字母,或一个字母和一个数字。在计算机内可用两 位八进制数表示字母和数字,如图9.11(a)所示。取表示符在计算机中的八 进制数为它的关键字。假设表长为512=29,则可取关键字平方后的中间9 位二进制数为哈希地址。例如,图9.11(b)列出了一些标识符及它们的哈希 地址。 A B C … Z 0 1 2 … 9 01 02 03 … 32 60 61 62 … 71 (a) 字符的八进制表示对照表 A 0100 0 010000 000 I 1100 1 210000 210 J 1200 1 440000 440 I0 1160 1 370400 370 P1 2061 4 310541 310 P2 2062 4 314704 314 Q1 2161 4 734741 734 Q2 2162 4 741304 741 Q3 2163 4 745651 745 记录 关键字 (关键字)2 哈希地址(217~29) (b) 标识符及其哈希地址 图9.11

(4)折叠法 1.定义 折叠法(folding):将关键字分割成位数相同的几部分(最后一 部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为 哈希地址。 移位叠加:将分割后的每一部分的最低位对齐,然后相加。 间界叠加:从一端向另一端沿分割界来回折叠,然后对齐相加。 2.使用前提 关键字位数很多,而且关键字中每一位上数字分布大致均匀时。 3.例子 例如,每一种西文图书馆都有一个国际标准图书编号(ISBN),它是一 个10位的十进制数字,若要以它作关键字建立一个哈希表,当馆藏书种类不 到10000时,可采用折叠法构造一个四位数的哈希函数。如国际标准图书馆 图书编号0-442-20586-4的哈希地址分别如图9.12(a)和(b)所示。 5864 5864 4220 0224 +) 04 +) 04 10088 6092 H(key) = 0088 H(key) = 6092 (b) 间界叠加 (a) 移位叠加 图9.12 由折叠法求得哈希地址

(5)除留余数法 1.定义 取关键字被某个不大于哈希表表长m的数p除后所得余数为哈希地址。 即: H(key) = key MOD p , p ≤ m 由经验得知:一般情况下,可以选p为质数或不包含小于20的质因数的合数。 2.例子 若p含有质因子pf,则所有含有pf因子的关键字的哈希地址均为pf的倍数。 例一:当p=21(=3×7)时,下列含因子7的关键字21取模的哈希地址均 为7的被数。 关键字 28 35 63 77 105 哈希地址 7 14 0 14 0 例二:假设有两个标识符xy和yx,其中x、y均为字符,又假设它们的机器 代码(6位二进制数)分别为c(x)和c(y),则上述两个标识符的关键字分别为: key1 = 26 c (x) + c (y) 或 key2 = 26 c (y)+ c (x) 用除留余数法求哈希地址,且p=tq,t是某个常数,q是某个质数。则当q=3时, 这两个关键字将被散列在差为3的地址上。因为:

= {[26 c (x) + c (y)] MOD p-[26 c (y)+ c (x)] MOD p} MOD q [H(key1)-H(key2)] MOD q = {[26 c (x) + c (y)] MOD p-[26 c (y)+ c (x)] MOD p} MOD q = {26 c (x) MOD p + c (y) MOD p-26 c (y) MOD p-c (x) MOD p} MOD q ={26 c (x) MOD q + c (y) MOD q-26 c (y) MOD q-c (x) MOD q} MOD q (因对任一x有(x MOD (t * q)) MOD q = (x MOD q) MOD q) 当q=3时,上式为: = {(26 MOD 3)c(x) MOD 3 + c(y) MOD 3-(26 MOD 3)c(y) MOD 3 - c(x) MOD 3} MOD 3 = 0 MOD 3 (6)随机数法 1.定义 选择一个随机函数,取关键字的随机函数值为它的哈希地址,即 H(key)=random(key),其中random为随机函数。 2.使用前提 关键字长度不等时,采用此法构造哈希函数较恰当。

£9.4.3 处理冲突的方法 假设哈希表的地址集为0~(n-1)。 冲突:指由关键字得到的哈希地址为j(0≤j≤n-1)的位置上已存 在记录。 处理冲突:为产生冲突的关键字的记录找到另一个“空”的哈希地址。 (1)开放定址法 1.定义 Hi = (H(key) + di) MOD m i = 1, 2, … , k (k≤m-1) 其中,H(key)为哈希函数;m为哈希表长;di为增量序列。 增量序列,可有下述3种取法: i.di = 1, 2, 3, … , m-1,称线性探测再散列; ii.di = 12, -12, 22, -22, 32, … , ±k2 (k≤m/2),称二次探测再散列; iii.di = 伪随机数序列,称伪随机探测再散列。 二次聚集:处理冲突过程中发生的两个第一个哈希地址不同的记录争夺 同一个后继哈希地址的现象。即,在处理同义词的冲突过程中又添加了非同 义词的冲突。

2.例子 例如,在长度为11的哈希表中已填有关键字分别为17,60和29的记 录(哈希函数H(key)= key MOD 11),现有第四个记录,其关键字为 38,由哈希函数得到的哈希地址为5,产生冲突。 线性探测再散列:得到下一个地址6,仍冲突;再求下一个地址7, 仍冲突;直到哈希地址为8的位置(“空”)时止。 二次探测再散列:应填入序号为4的位置。 伪随机探测再散列:伪随机数列9, … 应填入序号为2的位置。 0 1 2 3 4 5 6 7 8 9 60 17 29 (a) 插入前 60 17 29 38 (b) 线性探测再散列 38 60 17 29 (c) 二次探测再散列 60 17 29 38 (d) 伪随机探测再散列 图9.12 用开放定址处理冲突时,关键字为38的记录插入前后的哈希表

(2)再哈希法 Hi = RHi (key) i = 1, 2, … , k 其中,R、Hi均是不同的哈希函数,即在同义词产生地址冲突时计算另一 个哈希函数地址,直到冲突不再发生。 (3)链地址法 1.定义 将所有关键字为同义词的记录存储在同一线性链表中。 假设某哈希函数产生的哈希地址区间[0, m-1]上,则设立一个指针 型向量: Chain Chain Hash[m]; 其每个分裂的初始状态都是空指针。凡哈希地址为i的记录都插入到头指 针为Chain Hash[m]的链表中。在链表中的插入位置可以在表头或表尾, 也可以在中间,以保持同义词在同一线性链表中按关键字有序。 2.例子 例9-1,已知一组关键字(19,14,23,01,68,20,84,27,55,11, 10,79)。则按哈希函数H(key)=key MOD 13和链地址法处理冲突构造所得 的哈希表如图9.13所示。

设哈希函数的值域为[0, m-1],则设向量HashTable[0..m-1]为基本表,每 01 14 27 29 55 68 19 84 10 23 20 11 1 2 3 4 5 6 7 8 9 10 12 图9.13 链地址法处理冲突时的哈希表 (同一链表中关键字自小至大有序) (4)建立一个公共溢出区 设哈希函数的值域为[0, m-1],则设向量HashTable[0..m-1]为基本表,每 个分量存放一个记录,另设立向量OverTable[0..v]为溢出表。所有关键字和基本 表中关键字为同义词的记录,不管它们由哈希函数得到的哈希地址是什么,一旦 发生冲突,都填入溢出表。

£9.4.4 哈希表的查找及其分析 (1)相关术语 查找过程:给定K值,根据造表时设定的哈希函数求得哈希地址, 若表中此位置上没有记录,则查找不成功;否则比较关键字,若和给 定值相等,则查找成功;否则根据造表时设定的处理冲突的方法找“下 一地址”,直至哈希表中某个位置为“空”或者表中所填记录的关键字等 于给定值为止。 影响查找过程中需和给定值进行比较的关键字的个数的因素: 1.哈希函数 2.冲突处理方法 3.哈希表的装填因子 装填因子: 其中,α标志哈希表的装满程度。 直观的看,α越小,发生冲突的可能性就越小;反之,α越大,表中 已填入的记录越多,再填记录时,发生冲突的可能性就越大,则查找时, 给定值需与之进行比较的关键字的个数也就越多。

(2)查找算法实现(开放定址法处理冲突) //----------------------开放定址哈希表的存储结构---------------------- int hashsize[ ] = {997, …}; //哈希表容量递增表,一个合适的素数序列 typedef struct { ElemType *elem; //数据元素存储基址,动态分配数组 int count; //当前数据元素个数 int sizeindex; // hashsize[sizeindex]为当前容量 } HashTable; #define SUCCESS 1 #define UNSUCCESS 0 #define DUPLICATE -1 算法9.13如下: Status SearchHash (HashTable H, KeyType K, int &p, int &C) { //在开放定址哈希表H中查找关键字为K的元素,若查找成功,以p指示待查 //数据元素在表中位置,并返回SUCCESS;否则,以p指示插入位置,并返回 //UNSUCCESS,c用以计冲突次数,其初值置零,供建表插入时参考 p = Hash (K); //求得哈希地址 while (H.elem[p].key != NULLKEY && !EQ(K, H.elem[p].key)) //该位置中填有记录并且关键字不相等 collision (p, ++ c); //求得下一探查地址p

return SUCCESS; //查找成功,p返回待查数据元素位置 else return UNSUCCESS; if EQ(K, H.elem[p].key return SUCCESS; //查找成功,p返回待查数据元素位置 else return UNSUCCESS; //查找不成功(H.elem[p].key = = NULLKEY), p返回的是插入位置 } // SearchHash 算法9.14如下: Status InsertHash (HashTable &H, ElemType e) { //查找不成功时插入数据元素e到开放定址哈希表H中,并返回OK; //若冲突次数过大,则重建哈希表 c = 0; if (SearchHash (H, e.key, p, c)) return DUPLICATE; //表中已有与e相同关键字的元素 else if (c < hashsize[H.sizeindes]/2) { //冲突次数c未达到上限(c的阈值可调) H.elem[p] = e; //插入e ++H.count; return OK; } else { RecreateHashTable (H); //重建哈希表 return UNSUCCESS; } // InsertHash 算法9.14通过调用查找算法(算法9.13) 实现了开放定址哈希表的插入操作。

(3)查找的例子 例如,已知例9-1中所示的一组关键字按哈希函数H(key) = key MOD 13 和线性探测处理冲突构造所得哈希表a.elem[0..15]如图9.14所示 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 14 01 68 27 55 19 20 84 79 23 11 10 图9.14 哈希表a.elem[0:15] 给定值K=84: 首先求得哈希地址H(84)=6,因a.elem[6]不空且a.elem[6].key≠84, 则 找第一次冲突处理后的地址H1=(6+1) MOD 16=7,而a.elem[7]不空且 a.elem[7].key≠84,则找第二次冲突处理后的地址H2=(6+2) MOD 16=8, a.elem[8]不空且a.elem[8].key=84,则查找成功,返回记录在表中序号8。 给定值K=38: 先求哈希地址H(38)=12,a.elem[12]不空且a.elem[12].key≠38,则 找下一地址H1=(12+1) MOD 16=13,由于a.elem[13]是空记录,则表 明表中不存在关键字等于38的记录。

(4)平均查找长度 ①查找成功时的平均查找长度 1.线性探测再散列 2.伪随机探测再散列、二次探测再散列和再哈希表 3.链地址法 ②查找不成功时的平均查找长度 1.线性探测再散列 2.伪随机探测再散列、二次探测再散列和再哈希表 3.链地址法

③公式推导 仅以伪随机探测的一组公式为例进行分析推导。 假定:1.哈希函数是均匀的。即产生表中各个地址的概率相等; 2.处理冲突后产生的地址也是随机的。 若设pi表示前i个哈希地址均发生冲突的概率;qi表示需进行i次比较才 找到一个“空位”的哈希地址(即前i-1次发生冲突,第i次不冲突)的概率。 则有: 可见,在pi和qi之间,存在关系式: qi=pi-1-pi

由此,当长度为m的哈希表中已填有n个记录时,查找不成功的平均 查找长度为: (用归纳法证明) 由于哈希表中n个记录是先后填入的,查找每一个记录所需比较次数的期望 值,恰为填入此记录时找到此哈希地址所进行的比较次数的期望值。因此,对 表长为m、记录数为n的哈希表,查找成功时的平均查找长度为:

设对n个记录的查找概率相等。即pi=1/n则: 度限定在一个范围内。