目录
一、基本概念
二、静态查找表
(1)顺序表上的查找 —— 顺序查找
① 过程
② 算法
③ 算法分析
(2)有序表上的查找 —— 二分查找
① 二分查找思想
② 二分查找过程
③ 二分查找算法
④ 示例
⑤ 算法分析
(3)索引顺序表的查找 —— 分块查找
① 查找过程
② 示例
③ 算法分析
三、动态查找表(二叉排序树)
(1)二叉排序树
① 含义
② 性质
(2)二叉排序树上的查找
① 过程
② 二叉排序树查找算法
(3)二叉排序树的插入和生成
① 构造二叉排序树的原则
② 二叉排序树建树过程
③ 二叉排序树的查找分析
④ 示例
四、散列表(哈希表)
(1)基本概念
(2)常见散列法
① 数字分析法
② 除留余数法
③ 平方取中法
④ 基数转换法
(3)散列表解决冲突的方法
① 链地址法
② 线性探索法
③ 二次探索法
④ 多重散列法
⑤ 公共溢出区法
一、基本概念
查找表 :由同一类型的数据元素(或记录) 构成的集合
关键字(键) :用来标识数据元素的数据项称为 关键字 ,简称 键 ;其值称为 键值 。
主关键字:可唯一标识各个数据元素的关键字
查找 :根据给定的某个 k 值,在查找表寻找一个其键值等于 k 的数据元素。
- 静态查找表:进行的是引用型运算
- 动态查找表:进行的是加工型运算
注:施加在其上的操作不同!
二、静态查找表
【示例代码】查找表用顺序表表示:
const int maxsize = 20; // 静态查找表的表长 typedef struct { TableElem elem[maxsize+1]; // 一维数组,0号单元留空 int n; // 最后一个元素的下标,也即表长 } SqTable; typedef struct { keytype key; // 关键字域 ... // 其它域 } TableElem;
【代码详解】
- 这段代码是定义了一个静态查找表的数据结构。
- 这段代码定义了一个静态查找表数据结构
SqTable
,以及其中的元素类型TableElem
。- 通过
SqTable
结构体,我们可以创建静态查找表的实例,并对其中的元素进行操作。
const int maxsize = 20;
:这里定义了一个常量maxsize
,表示静态查找表的最大长度。它的值是20,是一个不可修改的常量。
typedef struct { ... } SqTable;
:使用typedef
关键字定义了一个名为SqTable
的结构体类型。这个结构体包含两个成员变量。
TableElem elem[maxsize+1];
:这是SqTable
结构体中的一个成员变量elem
,是一个一维数组。数组的大小为maxsize+1
,其中加1是为了空出0号单元。数组中的元素类型是TableElem
。
int n;
:这是SqTable
结构体中的另一个成员变量n
,表示最后一个元素的下标,即表的长度。
typedef struct { ... } TableElem;
:这里定义了另一个结构体类型TableElem
。它包含一个成员变量key
,表示关键字域。另外还可以有其他的域,这里没有具体列出。
(1)顺序表上的查找 —— 顺序查找
① 过程
- 从表中最后一个记录开始顺序进行查找,若当前记录的关键字=给定值,则查找成功;
- 否则,继续查上一记录…;
- 若直至第一个记录尚未找到需要的记录,则查找失败。
② 算法
【示例代码】使用一种设计技巧:设立岗哨:
int SearchSqtable(SqTable T, KeyType key) { /* 在顺序表 R 中顺序查找其关键字等于 key 的元素。 若找到,则函数值为该元素在表中的位置,否则为 0 */ T.elem[0].key = key; // 在表的首个位置设置待查找的关键字 int i = T.n; // 从最后一个元素开始向前查找 while (T.elem[i].key != key) // 循环直到找到目标元素或查找到表的开头 { i--; // 向前移动到下一个元素 } return i; // 返回找到的元素位置,若未找到则返回 0 }
【代码详解】
- 这段代码实现了在顺序表中顺序查找关键字等于
key
的元素,并返回其在表中的位置。- 这段代码实现了简单的顺序查找算法,时间复杂度为 O(n),其中
n
是顺序表的长度。- 通过设置
T.elem[0].key
为待查找的关键字,然后从最后一个元素开始向前查找,直到找到目标元素或查找到表的开头。- 代码在找到目标元素后,返回该元素在表中的位置;若未找到,则返回 0 表示未找到。
将待查找的关键字
key
设置在顺序表R
的首个位置,即T.elem[0].key = key;
。从最后一个元素开始向前查找,将当前查找位置指定为
T.n
,即int i = T.n;
。这里假设顺序表的最后一个元素的下标为T.n
。进入循环,判断当前查找位置的元素是否等于目标关键字
key
,即T.elem[i].key != key
。如果不相等,则向前移动到下一个元素,即
i--;
。重复步骤 3 和 4,直到找到目标元素或者查找到表的开头。
循环结束后,返回当前查找位置
i
,即找到的元素在表中的位置。如果未找到,则返回 0 表示未找到。
③ 算法分析
【成功查找】
【不成功查找】
【顺序查找优缺点】
- 优点:简单,对表无要求
- 缺点:比较次数多
(2)有序表上的查找 —— 二分查找
① 二分查找思想
二分查找基本思想: 每次将处于查找区间中间位置上的数据元素与给定值 K 比较,若不等则缩小查找区间并在新的区间内重复上述过程,直到查找成功或查找区间长度为 0(查找不成功)为止。
- 顺序方式存储,且元素按关键字有序
② 二分查找过程
- 表头指针 low = 1
- 表尾指针 high = n
1. 求中间点
- mid = (low + high) / 2
- { item[1],… , item[mid-1], item[mid], item[mid+1],… , item[n] }
2. 给定关键字 k 与中项记录关键字比较
- K<item [mid].key,则所查记录落在表的前半部;继续在前半部找,此时 low 不变,high=mid-1
- K=item [mid].key,则查找成功,中项即是,结束
- K>item [mid].key,则所查记录落在表的后半部;继续在后半部找,此时 low = mid+1,high 不变
3. 若 low ≤ high ,则转第 1 步 ,否则查找不成功
③ 二分查找算法
【示例代码】
int SearchBin(SqTable T, KeyType key) { /* 在有序表 R 中二分查找其关键字等于 K 的数据元素; 若找到,则返回该元素在表中的位置,否则返回 0 */ int low, high; low = 1; // 设置起始查找位置为表的第一个元素 high = T.n; // 设置结束查找位置为表的最后一个元素 while (low <= high) // 当起始位置小于等于结束位置时循环 { int mid = (low + high) / 2; // 计算起始位置和结束位置的中间位置 if (key == T.elem[mid].key) // 如果目标关键字等于中间位置的关键字 { return mid; // 返回中间位置作为结果,表示找到了目标元素 } else if (key < T.elem[mid].key) // 如果目标关键字小于中间位置的关键字 { high = mid - 1; // 更新结束位置为中间位置的前一个位置 } else // 如果目标关键字大于中间位置的关键字 { low = mid + 1; // 更新起始位置为中间位置的后一个位置 } } return 0; // 未找到目标元素,返回 0 }
【代码详解】
- 这段代码实现了在有序表
R
中进行二分查找,以查找关键字等于K
的数据元素。- 这段代码实现了二分查找算法,在有序表中进行查找,时间复杂度为 O(log n),其中 n 是有序表的长度。
定义起始位置
low
和结束位置high
,初始时分别设置low = 1
和high = T.n
,即表的第一个元素和最后一个元素。进入循环,当起始位置
low
小于等于结束位置high
时循环执行。在循环中,计算起始位置和结束位置的中间位置,即
int mid = (low + high) / 2;
。判断目标关键字
key
是否等于中间位置的关键字T.elem[mid].key
。如果相等,表示找到了目标元素,直接返回中间位置
mid
。如果目标关键字小于中间位置的关键字,更新结束位置为中间位置的前一个位置,即
high = mid - 1;
。如果目标关键字大于中间位置的关键字,更新起始位置为中间位置的后一个位置,即
low = mid + 1;
。重复步骤 3 到步骤 7,直到找到目标元素或起始位置大于结束位置。
循环结束后,如果未找到目标元素,返回 0 表示未找到。
④ 示例
⑤ 算法分析
- 查找成功时:比较次数最多为 log₂n + 1
- 查找不成功时:比较次数最多也为 log₂n + 1
(3)索引顺序表的查找 —— 分块查找
① 查找过程
1. 先建立最大(或小)关键字表 —— 索引表(有序)
- 即将每块中最大(或最小)关键字及指示块首记录在表中位置的指针依次存入一张表中,此表称为索引表;
2. 查找索引表,以确定所查元素所在块号
- 将查找关键字k与索引表中每一元素(即各块中最大关键字)进行比较,以确定所查元素所在块号;
3. 在相应块中按顺序查找关键字为 k 的记录
② 示例
③ 算法分析
静态查找表的上述三种不同实现各有优缺点,其中: (在实际应用中应根据需要加以选择)
- 顺序查找效率最低但限制最少
- 二分查找效率最高,但限制最强
- 而分块查找则介于上述二者之间
三、动态查找表(二叉排序树)
(1)二叉排序树
表结构是在查找过程中动态生成的;对于给定值 k ,若表中存在其关键 字等于 k 的记录,则查找成功返回,否则在表中插入关键字等于 k 的记录。
① 含义
一棵二叉排序树 (Binary Sort Tree) (又称二叉查找树) 或者是一棵空二叉树,或者是具有下列性质的二叉树:
- 若它的左子树不空,则左子树上所有结点的键值均小于它的根结点键值;
- 若它的右子树不空,则右子树上所有结点的键值均大于它的根结点键值;
- 根的左、右子树也分别为二叉排序树。
② 性质
中序遍历一棵二叉排序树所得的结点访问序列是键值的递增序列。
(2)二叉排序树上的查找
① 过程
当二叉排序树不空时,首先将给定值和根结点的关键字比较,若相等,则查找成功;否则根据给定值与根结点关键字间的大小关系,分别在左子树或右子树上继续进行查找。
② 二叉排序树查找算法
【注意】
- 二叉排序树,对每个结点,均有:左子树上的所有结点键值都比根的小;右子树上的所有结点键值都比根的大。
- 构造二叉排序树的同时也对序列排序了。
【示例代码】
BinTree SearchBST(BinTree bst, KeyType key) { /* 在根指针 bst 所指的二叉排序树上递归地查找键值等于 key 的结点。 * 若成功,则返回指向该结点的指针,否则返回空指针 */ if (bst == NULL) return NULL; // 不成功时返回 NULL 作为标记 else if (key == bst->key) return bst; // 成功时返回结点地址 else if (key < bst->key) return SearchBST(bst->lchild, key); // 继续在左子树中查找 else return SearchBST(bst->rchild, key); // 继续在右子树中查找 }
【代码详解】
- 这段代码实现了在根指针
bst
所指的二叉排序树上递归地查找键值等于key
的结点。- 这段代码实现了在二叉排序树中进行递归查找算法,时间复杂度平均为 O(log n),其中 n 是二叉排序树的结点个数。
判断根指针
bst
是否为空,即if (bst == NULL)
。如果根指针为空,表示没找到目标结点,返回空指针
NULL
作为标记。如果目标关键字
key
等于当前结点的关键字bst->key
,表示找到了目标结点,直接返回该结点的指针bst
。如果目标关键字
key
小于当前结点的关键字bst->key
,则在左子树中继续递归查找,即return SearchBST(bst->lchild, key);
。如果目标关键字
key
大于当前结点的关键字bst->key
,则在右子树中继续递归查找,即return SearchBST(bst->rchild, key);
。重复步骤 3 到步骤 5,直到找到目标结点或递归到叶子结点为止。
【说明】由上面的查找过程可知:
- 在二叉排序树上进行查找,若查找成功,则是从根结点出发走了一条从根结点到待查结点的路径
- 若查找不成功,则是从根结点出发走了一条从根到某个叶子的路径
- 因此与二分查找类似,关键字比较的次数不超过二叉树的深度
(3)二叉排序树的插入和生成
① 构造二叉排序树的原则
对序列 R = {k1,k2,…,kn }, k1~ kn 均为关键字值,则按下列原则可构造二叉排序树:
- 令 k1 为根
- 若 k1<k2 ,则令 k2 为 k1 的右孩,否则为左孩
- k3,k4,…,kn 递归重复第 2 步
② 二叉排序树建树过程
③ 二叉排序树的查找分析
二叉排序树上的平均查找长度是介于 O(n) 和 O(log₂n) 之间的,其查找效率与树的形态有关。
④ 示例
四、散列表(哈希表)
(1)基本概念
散列函数 ( 哈希函数 ) : 设记录表 A,长为 n,ai(1≤i≤n)为表中某一元素,ki 为其关键字,则关键字 ki 和元素 ai 在表中的地址之间有一函数关系,即:
为了使数据元素的存储位置和键值之间建立某种联系,以减少比较次数,可以用散列技术实现动态查找表。
散列地址 : 由散列函数决定数据元素的存储位置,该位置称为散列地址。
散列查找 :
散列表 :通过散列法建立的表称为散列表。
冲突:
(2)常见散列法
① 数字分析法
【说明】
- 数字分析法是散列函数设计中的一种常见方法。该方法是根据待散列数据中的数字特征来计算散列值。
- 在数字分析法中,通常假设待散列的数据是由数字组成的。该方法利用待散列数据中的数字分布和频率等特征,将其转化为散列值。
- 数字分析法的优点是简单易实现,特别适用于待散列数据中数字分布较为均匀且具有一定规律的情况。
- 然而,在数字分布不均匀或存在数字特殊规律的情况下,数字分析法可能导致碰撞(冲突)较多,使得散列效果下降。
- 因此,在实际应用中,需要根据具体情况选择合适的散列方法,并经过实验和性能评估来确定最佳的散列函数。
【步骤】
分析待散列数据的数字特征:观察待散列数据中的数字分布情况,如数字是否均匀分布、数字的位置是否具有一定规律等。
根据数字特征设计散列函数:根据数字分布情况,设计一个合适的散列函数,将待散列数据映射为散列值。
计算散列值:使用设计的散列函数对待散列数据进行计算,得到对应的散列值。
返回散列值:将计算得到的散列值作为数据在散列表中的存储位置,或用于其他需要散列值的操作。
② 除留余数法
【说明】
- 除留余数法是散列函数设计中的一种常见方法,也是一种基本的散列技术。它使用待散列数据除以一个特定的数(通常是散列表的大小),然后取余数作为散列值。
- 除留余数法的优点是简单易实现,计算速度快。然而,除留余数法的散列结果容易受到待散列数据的分布特点影响。如果待散列数据不均匀地分布在散列表中,可能导致散列冲突(碰撞)较多,影响散列效果。
- 为了避免散列冲突,可以采用一些技术手段,比如开放寻址法和链表法等。此外,为了提高散列函数的效果,还可以结合其他散列方法,如乘法散列法和平方取中法等。
- 在实际应用中,需要根据具体情况选择合适的散列方法,并经过实验和性能评估来确定最佳的散列函数。
【步骤】
确定散列表的大小:选择一个合适的数作为散列表的大小,通常为一个质数或接近质数的数。
计算散列值:对待散列的数据进行除法操作,将其除以散列表的大小,并取余数作为散列值。具体计算方式为
hash_value = data % table_size
。返回散列值:将计算得到的散列值作为数据在散列表中的存储位置,或用于其他需要散列值的操作。
【散列函数】 散列函数:取关键字被某个不大于散列表长 m 的数 p 除后所得余数作为散列地址。
- 即: H(key) = key mod p(p≤n)
③ 平方取中法
【说明】
- 平方取中法是一种常见的散列函数设计方法,用于将待散列的数据映射到散列表中。该方法通过对待散列数据进行平方操作,然后取中间的几位作为散列值。
- 平方取中法的优点是简单易实现,并且能够较好地处理一些数字特征不均匀的情况。通过平方操作,可以更好地分散待散列数据的分布,减少散列冲突的可能性。
- 然而,平方取中法也存在一些缺点。例如,当散列表的大小较小时,提取中间几位数可能导致散列值的空间效用(也称为散列值的利用率)较低。同时,在某些特定情况下,平方取中法可能仍然无法有效地解决散列冲突。
- 因此,在实际应用中,需要根据具体情况选择合适的散列方法,并经过实验和性能评估来确定最佳的散列函数。
【步骤】
确定散列表的大小:选择一个合适的数作为散列表的大小,通常为一个质数或接近质数的数。
计算平方值:将待散列数据进行平方操作。
提取中间的几位数:从平方值的中间提取一定数量的位数作为散列值。
返回散列值:将计算得到的散列值作为数据在散列表中的存储位置,或用于其他需要散列值的操作。
【平方取中法】
- 平方取中法以键值平方的中间几位作为散列地址。
- 这一方法计算简单,是一种较常用的构造散列函数的方法,通常在选定散列函数时不一定能知道键值的分布情况。
- 取其中哪几位也不一定合适,而一个数平方的中间几位与这个数的每一位都有关,所得散列地址比较均匀。
④ 基数转换法
【说明】
- 基数转换法是散列函数设计中的一种常见方法,适用于将待散列的数据转换为不同进制表示的散列值。该方法将待散列数据视为一串数字,通过对数字进行进制转换来计算散列值。
- 基数转换法的优点是灵活性较高,在处理一些特殊类型的数据(如字符串)时表现良好。通过将待散列数据转换为不同进制的散列值,可以一定程度上减少散列冲突的可能性。
- 然而,基数转换法也存在一些缺点。例如,散列函数的设计较为复杂,计算开销较大。此外,基数转换法在处理大量数据时可能会导致计算溢出或性能问题。
- 因此,在实际应用中,需要根据具体情况选择合适的散列方法,并经过实验和性能评估来确定最佳的散列函数。
【步骤】
确定散列表的大小:选择一个合适的数作为散列表的大小,通常为一个质数或接近质数的数。
将待散列数据视为一串数字:将待散列的数据表示为一串数字,可以按照字符的ASCII码值进行转换。
进行进制转换:将待散列数据的数字串转换为指定进制的数。常用的进制包括 10 进制、16 进制或 36 进制等。
计算散列值:将转换后的数字按照计算机所用的进制进行计算,得到对应的散列值。
返回散列值:将计算得到的散列值作为数据在散列表中的存储位置,或用于其他需要散列值的操作。
【基数转换法】
(3)散列表解决冲突的方法
① 链地址法
散列表的实现 —— 即用 “链地址法” 处理冲突
- 思想:将散列地址相同记录存储在同一单链表中(称同义词表),同时按散列地址设立一个表头指针向量。
- 示例:已知一组关键字为(13,41,15,44,06,68,25,12,38,64,19,49),按散列函数H(key)=key mod 13 和链地址法处理冲突构造散列表。
链地址是对每一个同义词都建一个单链表来解决冲突,其组织方式如下:
- 设选定的散列函数为 H, H 的值域(即散列地址的范围)为 0〜(n-1)。
- 设置一个 “指针向量” Pointer HP [n],其中的每个指针 HP[i] 指向一个单链表,该单链表用于存储所有散列地址为 i 的数据元素。
- 每一个这样的单链表称为一个同义词子表。
② 线性探索法
散列表的实现 —— 即用 “线性探测法” 处理冲突构造散列表
- 思想:计算出的散列地址已被占用,则按顺序找 “下一个” 空位。
- 过程:设有散列表 HT(向量),对给定记录 R,其关键字 k,对应哈希地址 H(k) => j
- 要点:
- 示例:
散列法的优缺点:
- 优点:直接由关键字通过哈希函数计算出哈希地址,查找效率高
- 缺点:常发生冲突,影响查找效率
③ 二次探索法
二次探测法的基本思想:
- 生成的后继散列地址不是连续的,而是跳跃式的,以便为后续数据元素留下空间从而减少堆积。
- 按照二次探测法,键值 key 的散列地址序列为:
④ 多重散列法
- 此法要求设立多个散列函数 Hi,i=1,…,k。
- 当给定值 key 与散列表中的某个键值是相对于某个散列函数氏的同义词而发生冲突时,继续计算这个给定值 key 在下一个散列函数 Hi+1 下的散列地址,直到不再产生冲突为止。
- 这种方法的优点是不易产生 “堆积”,缺点是计算量较大。
⑤ 公共溢出区法
- 按这种方法,散列表由两个一维数组组成。
- 一个称为基本表,它实际上就是上面所说的散列表,另一个称为溢出表。
- 插入首先在基本表上进行,假如发生冲突,则将同义词存入溢出表。
- 这样,基本表不可能发生 “堆积”。