DS博客作业05--查找
0.PTA得分截图
1.本周学习总结(0-4分)
1.1 总结查找内容
顺序查找:
原理:
对于任意一个序列以及一个给定的元素,将给定元素与序列中元素依次比较,
直到找出与给定关键字相同的元素,或者将序列中的元素与其都比较完为止。
代码实现:
int search(int a[],int len, int x)
{
int i;
for (i=0; i<len; i++)
{
if(x==a[i])
return i; // 返回元素的下标
}
return -1; // 没有找到
}
时间复杂度:O(n)
ASL计算:
ASLsuccess:1/n[n(n+1)/2]=(n+1)/2;
ASLunsuccess:n。
二分查找:
原理:
二分查找又称折半查找,折半查找方法适用于不经常变动而查找频繁的有序列表。
操作:
首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;
否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,
则进一步查找前一子表,否则进一步查找后一子表。
重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
代码实现:
int binarySearch(int a[],int len, int x)
{
int mid; // 中间下标
int low=0; // 区间的左端下标
int high=len-1; // 区间的右端下标
while(low <= high)
{
mid = low + (high-low)/2; // 计算中间下标
if(x==a[mid])
return mid; // 若找到返回元素的下标
else if(x>a[mid])
low=mid+1; // 在右半边区间搜索
else
high=mid-1; // 在左半边区间搜索
}
return -1; // 没有找到
}
时间复杂度:O(logn)
ASL计算:
ASLsuccess:lg(n+1)-1;
ASLunsuccess:lg(n+1)。
二叉搜索树:
概念:
二叉查找树,它或者是一棵空树,或者是具有下列性质的二叉树:
若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
它的左、右子树也分别为二叉排序树。
原理:
通常采取二叉链表作为二叉排序树的存储结构。
中序遍历二叉排序树可得到一个关键字的有序序列,
一个无序序列可以通过构造一棵二叉排序树变成一个有序序列,构造树的过程即为对无序序列进行排序的过程。
每次插入的新的结点都是二叉排序树上新的叶子结点,
在进行插入操作时,不必移动其它结点,只需改动某个结点的指针,由空变为非空即可。
时间复杂度:
查找、插入和删除的复杂度均为O(log(n)).
构建:
举例:
给出一组数据:38 26 62 94 35 50 28 55
第一步:把第一个元素作为根节点,如下图所示:
第二步:把第二个元素拿出来与第一个元素做比较,如果比根节点大就放在右边,如果比根节点小就放在左边,如下图所示:
第三步:同理得,如下图所示:
第四步:插入第四个元素94,先与38比较,进入右子树,然后与62比较,如下图所示:
最后(此处跳过n步),同理得:
代码实现:
TreeNode* Create(TreeNode* root, int n)
{
if (root == NULL)
{
root = new TreeNode(n); //根节点为空,插入,返回。
return root;
}
else
{
if (n <= root->data) //小于等于根节点,递归插入左子树
{
root->lchild = Create(root->lchild, n);
}
else //大于根节点,递归插入右子树
{
root->rchild = Create(root->rchild, n);
}
}
return root;
}
插入:
操作:
由于二叉搜索树的特殊性质确定了二叉搜索树中每个元素只可能出现一次,
所以在插入的过程中如果发现这个元素已经存在于二叉搜索树中,就不进行插入。
否则就查找合适的位置进行插入。
代码实现:
Insert(int X,int T) //搜索树的插入
{
if(T == NULL)
{
T = new TreeNode;
if(T == NULL)
return 0;
else
{
T->data=X;
T->left=T->right=NULL;
}
}
if (X < T->data)
T->left = Insert(X,T->left);
else if (X > T->data)
T->right = Insert(X,T->right);
return T;
}
删除:
操作:
如果要删除的元素都不在树中,就直接返回false;
否则,分为以下四种情况:
要删除的节点无左右孩子
要删除的节点只有左孩子
要删除的节点只有右孩子
要删除的节点有左、右孩子
解决方案:
对于第一种情况,我们完全可以把它归为第二或者第三种情况,
如果要删除的节点只有左孩子,那么就让该节点的父亲结点指向该节点的左孩子,然后删除该节点,返回true;
如果要删除的节点只有右孩子,那么就让该节点的父亲结点指向该节点的右孩子,然后删除该节点,返回true;
如果要删除的节点的左右孩子都存在,那么找到该节点的右子树中的最左孩子(也就是右子树中序遍历的第一个节点),
把它的值和要删除的节点的值进行交换,然后删除这个节点即相当于把我们想删除的节点删除了,返回true;
代码实现:
bool delete_BST(pnode p, int x) //返回一个标志,表示是否找到被删元素
{
bool find = false;
pnode q;
p = BT;
while(p && !find)//寻找被删元素
{
if(x == p->val)//找到被删元素
find = true;
else if(x < p->val) //沿左子树找
{
q = p;
p = p->lchild;
}
else //沿右子树找
{
q = p;
p = p->rchild;
}
}
if(p == NULL) //没找到
cout << "没有找到" << x << endl;
if(p->lchild == NULL && p->rchild == NULL) //p为叶子节点
{
if(p == BT) //p为根节点
BT = NULL;
else if(q->lchild == p)
q->lchild = NULL;
else
q->rchild = NULL;
free(p); //释放节点p
}
else if(p->lchild == NULL || p->rchild == NULL) //p为单支子树
{
if(p == BT) //p为根节点
if(p->lchild == NULL)
BT = p->rchild;
else
BT = p->lchild;
else
{
if(q->lchild == p && p->lchild) //p是q的左子树且p有左子树
q->lchild = p->lchild; //将p的左子树链接到q的左指针上
else if(q->lchild == p && p->rchild)
q->lchild = p->rchild;
else if(q->rchild == p && p->lchild)
q->rchild = p->lchild;
else
q->rchild = p->rchild;
}
free(p);
}
else//p的左右子树均不为空
{
pnode t = p;
pnode s = p->lchild; //从p的左子节点开始
while(s->rchild) //找到p的前驱,即p左子树中值最大的节点
{
t = s;
s = s->rchild;
}
p->val = s->val; //把节点s的值赋给p
if(t == p)
p->lchild = s->lchild;
else
t->rchild = s->lchild;
free(s);
return find;
}
AVL树:
定义:
在计算机科学中,AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下都是O(log n)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。
四种失衡状态及调整做法:
如果在AVL树中进行插入或删除节点,可能导致AVL树失去平衡,这种失去平衡的二叉树可以概括为四种姿态:LL(左左)、RR(右右)、LR(左右)、RL(右左)。
LL:LeftLeft,也称“左左”。插入或删除一个节点后,根节点的左孩子的左孩子还有非空节点,导致根节点的左子树高度比右子树高度高2,AVL树失去平衡。
RR:RightRight,也称“右右”。插入或删除一个节点后,根节点的右孩子的右孩子还有非空节点,导致根节点的右子树高度比左子树高度高2,AVL树失去平衡。
LR:LeftRight,也称“左右”。插入或删除一个节点后,根节点的左孩子的右孩子还有非空节点,导致根节点的左子树高度比右子树高度高2,AVL树失去平衡。
RL:RightLeft,也称“右左”。插入或删除一个节点后,根节点的右孩子的左孩子还有非空节点,导致根节点的右子树高度比左子树高度高2,AVL树失去平衡。
具体情况如下图所示:
LL调整:
1、将根节点的左孩子作为新根节点。
2、将新根节点的右孩子作为原根节点的左孩子。
3、将原根节点作为新根节点的右孩子。
如下图所示:
RR调整:
1、将根节点的右孩子作为新根节点。
2、将新根节点的左孩子作为原根节点的右孩子。
3、将原根节点作为新根节点的左孩子。
如下图所示:
LR调整:
1、围绕根节点的左孩子进行RR旋转。
2、围绕根节点进行LL旋转。
如下图所示:
RL调整:
1、围绕根节点的右孩子进行LL旋转。
2、围绕根节点进行RR旋转。
如下图所示:
B-树和B+树:
定义:
B-tree树即B树,B即Balanced,平衡的意思。因为B树的原英文名称为B-tree,而国内很多人喜欢把B-tree译作B-树,其实,这是个非常不好的直译,很容易让人产生误解。如人们可能会以为B-树是一种树,而B树又是另一种树。而事实上是,B-tree就是指的B树。特此说明。
B+ 树是一种树数据结构,是一个n叉排序树,每个节点通常有多个孩子,一棵B+树包含根节点、内部节点和叶子节点。根节点可能是一个叶子节点,也可能是一个包含两个或两个以上孩子节点的节点。
B-树的插入:
原理:
如果B树中已存在需要插入的键值对,则用需要插入的value替换旧的value。若B树不存在这个key,则一定是在叶子结点中进行插入操作。
1、根据要插入的key的值,找到叶子结点并插入。
2、判断当前结点key的个数是否小于等于m-1,若满足则结束,否则进行第3步。
3、以结点中间的key为中心分裂成左右两部分,然后将这个中间的key插入到父结点中,
这个key的左子树指向分裂后的左半部分,这个key的右子支指向分裂后的右半部分,然后将当前结点指向父结点,继续进行第3步。
举例:
下面以5阶B树为例,用关键字序列{1,2,6,7,11,4,8,13,10,5,17,9,16,20,3,12,14,18,19,15}来构建一棵B-树,
介绍B树的插入操作,在5阶B树中,结点最多有4个key,最少有2个key
第一步是插入1,2,6,7作为一个节点。然后插入11,得到1,2,6,7,11.
因为节点个数超过4,所以需要对该节点进行拆分。选取中间节点6,进行提升,提升为父节点,于是得到:
接着插入4,8,13,直接插入即可,得到
然后插入10. 得到
因为最右下的节点内有5个元素,超过最大个数4了,所以需要进行拆分,把中间节点10进行提升,上升到和6一起,如下图所示:
然后插入5,17,9,16,得到如下:
之后插入20,同理得:
之后插入3、12、14、18、19,后,如下图所示:
然后插入15,同理得:
B-树的删除:
计划删除:8,16,15,4
直接上图,原树如下图所示:
首先删除8,因为删除8后,不破坏树的性质,所以直接删除即可。如下图所示:
然后删除16,这导致该节点只剩下一个13节点,不满足节点内元素个数为2~4个的要求了,所以需要调整。
这里可以向孩子借节点,把17提升上来即可,得到下图。
这里不能和兄弟节点借节点,因为从3,6节点中把6借走后,剩下的3也不满要求了。
另外,也不能把孩子中的15提升上来,那样会导致剩下的14不满足要求。
然后删除15,删除15后同样需要调整。调整的方式是,18上升,17下降到原来15的位置,得到下图。
然后删除元素4,删除4后该节点只剩下5,需要调整。
可是它的兄弟节点也都没有多余的节点可借,所以需要进行节点合并。
节点合并时,方式会有多种,我们选择其中的一种即可。
这里,我们选择父节点中的3下沉,和1,2,以及5进行合并,如下图:
但这次调整,导致6不符合要求了。另外,6非根节点,但只有2个孩子,也不符合要求,需要继续调整。
调整的方式是,将10下沉,和6,以及13,18合并为根节点,如下图:
散列查找:
基本概念:
在进行查找时,在记录的存储位置与它的关键字之间建立一个确定的对应关系h,
以线性表中每个元素的关键字K为自变量,通过函数h(K)计算出该元素的存储位置,
我们将h函数称为散列函数或哈希函数。这种查找方法称为散列查找。
构造方法:
- 直接定址法:
所谓直接定址法就是说,取关键字的某个线性函数值为散列地址,即f(key)=axkey+b(a,b均为常数)
优点:简单、均匀,也不会产生冲突。
缺点:需要事先知道关键字的分布情况,适合查找表较小且连续的情况。 - 数字分析法:
如果现在要存储某家公司的登记表,若用手机号作为关键字,极有可能前7位都是相同的,选择后四位成为散列地址就是不错的选择。
若容易出现冲突,对抽取出来 的数字再进行反转、右环位移等。
总的目的就是为了提供一个散列函数,能够合理地将关键字分配到散列表的各个位置。
数字分析法通过适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布比较均匀,就可以考虑用这个方法。
3.除留余数法:
此方法为最常用的构造散列函数方法。对于散列表长为m的散列函数公式为:f(key)=key mod p(p<=m)
mod是取模(求余数)的意思。事实上,这方法不仅可以对关键字直接取模,也可以再折叠、平方取中后再取模。
很显然,本方法的关键在于选择合适的p,p如果选不好,就可能会容易产生冲突。
处理散列冲突的方法:
在理想的情况下,每一个关键字,通过散列函数计算出来的地址都是不一样的,可现实中,这只是一个理想。
市场会碰到两个关键字key1 != key2,但是却有f(key1) = f(key2),这种现象称为冲突。
出现冲突将会造成查找错误,因此可以通过精心设计散列函数让冲突尽可能的少,但是不能完全避免。
开放定址法:
所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
1.线性探测法:
所用公式为:
d0=h(k)
di=(di-1+1) mod m (1≤i≤m-1)
哈希函数值不相同的两个记录争夺同一个后继哈希地址->堆积(或聚集)现象。
2.平方探测法:
所用公式为:
d0=h(k)
di=(d0± i2) mod m (1≤i≤m-1)
查找的位置依次为:d0、 d0 +1、 d0 -1 、 d0 +4、 d0 -4、……
平方探查法是一种较好的处理冲突的方法,可以避免出现堆积现象。
它的缺点是不能探查到哈希表上的所有单元,但至少能探查到一半单元。
哈希表和哈希链:
哈希表:
基本概念:
散列表(哈希表),是根据关键码值而直接进行访问的数据结构。
也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。
这个映射函数叫做散列函数,存放记录的数组叫做散列表。
创建(代码实现):
void CreateHT(HashTable ha,KeyType x[],int n,int m,int p){
int i,n1=0;
for (i=0;i<m;i++) //哈希表置初值
{
ha[i].key=NULLKEY;
ha[i].count=0;
}
for(i=0;i<n;i++)
InsertHT(ha,n1,x[i],p);
}
AVL计算:
举个例子吧:
AVL=(1+2+1+1+1+3+3)/7=12/7
哈希链:
基本概念:
哈希链是将密码学中的哈希函数h(x)循环地用于一个字符串。(即将所得哈希值再次传递给哈希函数得至其哈希值)。
代码实现(伪代码,偷个懒):
void CreateHash(HashTable ht[],int n)
{
建n条带头结点的哈希链,初始化链表;
for i=1 to k:
输入数据data
InsertHash(ht,data)
}
AVL计算:
同样,直接上图:
AVLsuccess:(19+22)/11=13/11
AVLunsuccess:(17+22)/13=11/13
1.2.谈谈你对查找的认识及学习体会。
查找的内容和前两章树和图相比,要简单许多,在二次搜索树虽然也要用到树,但是也是很简单的树的操作;
查找的方法有很多,上学期就基本学过的顺序表的查找(数组嘛),二分法查找,还有二叉搜索树,哈希查找.
每种查找方法都有自己的利与弊,有的效率高有的操作简单,使用时要灵活。
2.PTA题目介绍(0--6分)
2.1 题目1(2分)
2.1.1 该题的设计思路
首先,根据先序遍历的序列,按照按照二叉搜索树的方法进行建树,得到的先序遍历和输入的先序遍历是一样的
其次,按照根左右的办法进行遍历(LCA)
2.1.2 该题的伪代码
int LCA(Tree T, int u, int v)
{
flagL,flagR用来标记u,v是否在树中,初始化为1
if u不在树中
then flagL置为-1
end if;
if v不在树中
then flagR置为-1
end if;
if flagR,flagL都为-1
then 输出提示
end if;
if flagR为-1
then 输出提示
end if;
if flagL为-1
then 输出e提示
end if;
if u>v
then 将u与v互换
end if;
while T!=NULL
if 树节点小于u
then 在右子树找
end if;
else if 树节点大于v
then 在左子树找
end if;
else
返回公共祖先
end while;
}
代码:
2.1.3 PTA提交列表
Q1:答案错误及多种答案错误;
A1:低级错误,for循环条件打错了(手误吧),看了好久,如下图所示:
原:
改:
Q2:部分正确,在测试点,3,4处,总是会运行超时;
A2:在建树方面出现问题,即用每次都要遍历出一条路径,在叶子节点插入的方法建树,低效;用改后的方法建树,复杂度更低,不易超时。
2.1.4 本题设计的知识点
1.如何更高效地用二叉搜索树的先序遍历建树
2.查找两个节点的最近公共祖先
2.2 题目2(2分)
2.2.1 该题的设计思路
输入数据然后根据二叉搜索树的插入操作建树。
2.2.2 该题的伪代码
bool VisitJdg(Tree root)
{
定义队列q;
if root
入队
while 队非空
出队;
输出数据;
if t->left
入队;
end if;
if t->right
入队;
end if;
if 队非空
输出“ ”
end if;
if (如果x 有左子树,但是却没有右子树)|| ( 如果 x 左右子树都没有)
&&剩余节点为叶子节点
为完全二叉树;
end if;
else
不是完全二叉树;
end while;
end if;
}
代码:
2.2.3 PTA提交列表
Q1:编译错误;
A1:这个属于低级问题;
Q2:该部分为在vs调试中发现的问题;
A2:在判断是否为完全二叉树的条件部分,疏漏了以下情况,未考虑到其余节点为叶子节点的条件
2.2.4 本题设计的知识点
判断是否完全二叉树的要点:
我们按照层次顺序遍历这颗树的过程中,对于任意一节点x
如果x 有右子树,但是却没有左子树,这肯定不是完全二叉树
(如果x 有左子树,但是却没有右子树)|| ( 如果 x 左右子树都没有)[其实这两种情况可以统一为一种情况,即没有右子树]
那么剩余的所有节点一定要为叶子节点
2.3 题目3(2分)
2.3.1 该题的设计思路
用两个map容器,一个用于检验账号是否存在,一个用于检验账号与密码是否匹配
2.3.2 该题的伪代码
定义map容器umExist;//检验是否存在账号
定义map容器umPass;//检验账号密码是否匹配
输入指令次数;
while n--
输入命令符,账号,密码;
检验是否存在账号;
检验账号密码是否匹配;
账号申请;
end while;
代码:
2.3.3 PTA提交列表
Q:答案错误;
A:对于map容器的使用不到位,且在条件判断时,遗漏申请账号的模块
2.3.4 本题设计的知识点
初步了解map容器的使用。