9.查找
9.1 查找的基本概念
查找表:是由同一类型的数据元素(或记录)构成的集合。
字(Key)是数据元素中某个数据项的值,又称为键值,用它可以标志一个数据元素。也可以标志一个记录的某个数据项(字段),我们称为关键码。
若此关键字可以唯一地标志一个记录,则称此关键字为主关键字(Primary Key)。
对于那些可以识别多个数据元素(或记录)的关键字,我们称为次关键字(Secondary Key)。次关键字也可以理解为不是唯—标志—个数据元素(或记录)的关键字,它对应的数据项就是次关键码。
查找就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。
对查找表经常进行的操作:
- 查询某个“特定的”数据元素是否在查找表中;
- 检索某个“特定的”数据元素的各种属性;
- 在查找表中插入一个数据元素;
- 删除查找表中的某个数据元素;
查找表分类:
静态查找表(Static Search Table):只作查找操作的查找表。
动态查找表(Dynamic Search Table):在查找过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已经存在的某个数据元素。
查找算法的评价指标:
9.2 线性表的查找
- 顺序查找(线性查找)
- 折半查找(二分查找)
- 分块查找
9.2.1 顺序查找
顺序查找(Sequential Search)又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若是某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果直到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。
顺序查找算法实现:
//顺序查找,a为数组,n为要查找的数组个数,key为要查找的关键字key
int Sequential_Search(int *a, int n, int key) {
int i;
for (i = 1; i <= n; i++) {
if (a[i] == key) {
return i;
}
}
return 0;
}
到这里并非足够完美,因为每次循环时都需要对i是否越界,即是否小于等于n作判断。事实上,还可以有更好的办法,设置一个哨兵,可以解决不需要每次让i与n作比较。看下面改进后的顺序查找算法代码:
//有哨兵顺序查找
int Sequential_Search2(int *a, int n, int key) {
int i;
a[0] = key;//设置a[0]为关键字值,我们称之为哨兵
i = n;//循环从数组尾部开始
while (a[i] != key) {
i--;
}
return i;//返回0则说明查找失败
}
此时代码时从尾部开始查找,由于a[0]=key,也就是说,如果在a[i]中有key则返回i值,查找成功。否则一定在最终的a[0]处等于key,此时返回的是0,即说明a[1]~a[n]中没有关键字Key,查找失败。
这种在查找方向的尽头放置哨兵免去了在查找过程中每一次比较后都要判断查找位置是否越界的小技巧,看似与原先差别不大,但在总数据较多时,效率提高很大,是非常好的编码技巧。当然,哨兵也不一定就必须在数组开始,也可以在末端。
9.2.2 折半查找
**折半查找(Binary Search)技术,又称为二分查找。**它的前提是线性表中的记录必须是关键码有序(通常从小到大有序),线性表必须采用顺序存储。折半查找的基本思想是:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右半区继续查找。不断重复上述过程,直到查找成功,或所有查找区域无记录,查找失败为止。
我们之前讲二叉树的性质,有过对“居于n个结点的完全二叉树的深度为log2 n +1."性质的推导过程。在这里尽管折半查找判定二叉树并不是完全二叉树,但同样由相同的推导可以得出,最坏情况是查找到关键字或查找失败的次数为log2 n + 1。因此最终我们折半算法的时间复杂度为O(logn),它显然远远好于顺序查找的O(n)时间复杂度。
9.2.3 分块查找
分块有序,是把数据集的记录分为了若干块,并且这些块需要满足以下两个条件:
块内无序,即每一块内的记录不要求有序。
块间有序,例如,要求第二块所有记录的关键字均要求第一块中所有记录的关键字,第三块所有记录的关键字均要大于第二块所有记录的关键字······
分块查找优缺点:
优点:插入和删除比较容易,无需进行大量移动
缺点:要增加一个索引表的存储空间并对初始索引表进行排序运算。
适用情况:如果线性表既要快速查找又经常动态变化,则可采用分块查找。
查找方法比较:
9.3 树表的查找
当表插入、删除操作频繁时,为维护表的有序性,需要移动表中很多记录。
9.3.1 二叉排序树
二叉排序树(Binary Sort Tree)又称为二叉搜索树、二叉查找树:定义:
二叉排序树或是空树,或是满足如下性质的二叉树:
- 若其左子树非空,则左子树上所有结点的值均小于根结点的值;
- 若其右子树非空,则右子树上所有结点的值均大于根结点的值;
- 其左、右子树本身又各是一棵二叉排序树。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {//二叉搜索树中的搜索
public:
TreeNode* searchBST(TreeNode* root, int val) {
while (root != NULL) {
if (root->val > val) root = root->left;
else if (root->val < val) root = root->right;
else return root;
}
return NULL;
}
};
含有n个结点的二叉排序树的平均查找长度和树的形态有关。
二叉搜索树的插入操作:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
if (root == NULL) {
TreeNode* node = new TreeNode(val);
return node;
}
if (root->val > val) root->left = insertIntoBST(root->left, val);
else if (root->val < val) root->right = insertIntoBST(root->right, val);
return root;
}
};
二叉排序树中的删除操作:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if (root == NULL) return NULL;
if (root->val == key) {
if (root->left == NULL && root->right == NULL) {
delete root;
return NULL;
}
else if (root->left == NULL) {
TreeNode* retNode = root->right;
delete root;
return retNode;
}
else if (root->right == NULL) {
TreeNode* retNode = root->left;
delete root;
return retNode;
}
else {
TreeNode* cur = root->right;
while (cur->left != NULL) {
cur = cur->left;
}
cur->left = root->left;
TreeNode* tmp = root;
root = root->right;
delete tmp;
return root;
}
}
if (root->val > key) root->left = deleteNode(root->left, key);
if (root->val < key) root->right = deleteNode(root->right, key);
return root;
}
};
9.3.2 平衡二叉树
平衡二叉树(balanced binary tree) 又称AVL树(Adelson-Velskii and Landis).
一棵平衡二叉树或者是空树,或者是具有下列性质的二叉排序树:
- 左子树与右子树的高度之差的绝对值小于等于1;
- 左子树和右子树也是平衡二叉排序树。
为了方便起见,给每个结点附加一个数字,给出该结点左子树与右子树的高度差。这个数字称为结点的平衡因子(BF)。
平衡因子 = 结点左子树的高度 - 结点右子树的高度
根据平衡二叉树的定义,平衡二叉树上所有结点的平衡因子只能是-1,0或1.
插入新结点后。平衡二叉树失衡:
具体构造方法参考:
9.4 散列表的查找
**散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。**查找时,根据这个确定的对应关系找到给定值key的映射f(key),若查找集合中存在这个记录,则必定在f(key)的位置上。
这里我们把这种**对应关系f称为散列函数,又称为哈希(Hash)函数。按这个思想,采用散列技术将记录存储在一块连续的内存空间中,这块联系存储空间称为散列表或哈希表(Hash Table)。**那么关键字对应的记录存储位置我们称为散列地址。
散列技术最适合的求解问题是查找与给定值相等的记录。
冲突:不同的关键码映射到同一个散列地址。
9.4.1 散列表的构造方法
构造散列函数考虑的因素:
执行速度(即计算散列函数所需时间)、关键字长度、散列表大小、关键字的分布情况、查找频率。
- 直接定址法
- 数字分析法
- 平方取中法
- 折叠法
- 除留余数法
- 随机数法
1.直接定址法
2.除留余数法(最常用的构造散列函数的方法)
9.4.2 处理散列冲突的方法
- 开放定址法(开地址法)
- 链地址法(拉链法)
- 再散列法(双散列函数法)
- 建立一个公共溢出区
1.开放地址法
2.链地址法
链地址法优点:
- 非同义词不会冲突,无“聚集”现象
- 链表上结点空间动态申请,更适合于表长不确定的情况
9.4.3 散列表的查找
散列表的查找效率分析:
几点结论:
- 散列表技术具有很好的平均性能,优于一些传统的技术
- 链地址法优于开地址法
- 除留余数法作散列函数优于其他类型函数