DS博客作业05--查找

| 这个作业属于哪个班级 | 数据结构--网络20 |
| ---- | ---- | ---- |
| 这个作业的地址 | DS博客作业05--查找 |
| 这个作业的目标 | 学习查找的相关结构 |
| 姓名 | 曹卉潼 |

0.PTA得分截图

1.本周学习总结(0-5分)

1.1 查找的性能指标(ASL)

关键字的平均查找次数,也叫做平均搜索长度(ASL)。一般计算某查找算法中查找成功和查找不成功的平均搜索长度来评判某算法的执行效率:如果ASL越大,其时间性能越差,反之,则时间性能越好,其计算公式为:

ASL成功:找到T内任一记录平均需要的关键字比较次数

1.2 静态查找

分析静态查找几种算法包括:的成功ASL和不成功ASL。

顺序查找

从表的一端开始,逐个扫描,将扫描到的关键字和给定的k值比较,若相等则查找成功,若扫描结束后,未找到关键字的记录,则查找失败。

int SeqSearch(int *a,int n,int k)
{
     int i=0;
     while(i<n&&a[i]!=k)
     {
        i++;
     }
     if(i>=n)  //没找到,返回-1
     {
        return -1;
     }
     else
     {
        return i;
     }
}
  • 时间复杂度:O(n)

ASL分析:
查找成功:从表的一端开始查找,按顺序扫描,每个数据的查找次数依次递增1,且每个位置上数据出现的概率都相等,所以第i个数据的查找次数ci为i,
平均比较次数约为表长一半。
查找不成功:若要查找一个不存在于顺序表中的数据时,一定要遍历完整个顺序表之后才能确定改数据不存在于顺序表中,平均查找长度为:n

二分查找

设n个元素的数组a已经有序,用low和high两个变量来表示查找的区间,即在a[low]~a[high]中去查找用户输入的值x,和区间中间位置的元素a[mid] (mid=(low+high)/2)作比较,如果相等则找到,算法结束;如果是大于则修改区间(数组为升序数列:low=mid+1;数组为降序数列:high=mid-1);如果是小于也修改区间(数组为升序的改为:high=mid-1,;数组为降序数列:low=mid+1);一直到x=a[mid] (找到该值在数组中的位置)或者是low>high(在数组中找不到x的值)时算法终止;

int BinSearch(int *a,int n,int k)
{
     int low=0,high=n-1,mid;
     while(low<=high)
     {
        mid=(mid+high)/2;
        if(a[mid]==k)
        {
            return mid+1;
        }
        if(k<a[mid])
        {
            high=mid-1;
        }
        else
        {
            low=mid+1;
        }
     } 
     return -1;   //没找到
}
//递归代码
int BinSearch(int *a,int low,int high,int k)
{
   int mid;
   if(low<high)
   {
      mid=(low+high)/2;
      if(a[mid]==k)
         return mid+1;
      if(a[mid]>k)
        BinSearch(a, low,mid-1, k) ;
      else
        BinSearch(a, low,mid+1, k) ; 
   }
   else
      return -1;
}

  • 时间复杂度:其实这样的过程,while循环查找的方法就很像建立了一个二叉搜索树,最后搜索的是一条路径,所以时间复杂度是O(logn)
  • ASL=log(n+1)-1

1.3 二叉搜索树

定义

二叉搜索树又称二叉排序树。在一棵二叉排序树中,若根节点的左子树不为空,那么左子树上所有的结点都小于根结点;若根节点的右子树不为空,那么右子树上所有结点都小于根节点。且根结点的左右子树叶都是二叉排序树。二叉排序树中所有的关键字都是唯一的。二叉树还有特殊的性质:按中序遍历可以得到一个递增有序序列。

结构体定义

typedef struct node
{
   KeyType key;//关键字;
   InfoType data;//其他数据域;
   struct node*lchild,*rchild;//左右孩子指针;
}BSTNode,*BSTree;

二叉排序树插入

在二叉排序树中插入一个关键字为k的结点要保证插入后仍然满足BST性质,其插入过程为:

  1. 若二叉排序树bt为空,则创建一个key域作为k的结点,将它作为根结点;
  2. 若不为空,则与根节点的关键字作比较:
    若相等,则说明树中已经存在该关键字,无需插入,直接返回;
    若比根节点的值小,就进入根节点的左子树中,否则进入右子树;

例:设有一个输入数据的序列是 { 46, 25, 78, 62, 12, 80 }, 试画出从空树起,逐个输入各个数据而生成的二叉搜索树。

BinTree Insert( BinTree BST, ElementType X )
{
    if(!BST)//如果BST为空的话,返回只有一个节点的树
    {
        BST=(BinTree)malloc(sizeof(struct TNode));
        BST->Data=X;
        BST->Left=NULL;
        BST->Right=NULL;
    }
    else//如果BST不是为空的话
    {//开始寻找要插入的位置
        if(X<BST->Data)
            BST->Left=Insert(BST->Left,X);
        else if(X>BST ->Data)
            BST->Right=Insert(BST->Right,X);
    }
    return BST;
}
  • ASL成功:(11+22+3*3)/6=7/3
  • ASL不成功:(11+62)/7=13/7

二叉排序树删除

1.p节点为叶子结点,直接删去该结点,双亲节点相应指针域修改为NULL;
2.p结点只有左/右子树,有其左子树或者右子树代替他,双亲节点相应的指针域修改;
3.p结点既有左子树也有右子树。根据二叉排序树的特点,可以从其左子树中选择关键字最大的结点r(最右结点),用结点r的值代替结点p的值(结点值替换),保存r结点的左孩子,再删去r结点。也可以从右子树中选择关键字最小的结点l(最左结点),用结点l的值代替结点p的值(结点替换),保存l结点的右孩子,再删去结点l。


代码

情况1:
int DeleBST(BSTree& bt, KeyType k)
{
	if (bt == NUL) return 0;//空树
	else
	{
		if (k < bt->key)
			return DeleBST(bt->left, k);
		else if(k>bt->key)
			return DeleBST(bt->right, k);
		else
		{
			Delete(bt);
			return 1;
		}
	}
}

情况2:
void DeleBST(BSTree& p)
{
	BSTree q;
	if (p->right == NULL)
	{//右子树为空或为叶子节点
		q = p;
		p = p->left;
		delete q;
	}
	else if (p->left == NULL)
	{//左子树为空
		q = p;
		p = p->right;
		delete q;
	}
	else//既有左孩子也有右孩子
		Delete1(p, p->left);
}
情况3:
void Delete1(BSTNode* p, BSTNode*& r)
{
	BSTNode* q;
	if (p->right)
		Delete1(p, p->right);//找左子树中最右结点
	else
	{
		p->key = r->key;
		q = r;
		r = r->left;
		delete q;
	}
}

1.4 AVL树(平衡二叉树)

定义

二叉排序树中查找的操作执行时间与树形有关,在最坏情况下执行的时间为O(n)(单边树时)。为了提高查找效率,在既能保持BST性质,又能保证树的高度在任何情况下为log2,这样在查找过程中,即使是最坏情况下,执行的时间也还是O(log2),这样的二叉树称为平衡二叉树。
一棵平衡二叉树中,每个结点的左,右子树高度最多相差1。在算法中,我们通过平衡因子来具体实现平衡二叉树的定义。一个结点的平衡因子是该结点左子树的高度减去右子树的高度(或者右子树的高度减去左子树的高度)。如果一棵二叉树中的所有结点平衡因子的绝对值都不超过1,那么这颗二叉树就是平衡二叉树。

插入结点和调整过程

若向平衡二叉树中插入一个新结点(总是作为叶子结点插入)后破坏了平衡性,首先从该新插入的结向根结点方向查找第一个失衡的结点,然后进行调整。调整的操作可以归纳为下列四种情况:LL、RR、LR、RL型调整
LL调整

在这棵树中,发现有两个失衡点,5和6,我们要从底部往上调节,第一个失衡点5,然后它的旋转点就是4,让4进行旋转,往往我们从下面开始,第二个失衡点也会平衡,不在失衡
RR调整

在这棵树中,有两个失衡点,2和3,从底部往上调节,第一个失衡点3,它的旋转点就是4,让4进行旋转

LR调整
若在结点A的左孩子的右子树上插入一个结点导致结点A失衡,进行LR调整:将A结点的左孩子B上的右孩子C上升代替结点A成为根节点,A作为C的右孩子,B作为C的左孩子,C原来的左子树作为B的右子树,C原来的右子树作为A的左子树。


RL调整
若在结点A的右孩子的左子树上插入一个结点导致结点A失衡,进行RL调整:将A结点的右孩子B上的左孩子C上升代替结点A 成为根节点,A作为C的左孩子,B作为C的右孩子,C原来的左子树作为A的右子树,C原来的右子树作为B的左子树.

插入9之后,发现8的平衡因子变为-2,所以失衡点是-2,开始寻找旋转点,9是在8的右子树的左孩子是,所以是RL调整,旋转点是10,10代替8的位置,8往左下旋转,那么本来是10的孩子9,就要按照AVL树的特点,被安排为12的左孩子。

根据某个序列构造一棵AVL树

输入关键字序列{16,3,7,11,9,26,18,14,15},构造一颗AVL树。


1.5 B-树和B+树

B树特性

B树(B-树是一种多路搜索树(并不是二叉的):

   1.定义任意非叶子结点最多只有M个儿子;且M>2;

   2.根结点的儿子数为[2, M];

   3.除根结点以外的非叶子结点的儿子数为[M/2, M];

   4.每个结点存放至少M/2-1(取上整)和至多M-1个关键字;(至少2个关键字)

   5.非叶子结点的关键字个数=指向儿子的指针个数-1;

   6.非叶子结点的关键字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];

   7.非叶子结点的指针:P[1], P[2], …, P[M];其中P[1]指向关键字小于K[1]的子树,
   P[M]指向关键字大于K[M-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树;

B树结构体定义

/*B-树存储结构*/
typedef int KeyType;
typedef struct node
{
   int keymun;//当前结点拥有的关键字个数;
   struct node *parent;//双亲结点指针
   struct node *ptr[MAXM];//孩子结点指针数组;
}BTNode


/*查找结果返回类型*/
typedef struct 
{
   BTnode *pt;//指向找的结点的指针;
   int i;//在结点中的关键字序号;
   int tag;//标志查找成功或者失败;
}Result;

B-树的查找

在一棵B-树上顺序查找关键字,将k于根结点中的key[i]比较:
1.若k==key[i],则查找成功;
2.若k < key[1]:,则沿指针ptr[0]所指的子树继续查找;
3.若key[i]<k<key[i+1],则沿着指针ptr[i]所指的子树进行查找;
4.若k>key[i],则沿着指针ptr[i]所指的子树进程查找;
5.查找某个结点,若相应指针为空,落入一个外部结点,表示查找失败;类似判断树和二叉排序树的查找。


B-树的插入

往B-树中插入一个结点,插入位置必定在叶子结点层,但是因为B-树中对关键字个数有限制,所以,插入情况分为以下两种:关键字个数n<m-1,不用调整;关键字个数n=m-1,进行“结点分裂”
设定B-树的阶为5。用关键字序列{1,2,6,7,11,4,8,13,10,5,17,9,16,20,3,12,14,18,19,15}来构建一棵B-树。

  • 第一步是插入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,插入20后,最右下节点内元素个数为5个,超过最大个数4个,所以,需要把16进行提升,形成如下结构
  • 之后插入3、12、14、18、19,后,形成如下结构。
  • 然后插入15,会导致13提升到根节点,这时,根节点会有5个节点,那么,根节点中的10会再次进行提升,形成如下结构。

B-树的删除

  • 删除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合并为根节点,如下图。

B-树的应用

B-树经常被用于对于检索时间要求苛刻的场合,比如:
1.B-树索引是数据库中存取和查找文件(称为记录或者键值)的一种方法;
2.硬盘中的结点也是B-树结构,B-树利用多个分支(称为子树)的结点,减少获取记录是所经历的结点数,从而达到节省存取时间的目的;

B+树

B+的特性:

   1.所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;

   2.不可能在非叶子结点命中;

   3.非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;

   4.更适合文件索引系统;

   5.所有分支节点只是起到索引的作用。

B+树的优点

1.B树查找并不稳定(最好的情况是查询根节点,最坏的情况是查询叶子节点),而B+树的每次查找都是稳定的。

2.由于B+树中间节点没有data数据,所以相同大小的磁盘页可以容纳更多的节点元素,所以数据量相同的情况下,B+树比B树更矮胖,查询次数更少

所以,比起B树,B+树查询性能很稳定,查询范围更广

1.6 散列查找。

哈希表概念

哈希表又称为散列表,是除顺序表存储结构,链接表存储结构和索引表存储结构之外的又一种存储线性表的存储结构,其基本思路是,设要存储的元素个数为n,设置一个长度为m的连续内存单元,以每个元素的关键字ki(i取值为0~n-1)为自变量,通过一个称为哈希函数的函数h(ki),把ki映射为内存单元的地址(或下标),并把该元素存储在这个内存单元中,这个地址也称为哈希地址。这样构造的线性存储结构就是哈希表。在建哈希表示可能存在两个关键字虽然不相同,但是其哈希地址却一样的情况,这种现象叫做哈希冲突。通常把具有不同关键字却有相同哈希地址的元素称为同义词。

哈希表的构造

直接定址法

例如:有一个从1到100岁的人口数字统计表,其中,年龄作为关键字,哈希函数取关键字自身。

但这种方法效率不高,时间复杂度是O(1),空间复杂度是O(n),n是关键字的个数

  • 优点:计算简单,并且不可能有冲突
  • 缺点:关键字分布不是连续的将造成内存单元浪费

数字分析法

有学生的生日数据如下:

年.月.日

75.10.03
75.11.23
76.03.02
76.07.12
75.04.21
76.02.15

经分析,第一位,第二位,第三位重复的可能性大,取这三位造成冲突的机会增加,所以尽量不取前三位,取后三位比较好。
除留取余数法

假设哈希表长为m,p为小于等于m的最大素数,则哈希函数为h(k)=k % p ,其中%为模p取余运算。

例1,已知待散列元素为(18,75,60,43,54,90,46),表长m=10,p=7,则有

h(18)=18 % 7=4    h(75)=75 % 7=5    h(60)=60 % 7=4   

h(43)=43 % 7=1    h(54)=54 % 7=5    h(90)=90 % 7=6   

h(46)=46 % 7=4

此时冲突较多。为减少冲突,可取较大的m值和p值,如m=p=13,结果如下:

h(18)=18 % 13=5    h(75)=75 % 13=10    h(60)=60 % 13=8    

h(43)=43 % 13=4    h(54)=54 % 13=2    h(90)=90 % 13=12   

h(46)=46 % 13=7

此时没有冲突:

解决哈希冲突

线性探测法
例2:将关键字序列{18,2,10,6,78,56,45,50,110,8}散列存储到散列表中,散列表的存储空间是一个下标从0开始的一维数组,散列函数为:H(key)=key)mod 11,处理冲突采用线性探测法,要求装填(载)因子为0.77。

  • 优势:计算简单,适用范围广泛;
  • 缺点:容易发生哈希冲突。如果在上面的例子中插入14:h(14)=14%7=0 于7发生了哈希冲突

ASL分析

  • 成功:查找一个关键字所需要比较的次数为对应的探测次数,故成功的ASL为
  • 不成功:在哈希表中查找某个关键字,其哈希地址为d,然后从下标为d的地址单元开始查找,如果该关键字不存在,会一直往后遍历到一个空的位置才会停止

拉链法
拉链法是把所有的同义词用单链表链接起来的方法,所有哈希地址为i元素对应的结点构成一个单链表,哈希表地址空间为0~m-1,地址为i的单元是一个指向对应单链表的头结点。这种方法中,哈希表的每个单元中存放的不再是元素本身,而是相应同一词单链表的首结点指针。

例:设一组初始记录关键字集合为(25,10,8,27,32,68),散列表的长度为8,散列函数H(k)=k mod 7

优势:
  • 处理冲突简单,无堆积现象,非同义词不会发生冲突;
  • 结点空间时动态申请的,空间利用效率高;
  • 在链表中,删除结点操作方便

ASL分析

  • 成功:在哈希表中查找存在的某个关键字,对应的结点应该在单链表ha[h(k)]中,其探测次数为在单链表中的位置,故ASL成功为:
  • 不成功为:在哈希表中查找不存在的关键字k时,需要在单链表ha[h(k)]中遍历所有的结点,判定到空指针才停止,所以ASL不成功为:

2.PTA题目介绍(0--5分)

介绍3题PTA题目

2.1 是否完全二叉搜索树(2分)

2.1.1 伪代码(贴代码,本题0分)

int main()
{
   输入结点数据;
   层次遍历;
   判断是否为完全二叉树;
}
BTree InsertTree(BTree T, int num)//建树
{
  if 树为空
   {
     建新结点;
     输入根结点数据;
     左右子树分别初始化;
   }
  end if
   else if (待插入数据大于根结点值)
     在左子树插入该结点;
  end if
   else if (待插入数据小于根结点值)
     在右子树插入该结点;
  end if
  返回建成后的树;
}

bool IsBST(BTree T)//判断是否完全二叉树
{
    if 树空
        return false;
     引入队列Q;
     将已建成的树的数据入队;
     while  队列不空
     {
        if 左右孩子都存在
          将左右孩子进队;
        end if
        else if  左孩子不存在,右孩子存在
           return false;
        end if
       else 左孩子存在,右孩子不存在,或者左右孩子都不存在
       {  
         if   左孩子存在,右孩子不存在
            将左孩子进队;
         end if
         while  队列不空
         {
              指针指向队头;
                出队;
                if 左孩子不存在或右孩子不存在
                    return false;
                 end if
         }
       }
     }
}
void LeverOrder(BTree T)//层次遍历
{
      引入队列qu;定义指针p;
      if 树为空
         输出NULL;
      end if
      将该树数据入队;
      while  队列不空
     {
         p指向队头;
         输出p指向的值;
         if 左孩子存在
            右左孩子入队;
         end if
         if 左孩子存在
            将右孩子入队;
         end if
         输出空格;//格式要求
     }
}





2.1.2 提交列表

2.1.3 本题知识点

  • 二叉搜索树的建立:由题面我们可以看到,题面要求我们左子树键值大,右子树键值小,故我们在建立二叉搜索树时,要注意如果待插入结点比当前结点小的话进右子树,比当前结点大进左子树;
  • 完全二叉树:若一棵二叉树的高度为h,那么1到h-1层的结个数都达到最大,而最后一层结点连续且都集中在最左边。
  • 完全二叉树的层次遍历特点:完全二叉树在进行层次遍历时,如果遍历到一个结点只有左孩子,或者左右孩子均为空时,那么剩余未遍历的结点,全都是叶子结点。
  • 二叉树的层次遍历:和之前在树中学习一样,运用队列辅助实现;
  • 二叉搜索树的插入操作:边查找边插入,算法中的根节点指针要用引用类型,这样才能将改变后的值传回给实参。

2.2 航空公司VIP客户查询(2分)

2.2.1 伪代码

 定义一个map容器,下标表示身份证号,key表示里程
  
 输入数据:
     小于最低里程的要按最低里程算
     if 已经有存在过的只需要加上b即可(m[a]+=b)
     else 等于m[a]=b;
 查询里程:
     if 查到该身份证号,输出里程
     else 输出  No Info


2.2.2 提交列表


2.2.3 本题知识点

  • 运用map容器
  • 这道题,容易超时的一个很大原因是用了c++的cin来输入字符串类型,因为会容易超时,后来我也去查看了网上的介绍,当数据量较大时,cin和cout的耗时可能会是scanf,printf的好几倍,所以就用了scanf和printf.

2.3 基于词频的文件相似度(1分)

2.3.1 伪代码

定义一个map二维数组,一维表示文件序号,二维表示该文件某个单词,值表示出现了几次
for i=1 to N i++
while 读取一行的内容,当内容为#就停止循环
令top=0;
for j=0 to s.length() j++
if 内容不是字母即为分割符
 then if 单词长度小于3 则不记录
end if 
continue
end if
if 单词长度大于10 
then 值记录前10
end if
else 加结束符 单词数+1
top=0
end if
else 是字母,大写该小写
需要队组后一个单词判断
开始计算相似度:
输入两个文件序号
map<string, int>:: iterator itl,it2//两个迭代器,类似指针
因为map可以自动排序(按照字典序),所以可以两个文本同时遍历
itl->first<it2->first 说明itl后面可能有和it2相同的单词itl向后移
itl->first>it2->first 说明it2后面可能有和itl相同的单词itl向后移
相同时,将分子++,两个迭代器都向后移
输出相似度 100.0*mole/(deno-mole)

2.3.2 提交列表

2.3.3 本题知识点

  • 运用map容器下标可以自动排序的特点,对于计算相似度比较方便string s 字符串型变量
  • 利用getchar()吸收回车
  • map<string,int>::iterator it1,it2;//两个迭代器,类似指针
posted @ 2021-06-13 20:36  SmileCHT  阅读(91)  评论(1编辑  收藏  举报