DS博客作业05--查找

0.展示PTA总分

1.本周学习总结

1.1 总结查找内容

查找的性能指标ASL

  • 在查找的过程中,其运算时间主要花费在关键字的比较上,所以我们把查找过程中的平均查找长度作为衡量查找算法优劣的标准。
  • 平均查找长度,即ASL,他的定义为:

静态查找

  • 顺序查找
  1. 思路:
    顺序查找的思路很简单,从表的一端开始,按顺序扫描线性表,依次将每一个扫描到的关键字与给定的关键字项k进行比较。如果扫描过程中有关键字与k相等,则查找成功;否则,当扫描完整个线性表之后仍然没有找到与k相等的关键字,则查找失败。如图:
  2. 代码实现:
int SeqSearch(RecType R[],int n,KeyType k)
{     int i=0;
      while (i<n && R[i].key!=k)	//从表头往后找
	i++;
      if (i>=n)			//未找到返回0
	return 0;
      else 
	return i+1;		//找到返回逻辑序号i+1
}
  1. 查找成功与不成功ASL
    (1)如果查找成功,如上图,当查找到第i个记录,此时为A[i-1],我们把信息代入上述的公式,可以得出查找成功的平均查找长度,即ASL(succ)=(n+1)/2。
    (2)如果查找失败,那么此时需要依次遍历完整个线性表,因此查找失败的平均查找长度,即ASL(unsucc)=n。
  • 二分查找
  1. 思路:
    二分查找也被称作折半查找,思路与之前学过的二分法基本相同,需要线性表中的所有记录必须按照关键字值有序排列,通过不断取中间值和选取对应的区间来对记录进行查找。
  2. 代码实现:
int BinSearch(RecType R[],int n,KeyType k)
{ 
       int low=0,high=n-1,mid;
       while (low<=high)		//当前区间存在元素时循环
       {	mid=(low+high)/2;
	if (R[mid].key==k)	//查找成功返回其逻辑序号mid+1
	      return mid+1;
	if (k<R[mid].key)	//继续在R[low..mid-1]中查找
	      high=mid-1;
	else
	      low=mid+1;		//继续在R[mid+1..high]中查找
      }
      return 0;
}
  1. 根据二分查找的特点,我们可以建立一棵二叉树,把每次选取的中间值作为根结点,其左边表的记录记为当前根的左子树;右边表的记录记为当前根的右子树,这样的二叉树我们称其为判定树或者比较树。
  2. 查找成功与不成功ASL
    ASL(succ)=log2(n+1)-1
    ASL(unsucc)=log2n

动态查找:二叉搜索树。如何构建、插入、删除。会操作及代码。

  • 二叉搜索树
  1. 二叉搜索树,也称二叉排序树(BST),是树表的一种。

  2. 二叉搜索树的性质:

    • 若它的左子树非空,则左子树上的所有结点置均小于根结点值;
    • 同理,若它的右子树非空,则其右子树上的所有结点值均大于根结点值;
    • 每个结点其左右子树本身又各是一棵二叉搜索树 ;
    • 二叉搜索树中不存在相同关键字(即结点值相同)的结点。
    • 如果对二叉排序树进行中序遍历,最终会得到一个递增的序列
  3. 结构体的定义

typedef struct node 
{       KeyType key;            	  //关键字项
       InfoType data;          	           //其他数据域
        struct node *lchild,*rchild; 	  //左右孩子指针
}  BSTNode;
  1. 二叉搜索树的构建
BSTree CreatBST(BSTree &t, int N)
{
	
	int i;
        int a[MAXN];
	for (i = 0; i < N; i++)
	{
                cin >> a[i]
		InsertBst(t, a[i]);
	}
	return t;
}

5.插入
(1)由上方的构建代码可以看出,二叉搜索树的构建核心就是结点的插入。
(2)我们的核心思路是将要插入的关键字与当前结点值进行对比,根据大小结果选择进入左子树或者右子树进行递归,直到找到的结点为空结点,此时可以直接插入。

int InsertBst(BSTree& T, int k)
{
	if (T == NULL)
	{
		T = new BSTNode;
		T->key = k;
		T->lchild = T->rchild = NULL;
	}
	else if (k == T->key)
	{
		return 0;
	}
	else if (k < T->key)
	{
		return InsertBst(T->lchild, k);/*插入到左子树*/
	}
	else
	{
		return InsertBst(T->rchild, k);/*插入到右子树*/
	}
}
  1. 二叉搜索树的查找
    (1)根据上述的建树代码等等,我们可以看出,二叉搜索树在构成上与二分法类似,因此,我们在二叉搜索树上进行查找是,所使用的方法也与二分查找类似,即根据与当前结点比较大小来选择进入左右子树,一步步缩小我们的查找范围,最终得到查找结果。
node* Find_Position(node* root, int x)/*寻找结点的位置*/
{
	if (root)
	{
		if (root->data == x)
		{
			return root;
		}
		if (root->data > x)
		{
			return Find_Position(root->left, x);
		}
		else
		{
			return Find_Position(root->right, x);
		}
	}
	return NULL;
}

  1. 删除
    删除总共分为三种情况,即:
    (1)删除的结点为叶子结点,此时直接删除该结点即可
    (2)删除的结点只有左子树或者只有右子树,那么只需要用其左子树或者右子树替换该结点即可
    (3)删除的结点既有左子树,也有右子树,那么可以使用它的前趋,即该结点的左子树中的最大(最右)结点进行替换,然后再删除该前趋结点;或者使用其后继,即该结点的右子树中的最小(最左)结点进行替换,然后再删除该后继结点。

    (4)代码实现
void Delete(BiTreeNode* tree ,DataType num)
{
	int times,pos;
	BiTreeNode *preNode,*preselect,*select,*p;
	preNode=Search(tree,num,&pos,&times);//返回删除结点的父节点
	if(pos!=-2)//若找得到该删除结点
	{
		if(pos==-1)//定位到该删除结点,指针指向它p
		p=preNode->left;
		else if(pos==1)//定位到该删除结点,指针指向它p
		p=preNode->right;
		else
		p=preNode;////定位到该删除结点,指针指向它p,此处为根节点,无父节点,故删除结点即为preNode
		if(p->left==NULL&&p->right==NULL)//第一中情况,p为叶子结点
		{
			free(p);
			if(pos==-1)
			preNode->left=NULL;
			else
			preNode->right=NULL;
		}
		else if(p->left==NULL)//第二种情况,p只有右孩子
		{
			if(pos==-1)
			preNode->left=p->right;
			else
			preNode->right=p->right;
			free(p);
		}
		else if(p->right==NULL)//第三种情况,p只有左孩子
		{
			if(pos==-1)
			preNode->left=p->left;
			else
			preNode->right=p->left;
			free(p);
		}
		else//第四种情况,有左右孩子(这里其实还有2种情况,是否为根节点)
		{
			select=preselect=p->right;
			while(select->left!=NULL)
			{
				preselect=select;
				select=select->left;
			}
			if(pos==0)//删除的为树的根结点
			{
				if(p->right==select)
					p->right=select->right;
				else{
				p->data=select->data;
				preselect->left=select->right;
				}
				free(select);
			}
			else//不是根节点
			{
			if(p->right==select)
			{
				select->left=p->left;
			}
			else
			{
			preselect->left=select->right;
			select->left=p->left;
			select->right=p->right;
			}
			if(pos==1)
				preNode->right=select;
			else if(pos==-1)
				preNode->left=select;
			free(p);
			}
		}
	}
	else if(pos==-2)//未找到删除结点的情况
	{
		printf("树中未找到该结点\n");
	}
}

AVL树的定义及4种调整做法。

  • 定义
    1. AVL树,也称为平衡二叉树,这样的树指的是在一棵二叉树中,每一个结点的左右子树的高度最多相差1,即平衡因子的绝对值≤1,则称此二叉树为平衡二叉树。
    2. 平衡因子:该结点左子树的高度-右子树的高度

  • 平衡二叉树的插入调整
    在我们对平衡二叉树进行插入新结点的操作时,可能会对二叉树的平衡性造成破坏,为了解决这个问题,我们要对插入后失去平衡的二叉树进行调整。

  1. LL型调整:即某一结点的左子树的左孩子插入一个左孩子之后导致二叉树失衡
    过程:进行LL型调整时,首先找到失衡结点,然后令失衡结点的左孩子连带着左孩子的左子树一起上升,使这个左孩子成为根结点,此时令原失衡结点成为新的根结点的右孩子,再将原二叉树的左孩子的右子树变为新二叉树的右孩子的左子树。

  2. RR型调整:即某一结点的右子树的右孩子插入一个右孩子之后导致二叉树失衡
    过程:与LL型调整类似,调整的旋转方向相反,即到失衡结点,然后令失衡结点的右孩子连带着右孩子的右子树一起上升,使这个右孩子成为根结点,此时令原失衡结点成为新的根结点的左孩子,再将原二叉树的右孩子的左子树变为新二叉树的左孩子的右子树。

  3. LR型调整:即某一结点的左子树的右子树插入一个结点之后导致二叉树失衡
    过程:首先找到失衡结点,然后令插入位置子树的根结点取代失衡结点成为新的根结点,此时原左孩子及其左子树位置不变,失衡结点作为新的根节点的右孩子,插入位置子树根结点的左子树作为新根结点左孩子的右子树,插入位置子树根结点的右子树作为新根结点右孩子的左子树。

  4. RL型调整:即某一结点的右子树的左子树插入一个结点之后导致二叉树失衡
    过程:与LR型调整类似,调整的方向相反,即找到失衡结点,然后令插入位置子树的根结点取代失衡结点成为新的根结点,此时原右孩子及其右子树位置不变,失衡结点作为新的根节点的左孩子,插入位置子树根结点的左子树作为新根结点左孩子的右子树,插入位置子树根结点的右子树作为新根结点右孩子的左子树。


B-树和B+树定义。主要介绍B-树的插入、删除的操作

B-树

  • B-树,也称多路平衡查找树
  1. 性质:对于一棵m阶的B-树来说,或者是一棵空树,否则是满足下列性质的m叉树
    • 根结点至少有两个孩子
    • 树的每个结点至多有m个孩子结点->即至多有m-1个关键字
    • 除了根结点之外,其他的非叶子结点至少有[m/2]个孩子结点->即至少有[m/2]-1=[(m-1)/2]个关键字
    • 所有的叶子结点位于同一层
    • 所有的外部结点也都在同义词上,且在计算树的高度的时候,需要计入最底层的外部结点。
    • 外部结点就是失败结点,指向它的指针为空,不含有任何信息,是虚设的。一棵B树中总有n个关键字,则外部结点个数为n+1。
  • 插入
    在进行插入关键字的操作时,分为2种情况:
  1. 当插入的结点有空位置,即该结点的关键字个数<m-1,此时我们直接把关键字按照顺序插入到结点种合适的位置上
  2. 当插入的结点没有空位置,即该结点的关键字个数已经为m-1个,此时我们需要进行分裂操作
  • 删除
    与插入类似,在进行删除关键字的操作时,也要分为2种情况(PS:结点的最少关键字数min=[m/2]-1)
  1. 在叶子结点层上删除关键字,此时还需要分为3种情况
    (1) 如果被删除关键字的结点中,关键字的个数大于min,说明删除关键字前的该结点时满足B-树的定义的,那么此时删去该关键字后该结点仍然满足B-树的性质和定义,那么此时可对该关键字进行直接删除,如图:

(2) 如果被删除结点的关键字恰好等于min,那么删除该结点后该结点将不满足B-树的性质与定义,如果此时被删除结点的兄弟结点中,有关键字数>min的结点,那么此时我们除了进行删除操作外,还需要在该结点的兄弟结点中调配关键字来“借”给该结点,如图:

(3) 与(2)的情况类似,如果被删除结点的关键字恰好等于min,那么删除该结点后该结点将不满足B-树的性质与定义,但如果此时被删除结点没有兄弟结点可以借关键字,此时需要与兄弟结点做“合并”操作。如图:

  1. 在非叶子结点层上删除关键字,我们首先从非叶子结点上删除关键字,之后观察,是否可以从其他结点处调配或者与其他结点合并

B+树

  • B+树是B树的一种变形
  • B+树的一些定义:
  1. 对于一棵m阶的B+树来说
    • 它的每个分支结点至多有m课子树,且除了根结点之外,每个分支 结点至少有[m/2]棵的子树
    • 根结点要么没有子树,否则至少有两棵子树
    • 一个结点的子树数量于其关键字数相同,即有n棵子树的结点恰好有n个关键字
    • 所有的叶子结点包括了全部关键字及其指向相应记录的指针(且叶子节点按照关键字大小顺序连接,并将所有的叶子结点连接起来)
    • 所有分支结点中仅包含它的各个子节点中最大的关键字和指向其子节点的指针
  • 所以,根据上述的定义来看,B+树除了可以从根结点开始查找之外,还可以根据关键字,利用指针直接从叶子结点开始进行顺序查找,对比与B-树只能从根结点开始进行查找,B+树在查找方面的时间复杂度等等要胜过B-树。

散列查找。哈希表和哈希链2种构造方法、相关AVL计算。

哈希表的概念:哈希表其实是一种存储结构,是有一定长度的连续内存单元。在我看来,他的存储模式有些类似于数组,但又不相同。哈希表适用于存储的关键字与关键字存储的地址存在某种函数关系的数据,让我们可以通过地址对关键字进行查找。
哈希函数h(k):即把关键字为ki的数据存放在相应的哈希地址中
哈希冲突:由上述概念可以得出,哈希表中存储的关键字与存储地址有着某种函数关系,换言之,我们通过关键字k,通过哈希函数可以得出哈希地址。但是,正是由于这种函数关系,在计算关键字的地址时,可能会出现2个关键字ki≠kj,但是哈希函数h(ki)=h(kj),通俗来讲就是2个不同的关键字却会对应同一个哈希地址。因此我们把这种现象称为哈希冲突。PS:在哈希表存储结构的存储中,哈希冲突是很难避免的!!
哈希表的设计
设计一个哈希表需要考虑的问题主要有3点:

  1. 装填因子:装填因子a=存储的记录个数/哈希表的大小=n/m -> 所以,a越小,产生哈希冲突的可能性越小;反之,冲突的可能性越大,通常我们会使a最终控制在0.6~0.9的范围内。
  2. 采用的哈希函数
  3. 所使用的解决冲突的方法

哈希表的构造方法

  1. 直接定址法:以关键字本身k或者关键字本身k加上某一个常数c作为该关键字的哈希地址。
  • 哈希函数:
    h(k)=k+c
  • 分析:直接定址法最明显的优势就是不会出现哈希冲突,同时哈希函数简单,但是这样的方法所获得的地址完全取决于关键字,因此如果需要存==存储的是一组完全无序的关键字,那么就有可能出现关键字在哈希表中完全无序且间隔较大的分布,会导致大量的空间浪费。
  1. 除留余数法:通过将关键字k除以某一个不大于哈希表长度的模p所得到的余数,以该余数作为哈希地址。
  • 哈希函数:
    h(k)=k mod p (p≤m,m为哈希表的长度)
  • 分析:相比于直接定址法,除留余数法最大的优点即为节省空间,输入的关键字会按照某一顺序连续分布在哈希表中,但是除留余数法并不能避免产生哈希冲突,因此我们在选取模p的值时,p最好为质数(素数),这样在构造过程中出现冲突的可能性会更小

哈希冲突的解决方法
一、开放定址法:即在出现哈希冲突时,向下寻找一个新的空闲的哈希地址

  1. 线性探测法
    (1)线性探测法是解决哈希冲突的一种方法,其数学递推描述公式为:
d0=h(k)
di=[(di-1)+1] mod m   (1≤i≤m-1)

(2)分析:线性探测法实质上就是在发生冲突时,地址继续往下,直至范围内寻找到一个空地址,此时再将当前关键字存入这个空地址中,这样可以保证所有的关键字都可以存入到表中。但是它的缺点很明显,只有一个关键字之后的表非空,发生冲突时,发生冲突的对象会堆积在该关键字之后,即发生非同义词冲突非同义词冲突:哈希函数值不相同的两个记录争夺同一个后继哈希地址。一但出现非同义词冲突,就会发生堆积现象。因此,线性探测法较容易发生堆积现象。
(3)相关AVL的计算

① 探查成功ASL,探查次数恰好等于查找到该记录所需要的挂机案子的比较次数,分母则为关键字的个数。
ASL(succ)=(2+1+1+1+1+4+1+1+1+1+1)/11=1.364
② 探查不成功ASL:我们在查找关键字时,首先会使用哈希函数进行地址的查找,但是如果次数查找到的关键字不是你想要查找的,那么说明在构造哈希表的时候这两个关键字发生了冲突,此时我们需要做的是从当前地址向下进行查找,直至找到关键字或者遇到空地址为止。因此,在向下查找的过程中,每向下一次,探查不成功的次数就要加1,因此就会有图中的探查不成功次数。计算不成功的ASL时,分母为mod的素数
ASL(unsucc)=(2+1+10+9+8+7+6+5+4+3+2+1+3)/13=4.692
2. 平方探查法
(1)平方探查法的数学描述公式为:

d0=h(k)
di=[(d0±i^2] mod m   (1≤i≤m-1)

(2)分析:平方探查法时另一种处理冲突的方法,相对于线性探测法,它可以很好地避免出现堆积现象;但是缺点也很明显,由于i^2的探查方式,导致平方探查法无法探测到哈希表上的所有单元,但是至少可以探查到一半的单元。因此,在我看来,平方探测法可能会造成一定程度上空间的浪费,但是可以避免堆积现象的出现,是一种较好的处理冲突的方法。

二、拉链法

  1. 拉链法就是把所有的同义词用单链表连接起来的方法,换言之,就是将哈希函数值相同的关键字使用一条单链表进行存储连接,最终构成我们所说的哈希链。如图:

  2. 相关ASL的计算

    因此,在我们需要查找某一关键字k时,只需通过哈希函数h(k)来得到链表头结点的地址,然后逐个访问该单链表的结点进行查找即可。

  • 探查成功ASL:如图,成功找到每条单链表第一层的结点,都需要进行1次关键字比较,一共9个结点;成功找到每条单链表第二层的结点,都需要进行2次关键字比较,一共2个结点;共11个关键字:
    ASL(succ)=(19+22)/11=13/11

  • 探查不成功ASL:如图,探查失败,即找不到该关键字。对于有一个结点的链表,不成功查找需要进行1次关键字比较,一共有7条只有一个关键字的单链表;对于只有2个结点的链表,不成功查找需要进行2次关键字比较,一个有2条这样的单链表;素数p为13:
    ASL(unsucc)=(17+22)/13=11/13


1.2.谈谈你对查找的认识及学习体会

这一章学的是查找,整体的理解起来并不是非常复杂,但是在刚刚接触这一章的学习中,我确实有些一头雾水,特别是在例如AVL树的几个调整,预习的时候看着课件确实有些难懂,最后通过查找一些相关的资料以及老师上课的讲解才逐渐弄懂。抛开预习,学习中遇到的困难,在这一章中我收获了许多知识知识,尤其是各种各样适用于各种情况的查找,从最早的静态查找,动语态查找的二叉搜索树,再到后面的B-树与B+树,以及最后的哈希表哈希链。多种的查找方法有利于对我们在练习的时候该使用哪一种方法的判断和取舍有了一定的锻炼,同时在现今的社会中,搜索查找是人们不可缺少的一部分,如何正确的使用查找对我们这个专业来说,十分的重要。


2.PTA题目介绍

2.1 题目1:7-1 是否完全二叉搜索树 (30分)

2.1.1 该题的设计思路

  • 题面
  • 分析
    1.看到题面的时候,我首先想到的这棵树的特点:一是二叉搜索树,而是完全二叉树,而二叉搜索树由我们自己构造建立,因此我们这道题的目标就是构造二叉搜索树树与判断完全二叉树。
    2.同时,我们在输出的时候也要注意题目的要求,题目要求我们最后输出该树的层序遍历结果,看到层序遍历,我第一时间会想起了之前二叉树的层次遍历,运用队列的思路对树进行按层次输出。
    3.到这里,我们的思路其实已经很明确了,函数的设计也有了比价清晰的规划,建树函数,判断函数以及输出函数。
  • 解法
    1.根据二叉搜索树的特点,即:若左子树不空,则左子树上所有结点的值均小于它的根结点的值;若右子树不空,则右子树上所有结点的值均大于它的根结点的值;且左右子树也分别为二叉搜索树->但是在本题中,我们根据题目,发现该题的二叉搜索树变为了左大右小,因此我们在写建树函数的时候应该略做修改,即在进行递归建树的时候将左右子树的递归交换,如图:

    2.建立二叉搜索树之后,我们要进行的是对完全二叉树的判断。根据定义:若二叉树中最多只有下面两层结点的度数可以小于2,并且最下面一层的叶子结点都依次排列在最左边的位置上,则这样的二叉树称为完全二叉树。所以,通俗的来说,完全二叉树只有最后一层可以为叶子结点,且必须从左往右依次排列,如图:

    3.所以,我们通俗的来理解,使用NULL来对空结点处进行填充,我们可以发现:如果是完全二叉树,其在进行层序遍历时,它的层序遍历序列结果中结点必然是连续的;反之,若为非完全二叉树,其层序遍历序列的结果中,若要遍历完所有的非空结点,则必然要先遍历过一些空结点,如图:

    4.因此,在判断完全二叉树是,我们可以利用循环与队列,设置一个跳出的条件:当取当前的队首元素时,若该元素为NULL,则直接跳出循环,同时在循环中设置count计算非空的结点数。当跳出的时候,若count与题目中所给的总结点数相同,就说明是完全二叉树,反之,就为非完全二叉树,如图:
  • 时间复杂度:O(N),N为结点的个数

2.1.2 该题的伪代码

#include<iostream>
#include<queue>

using namespace std;

typedef struct node
{
	定义关键字key;/*关键字项*/
	定义左孩子lchild;
	定义右孩子rchild;
}BSTNode,*BSTree;
BSTree CreatBST(int a[], int N);
int InsertBst(BSTree& T, int k);
bool JudgeBST(BSTree& T, int N);
void LevelPrint(BSTree& T, int N);
int main()
{
	输入N
	定义数组a[20]

	for i = 0 to N-1 do
		输入N个正整数存入a数组中
	end for

	
	建立二叉搜索树T = CreatBST(a, N);

	if 该树为完全二叉树 then
		输出层序遍历结果
		输出YES
	else
		输出层序遍历结果
		输出NO
        end if

	return 0;
}
BSTree CreatBST(int a[], int N)
{
	初始化头结点BSTree t = NULL;
	
	for i = 0 to N do
		将结点与对应的关键字项传入插入函数
	end for
	return t;
}
int InsertBst(BSTree& T, int k)
{
	if T为空结点 then
		对T结点进行赋值T->key = k;
		初始化该结点的两个孩子T->lchild = T->rchild = NULL;
	else if 遇到相同的关键字项 then
		return 0;
	else if 关键字项小于当前结点 then
		递归进入右子树进行寻找插入return InsertBst(T->rchild, k);
	else 关键字项大于当前结点 then
		递归进入左子树进行寻找插入return InsertBst(T->lchild, k);
	end if
}
bool JudgeBST(BSTree& T, int N)
{
	定义队列q;
	定义count = 0;

	if T为空树 then
		return true;
	else
		结点入队q.push(T)
		BSTree t;
		while 队首元素不为空 do
			取队首元素并赋值给t
			t的左右孩子入
			队首元素出队
			count自增
		end while

		if count与结点总数相同 then
 
			return true;
		else

			return false;
	end if
}
void LevelPrint(BSTree& T, int N)
{
	
	定义数组result[20];
	定义队列q;
	BSTree p;

	头结点入队
	while 队列不为空 do
		取队首元素并赋值给p
		队首元素出队
		按照层次遍历的顺序依次将元素赋值给resultS数组

		if 当前结点的左孩子不为空 then
			左孩子入队
                end if
		if 当前结点的右孩子不为空 then
		
			q.push(p->rchild);
                end if
	end while

	根据题目要求输出result中元素
}

2.1.3 PTA提交列表


Q1:部分正确
A1:错误部分在于结构体的定义出现的问题,导致最终得出了错误的结果
Q2:部分正确
A2:修改了结构体定义之后,还是部分正确,之后仔细看题,发现是建树的时候仍然是按照左小右大建树,因此出现答案错误,修改之后全部正确

2.1.4 本题设计的知识点

  • 对于完全二叉树的判断方法,这种判断我个人来说还是得从书本的定义出发,理解了书本的定义以及完全二叉树的特点之后再进行思考哦判断函数的书写。
  • 二叉搜索树的建立。这道题是编程题的第一题,同时也是让我们好好的复习练习一下二叉搜索树的写法。
  • 层序遍历的复习,这道题很显然是让我们又一次复习了层序遍历,从树到图再到查找,都有涉及层序遍历的题目,这也是对我们的一个提醒:学习新知识的同时也不能忘记之前的知识点。

2.2 题目2:7-2 二叉搜索树的最近公共祖先 (30分)

2.2.1 该题的设计思路

  • 题面
  • 分析
    1.本题的核心目的就是寻找祖先,在找祖先或者说找父亲的问题上,我们可以思考是否可以与之前学习过的并查集的思路进行联系。
    2.可以看到,题目所给出的两个正整数N和M,他们的值的上限都很大,因此,我们在进行代码编写的时候需要考虑到是否会出现运行超时等问题。
    3.对于给定的每一对数字,对于可能出现的情况需要进行充分的判断。
    4.给定的序列是树的先序遍历序列,因此,我们需要利用先序序列的性质,同时,这棵树是一棵二叉搜索树。利用好这两点性质可以对解题带来便利。
  • 解法
    1.首先我们需要通过上面提到的该二叉树的两个性质进行树的建立,通过先序遍历,我们可以得到该序列的首位数字就是这棵树的根结点,同时,根据二叉搜索树的性质,我们可以得到,首位数字之后的序列中,比根结点小的即为左子树上的结点,大的即为右子树上的结点,因此我这里是通过类似于之前还原二叉树中分割区间的方法来进行递归建树。
    2.结构体的建立。在写题的时候,我在这里遇到了一些难题,之后通过网上查询到的一些方法,在结构体中分别定义左右孩子,父结点以及对应的数据。如图:

3.对于结点是否存在的判断,我在这里使用的是map容器,在输入先序序列的时候,每输入一个数字,将其对应的map容器中位置的值修改,这样在之后对于是否存在该结点的判断的时候,直接查看对应位置的值即可判断该结点是否在树上。这里其实也可以使用数组进行存储,但是有可能会造成大量的空间浪费。如图:

4.在判断一组节点中一个节点是否为另外一个结点的祖先时,我可以使用的是类似于并查集的方法来在一个结点的子树中是否存在另一个结点。如图:

5.当两个结点均存在且互不存在祖先结点关系时,利用循环寻找父结点的方法来寻找共同祖先

  • 时间复杂度:O(N),N为二叉树结点个数

2.2.2 该题的伪代码

#include<bits/stdc++.h>
using namespace std;

typedef struct node
{
	node* left;
	node* right;
	node* father;
	int data;
}Node,*VNode;
定义a数组
定义容器Mp;

node* CreateTree(int left, int right);
int Find(node* root, int x);/*找孩子*/
int Find_Father(node* root, int u, int v);/*找父亲,判断u是否为v的祖先*/
int LCA(node* root, int x);
node* Find_Position(node* root, int x);

int main()
{
	输入n与m
	初始化根结点root
	for i = 1 to m do
		输入序列数
		将存在的结点标记在Mp容器中
	end for

	调用建树函数

	for i = 1 to n do
		输入u和v

		if 容器中u和v不存在,即不在树上 then
			输出ERROR: U and V are not found.
		else if 容器中u不存在 then
			输出ERROR: U is not found.
		else if 容器中v不存在 then
			输出ERROR: V is not found.
		else u与v均在树上
			调用Find_Father函数判断u是否为v的祖先结点
			调用Find_Father函数判断v是否为u的祖先结点

			if u为v的祖先节点 then
				输出u is an ancestor of v.
			else if v为u的祖先节点 then
				输出u is an ancestor of v.
			else 有共同祖先
				定义temp,调用Find_Position函数寻找u结点的位置并赋值给temp
				定义root_1 ,调用LCA函数寻找共同祖先并复制给root_1
				输出LCA of U and V is root_1
			end if
		end if
	end for
	return 0;
}
node* CreateTree(int left, int right)
{
	if 左区间大于右区间,即先序序列不存在 then
		return NULL;
        end if

	定义结点node
	左侧第一个为左子树的根结点,赋值给root
	初始化父结点
	初始化左右孩子

	for i = left + 1 to right do
		if 找出第一个比根结点大的数值,即左右子树的分割点
			break;
                end if
	end if
	左子树递归建树CreateTree(left + 1, i - 1);
	if 左孩子为空 then
		对该结点的father结点进行标记
	end if
	右子树递归建树CreateTree(i, right);
	if 右孩子为空 then
		对该结点的father结点进行标记
	end if
	return root;

}
int Find(node* root, int x)
{
	if 结点不为空 then
		if 找到结点值为x的结点 then
			return 1;
		end if
		if x小于当前结点值 then
			递归进入左子树寻找
		else
			递归进入右子树寻找
		end if
	end if
	return 0;
}
int Find_Father(node *root, int u, int v)
{
	if 当前结点不为空 then
		if 找到了u结点 then
                        在u子树下递归寻找是否存在v结点
			if 调用Find函数找到了v then
				return 1;
			end if
			return 0;
		end if
		if u小于当前结点的值 then
			递归向左子树寻找
		end if
		else
			递归向右子树寻找
		end else
	end if
	return 0;
}
int LCA(node* root, int x)
{
	while 当前结点存在父结点 do
                从前一个结点开始向上找第二个结点的祖先
		向上更新root的值
		定义root1, 调用Find在root的子树下寻找是否存在x,并将返回的值赋给root1
		if 在root下找到x then
			返回当前root的值
		end if
	end while
	return 0;
}
node* Find_Position(node* root, int x)
{
	if root不为空
		if 找到结点值为x的结点 then
			返回结点位置return root;
		end if
		if x小于当前结点值 then
			递归进入左子树寻找结点值为x的结点的位置
		end if
		else
			递归进入右子树寻找结点值为x的结点的位置
		end else
	end if
	return NULL;
}

2.2.3 PTA提交列表


Q1:部分正确
A1:在设计Find函数时,对于空结点的情况忘记加上一句return 0,导致在主函数判断一组数据是否有“祖孙”关系时,出现了均有关系结点的判断,即出现了u为v的祖先结点的同时v也为u的祖先结点,但是此时的情况应该是u与v有一个共同的祖先结点,后面经过一步步慢慢调试找出错误

2.2.4 本题设计的知识点

1.在本题中,我选择使用map容器作为判断结点是否在树上的依据,我认为在这题中,对于map容器的定义与使用时一个知识点
2.在本题中,并没有直接让我们建立二叉搜索树,而是需要我们利用先序序列的特点和二叉搜索树的性质来对该树的构成做出判断,再根据这些判断来建树,这也是本题设计的一个知识点。
3.找祖先结点的题目我们并不陌生,这道题设计的找公共祖先结合一些并查集的思想也是一个知识点。


2.3 题目37-4 整型关键字的散列映射 (25分)

2.3.1 该题的设计思路

  • 题面

  • 分析
    1.首先,这道题在题目中就已经很明显的给我们提示:这是一道有关于哈希表的题目,且需要使用线性探测法来解决可能出现的冲突问题。
    2.这道题的输出是输入一个数据后立即计算出其在表中的位置,并不是全部计算完之后按照哈希表地址的顺序输出,因此,计算地址,解决冲突已经输出应该放在一个循环里面。
    3.解决哈希冲突时需要注意地址的最大范围以及判断该地址上是否以及有关键字的存在

  • 解法
    1.在代码中,为了解决上述的问题,我定义了两个数组,如图:

    hashTable[]数组使用了bool进行定义,将其初始化为false,用于判断对应地址上是否存在关键字;isOK使用int进行定义,用于存储输入的数据所对应的地址。
    2.当我们在循环中输入一个数据后,首先利用除留余数法算出该数据本应该对应的地址,然后开始利用hashTable[]数组判断是否存在冲突问题
    3.如果存在冲突问题,我们将地址下移,如图:
    如果直到地址为P-1时都有关键字 ,那么将该关键字转移至0地址
    4.按照题目的测试点,有可能会有重复的关键字,我们遇到重复的关键字时依然需要输出

  • 时间复杂度:O(1)

2.3.2 该题的伪代码

#include <iostream>
using namespace std;
#define MAXN 2000

int main()
{
   
    定义判断数组hashTable[MAXN] = { false };
    定义存储数组isOK[MAXN];
    
	for i = 0 to MAXN-1 do
		初始化isOK数组为-1;
		初始化hashTable数组为false;
	end for
	输入N和P
	定义数据变量和地址变量num, ad;
	for i = 0 to N do
		cin >> num;
		if 表中没有该关键字
			使用除留余数法得出第一次的地址
			while 该地址上已经有关键字 then
			        if 地址已经找到最大范围 then
					将地址修改为0 
				else
					地址下移寻找空地址
				end if
			end while
			按照格式要求输出ad
			hashTable数组的对应位置修正为true
			isOK数组中对应位置修正为ad
		else
                        按格式输出isOK[num]			
		end if
	end for
	return 0;
}

2.3.3 PTA提交列表


Q1:编译错误
A1:一开始将判断数组定义为hash[]全局变量,然后发现会产生冲突,后来将其修改为hashTable[]数组后就没有这个问题了
Q2:部分正确
A2:这个的错误点有几个,一个是一开始没有考虑到重复关键字的问题,导致其中一个测试点过不了;二是一开始在判断范围的时候将最大地址范围写成了P,同时把==写成了=,因此导致运行超时,修正后通过测试点。

2.3.4 本题设计的知识点

1.本题的知识点的重点就是关于哈希表的知识。哈希表的利用十分广泛,这道题让我们对应哈希表的建立,解决哈希冲突所使用的线性探测法有了一定的了解和练习
2.一开始我关于判断数组的函数想法还是利用map容器,但是发现使用数组会更简单,因此最后采用了数组的写法。其实这道题应该还有其他的写法,数组会存在空间可能会大量浪费的问题。

posted on 2020-05-24 16:02  蔡浩伟  阅读(339)  评论(0编辑  收藏  举报