一、查找概论
在使用电脑的时候, 就会涉及到查找技术. 例如在硬盘中查找文件, 玩游戏加载数据等, 都要涉及到查找. 所有这些需要被查的数据所在的集合, 统一叫查找表.
查找表是由同一类型的数据元素构成的集合.
关键字是数据元素中某个数据项的值.
若此关键字可以唯一的标示一个记录, 则称此关键字为主关键字.
对于那些可以识别多个数据元素的关键字, 称之为次关键字.
查找实际上就是根据给定的某个值, 在查找表中确定一个关键字等于给定值的数据元素.
查找表按照操作方式分为两大种: 静态查找表和动态查找表.
静态查找表: 只操作查找操作的查找表.
主要操作为:
- 查询某个“特定的”数据元素是否在查找表中
- 检索某个“特定的”数据元素的各种属性
动态查找表: 在查找过程中同时插入查找表中不存在的数据元素, 或者从查找表中删除已存在的某个数据元素;
显然动态查找表的操作就是两个:
- 查找时插入数据元素
- 查找时删除数据元素
二、顺序表查找
例如, 在一大堆散乱的书籍中找到自己想要的那本非常的麻烦, 但是在图书馆里就很容易找到需要的书籍. 所以面对散乱的书都会先考虑做一件事, 那就是把这些书排列整齐. 这样就很容易找到需要的书籍.
散乱的图书可以理解为一个集合, 把它排列整齐, 就是将此集合构造成一个线性表. 针对这一线性表进行查找操作, 因为它就是静态查找表.
顺序查找又叫线性查找, 查找过程是: 从表的第一个或者最后一个记录开始, 依次逐个的进行对比, 如果一致则查找成功, 如果数据查找完毕仍没有相等的关键字, 则表中没有此数据.
2.1、顺序表查找算法
//a为查找表 n为查找表长度 key为关键字 a[0]为哨兵
int Sequential_Search(int *a, int n, int key) {
for (int i = 1; i <= n; i++) {
if (a[i] == key) {
return i;
}
}
return 0;;
}
在这里每次循环都要对i判断是否越界. 即 i <= n. 可以设置一个哨兵, 解决不需要每次让i与n做比较.
//a为查找表 n为查找表长度 key为关键字 a[0]为哨兵
int Sequential_Search2(int *a, int n, int key) {
int i = n;
a[0] = key;
while (a[i] != key) {
i--;
}
return 0;;
}
这样就直接少了一次比较处理.
三、有序查找表
3.1、折半查找
折半查找技术, 又称二分查找
-
它对前提是线性表中对记录必须是关键码有序(通常从大到小), 线性表必须采用顺序存储;
-
折半查找的基本思路是
- 在有序表中, 取中间记录作为比较对象, 若给定值与中间记录的关键字相等则查找成功.
- 若给定值小于中间记录的关键字, 则在中间记录的左半区继续查找
- 若给定值大于中间记录的关键字, 则在中间记录的右半区继续查找
- 不断重复此过程, 直到查找成功, 若无记录, 则查找失败
//a为查找表 n为查找表长度 key为关键字 a[0]为哨兵
int Binary_Search(int *a, int n, int key) {
int low = 1;//最低下标
int high = n;//最高下标
int mid = 0;//折半标记
while (low <= high) {
//折半计算
mid = (low + high) / 2;
if (key < a[mid]) {
//将左半边定为查找区
high = mid - 1;
} else if (key > a[mid]) {
//将右半边定为查找区
low = mid + 1;
} else {
return mid;
}
}
return 0;
}
3.2、插值查找
当我们在查找字典的时候, 例如查找 ‘apple’, 我们会下意识的翻到靠前的位置, 查找‘zoo’肯定会翻到靠后的位置, 绝对不会直接从中间开始. 同样的, 例如在0 ~ 10000 之间100个元素从小到大均匀分布的数组中查找5, 自然会考虑从数组较小的开始查找.
所以有了新的方程式:
//a为查找表 n为查找表长度 key为关键字 a[0]为哨兵
int Binary_Search(int *a, int n, int key) {
int low = 1;//最低下标
int high = n;//最高下标
int mid = 0;//折半标记
while (low <= high) {
//折半计算
mid = low + (high - low) * (key - a[low]) / (a[high] - a[low]);
if (key < a[mid]) {
//将左半边定为查找区
high = mid - 1;
} else if (key > a[mid]) {
//将右半边定为查找区
low = mid + 1;
} else {
return mid;
}
}
return 0;
}
3.3、斐波那契查找
假设 a = {1, 16, 24, 35, 47, 59, 62, 73, 88, 99} n = 10, key = 99
-
F[6] < n < F[7], 记录k = 7. 但是最大的位置只有a[10]. 所以将后续的两个元素赋值: a[11] = a[12] = a[10] = 99, 即 a = {1, 16, 24, 35, 47, 59, 62, 73, 88, 99, 99, 99}
-
low = 1, high = 10, mid = low + F[k-1] - 1 = 1 + F[6] - 1 = 8.
-
key > a[8], 所以 low = mid + 1 = 9
-
此时新范围为 [mid + 1, high] , 所以k = k - 2 = 5
-
low = 9, high = 10, mid = low + F[k-1] - 1 = 9 + F[4] - 1 = 11
-
key = a[11]. 此时 mid > 10, 即返回10, 表示这个key在数组的10位置上
斐波那契的核心在于:
- 当 key = a[mid]时, 表示查找成功.
- 当 key < a[mid]时, 新范围是第low个到第新mid-1个, 此范围个数为F[k-1] - 1个
- 当 key > a[mid]时, 新范围是第mid+1 个到第新high个, 此范围个数为F[k-2] - 1个
//斐波拉契查找
int F[100]; /* 斐波那契数列 */
int Fibonacci_Search(int *a,int n,int key){
int low,high,mid,i,k;
//最低下标为记录的首位;
low = 1;
//最高下标为记录的末位;
high = n;
k = 0;
//计算n为斐波拉契数列的位置;
while (n > F[k]-1) {
k++;
}
//将数组a不满的位置补全值;
for(i = n;i < F[k]-1;i++)
a[i] = a[n];
while (low <= high) {
//计算当前分隔的下标;
mid = low+F[k-1]-1;
if (key < a[mid]) {
//若查找的记录小于当前分隔记录;
//将最高下标调整到分隔下标mid-1处;
high = mid-1;
//斐波拉契数列下标减1位;
k = k-1;
}else if(key > a[mid]){
//若查找的记录大于当前的分隔记录;
//最低下标调整到分隔下标mid+1处
low = mid+1;
//斐波拉契数列下标减2位;
k = k-2;
}else{
if (mid <= n) {
//若相等则说明,mid即为查找的位置;
return mid;
}else
{
//若mid>n,说明是补全数值,返回n;
return n;
}
}
}
return 0;
}
四、二叉排序树
在某些情况下, 对一个线性表进行查找后删除、插入等操作, 会变的非常的麻烦, 所以就有了一种动态查找法. 二叉搜索树、二叉排序树就是对这样动态查找.
当在线性表中插入一个数据的时候, 需要让比这个数据大的数据向后移动, 这样操作就变的非常麻烦.
但是在二叉树中, 需要插入一个数据, 只需要判断是否比根节点大还是小, 小就直接放在这个结点的左子结点.
例如构建一段数据: 62, 88, 58, 47, 35, 73, 51, 99, 37, 93
以第一个数据62为根结点 58 88为子结点, 构建一个二叉树. 接下来依次读取接下来的数据.
4.1、二叉排序树的查找
先来构建一个二叉树:
typedef struct BiTNode {
int data;//结点数据
struct BiTNode *lchild, *rchild;//左右孩子指针
} BiTNode, *BiTree;
对二叉树进行查找:
- 首先判断二叉树是不是已经到达来叶子结点(递归条件), 到达叶子结点即查找完毕
- 判断当前数据是否查找到, 查找到即结束
- 判断key是否小于当前结点, 是则以在左子树继续查找
- 判断key是否大于当前结点, 是则以在右子树继续查找
Status SearchBST(BiTree T, int key, BiTree f, BiTree *p) {
if (!T) {
//已到达叶子结点
*p = T;
return 0;
} else if (T->data == key) {
//查找成功
*p = T;
return 1;
} else if (T->data > key) {
//查找左子树
return SearchBST(T->lchild, key, T, p);
} else {
//查找右子树
return SearchBST(T->rchild, key, T, p);
}
}
4.2、二叉排序树的插入
例如向这个二叉树中插入一个95
- 首先对二叉树进行一次查找, 如果查找成功则直接返回, 否则就会来到一个合适的位置
- 当二叉树中不存在这个key, 判断key大小, 放在这个合适位置的左边还是右边
Status InsertBST(BiTree *T, int key) {
BiTree p, s;
if (!SearchBST(*T, key, NULL, &p)) {
//新建结点
s = (BiTree)malloc(sizeof(BiTNode));
s->data = key;
s->lchild = s->rchild = NULL;
if (!p) {
*T = s;
} else if (key < p->data) {
p->lchild = s;
} else {
p->rchild = s;
}
return 1;
}
return 0;
}
4.3、二叉排序树的删除
4.3.1、删除叶子结点
4.3.2、删除的结点只有一个子结点
结点删除后, 将它的左子树或者右子树整个移动到删除结点的位置即可
4.3.3、删除的结点有左右子树
第一种方法:
将被删除的结点的一个子树直接移动到被删除的结点的位置, 另一个子树则重新逐个插入到这棵树中.
第二种方法:
4.3.4、代码实现
//从二叉排序树中删除结点p,并重接它的左或者右子树;
Status Delete(BiTree *p){
BiTree temp,s;
if((*p)->rchild == NULL){
//右子树为空只需要重新连接它的左子树
temp = *p;//将结点p临时存储到temp中;
*p = (*p)->lchild;//将p指向到p的左子树上;
free(temp);//释放需要删除的temp结点;
}else if((*p)->lchild == NULL){
//左子树为空只需要重新连接它的右子树
temp = *p;//将结点p临时存储到temp中;
*p = (*p)->rchild;//将p指向到p的左子树上;
free(temp);//释放需要删除的temp结点;
}else{
//删除的当前结点的左右子树均不为空;
temp = *p;//将结点p存储到临时变量temp, 并且让结点s指向p的左子树
s = (*p)->lchild;
//将s指针,向右到尽头(目的是找到待删结点的前驱)
//在待删除的结点的左子树中,从右边找到直接前驱
while (s->rchild) {
temp = s;//使用temp保存好直接前驱的双亲结点
s = s->rchild;
}
//将要删除的结点p数据赋值成s->data;
(*p)->data = s->data;
if(temp != *p)
temp->rchild = s->lchild;//重连右子树
else
temp->lchild = s->lchild;//重连左子树
//删除s指向的结点; free(s)
free(s);
}
return TRUE;
}