查找概论
查找就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素。
顺序表查找
顺序表查找算法
/* 顺序查找,a为数组,n为要查找的数组个数,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;
}
顺序表查找优化
/* 有哨兵顺序查找 */
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则说明查找失败 */
}
有序表查找
折半查找
/* 折半查找 */
int Binary_Search(int *a,int n,int key)
{
int low,high,mid;
low=1; /* 定义最低下标为记录首位 */
high=n; /* 定义最高下标为记录末位 */
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; /* 若相等则说明mid即为查找到的位置 */
}
}
return 0;
}
插值查找
/* 插值查找 */
int Interpolation_Search(int *a,int n,int key)
{
int low,high,mid;
low=1; /* 定义最低下标为记录首位 */
high=n; /* 定义最高下标为记录末位 */
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; /* 若相等则说明mid即为查找到的位置 */
}
return 0;
}
斐波那契查找
int Fibonacci_Search(int *a,int n,int key) /* 斐波那契查找 */
{
int low,high,mid,i,k;
low=1; /* 定义最低下标为记录首位 */
high=n; /* 定义最高下标为记录末位 */
k=0;
while(n>F[k]-1) /* 计算n位于斐波那契数列的位置 */
k++;
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]) /* 若查找记录小于当前分隔记录 */
{
high=mid-1; /* 最高下标调整到分隔下标mid-1处 */
k=k-1; /* 斐波那契数列下标减一位 */
}
else if (key>a[mid]) /* 若查找记录大于当前分隔记录 */
{
low=mid+1; /* 最低下标调整到分隔下标mid+1处 */
k=k-2; /* 斐波那契数列下标减两位 */
}
else
{
if (mid<=n)
return mid; /* 若相等则说明mid即为查找到的位置 */
else
return n; /* 若mid>n说明是补全数值,返回n */
}
}
return 0;
}
线性索引查找
稠密索引
稠密索引是指在线性索引中,将数据集中的每个记录对应一个索引项,索引项按关键码有序排列。
分块索引
分块有序,是把数据集的记录分成了若干块,并且这些块之间满足一下两个条件:
块内无序,即每一块内的记录不要求有序。
倒排索引
二叉排序树(左小右大)
如图是一棵二叉排序树的例子。
二叉排序树的查找操作
/* 二叉树的二叉链表结点结构定义 */
typedef struct BiTNode /* 结点结构 */
{
int data; /* 结点数据 */
struct BiTNode *lchild, *rchild; /* 左右孩子指针 */
} BiTNode, *BiTree;
Status SearchBST(BiTree T, int key, BiTree f, BiTree *p)
{ /* 递归查找二叉排序树T中是否存在key, */
if (!T) /* 若查找不成功,指针p指向查找路径上访问的最后一个结点并返回FALSE */
{
*p = f;
return FALSE;
}
else if (key==T->data) /* 若查找成功,则指针p指向该数据元素结点,并返回TRUE */
{
*p = T;
return TRUE;
}
else if (key<T->data)
return SearchBST(T->lchild, key, T, p); /* 在左子树中继续查找 */
else
return SearchBST(T->rchild, key, T, p); /* 在右子树中继续查找 */
}
二叉树f指向T的双亲,当T指向根结点时,f的初值就为NULL,它在递归时有用,最后的参数p是为了查找成功后可以查找到的结点位置。
对二叉排序树进行中序遍历,可以得到一个递增的有序序列。
二叉排序树的插入操作
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; /* 插入s为新的根结点 */
else if (key<p->data)
p->lchild = s; /* 插入s为左孩子 */
else
p->rchild = s; /* 插入s为右孩子 */
return TRUE;
}
else
return FALSE; /* 树中已有关键字相同的结点,不再插入 */
}
二叉排序树的删除操作
①若被删除结点z是叶结点,则直接删除,不会破坏二叉排序树的特性
②若结点z只有一棵左子树或右子树,则让z的子树成为z父结点的子树,替代z的位置
③若结点z有左、右两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。
Status Delete(BiTree *p)
{/* 从二叉排序树中删除结点p,并重接它的左或右子树。 */
BiTree q,s;
if((*p)->rchild==NULL) /* 右子树空则只需重接它的左子树(待删结点是叶子也走此分支) */
{
q=*p; *p=(*p)->lchild; free(q);
}
else if((*p)->lchild==NULL) /* 只需重接它的右子树 */
{
q=*p; *p=(*p)->rchild; free(q);
}
else /* 左右子树均不空 */
{
q=*p; s=(*p)->lchild;
while(s->rchild) /*循环找到左子树的右节点*/
{
q=s; s=s->rchild;
}
(*p)->data=s->data; /* s指向被删结点直接前驱(将被删结点前驱的值取代被删结点的值) */
if(q!=*p)
q->rchild=s->lchild;/* 重接q的右子树 */
else
q->lchild=s->lchild;/* 重接q的左子树 */
free(s);
}
return TRUE;
}
Status DeleteBST(BiTree *T,int key)
{/* 若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据结点 */
if(!*T) /* 不存在关键字等于key的数据元素 */
return FALSE;
else
{
if (key==(*T)->data) /* 找到关键字等于key的数据元素 */
return Delete(T);
else if (key<(*T)->data)
return DeleteBST(&(*T)->lchild,key);
else
return DeleteBST(&(*T)->rchild,key);
}
}
平衡二叉树
平衡二叉树是一种二叉排序树,其中每一个结点的左子树和右子树的高度差至多等于1。我们将二叉树上结点的左子树高度减去右子树高度的值称为平衡因子BF,平衡二叉树上所有结点的平衡因子只可能是-1、0和1。
平衡二叉树的实现原理
平衡二叉树的实现算法
/* 二叉树的二叉链表结点结构定义 */
typedef struct BiTNode /* 结点结构 */
{
int data; /* 结点数据 */
int bf; /* 结点的平衡因子 */
struct BiTNode *lchild, *rchild; /* 左右孩子指针 */
} BiTNode, *BiTree;
右旋操作如下
/* 对以p为根的二叉排序树作右旋处理, */
/* 处理之后p指向新的树根结点,即旋转处理之前的左子树的根结点 */
void R_Rotate(BiTree *P)
{
BiTree L;
L=(*P)->lchild; /* L指向P的左子树根结点 */
(*P)->lchild=L->rchild; /* L的右子树挂接为P的左子树 */
L->rchild=(*P);
*P=L; /* P指向新的根结点 */
}
左旋操作代码如下
/* 对以P为根的二叉排序树作左旋处理, */
/* 处理之后P指向新的树根结点,即旋转处理之前的右子树的根结点0 */
void L_Rotate(BiTree *P)
{
BiTree R;
R=(*P)->rchild; /* R指向P的右子树根结点 */
(*P)->rchild=R->lchild; /* R的左子树挂接为P的右子树 */
R->lchild=(*P);
*P=R; /* P指向新的根结点 */
}
左平衡旋转处理的函数代码,该函数被调用时,已经确认当前子树是不平衡状态,且左子树的高度大于右子树的高度,即T的根节点应该是平衡因子BF的值大于1的数。
#define LH +1 /* 左高 */
#define EH 0 /* 等高 */
#define RH -1 /* 右高 */
void LeftBalance(BiTree *T) //T为需要调整平衡性的子树
{
BiTree 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作右旋平衡处理 */
}
}
右平衡旋转处理的函数代码如下
/* 对以指针T所指结点为根的二叉树作右平衡旋转处理, */
/* 本算法结束时,指针T指向新的根结点 */
void RightBalance(BiTree *T)
{
BiTree R,Rl;
R=(*T)->rchild; /* R指向T的右子树根结点 */
switch(R->bf)
{ /* 检查T的右子树的平衡度,并作相应平衡处理 */
case RH: /* 新结点插入在T的右孩子的右子树上,要作单左旋处理 */
(*T)->bf=R->bf=EH;
L_Rotate(T);
break;
case LH: /* 新结点插入在T的右孩子的左子树上,要作双旋处理 */
Rl=R->lchild; /* Rl指向T的右孩子的左子树根 */
switch(Rl->bf) /* 修改T及其右孩子的平衡因子 */
{
case RH: (*T)->bf=LH;
R->bf=EH;
break;
case EH: (*T)->bf=R->bf=EH;
break;
case LH: (*T)->bf=EH;
R->bf=RH;
break;
}
Rl->bf=EH;
R_Rotate(&(*T)->rchild); /* 对T的右子树作右旋平衡处理 */
L_Rotate(T); /* 对T作左旋平衡处理 */
}
}
Status InsertAVL(BiTree *T,int e,Status *taller)
{
if(!*T) /* 插入新结点,树“长高”,置taller为TRUE */
{
*T=(BiTree)malloc(sizeof(BiTNode));
(*T)->data=e;
(*T)->lchild=(*T)->rchild=NULL;
(*T)->bf=EH;
*taller=TRUE;
}
else
{
if (e==(*T)->data) /* 树中已存在和e有相同关键字的结点则不再插入 */
{
*taller=FALSE;
return FALSE;
}
if (e<(*T)->data) /* 应继续在T的左子树中进行搜索 */
{
if(!InsertAVL(&(*T)->lchild,e,taller)) /* 未插入 */
return FALSE;
if(*taller) /* 已插入到T的左子树中且左子树“长高” */
{
switch((*T)->bf)/* 检查T的平衡度 */
{
case LH: /* 原本左子树比右子树高,需要作左平衡处理 */
LeftBalance(T);
*taller=FALSE;
break;
case EH: /* 原本左、右子树等高,现因左子树增高而使树增高 */
(*T)->bf=LH;
*taller=TRUE;
break;
case RH: /* 原本右子树比左子树高,现左、右子树等高 */
(*T)->bf=EH;
*taller=FALSE;
break;
}
}
}
else /* 应继续在T的右子树中进行搜索 */
{
if(!InsertAVL(&(*T)->rchild,e,taller)) /* 未插入 */
return FALSE;
if(*taller) /* 已插入到T的右子树且右子树“长高” */
{
switch((*T)->bf)/* 检查T的平衡度 */
{
case LH: /* 原本左子树比右子树高,现左、右子树等高 */
(*T)->bf=EH;
*taller=FALSE;
break;
case EH: /* 原本左、右子树等高,现因右子树增高而使树增高 */
(*T)->bf=RH;
*taller=TRUE;
break;
case RH: /* 原本右子树比左子树高,需要作右平衡处理 */
RightBalance(T);
*taller=FALSE;
break;
}
}
}
}
return TRUE;
}
多路查找树(B树)
一旦涉及外部存储设备,时间复杂度的计算就会发生变化,访问集合元素的时间已经不仅仅是寻找该元素所需比较次数的函数,必须考虑对硬盘等外部存储设备的访问时间以及将会对该设备做出多少次单独访问。
多路查找树,其每一个结点的孩子数可以多于两个,且每一个结点处可以存储多个元素。由于它是查找树,所有元素之间存在某种特定的排序关系。
2-3树
2-3树的插入实现
2-3树的插入可以分为以下三种情况:
(1) 对于空树,插入一个2结点即可
(2)插入结点到一个2结点的叶子上。将该结点变成一个3结点即可。
(3)要往3结点中插入一个新元素,因为3结点本身已经是2-3树的结点最大容量,因此就需要将其拆分,且将树中两元素或插入元素的三者中选择其一向上移动一层。
2-3树的删除实现
2-3-4树
2-3-4树其实就是2-3树的概念扩展,包括了4结点的使用。一个4结点包含小中大3个元素和4个孩子(或没有孩子)。如果某个4结点有孩子的话,左子树包含小于元素的元素;第二子树包含大于最小元素,小于第二元素的元素;第三子树包含大于第二元素,小于最大元素的元素;右子树包含大于最大元素的元素。
B树
B+树
散列表查找(哈希表)概述
散列函数的构造方法
什么才算是好的散列函数呢?
1.计算简单
直接定址法
数字分析法
平方取中法
折叠法
除留余数法
随机数法
处理散列冲突的方法
开放定址法
再散列函数法
链地址法
公共溢出区法
散列表查找的实现
散列表查找的算法实现
#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 12 /* 定义散列表长为数组的长度 */
#define NULLKEY -32768
typedef struct
{
int *elem; /* 数据元素存储基址,动态分配数组 */
int count; /* 当前数据元素个数 */
}HashTable;
int m=0; /* 散列表表长,全局变量 */
/* 初始化散列表 */
Status InitHashTable(HashTable *H)
{
int i;
m=HASHSIZE;
H->count=m;
H->elem=(int *)malloc(m*sizeof(int));
for(i=0;i<m;i++)
H->elem[i]=NULLKEY;
return OK;
}
/* 散列函数 */
int Hash(int key)
{
return key % m; /* 除留余数法 */
}
/* 插入关键字进散列表 */
void InsertHash(HashTable *H,int key)
{
int addr = Hash(key); /* 求散列地址 */
while (H->elem[addr] != NULLKEY) /* 如果不为空,则冲突 */
{
addr = (addr+1) % m; /* 开放定址法的线性探测 */
}
H->elem[addr] = key; /* 直到有空位后插入关键字 */
}
/* 散列表查找关键字 */
Status SearchHash(HashTable H,int key,int *addr)
{
*addr = Hash(key); /* 求散列地址 */
while(H.elem[*addr] != key) /* 如果不为空,则冲突 */
{
*addr = (*addr+1) % m; /* 开放定址法的线性探测 */
if (H.elem[*addr] == NULLKEY || *addr == Hash(key)) /* 如果循环回到原点 */
return UNSUCCESS; /* 则说明关键字不存在 */
}
return SUCCESS;
}
查找的代码与插入的代码非常类似,只需做一个不存在关键字的判断而已。
散列表查找的性能分析