目录
数据结构-查找
概念
- 查找:在数据集合寻找满足某种条件的数据元素的过程。结果只有成功和失败。
- 查找表(查找结构):用于查找的数据集合,一般由同种类型的数据元素构成。
- 静态查找表:不会对查找表进行插入和删除操作的查找表
- 动态查找表:回去查找表进行插入和删除操作的查找表。
- 关键字:数据元素中唯一标识该元素的某个数据项的值。比如学生元素的学号项。
- 平均查找长度:查找过程中平均比较关键字的次数。这是比较查询性能的主要指标。
\[ASL=\sum_{i=1}^n P_iC_i\]
其中P是查找低i个数据元素的概率,一般认为概率相同即1/n。C是找到第i个数据元素所需要的比较次数。
线性查找
顺序查找
对表进行之间的扫描
int Search(int a[], int len, int k){
for(int i=0;i<len-1;i++)
if(a[i]==k)
return i;
return -1;
}
\[ ASL_{成功}=\sum P_i(n-i+1)=\frac{n+1}{2} \]
\[ ASL_{失败}=n+1 \]
折半查找
int Search(int a[], int len, int k){
int low=0, high=len-1, mid;
while(low<=high){
mid=(low+high)/2;
if(a[mid]==k) return mid;
else if(a[mid]>k) high=mid-1;
else low=mid+1;
}
return -1;
}
查找的过程类似于二叉查找树,所以可以引入二叉树来描述,称为判定树。
查找次数不会超过树的深度。
\[ ASL= \frac { 1 } { n } \sum _ { i = 1 } ^ { n } l _ { i } = \frac { 1 } { n } \left( 1 \times 1 + 2 \times 2 + \cdots + h \times 2 ^ { h - 1 } \right) = \frac { n + 1 } { n } \log _ { 2 } ( n + 1 ) - 1 \approx \log _ { 2 } ( n + 1 )-1 \]
分块查找
分块查找将查找表分为若干个块,为每个区块建立一个索引,确定下标的上下界后使用顺序查找。查找索引使用二分法。
若索引查找和块内查找的ASL为Li和Ls,那么分块查找的ASL:
\[ ASL=L_i+L_s \]
二叉查找树
定义
二叉查找树又称二叉排序树(BST)。是一种特殊性质的二叉树,其中左子树的结点的关键字都小于根结点,右子树大于根结点。
typedef struct BNode{
int key;
struct BNode *lchild;
struct BNode *rchild;
}BNode, *BTree;
查找
BNode* BSTSearch(BTree T, int key){
if(T==NULL) return NULL;
if(T->key==key) return T;
if(key<T->key) return BSTSearch(T->lchild, key);
else return BSTSearch(T->rchild, key);
}
插入
int BSTInsert(BTree& T, int key){
if(T==NULL){
T=(BTree)malloc(sizeof(BNode));
T->key=key;
T->lchild=T->rchild=NULL;
return 1;
}
else if(key=T->key) return 0;
else if(k<T->key) return BSTInsert(T->lchild, key);
else return BSTInsert(T->rchild, key);
}
构造
void CreateBST(BTree &T, int[] a, int len){
T=NULL;
int i=0;
while(i<n){
BSTInsert(T, a[i]);
i++;
}
}
删除
删除这里一般不会考代码,知道有那么三种情况即可。
- 删除结点为叶结点,直接删除。
- 删除结点有一棵左子树或右子树,那么直接让子树成为删除结点父结点的子树。
- 删除结点有左右两棵子树,并沿着左子树的右指针一直向右或者右子树的左指针一直向左,找到替代结点,将删除结点与替代结点的值交换,此时情况一定会变为1或2,判断并处理。
第三种情况可能有点难理解,结合图来理解一下。
二叉平衡树
二叉搜索的搜索时间取决于树的长度,于是可能会出现一种极端的情况。
定义
二叉平衡树(AVL)是一种删除和插入结点时任意结点的左右子树相差不会超过1的二叉搜索树。这样可以增强搜索的性能。
定义平衡因子:左子树和右子树的高度差。
图中结点的值为其平衡因子:
插入
保证平衡的想法就是:当我们插入或者删除结点的时候,先检查插入路径上的结点的平衡因子的绝对值是否大于1。如果是,找到离插入点最近的一个绝对值大于1的结点,对其进行调整。
即我们调整的对象都是最小的不平衡树。
对于平衡操作这里分了四种情况讨论:
右单(LL)旋转
情况:A的左孩(L)的左子树(L)插入新结点。
操作:B右旋(R),BR替换AL。
左单(RR)旋转
情况:A的右孩(R)的右子树(R)插入新结点。
操作:B左旋(L),BL替换AR。
先左后右(LR)旋转
情况:A的左孩(L)的右子树(R)。
操作:将A的左孩子的右子树的结点C(BR)先左后右旋转到A。(注意这里的操作对于的都是上面的操作的复合)。
先右后左(RL)旋转
情况:A的右孩(R)的左子树(L)。
操作:将A的右孩的左子树的结点C(BL)先右后左旋转A
记住所有调整都是为了给插入结点空出位置来的,按照这个思路记忆会方便很多。
B树
定义
B树可以视作为二叉平衡树的一种拓展,也称多路平衡搜索树。
满足如下特性:(m作为B树的阶数,一般≥3)
- 非叶结点的根结点至少有两个子结点。
- 除根结点及叶结点最少有[m/2]棵子树,最多有 m 棵子树。
- 所有非叶结点结构为:\(n,P_0,K_1,P_1, \cdots, K_n,P_n\)。
- K 升序排列的关键字。
- P 为指向子树结点的指针。
- n 为结点关键字个数。
- \(p_i\)所指的子结点的所有关键字大于\(k_i\)且小于\(k_{i+1}\)。
- \(p_0\)所指结点小于\(K_1\),\(p_n\)所指大于\(k_n\)。
- 叶结点都处在同一层,且都为空。
查找
B 树的查找是跟二叉树类似的多路查找。
分为两步:
- 在 B 树内寻找结点。(磁盘中)
- 在结点内寻找关键字。(内存中)
由于 B 树一般用于数据库中,即在根据指针在磁盘中找到结点并读到内存中,再在内存中对有序表进行二分搜索,找不到就根据指针读下一个结点,一直找到叶结点为止。
插入
- 定位:利用上述的查找算法找到关键字最底层的某个非叶结点(终端结点)。
- 插入:当插入关键字后若结点关键字数量大于 m-1 则对结点进行分裂,否则正常插入。
分裂
- 将插入关键字后的原结点从中间位置[m/2]分裂成两个部分。
- 左边置于原结点。
- 右边置于新结点。
- 中间放到父结点。
- 检查父结点是否溢出,如果溢出重复这个操作。
删除
这里分删除的节点是否在终端节点来讨论。
删除的关键在于要使得结点的关键字数量≥[m/2]-1。
在终端节点上,分三类情况:
- 直接删除:结点内的关键字个数大于[m/2]-1,直接删除。
- 兄弟够借:结点内的关键字个数等于[m/2]-1,并且其左右兄中存在关键字个数大于[m/2]-1 的节点,则从中借关键字。
- 兄弟不够借:如果 2 情况中左右兄的节点都借不到关键字,则将关键字删除后与左右兄或者双亲节点进行合并,这种合并操作可能会重复。
不在终端节点上,这种情况要转化为上面那种情况来讨论:
即我们将要删除的非终端节点与终端节点进行交换,再按终端节点的方式进行删除。
这里引入一个相邻关键字的概念。对于不在终端节点上的结点来说,其相邻关键字为左子树中值最大的关键字和右子树中最小的关键字。
找相邻关键字的方法与找二叉排序树中前驱和后继的方法类似。
沿着左子树右指针或者右子树左指针一直到终端结点就是相邻关键字了。
B+树
定义
一棵m阶的 B+树满足:
- 每个结点最多有 m 棵子树。
- 非叶根节点至少有2棵子树,其他至少有[m/2]棵子树。
- 节点子树个数与关键字个数相同
- 所有叶节点包含全部关键字以及指向相应记录的指针,且关键字顺序排列。
- 相邻叶节点互相连接
- 所有分支节点都只是索引
区别
B+树与 B 树的主要区别在于:
- B+树中 n 个关键字对于 n 个子树,B 树中 n 个关键字对应 n+1 个子树。
- 在 B+树中,只有叶节点包含信息。
散列表
概念
- 散列函数,将查找关键字映射成对应地址的函数。记为 Hash(key)=Addr。
- 冲突/碰撞,散列函数将不同关键字映射到同一个地址的情况。
- 散列表,根据关键字直接访问的数据结构。
即理想情况下,散列表的查找复杂度应该为 O(1)。
散列函数构造
散列函数的构造要求:
- 散列函数的定义域必须包含所有要存储的关键字。
- 散列函数的计算出来的地址应该等概率均匀地分布在整个地址空间,减少冲突的发生。
- 散列函数应该尽量简单,容易计算。
下面是一些常用的散列函数:
- 直接定址法
- \(H(key)=a\times key+b\)
- 简单并且不会冲突,但是会浪费空间。
- 除留余数法
- \(H(key)=key\%p\)
冲突解决
一般来说,散列函数不可能避免冲突,所以会对 key 进行再散列,用Hi表示第 i 次探测到的散列地址。
开放定址法
开放定址法即如果发生了冲突,可以通过一个递推公式一直递推去找空闲的空间。
递推公式为:
\[H_i=(H(key)+d_i)\%m\]
其中 m 表示散列表表长,di为增量的一个序列。
- 线性探测法
- di=0,1,2,3,……,m-1。
- 即冲突发送时,沿着表的下一项查找可用的空间。
- 缺点在于这种方法容易发生堆积的问题。
- 平方探测法
- di=1^2,2^2……
- 再散列法
- di=Hash2(key)
- 当冲突发生时,使用另外一个散列函数进行再探测。
- 伪随机序列法
- di为伪随机序列。
在开放定值法的情况下, 物理表中的元素不能随意删除,因为删除元素会截断其他具有相同散列地址的元素的查找地址。暂时只能在逻辑上删除。
拉链法
为了避免同义词冲突,可以把所有同义词存储在一个线性表中。
如果关键词序列为{19,14,23,01,68,20,84,27,55,11,10,79}
散列函数为 H(key)=key%13。
拉链法图示为:
性能分析
散列表的性能一般取决于三个因素,散列函数,处理冲突,和装填因子。
装填因子 α = 表中记录 n / 散列长度 m。
字符模式匹配
串的模式匹配即求模式串在主串中的位置。
简单匹配
int Index(String S,String T){
for(int i=0;i<T.len;i++)
for(int j=0;j<S.len;j++){
if(S[j]=T[i])
if(j==S.len-1)
return TRUE;
else
break;
}
return FALSE;
}
复杂度为:O(n*m),n 和 m 分别为主串和模式串的长度。
KMP匹配
void GetNext(String S, int next[]){
int i=1,j=0;
next[1]=0;
while(i<S.len){
if(j==0||S.ch[i]==S.ch[j]){
i++;j++;
next[i]=j;
}
else
j=next[j];
}
}
int KMP(String S, String T, int next[])
{
int i=1;j=1;
while(i<T.len && j<=S.len){
if(j==0||T.ch[i]==S.ch[j]){
i++;
j++;
}
else{
j=next[j];
}
if(j>S.len)
return i-S.len;
else
return 0;
}
}