DS博客作业05--查找
| 这个作业属于哪个班级 | 数据结构--网络2011/2012 |
| ---- | ---- | ---- |
| 这个作业的地址 | DS博客作业05--查找 |
| 这个作业的目标 | 学习查找的相关结构 |
| 姓名 |廖浩轩|
0.PTA得分截图
1.本周学习总结
1.1 查找的性能指标
ASL(Average Search Length),即平均查找长度,在查找运算中,由于所费时间在关键字的比较上,所以把平均需要和待查找值比较的关键字次数称为平均查找长度。
其中n为查找表中元素个数,Pi为查找第i个元素的概率,通常假设每个元素查找概率相同,Pi=1/n,Ci是找到第i个元素的比较次数。
当然,有查找成功,就有查找不成功,即要查找元素不在查找表中。
一个算法的ASL越大,说明时间性能差,反之,时间性能好,这也是显而易见的。
1.2 静态查找
顺序查找(Sequence Search)表中,查找方式为从头扫到尾,找到待查找元素即查找成功,若到尾部没有找到,说明查找失败。所以说,Ci(第i个元素的比较次数)在于这个元素在查找表中的位置,如第0号元素就需要比较一次,第一号元素比较2次......第n号元素要比较n+1次。所以Ci=i;所以
可以看出,顺序查找方法查找成功的平均 比较次数约为表长的一半。当待查找元素不在查找表中时,也就是扫描整个表都没有找到,即比较了n次,查找失败
二分查找(Binary Search)首先待查找表是有序表,这是折半查找的要求。在折半查找中,用二叉树描述查找过程,查找区间中间位置作为根,左子表为左子树,右子表为右子树,,因为这颗树也被成为判定树(decision tree)或比较树(Comparison tree)。查找方式为(找k),先与树根结点进行比较,若k小于根,则转向左子树继续比较,若k大于根,则转向右子树,递归进行上述过程,直到查找成功或查找失败。在n个元素的折半查找判定树中,由于关键字序列是用树构建的,所以查找路径实际为树中从根节点到被查结点的一条路径,因为比较次数刚好为该元素在树中的层数。
Pi为查找k的概率,level(Ki)为k对应内部结点的层次。
而在这样的判定树中,会有n+!种查找失败的情况,因为将判定树构建为完全二叉树,又有n+1个外部结点(用Ei(0<=i<=n)表示),查找失败,即为从根结点到某个外部结点也没有找到,比较次数为该内部结点的结点数个数之和
qi表示查找属于Ei中关键字的概率,level(Ui)表示Ei对应外部结点的层次。
1.3 二叉搜索树
1.3.1 如何构建二叉搜索树(操作)
定义:二叉搜索树是一种节点值之间具有一定数量级次序的二叉树,对于树中每个节点:
- 若其左子树存在,则其左子树中每个节点的值都不大于该节点值;
- 若其右子树存在,则其右子树中每个节点的值都不小于该节点值。
查询复杂度:观察二叉搜索树结构可知,查询每个节点需要的比较次数为节点深度加一。如深度为 0,节点值为 “6” 的根节点,只需要一次比较即可;深度为 1,节点值为 “3” 的节点,只需要两次比较。即二叉树节点个数确定的情况下,整颗树的高度越低,节点的查询复杂度越低。
构造复杂度:二叉搜索树的构造过程,也就是将节点不断插入到树中适当位置的过程。该操作过程,与查询节点元素的操作基本相同,不同之处在于:
- 查询节点过程是,比较元素值是否相等,相等则返回,不相等则判断大小情况,迭代查询左、右子树,直到找到相等的元素,或子节点为空,返回节点不存在
- 插入节点的过程是,比较元素值是否相等,相等则返回,表示已存在,不相等则判断大小情况,迭代查询左、右子树,直到找到相等的元素,或子节点为空,则将节点插入该空节点位置。
由此可知,单个节点的构造复杂度和查询复杂度相同,为
节点的删除有以下三种情况
- 待删除节点度为零;
- 待删除节点度为一;
- 待删除节点度为二。
第一种情况如下图所示,待删除节点值为 “6”,该节点无子树,删除后并不影响二叉搜索树的结构特性,可以直接删除。即二叉搜索树中待删除节点度为零时,该节点为叶子节点,可以直接删除;
第二种情况如下图所示,待删除节点值为 “7”,该节点有一个左子树,删除节点后,为了维持二叉搜索树结构特性,需要将左子树“上移”到删除的节点位置上。即二叉搜索树中待删除的节点度为一时,可以将待删除节点的左子树或右子树“上移”到删除节点位置上,以此来满足二叉搜索树的结构特性。
第三种情况如下图所示,待删除节点值为 “9”,该节点既有左子树,也有右子树,删除节点后,为了维持二叉搜索树的结构特性,需要从其左子树中选出一个最大值的节点,“上移”到删除的节点位置上。即二叉搜索树中待删除节点的度为二时,可以将待删除节点的左子树中的最大值节点“移动”到删除节点位置上,以此来满足二叉搜索树的结构特性。
节点插入
把一个新的记录R插入到二叉查找树,应该保证在插入之后不破坏二叉查找树的结构性质。因此,为了执行插入操作首先应该查找R所在的位置。查找时,仍然采用上述的递归算法。若查找失败,则把包含R的结点插在具有空子树位置,若查找成功,则不执行插入,操作结束。
1.3.2 如何构建二叉搜索树(代码)
查找
BST Search(keytype k, BST F)//在F所指的二叉查找树中查找关键字为k的记录。若成功,则返回响应结点的指针,否则返回空
{
if(F == NULL) //查找失败
{
return NULL;
}
else if(k == F -> data.key)//查找成功
{
return F;
}
else if (k < F -> data.key)//查找左子树
{
return Search(k,F -> lchild);
}
else if (k > F -> data.key)//查找右子树
{
return Search(k,F -> rchild);
}
}
插入
void Insert(records R, BST &F)//在F所指的二叉查找树中插入一个新纪录R
{
if(F == NULL){
F = new celltype;
F -> data = R;
F -> lchild = NULL;
F -> rchild = NULL;
}
else if (R.key < F -> data.key){
Insert(R,F -> lchild);
}else if(R.key > F -> data.key){
Insert(R,F -> rchild);
}
//如果 R.key == F -> data.key 则返回
}
删除
//删除最小的节点void ABinarySearchTrees::DeleteMin()
{
RootNode = DeleteMin(RootNode);
}
ATreeNode* ABinarySearchTrees::DeleteMin(ATreeNode* X)
{ //如果X的左节点不存在,说明已经找到最小值了,删除它,返回它的右节点
if (!X->GetNode(true))
{
ATreeNode* TempNode = X->GetNode(false);
NodeArray.Remove(X);
X->Destroy(); return TempNode;
} //删除最小值后,把它的右节点和上一个节点连上
X->SetNode(true, DeleteMin(X->GetNode(true))); //更新节点数
X->SetCount(1 + Size(X->GetNode(true)) + Size(X->GetNode(false))); return X;
}void ABinarySearchTrees::Delete(int InputKey)
{
RootNode = Delete(RootNode, InputKey);
}
ATreeNode* ABinarySearchTrees::Delete(ATreeNode* X, int InputKey)
{
if (!X) return nullptr; int Temp = CompareTo(InputKey, X->GetKey()); //寻找要删除的节点,只要删了一个节点,它上面的所有节点都要更新一次,所以是SetNode
if (Temp < 0) X->SetNode(true, Delete(X->GetNode(true), InputKey)); else if (Temp > 0) X->SetNode(false, Delete(X->GetNode(false), InputKey)); //找到要删除的节点了
else
{ //如果要删除的节点只有一个子节点或没有,那好办,只需把那个子节点替代它就好
if (!X->GetNode(false))
{
ATreeNode* TempNode = X->GetNode(true);
NodeArray.Remove(X);
X->Destroy(); return TempNode;
} if (!X->GetNode(true))
{
ATreeNode* TempNode = X->GetNode(false);
NodeArray.Remove(X);
X->Destroy(); return TempNode;
}
//如果要删除的节点有两个个子节点,从它的右边找一个最小的节点来替代它
ATreeNode* T = X;
X = FindMin(T->GetNode(false));
X->SetNode(false, DeleteMin(T->GetNode(false)));
X->SetNode(true, T->GetNode(true));
NodeArray.Remove(T);
T->Destroy();
} //更新节点数
X->SetCount(Size(X->GetNode(true)) + Size(X->GetNode(false)) + 1); return X;
}
1.4 AVL树
AVL树本质上还是一棵二叉搜索树,它的特点是:
1.本身首先是一棵二叉搜索树。
2.带有平衡条件:每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1。
也就是说,AVL树,本质上是带了平衡功能的二叉查找树(二叉排序树,二叉搜索树)。
LL(右旋)
LL的意思是向左子树(L)的左孩子(L)中插入新节点后导致不平衡,这种情况下需要右旋操作,而不是说LL的意思是右旋,后面的也是一样。
private Node rightRotate(Node y){
Node x = y.left;
Node t3 = x.right;
x.right = y;
y.left = t3;
//更新height
y.height = Math.max(getHeight(y.left),getHeight(y.right))+1;
x.height = Math.max(getHeight(x.left),getHeight(x.right))+1;
return x;
}
RR
private Node leftRotate(Node y){
Node x = y.right;
Node t2 = x.left;
x.left = y;
y.right = t2;
//更新height
y.height = Math.max(getHeight(y.left),getHeight(y.right))+1;
x.height = Math.max(getHeight(x.left),getHeight(x.right))+1;
return x;
}
LR
RL
// 向二分搜索树中添加新的元素(key, value)
public void add(E e){
root = add(root, e);
}
// 向以node为根的二分搜索树中插入元素(key, value),递归算法
// 返回插入新节点后二分搜索树的根
private Node add(Node node, E e){
if(node == null){
size ++;
return new Node(e);
}
if(e.compareTo(node.e) < 0)
node.left = add(node.left, e);
else if(e.compareTo(node.e) > 0)
node.right = add(node.right, e);
//更新height
node.height = 1+Math.max(getHeight(node.left),getHeight(node.right));
//计算平衡因子
int balanceFactor = getBalanceFactor(node);
if(balanceFactor > 1 && getBalanceFactor(node.left)>0) {
//右旋LL
return rightRotate(node);
}
if(balanceFactor < -1 && getBalanceFactor(node.right)<0) {
//左旋RR
return leftRotate(node);
}
//LR
if(balanceFactor > 1 && getBalanceFactor(node.left) < 0){
node.left = leftRotate(node.left);
return rightRotate(node);
}
//RL
if(balanceFactor < -1 && getBalanceFactor(node.right) > 0){
node.right = rightRotate(node.right);
return leftRotate(node);
}
return node;
}
1.5 B-树和B+树
B-树:
B树也称B-树,它是一颗多路平衡查找树。我们描述一颗B树时需要指定它的阶数,阶数表示了一个结点最多有多少个孩子结点,一般用字母m表示阶数。当m取2时,就是我们常见的二叉搜索树。
一颗m阶的B树定义如下:
1)每个结点最多有m-1个关键字。
2)根结点最少可以只有1个关键字。
3)非根结点至少有Math.ceil(m/2)-1个关键字。
4)每个结点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它。
5)所有叶子结点都位于同一层,或者说根结点到每个叶子结点的长度都相同。
B+树
各种资料上B+树的定义各有不同,一种定义方式是关键字个数和孩子结点个数相同。这里我们采取维基百科上所定义的方式,即关键字个数比孩子结点个数小1,这种方式是和B树基本等价的。上图就是一颗阶数为4的B+树。
除此之外B+树还有以下的要求。
1)B+树包含2种类型的结点:内部结点(也称索引结点)和叶子结点。根结点本身即可以是内部结点,也可以是叶子结点。根结点的关键字个数最少可以只有1个。
2)B+树与B树最大的不同是内部结点不保存数据,只用于索引,所有数据(或者说记录)都保存在叶子结点中。
3) m阶B+树表示了内部结点最多有m-1个关键字(或者说内部结点最多有m个子树),阶数m同时限制了叶子结点最多存储m-1个记录。
4)内部结点中的key都按照从小到大的顺序排列,对于内部结点中的一个key,左树中的所有key都小于它,右子树中的key都大于等于它。叶子结点中的记录也按照key的大小排列。
5)每个叶子结点都存有相邻叶子结点的指针,叶子结点本身依关键字的大小自小而大顺序链接。
解决问题:与自平衡二叉查找树不同,B树为系统大块数据的读写操作做了优化。B树减少定位记录时所经历的中间过程,从而加快存取速度。B树这种数据结构可以用来描述外部存储。这种数据结构常被应用在数据库和文件系统的实现上。
B树插入操作
插入操作是指插入一条记录,即(key, value)的键值对。如果B树中已存在需要插入的键值对,则用需要插入的value替换旧的value。若B树不存在这个key,则一定是在叶子结点中进行插入操作。
1)根据要插入的key的值,找到叶子结点并插入。
2)判断当前结点key的个数是否小于等于m-1,若满足则结束,否则进行第3步。
3)以结点中间的key为中心分裂成左右两部分,然后将这个中间的key插入到父结点中,这个key的左子树指向分裂后的左半部分,这个key的右子支指向分裂后的右半部分,然后将当前结点指向父结点,继续进行第3步。
下面以5阶B树为例,介绍B树的插入操作,在5阶B树中,结点最多有4个key,最少有2个key
a)在空树中插入39
此时根结点就一个key,此时根结点也是叶子结点
b)继续插入22,97和41
根结点此时有4个key
c)继续插入53
插入后超过了最大允许的关键字个数4,所以以key值为41为中心进行分裂,结果如下图所示,分裂后当前结点指针指向父结点,满足B树条件,插入操作结束。当阶数m为偶数时,需要分裂时就不存在排序恰好在中间的key,那么我们选择中间位置的前一个key或中间位置的后一个key为中心进行分裂即可。
d)依次插入13,21,40,同样会造成分裂,结果如下图所示。
e)依次插入30,27, 33 ;36,35,34 ;24,29,结果如下图所示。
f)插入key值为26的记录,插入后的结果如下图所示。
当前结点需要以27为中心分裂,并向父结点进位27,然后当前结点指向父结点,结果如下图所示。
进位后导致当前结点(即根结点)也需要分裂,分裂的结果如下图所示。
分裂后当前结点指向新的根,此时无需调整。
g)最后再依次插入key为17,28,29,31,32的记录,结果如下图所示。
在实现B树的代码中,为了使代码编写更加容易,我们可以将结点中存储记录的数组长度定义为m而非m-1,这样方便底层的结点由于分裂向上层插入一个记录时,上层有多余的位置存储这个记录。同时,每个结点还可以存储它的父结点的引用,这样就不必编写递归程序。
一般来说,对于确定的m和确定类型的记录,结点大小是固定的,无论它实际存储了多少个记录。但是分配固定结点大小的方法会存在浪费的情况,比如key为28,29所在的结点,还有2个key的位置没有使用,但是已经不可能继续在插入任何值了,因为这个结点的前序key是27,后继key是30,所有整数值都用完了。所以如果记录先按key的大小排好序,再插入到B树中,结点的使用率就会很低,最差情况下使用率仅为50%。
B树删除操作
删除操作是指,根据key删除记录,如果B树中的记录中不存对应key的记录,则删除失败。
1)如果当前需要删除的key位于非叶子结点上,则用后继key(这里的后继key均指后继记录的意思)覆盖要删除的key,然后在后继key所在的子支中删除该后继key。此时后继key一定位于叶子结点上,这个过程和二叉搜索树删除结点的方式类似。删除这个记录后执行第2步
2)该结点key个数大于等于Math.ceil(m/2)-1,结束删除操作,否则执行第3步。
3)如果兄弟结点key个数大于Math.ceil(m/2)-1,则父结点中的key下移到该结点,兄弟结点中的一个key上移,删除操作结束。
否则,将父结点中的key下移与当前结点及它的兄弟结点中的key合并,形成一个新的结点。原父结点中的key的两个孩子指针就变成了一个孩子指针,指向这个新结点。然后当前结点的指针指向父结点,重复上第2步。
有些结点它可能即有左兄弟,又有右兄弟,那么我们任意选择一个兄弟结点进行操作即可。
下面以5阶B树为例,介绍B树的删除操作,5阶B树中,结点最多有4个key,最少有2个key
a)原始状态
b)在上面的B树中删除21,删除后结点中的关键字个数仍然大于等2,所以删除结束。
c)在上述情况下接着删除27。从上图可知27位于非叶子结点中,所以用27的后继替换它。从图中可以看出,27的后继为28,我们用28替换27,然后在28(原27)的右孩子结点中删除28。删除后的结果如下图所示。
删除后发现,当前叶子结点的记录的个数小于2,而它的兄弟结点中有3个记录(当前结点还有一个右兄弟,选择右兄弟就会出现合并结点的情况,不论选哪一个都行,只是最后B树的形态会不一样而已),我们可以从兄弟结点中借取一个key。所以父结点中的28下移,兄弟结点中的26上移,删除结束。结果如下图所示。
d)在上述情况下接着32,结果如下图。
当删除后,当前结点中只key,而兄弟结点中也仅有2个key。所以只能让父结点中的30下移和这个两个孩子结点中的key合并,成为一个新的结点,当前结点的指针指向父结点。结果如下图所示。
当前结点key的个数满足条件,故删除结束。
e)上述情况下,我们接着删除key为40的记录,删除后结果如下图所示。
同理,当前结点的记录数小于2,兄弟结点中没有多余key,所以父结点中的key下移,和兄弟(这里我们选择左兄弟,选择右兄弟也可以)结点合并,合并后的指向当前结点的指针就指向了父结点。
同理,对于当前结点而言只能继续合并了,最后结果如下所示。
合并后结点当前结点满足条件,删除结束。
1.6 散列查找
哈希表中的ASL查找成功的平均查找长度是指查找到哈希表中已有关键字的平均探测次数。而查找不成功的平均查找长度是指在哈希表中找不到待查的元素,最后找到空位置元素的探测次数平均值。
例如:散列表长度为13,地址空间为0~12,散列函数H(k) =K mod 13,关键字序列{19,14,23,01,68,20,84,27,55,11,10,79} 所以线性探测结果为:
当然成功的很好理解,12个元素,每个元素的探测次数之和除以12就行。而不成功的计算是这样的。散列表长度为13,根据定义,假设待查关键字不在散列表中,要一直找到空元素才算查找失败。
如H[0]为空,与待查找元素不等,不成功,比较一次,H[1],此时H[1]的元素与原本放在H[1]的元素不等(假设不在散列表在之中,但也不是空的),继续向后比,与H[2]比也不等,继续向后,一直到H[12],也不等,继续向后时,回到H[0],为空,也不等,查找失败,总计比较13次,然后计算第二号元素,一样的比较,一直把每个位置都统计一遍,从而得出ASL不成功的。
2.PTA题目介绍
2.1 是否完全二叉搜索树
2.1.1 伪代码
//借鉴了智凯学长的思路
Push(que,根节点); //根结点入队
while(队列不为空)
if(队列头节点为空)
对对应定义变量进行修改并且不在统计节点数
else
输出头节点数据
if(定义变量没被修改)
结点定义变量++
Push(que,左结点);
Push(que,右节点);
end if
//队列头出队
end while
return 结点数
//判断是否相同从而判断是否为完全二叉树
2.1.2 提交列表
2.1.3 本题知识点
建出二叉搜索树,这就需要熟悉二叉搜索树的建立方式,二叉搜索树的建立基础是插入数据,而插入数据的本质是查找,虽然是基础操作,但是也可以加深对二叉搜索树的理解。层序遍历法,层序遍历就好像从根结点开始,一层一层向下扩散搜索,这就跟我们队列实现迷宫算法非常类似,因为迷宫算法的不同路径也是无关联的,但是我们是用广度优先搜索的思想可以找到最短路径。层序遍历需要结合队列结构协同操作,在这里有熟悉了这个遍历手法。完全二叉树的性质,完全二叉树的概念不好理解,但是用“从上到下,从左到右”这个顺序就会变得形象。在这里对完全二叉树的判断提出要求,这就需要理解其特点和性质,同时这也是堆结构的基础,在这里加深理解是很必要的。
2.2 航空公司VIP客户查询
2.2.1 伪代码
定义long long型变量 len存里程,ID存身份证
定义哈希表链的结点指针变量p
定义哈希表数组Hash
对哈希表中每一条链表进行初始化
输入用户数量N和最小里程K
while N-- do
输入ID
if ID小于18位 then getchar() end if //把x给消掉
输入里程len
if len小于K then len=K end if
sum=ID%100000 //哈希函数,sum为下标
p指向下标sum的链表的第一个结点
while p不为空 do
寻找相同身份证号,找到则对里程进行修改
end while
if 找不到相同身份证 then
则新建一个结点,将结点插在下标sum链表头部
end if
end while
输入查询人数N
while N-- do
输入ID
if ID小于18位 then getchar() end if //把x给消掉
sum=ID%100000 //哈希函数,sum为下标
p指向下标sum的链表的第一个结点
while p不为空 do
寻找对应身份证号,找到输出里程并退出循环
end while
if p为空 then 输出找不到 end if
end while
2.2.2 提交列表
2.2.3 本题知识点
哈希链的运用,但结果有一个不对,调试不出来。
然后在网上搜索的时候发现用map来写比较简单,所以最后的答案正确的代码其实是用map来写的
2.3 基于词频的文件相似度
2.3.1 伪代码
//又借鉴了智凯学长的思路
单词在单词索引表中构建映射
for(小于对比组数就循环)
输入文件编号
for(遍历单词)//运用迭代器
if(单词在文件中都出现)
修改重复单词,重新合计单词数
else if(只在一个文件中出现)
重新合计单词数
end if
end for
end for
输出文件相似度
2.3.2 提交列表
2.3.3 本题知识点
把文件按照单词的归属,存储到每个单词的结构中,达到的效果是在一个结构中保存了含有该单词的所有文件。可以使用哈希表来存储,冲突处理使用直接定值法,通过这种手法可以直接确定单词的出现位置。而对于每个单词而言,可以使用哈希链来做,不过这里可以用 STL 库的 map 容器来存放。