DS博客作业05--查找

0.PTA得分截图

1.本周学习总结

1.1 总结查找内容

查找的性能指标ASL

  • 其中,n是查找表中元素的个数,pi是查找第i个元素的概率,ci是找到第i个元素所需的关键字比较次数。
  • ASL分为查找成功情况下的ASL成功,以及查找不成功情况下的ASL不成功。
  • ASL是衡量查找算法性能好坏的重要指标,一个查找算法ASL越大,其时间性能越差,反之,一个查找算法的ASL越小,其时间性能越好。

顺序查找的ASL

ASL成功=(n+1)/2
ASL不成功=n

  • 在顺序查找的过程中,ci取决于该元素在表中的位置。
  • 顺序查找方法在查找成功时的平均比较次数约为表长的一半。
  • 若k不在表中,则要进行n次比较后才能确定查找失败。

二分查找的ASL

ASL成功=log(n+1)-1
ASL不成功=log(n+1)

  • 二分查找过程可以用判定树(比较树)描述,将查找区间中间位置上的元素作为根,左子表作为左子树,右子表作为右子树。
  • 二分查找比较次数不超过判定树的高度,且最坏性能和平均性能接近。

对于二分查找的判定树以及二叉搜索树,ASL计算方法如下:

二叉搜索树

二叉搜索树又称二叉排序树(BST),它或者是一棵空树,或者是具有下列性质的二叉树:

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别为二叉排序树。

对于二叉搜索树的插入,操作方法如下:

  • 每个结点插入时需要从根结点开始比较,若比根结点值小,当前指针移到左子树,否则当前指针移到右子树,如此这样,直到当前指针为空,再创建一个结点,由当前指针指向它。

对于二叉搜索树的删除结点p,操作方法如下:

  • 若结点p是叶子结点,直接删除该结点。
  • 若结点p只有左子树而无右子树,将其左孩子代替结点p。
  • 若结点p只有右子树而无左子树,将其右孩子代替结点p。
  • 若结点p同时存在左右孩子,可将左子树中最大的结点r代替结点p的值,并删除结点r;也可以将右子树中最小的结点r的代替结点p的值,并删除结点r。

二叉搜索树的特点:

  • 任何一个结点插入到二叉搜索树时都是作为叶子结点插入的。
  • 对于一组关键字结合,若关键字序列不同,生成二叉搜索树可能不同。
  • 对于一颗二叉搜索树,其中序序列是一个有序序列。
/*构建*/
BinTree Create(KeyType A[],int n)
{
      BinTree BST=NULL;
      int i=0;
      while(i<n)
      {
            Insert(BST,a[i])
            i++;
      }
      return BST;
}
/*插入*/
BinTree Insert(BinTree BST, ElementType X)
{
    if (BST == NULL)    //空结点
    {
        BST = new BSTNode;    //生成新结点
        BST->Data = X;
        BST->Left = BST->Right = NULL;    
    }
    else if (X < BST->Data) 
    {
        BST->Left = Insert(BST->Left, X);   //插入左子树
    }
    else if (X > BST->Data)
    {
        BST->Right = Insert(BST->Right, X);   //插入右子树
    }  
    return BST;
}
/*删除*/
BinTree Delete(BinTree BST, ElementType X)
{
    BinTree t;
    if (BST==NULL)
    {
        printf("Not Found\n");
    }
    else
    {
        if (X < BST->Data)
        {
            BST->Left = Delete(BST->Left, X);
        }
        else if (X > BST->Data)
        {
            BST->Right = Delete(BST->Right, X);
        }
        else
        {
            if (BST->Left && BST->Right) //被删除的节点有两个孩子,从左子树中找最大的数代替删除的节点
            {
                t = FindMax(BST->Left);//找最大
                BST->Data = t->Data;//代替删除的节点
                BST->Left = Delete(BST->Left, t->Data);//删除拿来代替的那个节点
            }
            else //只有一个子节点
            {
                t = BST;
                if (!BST->Left) //只有右节点
                {
                    BST = BST->Right;
                }
                else if (!BST->Right) //只有左节点
                {
                    BST = BST->Left;
                }
                free(t);//删除
            }
        }
    }
    return BST;
}
/*查找*/
BinTree Find(BinTree BST, ElementType X)
{
    while (BST) 
    {
        if (X < BST->Data) 
        {
            BST = BST->Left;
        }
        else if (X > BST->Data) 
        {
            BST = BST->Right;
        }
        else 
        {
            return BST;
        }
    }
    return NULL;
   
}
/*查找最大或最小结点*/
BinTree FindMin(BinTree BST)
{
    if(BST!=NULL)
        while (BST->Left != NULL)
        {
            BST = BST->Left;
        }
    return BST;
}
BinTree FindMax(BinTree BST)
{
    if (BST != NULL)    
        while (BST->Right != NULL)
        {
            BST = BST->Right;
        }    
    return BST;
}

平衡二叉树

平衡二叉树的定义:

  • 若一颗二叉树中每个结点的左右子树高度差不超过1,则称次二叉树为平衡二叉树(AVL树)。
  • 一个结点的平衡因子是该结点左子树高度减去右子树高度,若平衡因子取值为1、0、-1,该结点是平衡的,否则是不平衡的。
  • 若一颗二叉树所有结点都是平衡的,称之为平衡二叉树。

平衡二叉树的4种调整

  • LL型调整
    这是因再A结点的左孩子的左子树上插入结点,使得A结点的平衡因子由1变为2而引起的不平衡。
    调整方法如下:

  • RR型调整
    这是因再A结点的右孩子的右子树上插入结点,使得A结点的平衡因子由-1变为-2而引起的不平衡。
    调整方法如下:

  • LR型调整
    这是因再A结点的左孩子的右子树上插入结点,使得A结点的平衡因子由1变为2而引起的不平衡。
    调整方法如下:

  • RL型调整
    这是因再A结点的右孩子的左子树上插入结点,使得A结点的平衡因子由-1变为-2而引起的不平衡。
    调整方法如下:

B-树和B+树

B-树定义:
B-树中所有结点的孩子结点的最大值称为B-树的阶。一棵m阶B-树或者是一颗空树,或者是满足下列要求的m叉树:

  • 树中每个结点最多有m棵子树。(即最多含有m-1个关键字)
  • 若根结点不是叶子结点,则根节点最少有两颗子树。
  • 除根节点以外,所有非叶子结点最少有m/2(取上整)棵子树。(即最少含有m/2-1(取上整)个关键字)
  • 每个结点结构为:|n|p0|k1|p1|k2|P2|……|kn|pn|
  • 所有的外部结点在同一层,并且不带信息。

B+树定义:

  • 每个分支结点最多有m棵子树。
  • 根结点或者没有子树,或者最少有2棵子树。
  • 除根结点以外,其他每个分支结点最少有m/2(取上整)棵子树。
  • 有n棵子树的结点有n个关键字。
  • 所有叶子结点包含全部关键字以及指向相应记录的指针,而且叶子结点按关键字大小顺序链接。
  • 所有分支结点中仅包含它的各个子结点中的最大关键字以及指向子结点的指针。

B-树和B+树的差异:

  • 在B+树中每个关键字对应一颗子树,在B-树中n个关键字对应n+1棵子树。
  • 除根结点外,在B+树中每个结点关键字个数n取值范围是m/2(取上整)至m,而B-树是m/2-1(取上整)至m-1。
  • B+树中所有叶子结点包含全部关键字,而B-树中关键字不重复。
  • B+树中的所有非叶子结点仅起到索引的作用,而B-树中,每个关键字对应一个记录的存储地址。
  • B+树可以进行随机查找和顺序查找,B-树只能进行随机查找。

B-树插入操作方法如下:

  • 找出关键字k的插入结点。
  • 插入关键字k,判断插入结点是否有空位置。(即关键字个数是否小于m-1)
    (1).有空位置,直接插入。
    (2).无空位置,将结点分裂成两个。取中间位置元素单独生成新结点,左部分的成为左子树,右部分的成为右子树。(树的高度可能增1)

B-树删除操作方法如下:

  • 找出删除关键字所在结点。
  • 删除结点分为两种情况:叶子结点层和非叶子结点层。
    (1).非叶子结点层,用删除结点所指子树的最大关键字或最小关键字代替。
    (2).叶子结点层,若删除后关键字个数仍然满足定义,直接删除;若删除后关键字个数不满足定义,向兄弟结点“借用关键字”;若又无法借用兄弟结点,合并结点。(树的高度可能减1)

散列查找

哈希函数的构造方法

  • 直接定址法
    直接定址法是以关键字k本身加上某个常数c作为哈希地址的方法。
    哈希函数为h(k)=k+c
    该方法的特点是哈希函数计算简单,若关键字分布不连续将造成内存单元大量浪费。

  • 除留余数法
    除留余数法是用关键字k除以某个不大于哈希表长度m的整数p所得的余数作为哈希地址。
    哈希函数为h(k)=k mod p
    该方法选取的p最好为不大于m的素数,从而尽量减少发生哈希冲突的可能性。

  • 数学分析法
    该方法是提取关键字中取值较均匀的数字位作为哈希地址。
    适用于所有关键字值都已知的情况,并需要对关键字中每一位的取值分布情况进行分析。

哈希冲突解决方法

  • 开放定址法
    (1).线性探测法
    从发生冲突的地址开始,依次探测下一个地址,直到找到一个空闲的单元为止。
    该方法的优点是解决冲突简单,缺点是容易产生堆积问题。
    (2).平方探测法
    从发生冲突的地址d开始,依次探测d+1^2,d-1^2,d+2^2,d-2^2...
    该方法不一定能探测到哈希表上的所有单元,但最少能探测到一半单元。

  • 拉链法
    拉链法是把所有的同义词用单链表链接起来的方法。
    与开放定址法相比,拉链法又以下几个优点:
    (1).拉链法处理冲突简单,且无堆积现象,即非同义词绝不会发生冲突,因此平均查找长度较短。
    (2).由于拉链法中各单链表上的结点空间是动态申请的,所以它更适合造表前无法确定表长的情况。
    (3).可取装填因子>=1,且增加的指针域可忽略不计,因此更加节省空间。
    (4).在用拉链法构造哈希表中,删除结点的操作更加容易实现。
    拉链法缺点:指针需要额外空间,当元素规模较小时,开放定址法较节省空间,若将节省的指针空间用来扩大哈希表规模,又减少了冲突,提高了平均查找速度。

/*创建*/
void CreateHT(HashTable ha, KeyType x[], int n, int m, int p)
{
	int i, cnt = 0;
	for (i = 0; i < m; i++)
	{
		ha[i].count = 0;
		ha[i].key = NULLKEY;
	}
	for (i = 0; i < n; i++)
		InsertHT(ha, cnt, x[i], p);
}
/*插入*/
void InsertHT(HashTable ha, int& n, KeyType k, int p)
{
	int adr,i;

	adr = k % p;
	if (ha[adr].key == NULLKEY || ha[adr].key == DELKEY)
	{
		ha[adr].key = k;
		ha[adr].count = 1;
	}
	else
	{
		i = 1;
		do
		{
			adr = (adr + 1) % p;
			i++;
		} while (ha[adr].key != NULLKEY && ha[adr].key != DELKEY);
		ha[adr].key = k;
		ha[adr].count = i;
	}
	n++;
}
/*查找*/
int SearchHT(HashTable ha, int p, KeyType k)
{
	int i, adr;
	
	adr = k % p;
	if (ha[adr].key == NULLKEY || ha[adr].key == DELKEY)
		return -1;
	else
	{
		if (ha[adr].key == k)
			return adr;
		else
		{
			uns_count++;
			while (ha[adr].key != NULLKEY && ha[adr].key != DELKEY)
			{
				adr = (adr + 1) % p;
				uns_count++;	
				if (ha[adr].key == k)
					return adr;					
			}
			return -1;			
		}
	}
}

ASL计算

  • 对于开放定址法构造的哈希表:
    ASL成功=∑每个关键字比较次数/关键字个数
    ASL不成功=∑关键字比较到空结点所需的次数/p

  • 对于拉链法构造的哈希表:
    ASL成功=∑关键字属于单链表的第几个结点/关键字个数
    ASL不成功=∑每条单链表结点个数/p

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

  • 查找又称检索,是指在某种数据结构中找出满足给定条件的元素。查找是一种十分有用的重要操作。
  • 在面对大量数据时,查找算法的时间性能就显得比较重要。
  • 在查找的同时对表做修改称为动态查找,不做修改称为静态查找。
  • 查找过程在内存进行称为内查找,在外存进行称为外查找。
  • 本章PTA中所涉及查找的题目容易出现运行超时和运行时错误的问题,而面对运行超时和运行时错误的问题也显得束手无策。
  • 本章所涉及的题目中,很多需要书面操作,如画图、ASL计算等。
  • 本章大量运用到树的内容,如二叉搜索树,平衡二叉树,B—树,B+树等,也算是对树的复习。

2.PTA题目介绍

2.1 题目1:是否完全二叉搜索树

2.1.1 该题的设计思路

  • 题中要求根据输入N个正整数生成二叉搜索树,输出二叉搜索树的层次遍历序列并判断该树是否为完全二叉搜索树。
  • 先将输入的N个正整数,依次插入二叉搜索树中生成一颗二叉搜索树。(需注意本题中左子树键值大,右子树键值小)
  • 再对二叉搜索树进行层序遍历,将空结点也入队 ,若空结点的后一个结点不是空结点,说明不是完全二叉树。(完全二叉树层次遍历序列中,空结点一定连续出现在序列的最后)
  • 时间复杂度O(n)

2.1.2 该题的伪代码

定义队列q;
输入n;
for i=0 to n do
{
      输入整数X;
      将X插入BST;
}
/*对BST层序遍历*/
if(!BST) return true;
根结点入队;
while(队列不空) do
{
      出队结点T;
      if(T不是空结点)
            输出T的值,将T的左右孩子入队;
      else if(队列不空&&队头不是空结点)
            BST不是完全二叉树搜索树;
}
BST是完全二叉搜索树;

2.1.3 PTA提交列表

  • 在T为空结点时,原先没有判断队列是否为空,导致出现错误。因为用q.front()函数的前提是队列不能为空,所以要增加!q.empty()这个条件。
  • 在得出不是完全二叉树的结论后不能立刻return false结束函数,因为此时层序遍历可能还没有全部遍历完成。
  • 将空结点的左右孩子入队,导致错误。要先判断目前结点是否是空结点,再根据判断进行不同的操作。

2.1.4 本题涉及的知识点

1.学习二叉搜索树的创建,二叉搜索树的创建是通过不断调用插入函数实现的。如果BST为空则生成新结点,根据插入数据k和BST的大小相比,递归调用左子树或右子树。
2.复习树的层次遍历,借助队列实现遍历,根据队列先进先出特点通过进出队列的先后顺序输出结点,产生层序序列。
3.在层序遍历的过程中,本题将空结点也入队,目的是为了判断空结点是否连续出现在序列最后,但是要注意不能再让空结点的左右孩子入队。
4.在使用取队头的函数时,要先确保队列不为空。

2.2 题目2: QQ帐户的申请与登陆

2.2.1 该题的设计思路

  • 题中要求输入N条信息,每条信息包括登录或注册,账号,密码。
  • 登陆或注册属于字符类型,而账号和密码属于字符串类型。
  • 使用map容器,将注册时输入的账号和密码进行关联,判断是否账号存在。
  • 登录时,根据输入的账号和密码在map容器中判断账号是否存在,以及密码是否正确。
  • 时间复杂度O(n)

2.2.2 该题的伪代码

char ch;      //区分登录还是注册
string account, password;      //账号,密码
map<string, int > mp;      //定义mp判断账号是否存在
map<string, string > mpPW;      //定义mpPW关联账号密码
输入n;
for i=0 to n do
{
      输入ch,account, password;
      if (ch=='L')      //登录
      {
            if (mp[account] == 0)      //mp中不存该信息;
		账号不存在;	
	    else if (mpPW[account] == password) //账号密码正确匹配
		登录成功;
	    else      //账号密码匹配错误
		密码错误;
      }
      else      //注册
      {
             if (mp[account] == 1)
                  账号已存在;
	     else
	     {
                  mp[account] = 1;      //在mp中添加该账号
	          mpPW[account] = password;      //将账号密码关联
 		  注册成功;
	     }	     
      }
}

2.2.3 PTA提交列表

2.2.4 本题涉及的知识点

1.学习map的使用,将两个类型的变量关联,可以应用于两个变量的匹配问题。
2.本题将string和int匹配,判断账号是否存在,不存在时mp值为0,存在将mp置为1。
3.本题将string和string匹配,判断账号和密码是否匹配。

2.3 题目3:整型关键字的散列映射

2.3.1 该题的设计思路

  • 题中要求输入N个整数和小于N的最小素数P,将N个整数映射在散列表中。
  • 先初始化散列表,要求使用除留余数法,对整数N取余数作为散列表的地址,插入散列表中。
  • 若非重复数据出现哈希冲突,采用线性探测法解决冲突,重复数据则不进行多次映射。
  • 最后输入出个整数各自在散列表中的位置。

2.3.2 该题的伪代码

定义数组key;
定义哈希表ha;
输入n,p;
for i=0 to n do
      将N个整数输入数组key;
创建哈希表;

/*创建哈希表*/
for i=0 to p do
      初始化ha;
求出地址adr,adr=k%p;
if (ha[adr].key为空)
   k插入表中下标为adr的位置;
else
{
      while (ha[adr].key != NULLKEY && ha[adr].key != k) //采用线性探测法对非重复数据进行探测
		adr = (adr + 1) % p;
      k插入表中下标为adr的位置;
}
输出adr;	

2.3.3 PTA提交列表

  • 两次部分正确是因为本题是散列函数,一个自变量唯一对应一个因变量,因此本题中出现重复数据时无需再次映射,只要输出第一次出现的位置即可。解决方法就是在线性探测的循环中加人条件ha[adr].key != k,防止重复数据进入循环。

2.3.4 本题涉及的知识点

1.使用开放定址法建立哈希表,将整数N映射在哈希表中,包含哈希表的初始化,建立,插入等操作,哈希表的建立是在初始化后不断调用插入函数实现的。
2.使用线性探测法解决哈希冲突,线性探测法是沿着出现冲突的地址依次向下进行探测,直到找到一个空闲地址的方法,该方法可能会出现堆积现象。
3.本题中散列函数是一一对应的关系,需要注意出现重复数据时不使用线性探测法,否则将出现一个数据对应两个地址的错误。

posted @ 2020-05-24 09:11  Po1s0n  阅读(109)  评论(0编辑  收藏