《数据结构》(C++)之第七章:查找技术
作者:互联网
- 查找以集合为数据结构,以查找为核心操作
7.1 概述
7.1.1 查找的基本概念
-
记录:在查找问题中,通常将数据元素称为记录
-
关键码(key):可以标识一个记录的某个数据称为关键码
- 键值(keyword):关键码的值
- 主关键码(primary key):该关键码可以唯一的标识一个记录
- 反之,称此关键码为次关键码
-
查找(serch):查找是在具有相同类型的记录构成的集合中找出满足给定条件的记录
- 匹配:查找关键码等于给定值的记录
-
查找的结果:
结果 定义 操作 查找成功 在查找集合中找到了与给定值相匹配的的记录 返回一个成功标志(查找到的记录的位置或值) 查找失败 找不到匹配记录 1⃣ 返回一个不成功的标志(空指针或0)2⃣ 将被查找的记录插入到集合中 -
动态查找、静态查找:
类别 定义 查找不成功时操作 适用场景 静态查找 不涉及插入和删除操作的查找 只返回一个不成功标志,查找的结果不改变查找集合 查找集合一经生成,便只对其进行查找,而不进行插入和删除操作(或经过一段时间的查找之后,集中的进行插入和删除等修改操作) 动态查找 设计插入和删除操作的查找 需要将被查找的记录插入到查找集合中,查找的结果可能会改变查找集合 查找与插入删除操作在同一个阶段进行 -
查找结构:面向查找操作的数据结构
-
出现原因:在某应用中,查找是最主要的操作,为了提高查找效率,需要专门为查找操作设计数据结构
-
常用的查找结构:
查找结构 适用场景 实现查找技术 线性表 适用于 静态查找 主要采用 顺序查找技术、折半查找技术 树表 适用于 动态查找 主要采用 二叉排序树的查找技术 散列表 静态查找和动态查找 均适用 主要采用 散列技术
-
7.1.2 查找算法的性能
-
时间消耗的决定性因素:关键码的比较次数
- (1)算法本身
- (2)问题规模
- (3)待查关键码在查找集合中的位置
-
查找算法的时间复杂度函数:
T(n, k)
- (1)n:问题规模
- (2)k:待查关键码在查找集合中的位置
-
平均查找长度(average search length):将查找算法进行的关键码比较次数的数学期望值
-
对于查找成功的情况:(i = 1、2、3、…、n)
ASL = Σ pi ci
表达式 | 含义 | 决定因素 --- | --- | --- pi | 查找第 i 个记录的概率 | 由具体应用决定 ci | 查找第 i 个记录所需关键码的比较次数 | 由算法决定
-
对于查找不成功的情况(一般不会失败,可忽略):
平均查找长度 = 查找失败对应的关键码的比较次数
-
总的平均查找长度:查找成功于查找失败两种情况下的查找长度的平均值
-
7.2 线性表的查找技术
7.2.1 顺序查找
-
定义:顺序查找(sequential search)又称线性查找
-
基本思想:从线性表的一端向另一端逐个将关键码于给定值进行比较
- 若相等,则查找成功,给出该记录在表中的位置
- 若整个表检测完仍未找到与给定值相等的关键码,则查找失败,给出失败信息
-
与其他查找技术相比的优缺点:
优点 算法简单且适用面广(表中记录可顺序存储或链接存储、对有序性无要求 缺点 平均查找长度较大(当n很大时,查找效率较低)
1、顺序表的顺序查找
-
优化后的算法:设置哨兵
- 哨兵:待查值
- 位置:将哨兵放在查找方向的起点(即线性表的第一个值)
- 好处:免去了查找过程中每一次比较后都要判断查找位置是否越界,从而提高查找速度(只需比较值是否相等)
-
伪代码:
1、设置哨兵; 2、初始化查找的起始下标 i = n; 3、若r[i]与k相等,则返回当前i的值 否则,继续比较前一个记录
-
代码描述:
int SeqSearchArray(int r[], int n, int k) //从数组下标1开始存放待查集合 { r[0] = k; //下标0用作监视哨 i = n; while (r[i] != k) //不用判断下标i是否越界 i--; //从最后一个向前遍历 return i; //返回0代表查找失败,返回其他值则为待查值在数组中的下标 }
-
平均查找长度:
O(n)
结果 关键码比较次数 平均查找长度 查找成功 对于具有n个记录的顺序表,查找第i个记录时,需要进行 n-i+1
次关键码的比较ASL = [∑(n - i + 1)]/n = (n + 1)/ 2 = O(n)
查找失败 n + 1
次O(n)
-
-
特例:当查找集合中每个记录的查找概率不相等时
-
解决方案:在每个记录中附设一个 访问频度域 ,并使表中记录始终保持按访问频度非递减(因为是倒着查)的次序排列
-
结果:使得查找概率大的记录在查找过程中不断向后移,以便在以后的查找中减少比较次数,从而提高查找效率
-
应用:LRU 算法
-
2、单链表的顺序查找
- 代码描述:假定带头结点的单链表的头指针为 first
int SeqSearchLinked(Node<int> * first, int k) { p = first -> next; //工作指针p初始化为指向第一个元素结点 while(p != NULL && p -> data != k) { p = p -> next; //工作指针后移 j++; } if (p -> data == k) return j; //查找成功,返回元素在查找集合中的序号 else return 0; //查找失败,返回0 }
7.2.2 折半查找
-
前提条件:表中的记录必须
- (1)按关键码有序
- (2)采用顺序存储
-
限制:一般只能应用于 静态查找
1、执行过程
-
基本思想:利用了 记录按关键码有序 的特点
序号 操作 1 在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键码相等,则查找成功 2 若给定值小于中间记录的关键码,则在中间记录的左半区继续查找 3 若给定值大于中间记录的关键码,则在中间记录的右半区继续查找 - 不断重复(1)(2)(3),直到查找成功;或所查找的区域无匹配记录,查找失败
-
伪代码:
1、设置初始查找区间:low = 1;high = n; 2、测试查找区间[low,high]是否存在,若不存在,则查找失败 3、若查找区间存在,则取中间位置 mid = (low + high)/ 2;比较k与r[mid],有以下3种情况: 3.1 若 k < r[mid],则 high = mid - 1;查找在左半区进行,转第二步 3.2 若 k > r[mid],则 low = mid + 1;查找在右半区进行,转第二步 3.3 若 k = r[mid],则查找成功,返回记录在表中位置mid
- 中间值取整:C/C++中失去精度时不四舍五入,而是采取 去尾 的方式,即只取整数部分(不能叫向下取整,因为存在负数的情况)
例:float 9.59 => (int) 9
- 中间值取整:C/C++中失去精度时不四舍五入,而是采取 去尾 的方式,即只取整数部分(不能叫向下取整,因为存在负数的情况)
2、非递归算法
- 设有序表的长度为n,待查值为k
int BinSearchNonRecursion(int r[], int n, int k) { low = 1; //从数组下标1开始存放待查集合 high = n; while (low < high) //当区间存在时 { mid = (low + high) / 2; if (k < r[mid]) high = mid - 1; else if (k > r[mid]) low = mid + 1; else return mid; //查找成功,返回匹配记录数组下标 } return 0; //查找失败,返回0 }
- 传入参数:1⃣ 待查集合数组 2⃣ 数组元素个数 3⃣待查值k
3、递归算法
- 递归算法可直接依照折半查找的定义给出
int BinSearchRecursion(int r[], int low, int high, k) { if (low > high) return 0; else { mid = (low + high) / 2; if (k < r[mid]) return BinSearchRecursion(r, low, mid - 1, k); else if (k > r[mid]) rerurn BinSearchRecursion(r, mid + 1, high, k); else return mid; } }
- 传入参数:1⃣ 待查集合数组 2⃣ 查找区间低端 3⃣ 查找区间高端 4⃣ 待查值k
4、性能分析
-
折半查找判定树(判定树):描述折半查找的二叉树
-
定义:树中的每个结点对应有序表中的一个记录,结点的值为该记录在表中的位置
- 根结点的左子树与有序表中
r[1]~r[mid-1]
对应 - 根结点的右子树与有序表中
r[mid+1]~r[n]
对应
- 根结点的左子树与有序表中
-
引出:从折半查找的过程看,以有序表的中间记录作为比较对象,并以中间记录将表分割为两个子表,对子表继续这种操作
-
性质:
序号 性质 1 任意两颗折半查找判定树,若他们的结点个数相同,则他们的 结构完全相同(长度为n的判定树是唯一的) 2 具有n个结点的折半查找判定树的深度为 ⌊log以2为底的n⌋ + 1
3 任意两个叶子所处的层数最多差1
-
-
平均时间复杂度:O(log以2为底的n)
- 查找成功/失败:所需关键码比较次数至多为树的深度
⌊log以2为底的n⌋ + 1
- 计算:以满二叉树为例,树的第i层上有
2的(i-1)次方
个结点ASL = (1 * 2的零次方 + 2 * 2的1次方 + ... + k * 2的k-1次方) / n ≈ log以2为底的(n-1) - 1
- 查找成功/失败:所需关键码比较次数至多为树的深度
7.3 树表的查找技术(动态查找)
7.3.1 二叉排序树
-
优点:查找和插入都高效
-
定义:二叉排序树(binary sort tree)又称二叉查找树
- 或者是一棵空的二叉树
- 或者是一棵具有下列性质的树
序号 性质 1 若它的左子树不空,则 左子树上所有 结点的值均 小于 根结点的值 2 若它的右子树不空,则 右子树上所有 结点的值均 大于 根结点的值 3 它的左右子树也都是二叉排序树
-
特点:
- (1)中序遍历 二叉排序树可获得一个按照关键码有序的序列(是记录之间满足一定次序关系的二叉树)
- (2)同一个查找集合,可以有不同的二叉排序树的形式(二叉排序树不唯一) -> 可能有不同的树的深度 -> 可能有不同的查找效率
- 关键因素:构造时的记录的插入顺序
-
结点结构:二叉链表的结点结构(孩子兄弟表示法)
- 第一个指针域指向该结点的第一个孩子结点
- 第二个指针域指向该结点的右兄弟结点
1、二叉排序树的插入(递归)
-
特点:始终都是作为 叶子结点 插入
-
伪代码:
1、若root是空树,则将结点s作为根结点插入 2、否则,若 s->data 小于 root->data ,则把结点s插入到root的左子树中 3、否则把结点插入到root的右子树中
-
代码描述:
void BinSortTree::InsertBST(BiNode<int> * root, BiNode<int> * s) //root为二叉排序树(即二叉链表)的根指针,s为指向待插入的结点 { if (root == null) root = s; //root为全局变量,一开始声明为 BiNode<int> * root = null; else if (s -> data < root -> data) { InsertBST(root -> lchild, s); } else InsertBST(root -> rchild, s); }
- 找到插入位置后,向二叉排序树中插入结点的操作只是修改指针
- 寻找插入位置的比较次数不超过树的深度(效率较高)
2、二叉排序树的构造(非递归)
-
伪代码:设查找集合中的记录存放在数组
r[n]
中1、依次取每一个记录 r[i],执行下述操作: 1.1 申请一个数据域为 r[i] 的结点 s ,令结点 s 的左右指针域为空; 1.2 调用算法InsertBST,将结点 s 插入到二叉排序树中
-
代码描述:
BiSortTree::BiSortTree(int r[], int n) { for (i = 0; i < n; i++) { s = new BiNode; s -> data = r[i]; s -> lchild = s -> rchild = null; InsertBST(root, s); //root指向第一个插入的结点,为全局变量 //因此构造时每次插入都要先与同样的最初的那个根结点做比较进行二分 } }
3、二叉排序树的删除
-
算法要求:当删除的是分支结点时,需要重新修改指针,使得删除结点后仍为一棵二叉排序树
-
删除二叉排序树中最小的结点:
- 寻找:沿左子树下移,直到 最左下结点(不是 最左下叶结点)
- 最左下叶结点可能是最左下结点的右孩子
- 删除:将最小结点 s 的父节点中原来指向 s 的右孩子(最左下叶结点),若没有则指向null
- s 作为最左下结点一定没有左孩子,否则它就不是值最小的结点
- 寻找:沿左子树下移,直到 最左下结点(不是 最左下叶结点)
-
删除的三种情况讨论:设待删除结点为 p ,其双亲结点为 f ,且 p 是 f 的左孩子
序号 情景 删除操作 1 p 结点为叶子,p 既没有左子树也没有右子树 f -> lchild = null
2 p 只有左子树 pl ,或只有右子树 pr f -> lchild = p -> lchild
或f -> lchild = p -> rchild
3 p 既有左子树 pl ,又有右子树 pr 方法一/方法二 -
方法一(直接删除重排):让双亲结点 f 的左指针指向 p 的任意一个子树,然后将另一个子树中的结点重新插入
- 缺点:代价高昂,将使二叉排序树的结构发生变化并可能增加其高度
- 优点:思路简单
-
方法二(替代法):从待删除结点的某个子树中找出一个结点 s ,其值能够代替 p 的值,然后用结点 s 的值去替换结点 p 的值,再删除结点 s(较优的选择)
- 寻找替代结点 p 的值的结点 s :s的值应该是大于结点 p 的最小值(或小于结点 p 的最大值 -> 最右下结点)
情况 情景描述 一般情况 s 为 p 的右子树的最左下结点 特殊情况 p 的右孩子(直接右子树)即是右子树中最小值结点
- 寻找替代结点 p 的值的结点 s :s的值应该是大于结点 p 的最小值(或小于结点 p 的最大值 -> 最右下结点)
-
-
算法描述:
-
伪代码:
1、若结点p是叶子,则直接删除结点p; 2、若结点p只有左子树,只需重接p的左子树; 若结点p只有右子树,只需重接p的右子树; 3、若结点p的左右子树均不空,则 3.1 查找结点p的右子树上的最左下结点s以及结点s的双亲结点par; 3.2 将结点s的数据域替换到被删除结点p的数据域; 3.3 若结点p的右孩子无左子树,则将s的右子树接到par的右子树上(特殊情况) 否则,将s的右子树接到结点par的左子树上; 3.4 删除结点s;
-
代码描述:既有左子树又有右子树时采用方法二替代法,寻找大于结点p的最小值
void BiSortTree::DeleteBST(BiNode<int> * p, BiNode<int> * f) { if ((p -> lchild) == null && (p -> rchild == null)) { //p为叶子结点 f -> lchild = null; delete p; } else if (p -> rchild == null) { //p只有左孩子 f -> lchild = p -> lchild; delete p; } else if (p -> lchild == null) { //p只有右孩子 f -> lchild = p -> rchild; delete p; } else { par = p; //par指向被删结点s的父节点 s = p -> rchild; //初始s为p的右孩子 while (s -> lchild != null) { //查找s的最左下结点 par = s; //par始终指向s的父节点 s = s -> lchild; } p -> data = s -> data; //将s的值赋给p(覆盖掉p的原值) if (par == p) { par -> rchild = s -> rchild; //处理特殊情况 } else { par -> lchild = s -> rchild; //处理一般情况 } delete s; } }
-
4、二叉排序树的查找及性能分析
-
查找算法(递归):查找给定值 k
- 伪代码:
1、若root是空树,则查找失败 2、否则: 2.1 若 k = root -> data ,则查找成功; 2.2 若 k < root -> data ,则在root的左子树查找 2.3 若 k > root -> data ,则在root的右子树查找
- 代码描述:
BiNode<int> * BiSortTree::SearchBST(BiNode<int> * root, int k) { if (root == null) return null; else if (root -> data == k) return root; else if (root -> data < k) return SearchBST(root -> lchild, k); else return SearchBST(root -> rchild, k); }
- 伪代码:
-
优点:动态查找
- 查找失败时,恰好找到了以k为键值的新结点在二叉排序树中的插入位置
-
缺点:不唯一
-
与折半查找相比:给定一个查找集合
结构的唯一性 时间复杂度 备注 折半查找判定树是唯一的(虽然要先排序) O(log以2为底的n)
查找次数等于树的深度 二叉排序判定树是不唯一的 O(log以2为底的n)
~O(n)
情景(1)、(2) -
含有n个结点的二叉排序树的形状 取决于各个记录被插入二叉排序树的先后顺序
序号 情景 二叉排序树的高度 查找效率 1 二叉排序树是平衡的(形态均匀) ⌊log以2为底的n⌋ + 1
O(log以2为底的n)
2 二叉树完全不平衡(斜树) n
O(n)
(退化为顺序查找)
-
-
解决方案:为了获得更好的查找性能,需构造平衡二叉树
-
7.3.2 平衡二叉树
7.4 散列表的查找技术
7.4.1 概述
-
查找:实际上就是要确定 关键码等于给定值的记录 在 查找结构中的存储位置
-
引出:寻找一种 不经过任何比较,直接便能得到待查记录的存储位置
-
两类查找方式:
-
(1)基于给定值与关键码比较的查找:
-
之前的查找技术,由于 记录的存储位置和关键码之间不存在确定的对应关系 ,查找只能通过一系列的给定值与关键码的比较
-
查找的效率依赖于查找过程中进行的给定值与关键码的比较次数,与以下三点有关
序号 影响因素 1⃣ 查找集合的存储结构 2⃣ 查找集合的大小 3⃣ 待查记录在集合中的位置
-
-
(2)散列技术:
-
前提:在记录的存储位置和他的关键码之间建立一个确定的对应关系 H ,使得 每个关键码 key 和唯一的存储位置 H(key) 相对应
概念 含义 散列表(hash table) 采用散列技术将记录存储在一块 连续 的存储空间中,这块连续的存储空间(即数组)称为散列表 散列函数(hash function) 将关键码映射为散列表中适当存储位置的函数(建立了从记录的关键码集合到散列表的地址集合的一个映射) 散列地址(hash address) 关键码由散列函数计算所得的存储位置 -
映射:在查找时,根据这个确定的对应关系找到给定值 k 的映射 H(k) ,若查找集合中存在这个记录,则必定在 H(k) 的位置上
范围 内容 定义域 查找集合中全部记录的关键码 值域 如果散列表有 m 个地址单元,则散列函数的值域必在 0~m-1
之间 -
优点:记录的定位主要基于散列函数的计算 ,不需要进行关键码的多次比较,一般较基于比较的查找技术速度更快
-
缺点:1⃣ 不适于多个记录有同样关键码的情况 2⃣ 不适于范围查找
-
-
-
“散列既是一种存储方法,也是一种查找方法”:
-
散列过程:
方法 操作 存储记录时 通过散列函数计算记录的散列地址,并按此散列地址存储该记录 查找记录时 通过同样的散列函数计算记录的散列地址,按此散列地址访问该记录 -
“散列不是一种完整的存储结构”:它只是通过记录的关键码定位该记录,很难完整的表达记录之间的逻辑关系
- 散列主要是 面向查找的存储结构
-
-
“散列冲突”与“同义词”:
- 引出:对于两个不同的关键码
k1 ≠ k2
,有H(k1) = H(k2)
概念 含义 备注 冲突(collision) 即两个不同的记录需要存放在同一个存储位置中的现象 如果记录按散列函数计算出的地址加入散列表时产生了冲突,就必须另外再找一个地方存储(–> 如何处理冲突) 同义词(Synonym) k1 和 k2 相对于 H 称为同义词
- 引出:对于两个不同的关键码
-
散列技术需要考虑的两个主要问题:
- (1)散列函数的设计
- (2)冲突的处理
7.4.2 散列函数的设计
-
设计散列函数应遵循的基本规则:
序号 规则 含义 1 计算简单 散列函数不宜有很大的计算量,否则会降低查找效率 2 函数值(即散列地址)分布均匀 函数值尽量均匀的散布在地址空间,保证存储空间的有效利用,并减少冲突 - 一般来说,散列函数依赖于 关键码的分布情况
1、直接定址法
-
方法:散列函数是关键码的线性函数
H(key) = a * key + b (a、b为常数)
-
优点:单调、均匀、不会产生冲突
-
缺点:可能会造成地址空间利用效率不高(需预留空间)
-
适用情况:事先 知道 关键码的分布 ,且 关键码集合不是很大而连续性较好 的情况
2、除留余数法
-
方法:选择某个适当的正整数 p ,以关键码除以 p 的余数作为散列地址:
H(key) = key mod p
-
优点:最简单、最常用,事先不要求知道关键码的分布
-
缺点:(大概率)可能会造成散列冲突
- 关键:需选取合适的 p ,若 p 选的不好,容易产生同义词
- eg:若 p 含有质因子
p = m * n
,则含有 m 或 n 因子的关键码的散列地址均为 m 或 n 的倍数
-
适用情况:事先 不知道 关键码的分布
- 被除数 p 的选取:若散列表表长为 m ,通常选 p 为 小于或等于表长(最好接近m)的最小素数 或 不包含小于20质因子的合数
3、数字分析法
-
方法:根据关键码在各个位上的分布情况,选取分布比较均匀的若干位(即不相同的若干位)组成散列地址
-
适用情况:事先 知道 关键码的分布 且 关键码中有若干位分布较均匀的情况
4、平方取中法
-
方法:对关键码平方后,按散列表大小,取中间的若干位作为散列地址(平方后截取)
- 原理:一个数平方后,中间的几位分布较均匀(也就是不同的关键码),较少有相同的平方后截取(即发生冲突)
-
缺点:可能会造成散列冲突
-
适用情况:事先 不知道 关键码的分布 且 关键码的位数不是很大 的情况
5、折叠法
-
方法:将关键码从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分 叠加求和,并按散列表长,取后几位作为散列地址
- (1)移位叠加:将各部分的最后一位对齐相加
- (2)间界叠加:从一端向另一端沿各部分分界来回折叠后,最后一位对齐相加
eg: key = 253 463 587 05 //移位叠加 //间界叠加 253 253 463 364 587 587 + 05 + 50 _______ —————— 1308 1254 H(key) = 308 H(key) = 254
-
适用情况:事先 不知道 关键码的分布 , 关键码的位数很多且每一位分布都不均匀的情况
7.4.2 处理冲突的方法
1、开放定址法(闭散列表)
-
基本思想:由关键码得到的散列地址一旦产生了冲突,就去寻找 下一个空的散列地址(只要散列表足够大,总能找到空的散列地址)
-
闭散列表:用开放定制法(open addressing)处理冲突得到的散列表
散列地址 ··· 关键码 ··· 比较次数 ··· - ASL平均查找长度 = ∑ ( 比较i次插入的元素个数 * i ) / 插入集合元素总个数
-
-
引出新的问题:因冲突而采用开放定址法求得的下一个空的散列地址,占据了本身应当在该位置的元素的存储空间
-
堆积:在处理冲突过程过程中出现的非同义词之间对同一个散列地址争夺的现象(于是不得不再次处理冲突,大大降低了查找效率)
-
删除时的两点要求:需对删除位置设置标记
序号 要求 标记的含义 是否继续查找 1 删除一个记录一定不能影响以后的查找 标记标志一个记录曾经占用这个单元,但现在已经不再占用了 如果沿着一个搜索序列查找时遇到一个标记,则应该 继续查找 下去 2 删除记录后的存储单元应该能够为将来的插入使用 当在插入时遇到一个标记,那个单元在继续向后探测后确保表中不存在待插入记录,才可以用于存储新记录 为了避免插入同样的关键码,查找过程仍然要沿着探测序列 继续查找 下去
-
-
三种插入/查找方法
-
(1)线性探测法:
-
方法:当法生冲突时,线性探测法从冲突的下一个位置(即数组的下一个地址)起,依次寻找空的散列地址
对于键值 key ,设 H(key) = d ,比散列表的长度为 m ,则发生冲突时,寻找下一个散列地址的公式为:(除留余数法散列函数设计) Hi = (H(key) + di) % m (di = 1, 2, ···, m-1)
-
算法描述:
//伪代码 1、计算散列地址j 2、若 ht[j] = k ,则查找成功,返回记录在散列表中的下标; 否则 3、若 ht[j] = empty 或 整个散列表探测一遍 ,则查找失败,转4; 否则,j 指向下一单元,转2; 4、若整个散列表探测一遍,则表满,抛出溢出异常; 否则,将待查值插入;
//代码描述 int HashSearchCloseLinear (int ht[], int m, int k) { j = H(k); //计算散列地址 if (ht[j] == k) //没有发生冲突,比较一次即查找成功 return j; else if (ht[j] == empty) //散列地址为空,表示该元素尚未插入 { ht[j] = k; //插入元素 return 0; //查找失败,但已插入 } i = (j + 1) % m; //设置探测的起始下标(+1表示是线性探测) while (h[t] != Empty && i != j) //开始循环探测 { if (h[t] == k) //找到已插入的元素,表示插入时发生了冲突,比较若干次后才能查找成功 return i; else i = (i + 1) % m; //向后探测一个位置,i = j时表示遍历了一边散列表 时表示散列表探测了一遍 } if (i == j) //探测遍历了一遍散列表,仍未找到该元素,且散列表也没有空位置可以插入 { throw "溢出"; } else //循环探测时发现了空的位置,表示查找元素尚未插入,现在插入 { ht[i] = k; return 0; } }
-
-
(2)二次探测法(平方)
- 方法:当发生冲突时,二次探测法寻找下一个散列地址的增量为
正负X的平方
,公式为:Hi = (H(key) + di) % m (di = 1的平方,- 1的平方, 2的平方, - 2的平方, ···,q的平方, - q的平方 且 q =< 根号m)
- 方法:当发生冲突时,二次探测法寻找下一个散列地址的增量为
-
(3)随机探测法
- 方法:当发生冲突时,随机探测法探测下一个散列地址的位移量是一个
随机数列
,公式为Hi = (H(key) + di) % m (di是一个随机数列,i = 1,2···,m-1)
- 方法:当发生冲突时,随机探测法探测下一个散列地址的位移量是一个
-
2、拉链法/链地址法(开散列表)
-
基本思想:将所有散列地址相同的记录,即所有关键码为同义词的记录存储在一个单链表中,称为 同义词子表
-
在散列表中存储的是所有 同义词子表的头指针
-
设 n 个记录存储在长度为 m 的开散列表中,则同义词子表的平均长度为
n/m
-
开散列表:用拉链法(chaining)处理冲突构造的散列表叫做开散列表
-
-
开散列表的查找算法描述
//伪代码 1、计算散列地址 j; 2、在第 j 个同义词子表中顺序查找; 3、若查找成功,则返回结点的地址; 否则,将待查记录插在第 j 个同义词子表的表头
//代码描述 Node<int> * HashSearchOpen (Node<int> * ht[], int m, int k) //设开散列表ht[m],散列表表长为m,关键码为k { j = H(k); //计算散列地址 p = ht[j]; //工作指针p初始化为指向第j个同义词子表 while ((p != null) && (p -> data != k)) //遍历同义词子表 p = p -> next; if (p -> data == k) return p; //查找成功 else { //查找失败,插入 q = new Node; q -> data = k; q -> next = ht[j]; //头插法插在第j个同义词子表的表头 ht[j] = q; } }
7.4.4 散列查找的性能分析
-
衡量查找效率的标准:平均查找长度
- 引出:在查找过程中,关键码的比较次数取决于产生冲突的概率
-
影响冲突产生的概率的三个因素:
序号 因素 含义 备注 1 散列函数是否均匀 直接影响冲突产生的概率 一般情况下,总认为所选的散列函数是均匀的,可以不考虑散列函数对平均查找长度的影响 2 处理冲突的方法 开放定址法可能会产生堆积,而拉链法则不会 拉链法更优 3 散列表的装填因子(load factor) 标志着散列表装满的程度,其计算公式为: 散列表的装填因子 α = 填入表中的记录个数 / 散列表的长度
由于表长是定值,α 与填入表中的记录个数成正比,所以,填入表中的记录越多,α 越大,产生冲突的可能性就越大 - 散列表的平均查找长度是装填因子 α 的函数,而不是查找集合中记录 n 的函数
7.4.5 开散列表与闭散列表的比较
- 散列技术的原始动机:无需经过关键码与待查值的比较而完成查找
类型 | 特点 | 优点 | 缺点 | 表容量 | 总结 |
---|---|---|---|---|---|
开散列表 | 用链接方法存储同义词 | 1⃣ 不产生堆积现象 2⃣ 动态查找、插入、删除等基本操作实现,且平均查找长度较短 | 附加指针域增加了存储开销 | 无需事先确定表的容量(开散列表中个同义词子表的表长是动态变化的,更适合于事先难以估计容量的场景) | 牺牲空间换时间 |
闭散列表 | 向后查找同义词 | 无附加指针,存储效率较高 | 1⃣ 堆积现象使查找效率降低 2⃣ 删除操作较复杂,运行一段时间后,需要经过整体整理,才能真正删除有标记的单元 | 必须事先估计容量 | 牺牲时间换空间 |
标签:结点,关键码,记录,C++,列表,查找,数据结构,散列 来源: https://blog.csdn.net/chileme/article/details/96101461