本节为查找章节内容,回到总目录:点击此处
本部分目录
列表:由同一类型的数据元素(或记录)构成的集合,可利用任意数据结构实现
关键字:数据元素的某个数据项的值,用它可以标识列表中的一个或一组数据元素。如果一个关键字可以唯一标识列表中的一个数据元素,则称其为主关键字,否则为次关键字。
查找:根据给定的关键字值,在特定的列表中确定一个其关键字与给定值相同的数据元素,并返回该数据元素在列表中的位置。查找什么----查找范围----查找结果
平均查找长度 —> ASL =
∑
i
=
1
n
P
i
C
i
\sum_{i=1}^nP_iC_i
∑i=1nPiCi
查找的基本方法:比较式查找法(基于线性表的查找法&基于树的查找法)
计算式查找法(哈希查找法)
基于线性表的查找法
顺序查找法
(注意越界问题–>可以从后往前遍历 ----中间可以设置监视哨)
其平均查找长度ASL = 1 / 2 ( n + 1 ) 1/2(n+1) 1/2(n+1)
折半查找法
二分查找—先排序 —类似于一棵树的结构!
其平均查找长度ASL = l o g 2 ( n + 1 ) − 1 log_2(n+1)-1 log2(n+1)−1
评价:比较次数少、查找速度快、但必须单调性分界(有序)并且插入数据困难
分块查找法
①将列表分成若干个子块(子表),块内无序,块之间有序,
②构造一个索引表。其中每个索引项对应一个块并记录每块的起始位置,以及每块中的最大关键字(或最小关键字)。索引表按关键字有序排列
其查找过程:
①将待查关键字k与索引表中的关键字进行比较,以确定待查记录所在的块。(可以顺序或折半[索引表是有序的])
②进一步使用顺序查找法,在相应块内查找关键字为k的元素
其平均查找长度:
两种情况(先折半后顺序)(两次都顺序)
1、
l
o
g
2
(
n
/
s
+
1
)
+
s
/
2
log_2(n/s+1) + s/2
log2(n/s+1)+s/2
2、
(
b
+
s
)
/
2
+
1
(b+s)/2 + 1
(b+s)/2+1
基于树的查找法
二叉排序树
定义:二叉排序树要么是一棵空树,要么满足下面性质
1、若它的左子树非空,则左子树上所有结点的值均小于根结点的值
2、若它的右子树非空,则右子树上所有结点的值均大于根结点的值
3、它的左右子树也分别为二叉排序树
注意:二叉排序树中,不能有相同的元素
· 二叉排序树的插入与创建
插入(递归创建算法)
1、若二叉排序树是空树,则key称为二叉排序树的根
2、若二叉排序树非空,则让key与根进行比较
a、如果key的值等于根结点的值,则停止插入
b、如果key的值小于根节点的值,则插入左子树
c、如果key的值大于根节点的值,则插入右子树
void InsertBST(BSTree *bst, KeyType key)
{
BSTree s;
if(*bst == NULL) //递归停止条件
{
s = (BSTree)malloc(sizeof(BSTNode));
s -> key = key;
s -> Lchild = s -> Rchild = NULL;
*bst = s;
}
else if(key < (*bst) -> key)
InsertBST(&((*bst)->Lchild),key);
else if(key > (*bst) -> key)
InsertBST(&((*bst)->Rchild),key);
}
每次插入的结点都是作为叶子结点插入的,将其插入到二叉树的合适位置,插入时不需要移动元素,不涉及树的整体改动
插入算法是时间复杂度为
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n)
在一个树中查找一个数字,
第一次在根节点判断,第二次在第二层节点判断
以此类推,树的高度是多少就会判断多少次
树的高度和节点的关系就是以2为底,树的节点总数n的对数
创建
void CreateBST(BSTree *bst)
{
KeyType key;
*bst = NULL;
scanf("%d", &key);
while(key != ENDEKEY)
{
InsertBST(bst,key);
scanf("%d",&key);
}
}
【算法分析】假设有n个元素,要插入n个结点需要n次插入操作,而插入一个结点的算法时间复杂度是 O ( l o g 2 n ) O(log_2n) O(log2n) 因此创建二叉排序树的时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
· 二叉排序树的查找
和折半查找类似,也是一个不断缩小查找范围的过程
将关键字key与根节点关键字t进行比较
1、key = t,返回根节点的地址
2、key < t 查找左子树
3、key > t 查找右子树
【递归方法】
BSTree SearchBST(BSTree bst, KeyType key)
{
if(!bst) return NULL;
else if (bst -> key == key) return bst;
else if (bst -> key > key) return SearchBST(bst -> Lchild, key);
else
return SearchBST(bst->Rchild,key);
}
【非递归方法】 (循环方式)
BSTree SearchBST(BSTree bst, KeyType key)
{
//查找成功返回指向钙元素的结点指针,否则返回空指针
BSTree q;
q = bst;
while(q)
{
if(q -> key == key) return q;
if(q -> key > key) q = q -> Lchild;
else q = q -> Rchild;
}
return NULL;
}
【算法分析】二叉排序树的查找算法与二叉树的形态有关,二叉树越均衡,查找到高度越低,所耗费的时间越少。
平均时间和折半查找类似
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n) ,但插入和删除结点十分方便
★二叉树排序树的删除
难点:要保证删除后的二叉排序树仍然满足要求
【算法思想】
1、首先找到该二叉排序树中是否存在这个要删除的元素
2、情况讨论
A、如果p为叶结点,则可以直接删除f->Lchild =NULL;free(p)
B、如果p结点只有左子树或者只有右子树,则可将p的左子树或右子树,直接改为其双亲结点f的左子树
f->Lchild = p -> Lchild;free(p)
或f->Lchild = p -> Rchild; free(p);
C、如果p既有左子树又有右子树,则有如下两种处理方法:
a、首先找到p结点在中序序列中的直接前驱s结点,然后将p的左子树改为f的左子树,而将p的右子树改为s的右子树
f->Lchild = p -> Lchild; s->Rchild = p ->Rchild;free(p);
b、首秀按找到p结点在中序序列中的直接前驱s结点,然后用s结点的值替代p结点的值,再将s结点删除,原s结点的左子树改
为s的双亲结点q的右子树
p->data=s->data;q->Lchild=s->Lchild;free(s);
//采用方法二
BSTNode *DelBST(BSTree t, KeyType k)
{
BSTNode *p,*f,*s,*q;
p = t;
f = NULL;//如果是根节点的话,其双亲结点为NULL
while(p) //找待删结点
{
if(p -> key == k) break;
f = p; //f指向p结点的双亲结点
if(p -> key > k) p = p -> Lchild;
else p = p -> Rchild;
}
if(p == NULL) return t; //找不到则返回原来的二叉排序树
if(p -> Lchild == NULL) //p没有左子树
{
if(f == NULL) t = p -> Rchild; //删除的是根结点
else if(f -> Lchild == p)
f -> Lchild = p -> Rchild;
else
f -> Rchild = p -> Rchild;
free(p);
}
else //p有左子树
{
q = p;
s = p -> Lchild;
while(s->Rchild)
{
q = s;
s = s -> Rchild;
}
if(q==p) q -> Lchild = s -> Lchild;
else q -> Rchild = s -> Lchild;
p -> key = s -> key;
free(s);
}
return t;
}
【分析】 删除操作的基本过程是查找操作,所以其时间复杂度仍然是
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n)
特性:中序遍历一个二叉排序树可以得到一个递增的有序序列。
逆中序定义(RDL)可以得到一个递减的有序序列
平衡二叉排序树AVL树
一棵平衡二叉树要么是一棵空树,要么是具有下面性质的二叉排序树
1、左子树与右子树的高度之差的绝对值小于等于1
2、左子树和右子树也是平衡二叉排序树。
引入平衡二叉排序树的目的是为了提高查找效率,其平均查找长度为 O ( l o g 2 n ) O(log_2n) O(log2n)
结点的平衡因子定义为:**结点的左子树深度与右子树深度之差。**显然对一棵平衡二叉树而言,其所有结点的平衡因子只能是-1、0和1
平衡二叉排序树的恢复平衡的几种方式
· LL型 (单项旋转 右旋)
· LR型 (双向旋转 先左后右)
· RR型 (单向左旋)
· RL型 (双向旋转,先右后左)
算法思想:
1、查找应插的位置,同时记录离插入位置最近的可能失衡结点A(A的平衡因子不等于0)
2、插入新结点S
3、确定结点B,并修改A的平衡因子
4、修改从B到S路径上各结点的平衡因子(原值必为0,否则A将下移)
5、根据A、B的平衡因子,判断是否失衡以及失衡类型,并做相关处理
typedef struct BSTNode{ ElemType data; int bf;//平衡因子 struct BSTNode *lchild,*rchild; }*BSTree,BSTNode;
void ins_AVLtree(AVLTree *avlt, KeyType k)
{
//在平衡二叉树中插入元素k,使之称为一棵新的平衡二叉排序树
S = (AVLTree)malloc(sizeof(AVLTNode));
S -> key = k;
S -> Lchild = S -> Rchild = NULL;
S -> bf = 0;
if(*avlt == NULL) *avlt = S;
else
{
//首先查找S的插入位置fp,同时记录距S的插入位置最近且平衡因子不等于0(等于-1或1)的结点A,A为可能的失衡结点
A = *avlt; FA = NULL;
p = *avlt; fp = NULL;
while(p != NULL)
{
if(p -> bf != 0)
{
A = p; FA = fp;
}
fp = p;
if(k < p -> key) p = p -> Lchild;
else p = p -> Rchild;
}
}
//插入S
if(k < fp -> key) fp -> Lchild = S;
else fp -> Rchild = S;
//确定结点B,并修改A的平衡因子
if(k < A -> key)
{
B = A -> Lchild; A -> bf = A -> bf + 1;
}
else {B = A -> Rchild; A -> bf = A -> bf - 1;}
//修改B到S路径上各结点的平衡因子(原值均为0)
p = B;
while(p != S)
{
if(k < p -> key) {p -> bf = 1; p = p -> Lchild;}
else {p -> bf = -1; p = p -> Rchild;}
}
if(A -> bf == 2 && B -> bf == 1) //LL型
{
A -> Lchild = B -> Rchild;
B -> Rchild = A;
A -> bf = 0; B -> bf = 0;
if(FA == NULL) *avlt = B;
else if(A == FA -> Lchild) FA -> Lchild = B;
else FA -> Rchild = B;
}
else if(A -> bf == 2 && B -> bf == -1) //LR型
{
C = B -> Rchild;
B -> Rchild = C -> Lchild;
A -> Lchild = C -> Rchild;
C -> Lchild = B; C -> Rchild = A;
if(S -> key < C -> key)
{
A -> bf = -1;
B -> bf = 0;
C -> bf = 0;
}
else if(S -> key > C -> key)
{
A -> bf = 0;
B -> bf = 1;
C -> bf = 0;
}
else
{
A -> bf = 0;
B -> bf = 0;
}
if(FA == NULL) *avlt = C;
else if(A == FA -> Lchild) FA -> Lchild =C;
else FA -> Rchild = C;
}
else if (A -> bf == -2 && B -> bf == 1) //RL型
{
C = B -> Lchild;
B -> Lchild = C -> Rchild;
A -> Rchild = C -> Lchild;
C -> Lchild = A;
C -> Rchild = B;
if(S -> key < C -> key)
{
A -> bf = 0;
B -> bf = -1;
C -> bf = 0;
}
else if(S -> key > C -> key)
{
A -> bf = 1;
B -> bf = 0;
C -> bf = 0;
}
else
{
A -> bf = 0;
B -> bf =0;
}
if(FA == NULL) *avlt = C;
else if(A==FA->Lchild) FA->Lchild = C;
else FA -> Rchild =C;
}
else if(A->bf == -2 && B -> bf == -1) //RR型
{
A -> Rchild = B -> Lchild;
B -> Lchild = A;
A -> bf = 0; B -> bf =0;
if(FA == NULL) *avlt = B;
else if(A==FA->Lchild) FA->Lchild =B;
else FA->Rchild = B;
}
}
B树
·m路查找树
与二叉树类似,可以定义一种"m叉排序树",通常称为m路查找树
满足以下性质:
1、结点最多有m棵子树,m-1关键字
·B树及其查找
一棵B树是一棵平衡的m路查找树,它或者是空树,或者满足如下性质:
1、树中每个结点最多有m棵子树
2、根结点至少有两棵子树
3、除根结点之外的所有非叶结点至少有m/2棵子树
4、所有叶结点出现在同一层上,并且不包含信息,通常称为失败结点。(失败结点为虚结点,在B树中并不存在,指向它们的指针为空指针
具有n个非叶结点的m阶B树,至少含有多少个关键字?
· 根据m阶B树的定义,根节点至少有两棵子树,所以根节点至少有一个关键字 1’
· 除根节点之外的所有非叶结点,每个结点至少有[m/2]棵子树,所以除根结点之外的所有非叶结点,每个结点至少有[m/2]-1个关键字。 m/2向上取整
综上:n个非叶结点的m阶B树,至少含有 1 + [(m/2)-1] x (n-1) 个关键字
B-树具有分支多层数少的特点,使得它更多的是应用在数据库系统中
· B 树的查找 (类似于二叉排序树)
如果查找到第3层还没有找到的话,则查找会继续进行下去,直到第4层,也就是叶子结点,但B树的叶子结点本身并不存储任何信息NULL,故查找失败
#define m<阶数>
typedef int Boolean;
typedef struct Mbtnode
{
struct Mbtnode *parent;
int keynum;
KeyType key[m+1];
struct Mbtnode *ptr[m+1];
}Mbtnode, *Mbtree;
Boolean search_mbtree(Mbtree mbt, KeyType k, Mbtree *np, int *pos)
{
//在根为mbt的B_树中查找关键字k,如果查找成功,则将所在结点地址放入np,将结点内位置序号放入pos,并返回true;否则,将k应被插入的结点地址放入np,将结点内应插位置需要放入pos,并返回false
p = mbt; fp = NULL; found = false; i = 0;
while(p != NULL && !found)
{
i = search(p,k);
if(i > 0 && p -> key[i] == k) found = true;
else { fp = p; p = p -> ptr[i];}
}
if(found) {*np = p; *pos = i; return true;}
else {*np = fp; *pos = i; return false;}
}
int search(Mbtree mbt, Keytype key)
{
//在mbt指向的结点中,寻找小于等于key的最大关键字序号
n = mbt -> keynum;
i = 1;
while(i <= n && mbt -> key[i] <= key) i++;
return i-1;
//返回小于等于key的最大关键字序号,为0时表示应到最左分支找,越界时表示应到最右分支找
}
· B 树中插入关键字
B-树也是从空树开始,通过不断地插入新的数据元素构建的。但是 B-树构建的过程同前面章节的二叉排序树和平衡二叉树不同,B-树在插入新的数据元素时并不是每次都向树中插入新的结点。
对于m阶B_树来说,规定所有的非终端结点(终端结点是叶子结点,其关键字个数为0)中包含关键字的个数范围是[[m/2]-1, m-1],因此在插入新的数据元素时,首先向最底层的某个非终端结点中添加,如果该结点的关键字个数没有超过m-1,则直接插入成功,否则要对结点进行处理
· B-树中删除关键字
在 B-树种删除关键字时,首先前提是找到该关键字所在结点,在做删除操作的时候分为两种情况一种情况是删除结点为 B-树的非终 端结点(不处在最后一层);
另一种情况是删除结点为 B-树最后一层的非终端结点。
例如:
如果该结点为非终端结点且不处在最后一层,假设用 Ki 表示,则只需要找到指针 Ai 所指子树中最小的一个关键字代替 Ki,同时将该最 小的关键字删除即可。
如果要删除关键字 45 ,只需要使用关键字 50 代替 45 ,同时删除 f 结点中的 50 即可。
如果该结点为最后一层的非终端结点,有下列 3 种可能
被删关键字所在结点中的关键字数目不小于⌈m/2⌉,则只需从该结点删除该关键字 Ki 以及相应的指针 Ai 。
例如,在图 3 中,删除关键字 12 ,只需要删除该关键字 12 以及右侧指向 NULL 指针即可。
被删关键字所在结点中的关键字数目等于⌈m/2⌉-1,而与该结点相邻的右兄弟结点(或者左兄弟)结点中的关键字数目大于⌈m/2⌉-1,
只需将该兄弟结点中的最小(或者最大)的关键字上移到双亲结点中,然后将双亲结点中小于(或者大于)且紧靠该上移关键字的关键
字移动到被删关键字所在的结点中。删除关键字 50,其右兄弟结点 g 中的关键字大于 2,所以需要将结点 g 中最小的关键字 61 上移到其双亲结点 e 中(由
此 e 中结点有:53,61,90),然后将小于 61 且紧靠 61 的关键字 53 下移到结点 f 中被删除关键字所在的结点如果和其相邻的兄弟结点中的关键字数目都正好等于⌈m/2⌉-1,假设其有右兄弟结点,且其右兄弟结点是由双
亲结点中的指针 Ai 所指,则需要在删除该关键字的同时,将剩余的关键字和指针连同双亲结点中的 Ki 一起合并到右兄弟结点中。例如,在图 10 中 B-树中删除关键字 53,由于其有右兄弟,且右兄弟结点中只有 1 个关键字。在删除关键字 53 后,结点 f 中只剩指 向叶子结点的空指针,连同双亲结点中的 61(因为 61 右侧指针指向的兄弟结点 g)一同合并到结点 g 中,最终删除 53 后的 B-树为:
在合并的同时,由于从双亲结点中删除一个关键字,若导致双亲结点中关键字数目小于⌈m/2⌉-1,则继续按照该规律进行合并。例如在 图 11 中 B-树的情况下删除关键字 12 时,结点 c 中只有一个关键字,然后做删除关键字 37 的操作。此时在删除关键字 37 的同时, 结点 d 中的剩余信息(空指针)同双亲结点中的关键字 24 一同合并到结点 c 中,效果图为:
,由于结点 b 中一个关键字也没有,所以破坏了 B-树的结构,继续整合。在删除结点 b 的同时,由于 b 中仅剩指向结点 c 的指针,所以 连同其双亲结点中的 45 一同合并到其兄弟结点 e 中,最终的 B-树为:
计算式查找法—哈希法
哈希法:散列法、杂凑法、关键字地址计算法
哈法法关键需要解决两个问题:1、构造哈希函数 2、如何处理冲突
哈希函数的构造
构造原则:一是函数本身便于计算;二是计算出来的地址分布均匀,即对任意关键字k,H(k)对应不同地址的概率相等,目的是尽可能减少冲突
· 数字分析法
事先要知道关键字集合,并且每个关键字的位数比哈希表的地址码位数多时,可以从关键字中选出分布较均匀的若干位,构成哈希地址
· 平方取中法
当无法确定关键字中哪几位分布较均匀时,可以先求出关键字的平方值,然后按需要取平方值的中间几位作为哈希地址。这是因为完成平方运算后中间几位和关键字中每一位都相关,故不同关键字会以较高的概率产生不同的哈希地址
· 分段叠加法
按哈希表地址位数将关键字分成位数相等的几部分(最后一部分可以较短),然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。具体方法有折叠法与移位法。
· 除留余数法
假设哈希表长为m,p为小于等于m的最大素数,则哈希函数为
H ( k ) = H(k) = H(k)= K % p
· 伪随机数法
采用一个伪随机函数作为哈希函数,即 H ( k e y ) = r a n d o m ( k e y ) H(key) = random(key) H(key)=random(key)
处理冲突的方法
· 开放定址法
也叫再散列法,其基本思想是:当关键字key的初始哈希地址h0 = H(key)出现冲突时,以h0为基础,产生另一个地址h1,如果h1仍然冲突,再以h0为基础,产生另一个哈希地址h2,直到找出一个不冲突的地址hi,将相应元素存入其中
线性探测【mod表长而不是mod元素个数】再散列的有优点:只要哈希表不满,则一定可以找到一个位置。但二次探测和伪随机探测不一定。
· 再哈希法
同时构造多个不同的哈希函数,但H1冲突时,再计算H2,直到不发生冲突
· 链地址法
将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
· 建立公共溢出区
将哈希表分为 基本表 和 溢出表,凡是和基本表发生冲突的元素一律填入溢出表
哈希表的查找过程
哈希表的查找过程与创建过程是一致的。
① 首先计算 h0 = hash(K)
② 如果单元h0为空,则所查元素不存在
③ 如果单元h0中元素的关键字为K,则找到所查元素
④ 否则重复下述解决冲突的过程:
a.按解决冲突的方法,找出下一个哈希地址hi
b.如果单元hi为空,则所查元素不存在
c.如果单元hi中元素关键字为K,则找到所查元素