静态查找:数据集合稳定,不需要添加、删除元素的查找操作。
动态查找:数据集合在查找的过程中需要同时添加或删除元素的查找操作。
· 顺序查找又叫线性查找,是最基本的查找技术,它的查找过程是:从第一个
(或者最后一个)记录开始,逐个进行记录的关键字和给定值进行比较,若某
个记录的关键字和给定值相等,则查找成功。如果查找了所有的记录仍然找
不到与给定值相等的关键字,则查找失败。
下面贴出简单的代码案例:
//顺序查找,a为要查找的数组,n为要查找数组的长度,key为查找的关键字
int Sq_Search1(int *a,int n,int key){
//a[0]不存放数据,这里需要判断2次,复杂度n
for (int i = 1; i <= n; ++i)
if(a[i]==key)
return i;
return 0;
}
//顺序查找优化算法(对CPU优化)
int Sq_Search2(int *a,int n,int key){
int i=n;
a[0]=key;
//这里只需要判断1次,复杂度n
while (a[i]!=key)
i--;
return i;
}
折半查找
· 先确定要查找的范围,然后逐渐缩小这个范围,可以设指针low和high分别
指向关键字序列的上界和下界,指针mid指中间位置,即mid=(low+high)/2
like:
如此往复,直到查找到关键字
代码实现:
//折半查找,假设有序数组从小到大排序
int bin_search(int str[],int n,int key){
//找到返回在数组中的位置,没有则返回-1
int low,high,mid;
low=0;
high=n-1;
while (low<=high){
mid=(low+high)/2;
if(str[mid]==key)
return mid;
else if(str[mid]>key)
high=mid-1;
else
low=mid+1;
}
return -1;
}
时间复杂度O(log2n),2为底数
斐波那契查找
斐波那契数列={1,1,2,3,5,8,13,21,34,55,89,...},下一项为前两项之和。
前一项除以后一项,值接近于0.618
· 例如,现在有一个长度为9的数组,这个长度是在8和13之间,那么第一次
拆分是在第8个元素,即arry[7],mid=7。如果被查找的树比这个小,则第
二次则在第5个元素拆分,即arry[4],mid=4。以此类推。
· 推演到一般情况,假设有待查找数组array[n]和斐波那契数组F[k],并且n
满足n>=F[k]-1&&n < F[k+1]-1,则它的第一个拆分点middle=F[k]-1。
· 这里得注意,如果n刚好等于F[k]-1,待查找数组刚好拆成F[k-1]和F[k-2]两部分,
那万事大吉你好我好;然而大多数情况并不能尽人意,n会小于F[k]-1,这时候
可以拆成完整F[k-1]和残疾的F[k-2]两部分,那怎么办呢?
· 聪明的前辈们早已想好了解决办法,对了,就是补齐,用最大的数来填充
F[k-2]的残缺部分,如果查找的位置落到补齐的部分,那就可以确定要找的
那个数就是最后一个最大的了。
这里理解就好,代码具体就不展开了,感兴趣可以自行百度。
· 斐波那契查找的平均性能比折半查找好,但是最坏情况下的性能(虽然仍是O(logn))
却比折半查找差。它还有个优点就是分割时只需进行加减运算。
插值查找
是按数字在关键字数组中所占比例来确定mid值的。
mid=(key-[L].key)*(h-L+1)/([h].key-[L].key)
其中[L].key表示最小关键字,[h].key表示最大关键字
这种插值查找只适于关键字均匀分布的表,这样,表长比较大的顺序表,其平均性能比折半好。
索引顺序查找
又称分块查找
索引表有序,列表(块内)不要求有序
索引表用折半查找,列表内用顺序查找
则分开查找的平均查找长度为log2((n/s)+1)+(s/2)
二叉排序树
· 又称为二叉查找树,它可以是空树,或是具有如下性质的二叉树:
-若它的做字数不为空,则左子树上所有结点的值均小于它的根结构的值。
-若它的右子树不为空,则右子树上所有结点的值均大于它的根结构的值。
-它的左、右子树也分别为二叉排序树(递归)。
它的查找和插入比较容易,这里直接贴出代码
int SearchBST(BiTree T,KeyType key,BiTree f,BiTree &p){
//递归查找二叉排序树T中是否存在key
//指针f指向T的双亲,其初始值调用值为NULL
//若查找成功,则指针p指向该数据元素结点,并返回1
//否则p指向查找路径上访问的最后一个结点,并返回0
if(!T){//查找不成功
p=f;//返回双亲结点指针
return 0;
} else if(key==T->data){//查找成功
p=T;//返回该结点指针
return 1;
} else if(keydata)//递归左子树遍历
return SearchBST(T->lchild,key,T,p);
else //递归右子树遍历
return SearchBST(T->rchild,key,T,p);
}
int InsertBST(BiTree &T,int key){
//当二叉排序树T中不存在关键字等于key的数据元素时
//插入key并返回1,否则返回0
BiTree p,s;
if(!SearchBST(T,key,NULL,p)){
s=(BiTree)malloc(sizeof(BiTNode));
s->data=key;
s->rchild=s->lchild=NULL;
if(!p)
T=s;//插入s为新的根结点
else if(keydata)
p->lchild=s;//插入s为左孩子
else
p->rchild=s;//插入s为右孩子
return 1;
}
return 0;//树中已存在该关键字,不再插入
}
· 删除算法,如果删除的结点有左右子树,就比较麻烦,需要找到中序遍历的前驱
然后赋值之后,接上前驱结点的左子树,再删除前驱结点即可。
算法实现:
int Delete(BiTree &p){
//从二叉排序树中删除结点p,并重接它的左或右子树
//注:p相当于 双亲.lchild或者.rchild
BiTree q,s;
if(p->rchild==NULL){//右子树空,则只需重接它的左子树
q=p; //p的地址给q
p=p->lchild;//p指向它的左子树
free(q);
} else if(p->lchild==NULL){//左子树空,只需重接它的右子树
q=p; //p的地址给q
p=p->rchild;//p指向它的右子树
free(q);
} else{//左右子树均不为空
q=p;
s=p->lchild;//转左,然后向右到尽头
while (s->rchild){//找到中序遍历删除结点的前驱
q=s;
s=s->rchild;
}
//q指向 s指针指向结点的双亲
p->data=s->data;
if(q!=p)//重接q的右子树
q->rchild=s->lchild;
//当p.lchild没有右子树时,p==q
else//重接q的左子树
q->lchild=s->lchild;
free(s);
}
return 1;
}
int DeleteBST(BiTree &T, int key){
//若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点
//并返回1,否则返回0
if(!T)//不存在关键字等于key的数据元素
return 0;
else{
if(key==T->data)//找到关键字等于key的数据元素
return Delete(T);
else if(keydata)
return DeleteBST(T->lchild,key);
else
return DeleteBST(T->rchild,key);
}
}
平衡二叉树
· 也叫AVL树,要么是空树,要么它的左子树和右子树都是平衡二叉树,
且左子树和右子树的深度之差绝对值不超过1。
平衡二叉树旋转:
RR型,parent平衡因子为-2,subR平衡因子为-1
LL型,parent平衡因子为2,subR平衡因子为1
LR型,30平衡因子为2,10平衡因子为-1
即:T树的左子树高,T左孩子的右子树高
RL型,10平衡因子为-2,30平衡因子为1
即:T树的右子树高,T右孩子的左子树高
代码实现:
typedef int ElemType;
#define LH 1 //左高
#define EH 0 //等高
#define RH -1 //右高
typedef struct BSTNode {
ElemType data;
int bf; //结点平衡因子
struct BSTNode *lchild, *rchild;//左右孩子指针
} BSTNode, *BSTree;
void R_Rotate(BSTree &p) {
//对以p为根的二叉排序树作右旋处理,处理后p指向新的树根结点,
//即旋转处理之前左子树的根结点
BSTree L;
L = p->lchild; //L指向指向p的左子树根结点
p->lchild = L->rchild;//L的右子树挂接为p的左子树
L->rchild = p;
p = L; //p指向新的根结点
}
void L_Rotate(BSTree &p) {
//对以p为根的二叉排序树作左旋处理,处理后p指向新的树根结点,
//处理之前的右子树的根结点
BSTree L;
L = p->rchild; //L指向的p的右子树根结点
p->rchild = L->lchild;//L的左子树挂接为p的右子树
L->lchild = p;
p = L; //p指向新的根结点
}
void RightBalance(BSTree &T) {
//对以指针T所指结点为根的二叉树作右平衡旋转处理,
//本算法结束时,指针T指向新的根结点
BSTree R, Rl;
R = T->rchild;//R指向T右子树根结点
switch (R->bf) {//检查T右子树平衡度,并作相应平衡处理
case RH://新结点插入在右孩子的右子树上,要作单左旋处理
T->bf = EH;
R->bf = EH;
L_Rotate(T);
break;
case LH://新结点插入在右孩子的左子树上,要作双旋处理
Rl = R->lchild;//Rl指向T右孩子的左子树的根
switch (Rl->bf) {//修改T及其右孩子的平衡因子
case RH:
T->bf = LH;
R->bf = EH;
break;
case EH:
T->bf = EH;
R->bf = EH;
case LH:
T->bf = EH;
R->bf = RH;
break;
}
Rl->bf = EH;
R_Rotate(T->rchild);//对T的右子树作右旋处理
L_Rotate(T);//对T作左旋平衡 处理
}
}
void LeftBalance(BSTree &T) {
//对以指针T所指结点为根的二叉树作为左平衡旋转处理,
//算法结束时,指针T指向新的根结点
BSTree L, Lr;
L = T->lchild; //L指向T的左子树根结点
switch (L->bf) {//检查T的左子树的平衡度,并作相应平衡处理
case LH://新结点插入在T的左孩子的左子树上,要作单右旋处理
T->bf = L->bf = EH;
R_Rotate(T);
break;
case RH://新结点插入在T的左孩子的右子树上,要作双旋处理
Lr = L->rchild;//Lr指向T的左孩子的右子树根
switch (Lr->bf) {//修改T及其左孩子的平衡因子
case LH:
T->bf = RH;
L->bf = EH;
break;
case EH:
T->bf = L->bf = EH;
break;
case RH:
T->bf = EH;
L->bf = LH;
break;
}
Lr->bf = EH;
L_Rotate(T->lchild);//对T的左子树作左旋处理
R_Rotate(T); //对T作右旋平衡处理
}
}
int InsertAVL(BSTree &T, int e, int &taller) {
//若在平衡的二叉排序树T中不存在和e有相同关键字的结点,则插入一个数据元素
//为e的新结点,并返回1,否则返回0。若因插入而使二叉排序树失去平衡,
//则作平衡旋转处理,taller变量0或1反映T长高与否
if (!T) {
T = (BSTree) malloc(sizeof(BSTNode));
T->data = e;
T->lchild = T->rchild = NULL;
T->bf = EH;
taller = 1;
} else {
if (e == T->data) {//树中已存在和e有相同关键字的结点
taller = 0;
return 0; //则不再插入
} else if (e < T->data) {//继续在左子树中探索
if (!InsertAVL(T->lchild, e, taller))
return 0; //存在相同关键字结点,未插入
if (taller) {//已插入到T的左子树中且左子树长高
switch (T->bf) {//检查T的平衡度
case LH://原本左子树比右子树高,需要作左平衡处理
LeftBalance(T);
taller = 0;
break;
case EH://原本左子树和右子树等高,现因左子树增高而使树增高
T->bf = LH;
taller = 1;
break;
case RH://原本右子树比左子树高,现在左右子树等高
T->bf = EH;
taller = 0;
break;
}
}
} else {//应继续在T的右子树中进行搜索
if (!InsertAVL(T->lchild, e, taller))
return 0;//未插入
if (taller) {//已插入到T的右子树且右子树长高
switch (T->bf) {//检查T的平衡度
case LH://原本左子树比右子树高,现左右子树等高
T->bf = EH;
taller = 0;
break;
case EH://原本左右子树等高,现因右子树增高而使树增高
T->bf = RH;
taller = 1;
break;
case RH://原本右子树比左子树高,需要作右平衡处理
RightBalance(T);
taller = 0;
break;
}
}
}
}
return 1;
}
在平衡树上进行查找的时间复杂度为O(logn),n表示结点数。
B-树和B+树
1970年,R.Bayer和E.mccreight提出了一种适用于外查找的树,它是一种平衡的多叉树,称为B树(或B-树、B_树)。
一棵m阶B树(balanced tree of order m)是一棵平衡的m路搜索树。
它或者是空树,或者是满足下列性质的树:
1、根结点至少有两个子女;
2、每个非根节点所包含的关键字个数 j 满足:┌m/2┐ - 1 <= j <= m - 1;
3、除根结点以外的所有结点(不包括叶子结点)的度数正好是关键字总数加1,
故内部子树个数 k 满足:┌m/2┐ <= k <= m ;
4、所有的叶子结点都位于同一层。
特点:
是一种多路搜索树(并不是二叉的):
1.定义任意非叶子结点最多只有M个儿子;且M>2;
2.根结点的儿子数为[2, M];
3.除根结点以外的非叶子结点的儿子数为[M/2, M];
4.每个结点存放至少M/2-1(取上整)和至多M-1个关键字;(至少2个关键字)
5.非叶子结点的关键字个数=指向儿子的指针个数-1;
6.非叶子结点的关键字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];
7.非叶子结点的指针:P[1], P[2], …, P[M];其中P[1]指向关键字小于K[1]的
子树,P[M]指向关键字大于K[M-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树;
8.所有叶子结点位于同一层;
(M=3)
B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果
命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为
空,或已经是叶子结点;
1.关键字集合分布在整颗树中;
2.任何一个关键字出现且只出现在一个结点中;
3.搜索有可能在非叶子结点结束;
4.其搜索性能等价于在关键字全集内做一次二分查找;
5.自动层次控制;
· B+ 树是一种树数据结构,是一个n叉树,每个节点通常有多个孩子,
一棵B+树包含根节点、内部节点和叶子节点。根节点可能是一个叶子节点,
也可能是一个包含两个或两个以上孩子节点的节点。
用途:
· B+ 树通常用于数据库和操作系统的文件系统中。NTFS, ReiserFS,
NSS, XFS, JFS, ReFS 和BFS等文件系统都在使用B+树作为元数据索引。
· B+ 树的特点是能够保持数据稳定有序,其插入与修改拥有较稳定的对数时间复杂度。
· B+ 树元素自底向上插入。
B+树的定义
B+树是应文件系统所需而出的一种B-树的变型树。一棵m阶的B+树和m阶的B-树的差异在于:
1.有n棵子树的结点中含有n个关键字,每个关键字不保存数据,
只用来索引,所有数据都保存在叶子节点。
2.所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,
且叶子结点本身依关键字的大小自小而大顺序链接。
3.所有的非终端结点可以看成是索引部分,结点中仅含其子树(根结点)中的最大(或最小)关键字。
通常在B+树上有两个头指针,一个指向根结点,一个指向关键字最小的叶子结点。
B+树是B-树的变体,也是一种多路搜索树:
1.其定义基本与B-树同,除了:
2.非叶子结点的子树指针与关键字个数相同;
3.非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树
(B-树是开区间);
5.为所有叶子结点增加一个链指针;
6.所有关键字都在叶子结点出现;
(M=3)
· B+的搜索与B-树也基本相同,区别是B+树只有达到叶子结点才命中(B-树可以在
非叶子结点命中),其性能也等价于在关键字全集做一次二分查找;
B+的特性:
1.所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好
是有序的;
2.不可能在非叶子结点命中;
3.非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储
(关键字)数据的数据层;
4.更适合文件索引系统;
哈希表(散列表)
· 记录的存储位置=f(关键字)
· 散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应
关系f,使得每个关键字key对应一个存储位置f(key)。
· 把这种对应关系f称为散列函数,又称为哈希(Hash)函数。采用散列技术将记录
存储在一块连续的存储空间中,这块连续存储空间成为散列表或哈希表(Hash Table)。
· 当存储记录时,通过散列函数计算出记录散列的地址。
· 当查找记录时,通过同样的散列函数计算记录的散列地址,并按这散列地址访问该记录。
哈希函数的构造方法
1. 直接定址法
· 取关键字或关键字的某个线性函数值为哈希值
即:f(key)=key 或 f(key)=a*key+b
对于不同的关键字不会发生冲突,但实际中能使用这种哈希函数的情况少
2.数字分析法
· 数字分析法通常适合处理关键字位数比较大的情况
取关键字中的一部分作为哈希地址。
3.平方取中法
· 取关键字平方后中间几位为哈希地址。取的位数由表长决定。
适合于不知道关键字分布,位数也不是很大的情况 。
4.折叠法
· 将关键字从左到右分割成位数相等的几部分,最后一部分不足用0补充,
然后将这几部分叠加求和,并按散列表表长取后几位作为散列地址。
适合知道关键字位数比较多的情况,且关键字中每一位上数字分布大致均匀。
5.除数余留法
· 此方法为最常用的构造散列函数方法,对于散列表长为m的散列函数计算公式为:
f(key) = key mod p (p<=m)
· 这个方法不仅可以对关键字直接取模,也可以通过折叠、平方取中后再取模。
一般情况下,可以选p为质数或不包含小于20的质因数的合数。
6.随机数法
· 选择一个随机数,取关键字的随机函数值为它的散列地址。
即:f(key) = random(key)
· 这里的random是随机函数,当关键字的长度不等时,采用这个方法
构造散列函数是比较合适的。
实际工作中需视不同的情况采用不同的哈希函数。通常,考虑的因素有:
-计算哈希函数所需时间(包括硬件指令的因素)。
-关键字的长度。
-哈希表的大小。
-关键字的分布情况。
-记录的查找频率。
处理冲突的方法
1.开放地址法
· 一旦发生冲突,就去寻找下一个空的散列地址,只要散列表足够大,
空的散列地址总能找到,并将记录存入。
公式是: fi(key) = (f(key)+di) MOD m (di=1,2,...,m-1)
· 上图称线性探测再散列,在填充48时,从0下标到5下标都存在冲突,0-5的记录都要争抢后面空
的位置,这叫“二次聚集”。
· 可以修改di的取值方式,例如使用平方运算来尽量解决堆积问题:称二次探测再散列
fi(key) = ( f(key) + di ) MOD (di=1^2, -1^2, ...,q^2, -q^2, q<=m/2)
· 还有方法是,在冲突时,对于位移量di采用随机函数计算得到,称伪随机探测再散列:
fi(key) = ( f(key) + di ) MOD m (di是由一个随机函数获得的数列)
2.再哈希法
fi(key) = RHi(key) (i = 1,2,3,...,k)
· RHi均是不同的哈希函数,即在同义词产生地址冲突时计算另一个哈希函数地址,
直到冲突不再发生。这种方法不易产生“聚集”,但是增加了计算时间。
3.链地址法
4.公共溢出法
一旦发生冲突,就按顺序填入溢出表,不管他的哈希函数得到的地址是什么
#define HASHSIZE 12 //Hash表长
#define NULLKEY -32768 //任取一个不可能的数,用来初始化
typedef struct {
int *elem;//数据元素存储基址,动态分配数组
int count; //当前数据元素个数
//int sizeindex;//hashsize[sizeindex]为当前容量
} HashTable;
int InitHashTable(HashTable &H) {
H.count = 0;
H.elem = (int *) malloc(HASHSIZE * sizeof(int));
if (!H.elem)
return -1;
for (int i = 0; i < HASHSIZE; ++i) {
H.elem[i] = NULLKEY;
}
return 0;
}
//使用除留余数法
int Hash(int key) {
return key % HASHSIZE;
}
//插入关键字到散列表
void InsertHash(HashTable &H, int key) {
int addr;
if(H.count==HASHSIZE)
exit(0);
addr = Hash(key);
while (H.elem[addr] != NULLKEY) {//如果不为空,则冲突出现
addr = (addr + 1) % HASHSIZE;//开放定址法的线性探测
}
H.elem[addr] = key;
H.count++;
}
//散列表查找关键字
int SearchHash(HashTable H, int key, int &addr) {
//找到返回0,否则返回-1
addr = Hash(key);
while (H.elem[addr] != key) {
addr = (addr + 1) % HASHSIZE;
//循环到了一个空地址,或者返回到了原点
if (H.elem[addr] == NULLKEY || addr == Hash(key))
return -1;
}
return 0;
}
· 查找过程中需和给定值进行比较的关键字的个数取决于下列三个因素:
哈希函数,处理冲突的方法和哈希表的装填因子。
· 哈希函数的“好坏”影响出现冲突的频繁程度。