查找
查找(Search)就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素
查找概论
1)查找表(Search Table)是由同一类型的数据元素或记录构成的集合
2)关键字(Key)是数据元素中某个数据项的值,又称为键值,用它可以标识一个数据元素,也可以标识一个记录的某个数据项(字段),称为关键码
若此关键字可以唯一地标识一个记录,则称此关键字为主关键字(Primary Key)
对于可以识别多个数据元素的关键字,称为次关键字(Secondary Key)
3)查找表按照操作方式来分有两大种:静态查找表和动态查找表
1----静态查找表(Static Search Table):
只作查找操作的查找表,其主要操作有:
1)查询某个特定的数据元素是否在查找表中
2)检索某个特定的数据元素和各种属性
2----动态查找表(Dynamic Search Table)
在查找过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已经存在的某个数据元素,其操作有:
1)查找时插入数据元素
2)查找时删除数据元素
为了提高查找的效率,需要为其专门设置数据结构,这种面向查找操作的数据结构称为查找结构
对于静态查找表,不妨使用线性表结构,而动态查找则可以考虑使用二叉排序树的查找技术
顺序表查找
顺序查找(Sequential Search)又叫线性查找,是最基本的查找技术
其查找过程是:从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,
则查找成功,找到所查记录;否则查找不成功
//顺序查找算法
//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;
}
//顺序表查找优化
上面代码只需要在表中依次比较即可,但是每次循环都要对 i 是否越界作判断,可以设置一个哨兵,解决不需要让 i 每次都与 n 作比较
//有哨兵顺序查找
int Sequential_Search(int *a, int n, int key)
{
int i;
a[0] = key; //设置 a[0] 为关键字,称为哨兵,通过在尽头放置哨兵,防止越界
i = n; //循环从数组尾部开始
while (a[i] != key) //相比于上一个,在同一次循环里少做了一次运算
i--;
return i;
}
NOTE 哨兵未必一定要放在表的尽头,如果先能大致确定查找的范围,设置在范围边界即可
有序表查找
1)折半查找
折半查找(Binary Search),又称二分查找,其前提是线性表中的记录必须是关键码有序,线性表必须采用顺序存储
其基本思想是:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;
若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;否则在右半区查找,不断重复上述过程直至查找成功或失败
//折半查找
int Binary_Search(int *a, int n, int key)
{
int low, high, mid;
low = 1;
high = n;
while (low <= high)
{
mid = low + (high - low) / 2;
if (key < a[mid])
high = mid - 1;
else if (key > a[mid])
low = mid + 1;
else
return mid;
}
return 0;
}
NOTE 有关折半查找存在许多变种,在此不做叙述,详见另一篇文章
2)插值查找
可以看出折半查找的效率很高,但是思考一个新问题,为什么一定要折半而不是折四分之一或者更多呢
看见折半查找设置中间边界的式子 mid = low + (high-low)/2,折半就意味着是 1/2,现在考虑改进这个系数,见下式
mid = low + (high - low)*(key - a[low]) / (a[high] - a[low])
插值查找(Interpolation Search) 是根据要查找的关键字 key 与查找表中最大最小记录的关键字比较后的查找方法,其关键
就在于插值的计算公式 (key - a[low]) / (a[high] - a[low])
NOTE 对于对于表长较大,关键字分布均匀的查找表来说,插值查找的性能要不折半查找好得多,反之如果数组的元素分布为{0,1,2,2000,2001...99999}
等这种极端不均匀的数据,插值查找就未必合适
3)斐波那契查找
斐波那契查找(Fibonacci Search)与折半查找的一分为二不同,是利用了黄金分割原理来实现的
斐波那契数列F
F 0 1 1 2 3 5 8 13 21 34
下标 0 1 2 3 4 5 6 7 8 9
//斐波那契查找
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;
}
NOTE 斐波那契查找如果查找记录在右侧(右侧较短),则查找性能优于折半查找,反之左侧长侧则低于折半查找
NOTE 上面三种查找方式本质是分隔点的选择不同,各有优劣,要根据实际问题来选择
线性索引查找
数据结构的最终目的是提高数据的处理速度,索引是为了加快查找速度而设计的一种数据结构
索引就是把一个关键字与它对应的记录相关联的过程,一个索引由若干索引项构成,每个索引项至少应包含关键字和其对应得记录在存储器中
的位置等信息
索引按照结构可以分为线性索引,树形索引和多级索引,此处仅介绍线性索引
线性索引就是将索引项集合组织为线性结构,又称为索引表,此处重点介绍3种线性索引:稠密索引,分块索引和倒排索引
1)稠密索引
稠密索引是指在线性索引中,将数据集中的每个记录对应一个索引项,对于稠密索引要应对的数据很大,索引项就必须是按照关键码的有序排列
2)分块索引
分块有序是把数据集的记录分成了若干块,并且这些块需要满足两个条件:
(1)块内无序,即每一块内的记录不要求有序
(2)块间有序
对于分块有序的数据集,每块对应一个索引项,这种索引方法叫做分块索引。定义分块索引的索引项结构分为3个数据项:
最大关键码:它存储每一块中的最大关键字,这样的好处就是可以使得在它之后的下一块中的最小关键字也能比这一块最大的关键字大
存储了块中记录个数,以便循环时使用
用于指向块首数据元素的指针,便于开始对这一块记录进行遍历
在分块索引表中查找,就是分两步进行
(1)在分块索引表中查找要查关键字所在的块
(2)根据块首指针找到相应的块,并在块中顺序查找关键码,因为块中可以是无序的,必须顺序查找
3)倒排索引
索引项通用结构结构
次关键码
记录号表
其中记录号表存储具有相同次关键字的所有记录的记录号(可以是指向记录的指针或者是该记录的主关键字),这样的索引方法就是倒排索引
倒排索引源于实际应用中需要根据属性(或字段,次关键码)的值来查找记录,这种索引表中的每一项都包括一个属性值和具有该属性值
的各记录的地址,由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引
二叉排序树
二叉排序树(Binary Sort Tree),又称二叉查找树,它或者是一棵空树,或者是具有下列性质的二叉树
1)若它的左子树不空,则左子树上所有结点的值均小于它根结构的值
2)若它的右子树不空,则右子树上所有结点的值均大于它的根节点的值
3)它的左右子树也分别为二叉排序树
NOTE 构造一棵二叉排序树的目的,其实并不是为了排序,而是为了提高查找和插入删除关键字的速度,二叉排序树这种非线性结构有利于插入和删除的实现
二叉排序树查找操作
//二叉树的二叉链表结点结构定义
typedef struct BiTNode { //结点结构
int data; //结点数据
struct BiTNode *lchild, *rchild; //左右孩子指针
}BiTNode,*BiTree;
//递归查找二叉排序树T中是否存在key
//指针 f 指向 T 的双亲,其初始调用值为 NULL
//若查找成功,则指针 p 指向该数据元素结点,返回 TRUE
//否则指针 p 指向查找路径上访问的最后一个结点,返回 FLASE
Status SearchBST(BiTree T, int key, BiTree f, BiTree *p)
{
if (!T)
{
*p = f;
return FALSE;
}
else if (key == T->data)
{
*p = T;
return TRUE;
}
else if (key < T->data)
return SearchBST(T->lchild, key, T, p); //在左子树继续查找
else
return SearchBST(T->rchild, key, T, p); //在右子树继续查找
}
二叉排序树插入操作
//当二叉排序树T中不存在关键字等于 key 的数据元素时
//插入 key 并返回 TRUE,否则返回 FALSE
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 TURE;
}
else
return FALSE; //树中以有关键字相同的结点,不再插入
}
//可以采用下面的代码创建一棵二叉排序树
int a[MAX];
BiTree T = NULL;
for (int i = 0; i < MAX; i++)
insertBST(&T, a[i]);
二叉排序树删除操作
删除结点比较复杂,需要在删除后仍然保证二叉排序树的有序性,故需要考虑多种情况
1)叶子结点
2)仅有左或右子树的结点
3)左右子树都有的结点
//若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点,并返回 TRUE 否则返回 FALSE
Status DeleteBST(BiTree *T, int 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); //递归调用找右子树
}
}
//从二叉排序树中删除结点 P,并重接它的左或右子树
Status Delete(BiTree *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;
}
平衡二叉树(AVL树)
平衡二叉树(Self-Balancing Binary Search Tree 或 Height-Balanced Binary Search Tree)是一种二叉排序树,
其中每一个结点的左子树和右子树的高度至多等于 1
二叉平衡树是一种高度平衡的二叉排序树,其高度平衡的含义是说要么它是一棵空树,要么它的左子树和右子树都是
平衡二叉树,且左右子树的深度之差的绝对值不超过 1
我们将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF(Balance Factor),那么平衡二叉树上所有
结点的平衡因子只可能是 -1,0,1,只要二叉树上有一个结点的平衡因子的绝对值大于 1,该二叉树就不平衡
距离插入结点最近的,且平衡因子的绝对值大于 1 的结点为根的子树,我们称为最小不平衡树
平衡二叉树实现原理
平衡二叉树构建的基本思想就是在构建二叉排序树的过程中,每当插入一个结点时,先检查是否因插入而破坏了树的平衡性,
若是,则找出最小不平衡子树。在保持二叉排序树特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应
的旋转,使之称为新的平衡子树
平衡二叉树实现算法
在二叉排序树的基础上,增加一个 bf 用来存储平衡因子
//二叉树的二叉链表结点结构定义
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指向新的根结点
}
//左平衡旋转处理的函数代码
#define LH +1 //左高
#define EH 0 //等高
#define RH -1 //右高
//对以指针T所指结点为根的二叉树作左平衡旋转处理
//本算法结束时,指针T指向新的根结点
void LefrBalancee(BiTree *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中不存在和 e 有相同关键字的结点,则插入一个数据元素为 e 的新结点并返回 1
//否则返回 0.若因插入而使二叉排序树失去平衡,则作平衡旋转处理,布尔变量taller反映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 = FLASE;
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;
}
//在构建平衡二叉树的时候执行如下代码即可生成一棵平衡二叉树
BiTree T = NULL;
Status taller;
int a[MAX];
for (int i = 0; i < MAX; i++)
InsertAVL(&T, a[i], &taller);
算法来源 -- 大话数据结构