1. 查找概论
查找表(Search Table)是由同一类型的数据元素(或记录)构成的集合。
关键字(Key)是数据元素中某个数据项的值。
若此关键字可以唯一地标识一个记录,则称此关键字为主关键字(Primary Key)。
对于那些可以识别多个数据元素(或记录)的关键字,我们称为次关键字(Secondary Key)。
查找(Searching)就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。
静态查找表(Static Search Table):只作查找操作的查找表。
动态查找表(Dynamic Search Table):在查找的过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已经存在的某个数据元素。
1.1 顺序表查找
顺序查找(Sequential Search)又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果直到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。
1.1.1 顺序表查找算法
/* 顺序查找,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;
}
1.1.2 顺序表查找优化
/* 有哨兵顺序查找 */
int Sequential_Search2(int *a, int n, int key) {
int i;
a[0] = key;
while(a[i] != key) {
i--;
}
return i;
}
2. 有序表查找
2.1 折半查找
折半查找(Binary Search)技术,又称为二分查找。它的前提是线性表中的记录必须是关键码有序(通常从小到大有序),线性表必须采用顺序存储。折半查找的基本思想是:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右半区继续查找。不断重复上述过程,直到查找成功,或所有查找区域无记录,查找失败为止。
/* 折半查找 */
int Binary_Search(int *a, int n, int key) {
intlow, heigh, 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;
}
return 0;
}
2.2 插值查找
mid = low + (high - low) * (key - a[low]) / (a[hight] - a[low]); /* 插值 */
插值查找(Interpolation Search)是根据要查找的关键字key与查找表中最大最小记录的关机子比较后的查找方法,其核心就在于插值的计算公式key - a[low] / a[hight] - a[low]。
2.3 斐波那契查找
/* 斐波那契查找 */
int Fibonacci_Search(int *a, int n, in 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;
}
3. 线型索引查找
索引就是把一个关键字与它对应的记录相关联的过程。
线型索引就是将索引项集合组织为线性结构,也称为索引表。
3.1 稠密索引
稠密索引是指在线型索引中,将数据集中的每个记录对应一个索引项。
对于稠密索引这个索引表来说,索引项一定是按照关键码有序的排列。
3.2 分块索引
分块索引,是把数据集的记录分成了若干块,并且这些块需要满足两个条件:
- 块内无序,即每一块内的记录不要求有序
- 块间有序
3.3 倒排索引
索引项的通用结构是:
- 次关键码
- 记录号表
其中记录号表存储具有相同次关键字的所有记录的记录号(可以是指向记录的指针或者是该记录的主关键字)。这样的索引方法就是倒排索引(inverted index)。
4. 二叉排序树
二叉排序树(Binary Sort Tree),又称为二叉查找树。它或者是一棵空树,或者是具有夏磊性质的二叉树。
- 若它的左子树不空,则左子树上所有结点的值均小于它的跟结构的值
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值
- 它的左、右子树也分别为二叉排序树
4.1 二叉排序树查找操作
/* 二叉树的二叉链表结点结构定义 */
typedef struct BiTNode { /* 结点结构 */
int data; /* 结点数据 */
struct BiTNode *lchild, *rchild; /* 左右孩子指针 */
} BiTNode, *BiTree;
/* 递归查找二叉排序树T中是否存在key */
/* 指针f指向T的双亲,其初始调用值为NULL */
/* 若查找成功,则指针P指向该数据元素结点,并返回TRUE */
/* 否则指针p指向查找路径上访问的最后一个结点并返回FALSE */
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); /* 在右子树继续查找 */
}
}
4.2 二叉排序树插入操作
/* 当二叉排序树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 TRUE;
} else
return FALSE;
}
4.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;
if(q != *p)
q->rchild = s->lchild; /* 重接q的右子树 */
else
q->lchild = s->lchild; /* 重接q的左子树 */
free(s);
}
return TRUE;
}
5. 平衡二叉树(AVL树)
平衡二叉树(Self-Balancing Binary Search Tree或Height-Balanced Binary Search Tree),是一种二叉排序树,其中每一个结点的左子树和右子树的高度差至多等于1。
二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF(Balance Factor)。
距离插入结点最近的,且平衡因子的绝对值大于1的结点为根的子树,我们称为最小不平衡子树。
5.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指向新的根结点 */
}
#define LH +1 /* 左高 */
#define EH 0 /* 等高 */
#define RH -1 /* 右高 */
/* 对以指针T所指结点为根的二叉树作左平衡旋转处理 */
/* 本算法结束时,指针T指向新的根结点 */
void LeftBalance(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);
R_Rotate(T);
break;
}
}
/* 若在平衡的二叉排序树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 = 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;
}
6.多路查找树(B树)
多路查找树(multi-way search tree),其每一个结点的孩子数可以多于两个,且每一个结点处可以存储多个元素。
6.1 2-3树
2-3树是这样的一棵多路查找树:其中的每一个结点都具有两个孩子(我们称它为2结点)或三个孩子(我们称它为3结点 )。
一个2结点包含一个元素和两个孩子(或没有孩子),且与二叉排序树类似,左子树包含的元素小于该元素,右子树包含的元素大于该元素。不过,与二叉排序树不同的是,这2结点要么没有孩子,还有就有两个,不能只有一个孩子。
一个3结点包含一小一大两个元素和三个孩子(或没有孩子),一个3结点要么没有孩子,要么有3个孩子。如果3结点右孩子的话,左子树包含小于较小元素的元素,右子树包含大于较大元素的元素,中间子树包含介于两元素之间的元素。
6.2 2-3-4树
2-3-4树就是2-3树的概念扩展,包括了4结点的使用。
6.3 B树
B树(B-tree)是一种平衡的多路查找树,结点最大的孩子数目称为B树的阶。
一个m阶的B树具有如下属性:
- 如果根结点不是叶结点,则其至少有两棵子树
- 每一个非根的分支结点都有k-1个元素和k个孩子
- 所有叶子结点都位于同一层次
6.4 B+树
如图所示,这就是一棵B+树,灰色关键字即是根节点中的关键字在叶子结点再次列出,并且所有叶子结点都链接在一起。
7. 散列表查找(哈希表)概述
7.1 散列表查找定义
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,是的每个关键字key对应一个存储位置f(key)。
这里我们把这种对应关系f称为散列函数,又称为哈希(Hash)函数。
采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(Hash table)。
7.2 散列表查找步骤
散列技术既是一种存储方法,也是一种查找方法。
散列技术最适合的求解问题是查找与给定值相等的记录。
两个关键字key1 != key2,但是却有f(key1) = f(key2),这种现象我们称为冲突(collision),并把key1和key2称为这个散列函数的同义词(synonym)。
8 散列函数的构造方法
原则:
1. 计算简单
2. 散列地址分布均匀
8.1 直接定址法
取关键字的某个线型函数值为散列地址:
f(key) = a x key + b
8.2 数字分析法
8.3 平方取中法
这个方法计算很简单,假设关键字是1234,那么它的平方就是1522756,再抽取中间的3位就是227,用作散列地址。
8.4 折叠法
折叠法是将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够时可以短些),然后将这几部分叠加求和,并按散列表表长,取最后几位作为散列地址。
8.5 除留余数法
此方法为最常用的构造散列函数方法。
f(key) = key mod p (p <= m)
8.6 随机数法
选择一个随机数,取关键字的随机函数值为它的散列地址。
9. 处理散列冲突的方法
9.1 开放地址法
所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到。
9.2 再散列函数法
9.3 链地址法
9.4 公共溢出区法
为所有冲突的关键字建立了一个公共的溢出区来存放。
10. 散列表查找实现
10.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) {
reutrn 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;
}
10.2 散列表查找性能分析
- 散列函数是否均匀
- 处理冲突的方法
- 散列表的装填因子