一、查找概述
1、查找以集合为数据结构,以查找为核心操作
2、查找结构:
(1)线性表:适用于静态查找,主要采用顺序查找技术、折半查找技术
(2)树表:适用于动态查找,主要采用二叉排序树的查找技术
(3)散列表:静态查找和动态查找均适用,主要采取散列技术
二、线性表查找技术
2.1 顺序查找
基本思想:从线性表的一端向另一端逐渐将关键码与给定值进行比较,若相等,则查找成功;反之则查找失败
2.2 折半查找
基本思想:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键码相等,则查找成功;若小于中间记录的关键码,则在中间记录的左半区继续查找;若大于中间记录的关键码,则在中间记录的右半区继续查找
折半查找的非递归算法
void search(int a[],int n,int number){
int min = 0;
int max = n;
int mid = (min+max)/2;
while(a[mid] != number && min<=max){
if(a[mid] != number){
if(a[mid] > number)
max = mid-1;
else
min = mid+1;
mid = (max+min)/2;
}
}
if(a[mid] == number){
cout<<"search success: "<<++mid<<endl;
}else{
cout<<"search failure"<<endl;
}
}
折半查找的递归算法
void search(int low,int high,int a[],int number){
int mid;
mid=(low+high)/2;
if(low>high)
printf("查找无果");
else {
if(a[mid]==number)
printf("查找有果%d",number);
else if(a[mid]>number)
search(low,mid-1,a,number);
else
search(mid+1,high,a,number);
}
}
三、树表查找技术
3.1 二叉排序树
二叉排序树又称二叉查找树,他或者是一棵空的二叉树,或者是具有下列性质的二叉树
(1)若他的左子树不为空,则左子树上所有节点的值均小于根节点的值
(2)若他的右子树不为空,则右子树上所有节点的值均大于根节点的值
(3)他的左右子树也都可以是二叉排序树
中序遍历二叉排序树可以得到一个按关键码有序的序列
二叉排序树常用二叉链表进行存储:
struct BiNode{
int data;
struct BiNode* lchild;
struct BiNode* rchild;
};
class BiSortTree{
private:
BiNode* root;
int k; //保存待查找的定值
void InOrder(BiNode* bt);
void SearchBST(BiNode* root,int k); //查找定值k
public:
BiSortTree(int a[],int n,int number);
~BiSortTree(){
Release(root);}
void Release(BiNode* bt);
BiNode* InsertBST(BiNode* root,BiNode* s); //插入一个结点s
void DeleteBST(BiNode* p,BiNode* f); //删除节点f的左孩子p
void SearchBST(int k){
SearchBST(root,k);} //将待查找的定值k传入到内部函数当中
void InOrder(){
InOrder(root);} //调用中序遍历的函数
};
二叉排序树插入算法
BiNode* BiSortTree::InsertBST(BiNode* root,BiNode* s){
if(root == NULL)
root = s;
return root;
else{
if(s->data < root->data)
root->lchild = InsertBST(root->lchild,s);
else
root->rchild = InsertBST(root->rchild,s);
}
}
二叉排序树的构造
BiSortTree::BiSortTree(int a[],int n,int number){
root = NULL;
for(int i=0;i<n;i++){
BiNode* s= new BiNode;
s->data = a[i];
s->lchild = NULL;
s->rchild = NULL; //申请一个空结点
root = InsertBST(root,s);
}
k = number;
}
析构函数的调用函数
void BiSortTree::Release(BiNode* bt){
if(bt != NULL){
Release(bt->lchild);
Release(bt->rchild);
delete bt;
}
}
二叉排序树的删除算法
void BiSortTree::DeleteBST(BiNode* p,BiNode* f){
//删除节点f的左孩子p
if(p->lchild == NULL && p->rchild == NULL){
//如果p既不存在左子树也不存在右子树
f->lchild = NULL; //只需要双亲结点的左孩子指向空就行
delete p;
}else if(p->rchild == NULL){
//如果结点p存在左孩子不存在右孩子
f->lchild = p->lchild; //只需要双亲结点的左孩子指向p的左孩子
delete p;
}else if(p->lchild == NULL){
//如果结点p存在右孩子不存在左孩子
f->lchild = p->rchild; //只需要双亲结点的左孩子指向p的右孩子
delete p;
}else{
//如果结点p左右孩子都存在
//思路:从结点p的子树中寻找一个结点s,将结点s的值传给结点p,然后再将结点s删除,对结点s的子树进行处理,
//该结点s的值应该是大于结点p的最小值(或者小于结点p的最大值)
BiNode* par = p; //结点par:指向结点s的双亲结点,初始化时指向待删除的结点p
BiNode* s = new BiNode;
s = p->rchild; //结点s指向结点p的右孩子
while(s->lchild != NULL){
//循环:找到结点s的最后一个左孩子
par = s; //令par指向结点s,再让结点s指向他的左孩子
s = s->lchild; //这样一轮下来结点par就指向结点s的双亲结点
}
p->data = s->data; //找到后,将结点s的值赋值给结点p
if(par == p){
//如果结点s就是结点p的右孩子
par->rchild = s->rchild; //那么就让par的右子树指向结点s的右子树
}else{
par->lchild = s->rchild; //否则就让par的左子树指向结点s的右子树
}
delete s;
}
}
二叉排序树查找算法
void BiSortTree::SearchBST(BiNode* root,int k){
if(root == NULL){
//如果为空树,查找失败
cout<<"没有值"<<endl;
return;
}else{
if(root->data == k){
//如果根节点的值 == 给定值 ,返回根节点,查找成功
cout<<"查找成功"<<endl;
return;
}else if(root->data > k){
//如果根节点的值大于给定值k,递归遍历左孩子
SearchBST(root->lchild,k);
}else{
SearchBST(root->rchild,k); //反之如果根节点的值小于于给定值k,递归遍历右孩子
}
}
}
中序遍历输出排序后的数值
void BiSortTree::InOrder(BiNode* bt){
if(bt == NULL){
return ;
}else{
InOrder(bt->lchild);
cout<<bt->data<<" ";
InOrder(bt->rchild);
}
}
3.2 平衡二叉树
1、平衡二叉树或者是一棵空的二叉排序树,或者时具有下列性质的二叉排序树:
(1)根节点的左子树和右子树的深度最多相差1;
(2)根节点的左子树和右子树也都是平衡二叉树。
2、每个结点的平衡因子是该结点的左子树的深度和右子树的深度之差
3、最小不平衡子树是指在平衡二叉树的构造中,以距离插入结点最近的、且平衡因子的绝对值大于1的结点为根的子树
4、构造平衡二叉树的基本思想:在构造二叉排序树的过程中,每当插入一个结点时,首先检查是否因插入而破坏了树的平衡性。若是,则找出最小不平衡子树。在保持二叉排序树特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应的旋转,使之成为新的平衡子树
一般情况下,设结点A为最小不平衡子树的根结点,对该子树进行平衡化调整归纳起来有以下4种情况
- (1)LL型:
新插入的结点时插在结点A的左孩子的左子树上,属于LL型,其中结点A的左孩子为结点B。由“扁担原理”可得,将支撑点由结点A改为结点B,结点B成为根结点。然后将结点B的原先的右子树成为结点A的左孩子 - (2)RR型:
新插入的结点时插在结点A的右孩子的右子树上,属于RR型,其中结点A的右孩子为结点B。由“扁担原理”可得,将支撑点由结点A改为结点B,结点B成为根结点。然后将结点B的原先的左子树成为结点A的右孩子 - (3)LR型调整:
新插入的结点时插在结点A的左孩子的右子树上,属于LR型,其中结点A的左孩子为结点B,结点B的右孩子为结点C。由“扁担原理”可得,首先将支撑点由结点B改为结点C,相应地,需进行逆时针旋转。在旋转过程中,将结点B作为结点C的左孩子,将结点C的原先的左子树作为结点B的右子树。
然后再将支撑点由结点A调整到结点C,结点C为根结点,结点A作为结点C的右子树,结点C原先的右子树作为结点A的左子树 - (4)RL型调整:
新插入的结点时插在结点A的右孩子的左子树上,属于RL型,其中结点A的右孩子为结点B,结点B的左孩子为结点C。由“扁担原理”可得,首先将支撑点由结点B改为结点C,相应地,需进行顺时针旋转。在旋转过程中,将结点B作为结点C的右孩子,将结点C的原先的右子树作为结点B的左子树。
然后再将支撑点由结点A调整到结点C,结点C为根结点,结点A作为结点C的左子树,结点C原先的左子树作为结点A的右子树
四、散列表查找技术
4.1 散列表的概述
1、在查找时,根据一个确定的对应关系找到给定值k的映射H(k),若查找集合中存在这个记录,则必定再H(k)的位置上,这种查找技术称为散列技术;采用散列技术将记录存储在一块连续的存储空间中,这块连续的存储空间称为散列表;将关键码映射为散列表中适当存储位置的函数称为散列函数;所得的存储位置称为散列地址。
2、散列过程为:
- (1)存储记录时,通过散列函数计算记录的散列地址,并按此散列地址存储该记录
- (2)查找记录时,通过同样的散列函数计算记录的散列地址,按此散列地址访问该记录
3、散列方法一般不适用于多个记录有同样关键码的情况,也不适用于范围查找
4、对于两个不同的关键码k1 ×= k2,有H(k1) = H(k2),即两个不同的记录需要存放在同一个存储位置中,这种现象称为冲突,此时k1和k2相对于H称作同义词
5、采用散列技术需要考虑两个主要问题
- (1)散列函数的设计
- (2)冲突的处理
4.2 散列函数的设计
1、设计原则:(1)计算简单 (2)函数值即散列地址分布均匀
2、设计方法
- (1)直接定值法:适用于事先知道关键码的分布,且关键码集合不是很大而连续性较好的情况
H(key) = a × key + b - (2)除留余数法: 基本思想:选择某个适当的正整数p,以关键码除以p的余数作为散列地址
H(key) = key mod p - (3)数字分析法 根据关键码在各个位上的分布情况,选取分布比较均匀的若干位组成散列地址,该方法适用于事先知道关键码的分布且关键码中有若干位分布较均匀的情况
- (4)平方取中法
- (5)折叠法 将关键码从左到右分割成位数相等的几部分,最后一部分位数可以短些,然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址,通常有两种叠加方法
1)移位叠加:将各部分的最后一位对齐相加
2)间界叠加:从一端向另一端沿各部分分界来回折叠后,最后一位对齐相加
4.3 处理冲突的方法
1、开放定址法 用开放定址法处理冲突得到的散列表叫做闭散列表
定义:由关键码得到的散列地址一旦产生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入
- (1)线性探测法:当发生冲突时,线性探测法从冲突位置的下一位置起,依次寻找空的散列地址,即对于键值key,设H(key) = d,闭散列表的长度为m,则发生冲突时,寻找下一个散列地址的公式为:
H = ( H(key) + d) % m (d = 1,2,3,4,…,m-1)