0.PTA得分截图


1.本周学习总结

1.1 总结查找内容

  • 静态查找:查找表仅作查询和检索操作

    • 查找算法的评价指标ASL:关键字的平均比较次数,也称平均搜索长度。成功情况下(概率相等)的平均查找长度是指找到任一记录平均需要的关键字比较次数

    • 顺序查找:从查找表的一端开始,顺序扫描线性表,依次将扫描到的关键字与给定关键字k相比较,若当前扫描到的关键字与k相等,则查找成功;若扫描结束后,仍未找到关键字等于k的记录,则查找失败

      • 顺序查找算法的时间复杂度为O(n),顺序查找成功时的平均查找长度为 ASL = (n+1)/2,查找不成功时的平均查找长度 ASL = n
      int SeqSearch(SeqList R, int n, int k)  //顺序查找算法
      {
          int i = 0;
          while (i < n && R[i].key != k)  //从表头往后找
          {
              i++;
          }
          if (i >= n)  //未找到返回0
          {
              return 0;
          }
          else
          {
              return i + 1;  //找到返回逻辑序号i+1
          }
      }
      
    • 二分查找:二分查找也称为折半查找,要求线性表中的节点必须已按关键字值的递增或递减顺序排列

      • 二分查找的时间复杂度为O(log2n)。对于n个元素,二分查找,成功时的ASL为log2(n+1)取上限,不成功时的ASL也为log2(n+1)取上限
      int BinSearch(SeqList R, int n, int k)
      {
          int low = 0, high = n - 1, mid;
          while (low <= high)  //当前区间存在元素时循环
          {
              mid = (low + high) / 2;
              if (R[mid].key == k)  //查找成功
              {
        	      return mid + 1;
              }
              if (k < R[mid].key)
              {
        	      high = mid - 1;
              }
              else
              {
        	      low = mid + 1;
              }
          }
          return 0;
      }
      
  • 动态查找:有时在查询之后,还需要将查询结果为“不在查找表中”的数据元素插入到查找表中,比如手机通讯录;或者从查找表中删除查询结果为“在查找表中”的数据元素

    • 二叉排序树:又称二叉查找树,亦称二叉搜索树。空树也是二叉排序树,且二叉排序树中没有相同关键字的节点,中序遍历二叉排序树得到的序列为一个递增有序序列

      • 若一棵二叉排序树不空,则其满足下列性质: 1.若它的左子树不空,则左子树上所有节点的值均小于根节点的值;2.若它的右子树不空,则右子树上所有节点的值均大于根节点的值;3.它的左,右子树也都分别是二叉排序树
      • 二叉排序树上的查找:因为二叉排序树可看成是一个有序表,所以在二叉排序树上进行查找,和二分查找类似,也是一个逐步缩小查找范围的过程
      BSTNode* SearchBST(BSTNode* bt, int k)  //非递归算法
      {
          while (bt != NULL)
          {
              if (k == bt->key)
              {
        	      return bt;
              }
              else if (k < bt->key)
              {
        	      bt = bt->lchild;  //在左子树中迭代查找
              }
              else
              {
        	      bt = bt->rchild;  //在右子树中迭代查找
              }
          }
          return NULL;  //没有找到返回NULL
      }
      
      • 二叉排序树的插入操作:插入的元素一定在叶节点上
      BinTree Insert(BinTree BST, ElementType X)  //二叉排序树中插入元素X
      {
          if (BST == NULL)  //若二叉排序树为空,创建新节点
          {
              BST = (struct TNode*)malloc(sizeof(struct TNode));  //新节点申请空间
              BST->Data = X;
              BST->Left = NULL;
              BST->Right = NULL;
          }
          else if (BST->Data == X)  //二叉排序树中已有此关键字,无需插入
          {
              return BST;
          }
          else if (X < BST->Data)
          {
              BST->Left= Insert(BST->Left, X);  //元素插入到左子树中
          }
          else
          {
              BST->Right= Insert(BST->Right, X);  //元素插入到右子树中
          }  
          return BST;
      }
      
      • 二叉排序树的创建操作:创建过程就是遍历数组,调用插入函数
      BinTree CreateBST(int A[], int n)  //返回树根指针
      {
          BinTree bt = NULL;  //初始时bt为空树
          int i = 0;
          while (i < n)
          {
              bt = Insert(bt, A[i]);  //将A[i]插入到二叉排序树中
              i++;
          }
          return bt;  //返回建立的二叉排序树的根指针
      }
      
      • 二叉排序树的删除操作:先查找要删除的关键字k,再删除关键字k
      BinTree Delete(BinTree BST, ElementType X)
      {
        BinTree ptr, q;
        if (BST == NULL)  //树为空,直接返回
        {
            printf("Not Found\n");
            return BST;
        }
        else
        {
            if (X < BST->Data)
            {
        	    BST->Left = Delete(BST->Left, X);  //递归在左子树中删除关键字为X的节点
            }
            else if (X > BST->Data)
            {
        	    BST->Right = Delete(BST->Right, X);  //递归在右子树中删除关键字为X的节点
            }
            else
            {
        	    if (BST->Right == NULL)  //没有右子树
        	    {
        		    ptr = BST;
        		    BST = BST->Left;
        		    free(ptr);
        	    }
        	    else if (BST->Left == NULL)  //没有左子树
        	    {
        		    ptr = BST;
        		    BST = BST->Right;
        		    free(ptr);
        	    }
        	    else if (BST->Left != NULL && BST->Right != NULL)  //既有左子树又有右子树
        	    {
      
        		    ptr = FindMax(BST->Left);  //查找左子树最右孩子
        		    BST->Data = ptr->Data;
        		    BST->Left = Delete(BST->Left, BST->Data);
        	    }
            }
            return BST;
        }
      }
      
  • AVL树:平衡二叉树(AVL树)首先要是一棵二叉排序树,其平均查找长度和log2n是同数量级的,查找关键字的时间复杂度为O(log2n)

    • 平衡二叉树满足以下两个性质:1.左,右子树为平衡二叉树;2.所有节点的左,右子树深度之差的绝对值最大为1。每个节点都有平衡因子,即该节点左子树与右子树的高度差,所以每个节点的平衡因子BF只能取-1,0,1

    • AVL树的四种调整:如果在一棵AVL树中插入一个新节点,就有可能造成失衡,此时必须重新调整树的结构,使之恢复平衡。且若有多个失衡点,则从最下面失衡点开始调整。调整平衡过程称为平衡旋转,有四种调整:LL平衡旋转,RR平衡旋转,LR平衡旋转,RL平衡旋转。AVL树调整后必须保证为二叉排序树,且LL调整和RR调整是选择失衡点的L或R孩子节点旋转,LR调整和RL调整是选择失衡点的LR或RL孙子节点旋转

      • LL型调整:某节点A的左孩子B的左子树插入节点引起失衡。需要进行一次顺时针旋转,分为3步:1.A的左孩子B右上旋转作为A的根节点;2.A节点右下旋转成为B的右孩子;3.B原右子树成为A左子树

      • RR型调整:某节点A的右孩子B的右子树上插入节点引起失衡,需要进行一次逆时针旋转,分为3步:1.A的右孩子B左上旋转作为A的根节点;2.A节点左下旋转成为B的左孩子;3.B原左子树成为A右子树

      • LR型调整:某节点A的左孩子B的右子树C上插入节点引起失衡,调整时先C进行逆时针旋转,A再顺时针旋转,分为3步:1.C向上旋转到A的位置,A作为C右孩子;2.C原左孩子作为B的右孩子;3.C原右孩子作为A的左孩子

      • RL型调整:某节点A的右孩子B的左子树C上插入节点引起失衡,调整时分3步:1.C向上旋转到A的位置,A作为C左孩子;2.C原左孩子作为A的右孩子;3.C原右孩子作为B的左孩子

    • 若T(h)表示高度为h且节点数尽可能少的平衡二叉树,设N(h)为T(h)的节点数,则平衡二叉树有如下性质:N(1)=1,N(2)=2,N(h)=N(h-1)+N(h-2)+1

  • B-树和B+树:一个节点可放多个关键字,从而降低树的高度。适合大数据量查找,如数据库中的数据,B-树又称为多路平衡查找树

    • B-树:对于一棵m阶B-树,每个节点最多m个孩子节点,最多m-1个关键字。除根节点外,其他节点的孩子节点个数至少为m/2取上限,关键字的个数至少为m/2取上限后再减一。如果根节点不是叶子节点,根节点至少两个孩子节点,而一棵空树也是一棵B-树
      • B-树有如下特点:1.B-树是所有节点的平衡因子均等于0的多路查找树,所有外部节点都在同一层上;2.在计算B-树的高度时,需要计入最底层的外部节点;3.外部节点就是失败节点,指向它的指针为空,不含有任何信息,是虚设的。一棵B-树中若有n个关键字,则外部节点个数为n+1
      #define MAXM 10  //定义B-树的最大阶数
      typedef int KeyType;  //KeyType为关键字类型
      typedef struct node {  //B-树节点类型定义
          int keynum;  //节点当前拥有的关键字的个数
          KeyType key[MAXM];  //[1,...,keynum]存放关键字,[0]不用
          struct node* parent;  //双亲节点指针
          struct node* ptr[MAXM];  //孩子节点指针数组[0,...,keynum]
      }BTNode;
      

  • B-树的插入:向一棵m阶B-树中插入关键字,插入的位置一定在叶子节点层,有两种情况:1.该叶子节点的关键字个数n<m-1,不修改指针,直接插入关键字到此节点中;2.该叶子节点的关键字个数n=m-1,则需进行“节点分裂”,此时若该叶子节点没有双亲节点,则新建一个双亲节点,树的高度增加一层,且双亲节点的值为叶子节点插入关键字k后的中间位置关键字。若该叶子节点有双亲节点,则将待插入的关键字k插入到双亲节点中

  • B-树的删除:删除关键字k分两种情况:1.在叶子节点层上删除关键字k;2.在非叶子节点层上删除关键字k。对于一棵m阶B-树,非根、非叶子节点的关键字最少个数Min为 m/2取上限再减一

    • B-树非叶子节点删除:1.从此节点的子树节点中借调最大或最小关键字代替要删除的关键字;2.子树中删除借调的关键字;3.若子树节点关键字个数小于最小值,重复步骤1;4.若删除关键字在叶子节点层,按叶子节点删除操作法
    • B-树叶子节点删除:B-树的叶子节点b上删除关键字有如下三种情况:1.若b节点的关键字个数大于Min,则可直接删去该关键字;2.若b节点的关键字个数等于Min,且其兄弟节点的关键字个数大于Min,则可以从兄弟节点借。此时兄弟节点最小关键字上移双亲节点,而双亲节点中大于删除关键字的关键字下移删除节点;3.若b节点的关键字个数等于Min,且其兄弟节点的关键字个数等于Min。此时先删除关键字,兄弟节点及删除关键字节点、双亲节点中分割两者的关键字再合并为一个新叶子节点,若双亲节点关键字个数小于Min,重复上一合并节点的步骤
  • B+树:B+树是大型索引文件的标准组织方式,支持顺序查找和分块索引,一棵 m阶B+树满足以下六个条件
    1.每个分支节点最多有m棵子树
    2.根节点或者没有子树,或者至少有两棵子树
    3.除根节点外,其他每个分支节点的子树棵数至少为m/2取上限
    4.有n棵子树的节点有n个关键字
    5.所有叶子节点包含全部关键字及指向相应记录的指针,叶子节点按关键字大小顺序链接,叶子节点是直接指向数据文件中的记录
    6.所有分支节点包含子节点最大关键字及指向子节点的指针

  • 散列查找:散列查找的查找时间与问题规模无关,查找的时间复杂度为O(1)

    • 哈希表:又称散列表,是除顺序表存储结构,链接表存储结构和索引表存储结构之外的又一种存储线性表的存储结构,主要适合记录的关键字与存储地址存在某种函数关系的数据
      • 哈希函数 h(key):把关键字为key的对象存放在相应的哈希地址中。而哈希地址则是某一关键字经过哈希函数这一运算后所得到的哈希表中的位置
      • 哈希冲突:对于两个关键字分别为i和j的记录,其中关键字i和j不同,但是他们的哈希地址 h(i)=h(j),把这种现象称为哈希冲突(同义词冲突)。而在哈希表存储结构中,哈希冲突是很难避免的。哈希冲突主要与三个因素有关:1.与装填因子 a有关,装填因子 a =存储的记录个数/哈希表的大小= n/m。装填因子 a越小,冲突可能性越小,一般将装填因子控制在0.6到0.9之间;2.与采用的哈希函数有关;3.与解决冲突的方法有关
      • 哈希函数构造方法:1.直接定址法:直接定址法是以关键字k本身或关键字加上某个数值常量c作为哈希地址的方法,哈希函数为 h(k)=k+c。优点是计算简单,并且不可能有冲突发生。缺点是关键字分布不连续将造成内存单元的大量浪费;2.除留余数法:除留余数法是用关键字k除以某个不大于哈希表长度m的数p所得的余数作为哈希地址的方法,哈希函数为 h(k)=k mod p(mod为求余运算,p<=m),其中数p最好是质数;3.数字分析法:适合于所有关键字值都已知的情况,并需要对关键字中每一位的取值分布情况进行分析
    • 哈希冲突解决方法:开放定址法和拉链法
      • 开放定址法:冲突时找一个新的空闲的哈希地址。有线性探查法和平方探查法两种具体实现方法
        1.线性探查法:先调用哈希函数求得某个关键字的哈希地址,若此哈希地址上已有其他关键字,则逐个往下寻找空的哈希地址来进行关键字的存放。线性探查法容易出现堆积

        2. 平方探查法:平方探查法是一种较好的处理冲突的方法,可以避免出现堆积现象,它的缺点是不能探查到哈希表上的所有单元,但至少能探查到一半单元

      • 拉链法:拉链法是把所有的同义词用单链表链接起来的方法

      • 如上图所示,拉链法中成功查找的
        查找不成功的

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

  • 查找这一章和其他章节一样,都是先讲基本概念,再讲具体实现。学习了静态查找和动态查找,静态查找有顺序查找,二分查找,分块查找三种,它们是逐渐优化的,其中顺序查找成功时的平均查找长度 ASL=(n+1)/2,二分查找成功时的平均查找长度 ASL为log2(n+1)取上限。动态查找则扩展到了树的内容:最开始为二叉排序树(二叉搜索树,二叉查找树),学习了二叉排序树的性质,可以借助递归进行判断一棵树是否为二叉排序树,学习了二叉排序树的相关操作,如二叉排序树的生成,节点的插入、删除、查找等。接着从二叉排序树过渡到形态均衡的平衡二叉树(AVL树),学习了平衡二叉树的相关性质,重点学习了平衡二叉树的四种调整:LL调整,RR调整,RL调整,LR调整。同时学习了已知平衡二叉树的高度,怎么求其最少节点数的公式。接着学习了适合大数据量查找的B-树和B+树,分别学习了B-树和B+树中节点中的关键字个数以及孩子节点的个数跟树的最大阶数的关系,重点学习了B-树中关键字的插入和删除操作,并对比了B-树和B+树的相同点和不同点。最后学习了哈希表(散列表)方面的知识,首先学习了相关的基本概念,如哈希函数,哈希地址,哈希冲突等。而重点学习了解决哈希冲突的两种方法:开放地址法和拉链法,了解了两种方法中查找成功和不成功时的ASL的计算方法。开放地址法中重点学习了线性探查法,即在哈希表中一个一个地去找空闲的地址。接着学习了哈希链方面的内容,重点学习了哈希链的构建,以及关键字的查找,插入等操作。

  • 查找的应用:动态查找的一个应用为手机通讯录,若一个电话号码未找到则可将其添加到通讯录里;网络的搜索引擎,有应用到相关的查找方法,如倒排索引;图书馆检索书目,输入书名,可进行查找;一篇文档中查找所需关键词,输入关键词进行查找;论文查重,代码查重等。变种的AVL树--红黑树的应用:C++STL中的关联式容器,如映射map,多重映射multimap等。B-树索引是数据库中存取和查找文件(称为记录或键值)的一种方法。


2.PTA题目介绍

2.1 7-2 二叉搜索树的最近公共祖先

解题代码






2.1.1 该题的设计思路

  • 数据表达:变量m表示待查询的节点对数,变量n表示二叉搜索树中节点个数。接着输入n个不同的整数,表示二叉搜索树先序遍历时每个节点的值。再输入m行数据,每行数据包括两个整数 u和v,用以表示二叉搜索树中两个不同节点的值。输出分为四种情况:1.若值为u或v的节点无法在二叉搜索树中查找到,则按照题面要求输出u或者v找不到;2.若值为u和值为v的节点都无法在二叉搜索树中查找到,则按照题面要求输出u和v找不到;3.若值为u和值为v的两个节点中,有一个节点是另一个节点的祖先节点,则按照题面要求输出;4.按照题面要求正常输出值为u和值为v的两个节点的最近的公共祖先节点

  • 解法:建二叉搜索树的函数中不断调用插入函数,来插入每一个节点,节点的值为输入的整数值。在建树插入节点的过程中,可用一个数组来存储每个节点的值。之后在建成的二叉搜索树中进行查找每对输入的整数值,若值为u和值为v的两个节点都找得到,且两个节点都不是对方的祖先节点,则遍历这个数组,若某个数组元素的值第一次在整数值u和v之间,那么二叉搜索树中值等于此数组元素的值的节点就是最近的公共祖先节点

  • 时间复杂度为O(m*n),m为待查询的节点对数,n为二叉搜索树中节点个数

2.1.2 该题的伪代码

设置全局变量flag,判断值为u和值为v的两个节点是否是对方的祖先节点
int main()
{
    输入n个整数,用数组Key保存所有节点的值,并调用Insert函数插入二叉搜索树的每一个节点
    输入m对整数键值u和v,并调用Find函数在构建好的二叉搜索树中进行查找,依照查找结果根据题目要求输出
    分别调用PreTraverse函数,判断值为u和值为v的两个节点是否是对方的祖先节点,若都不是,则置全局变量flag的值为1
    if (flag)
    {
        for (j = 0; j < n; j++)  //遍历整个数组Key
        {
            if(数组元素的值位于整数u和v之间)
                then 按照题目要求输出两节点的最近公共祖先节点,break跳出循环
        }
    }
    return 0;
}

BinTree Insert(BinTree BST, int X)  //在二叉搜索树BST中插入值为X的节点
{
    if (BST == NULL)  //若树为空树
        then 创建新节点,节点的值为X,节点的左右孩子置空
    else if (BST->Data == X)  //树中已有值为X的节点
        then 返回树的根节点
    else if (X < BST->Data)
        then 递归调用Insert函数,将值为X的节点插入到左子树中
    else
        then 递归调用Insert函数,将值为X的节点插入到右子树中
    返回树的根节点
}

BinTree Find(BinTree BST, int X) //在二叉搜索树中查找值为X的节点
{
    while (BST != NULL)  //当树不空时查找
    {
        if (BST->Data == X)  //查找成功
            then 返回根节点
        else if (BST->Data > X)
            then 根节点移到左孩子节点上,迭代查找
        else
            then 根节点移到右孩子节点上,迭代查找
    }
    return NULL;  //未找到,返回NULL
}

void PreTraverse(BinTree BST, int n, int m)  //根节点值为m,对这棵二叉搜索树进行先序遍历
{
    if (BST == NULL)  //若树为空树
        then 全局变量flag的值置为1,直接返回
    else
    {
        if (n == BST->Data)  //找到值为n的节点
            then 按照题目要求输出值为m的节点是值为n的节点的祖先节点
        else if (n < BST->Data)
            then 递归调用PreTraverse函数,在左子树中查找
        else
            then 递归调用PreTraverse函数,在右子树中查找
    }
}

2.1.3 PTA提交列表

  • 在求两节点的最近公共祖先节点时,一开始想给每个节点添加一个成员parent,用以指向节点的双亲节点,但没能实现。然后想到二叉搜索树中,对于先序遍历序列,可用一个数组存储所有节点的值,这样就只需要遍历数组,找第一个值位于u和v之间的数组元素就行了,这个数组元素在二叉搜索树中对应的节点就是两个节点的最近公共祖先节点

  • 在判断一个节点是否是另一个节点的祖先节点时,开始时看PTA的例子,误以为两个节点只要是上下两层的关系且一个节点是另一个节点的双亲节点就行了。后来发现是两个节点都不能为对方的祖先节点,所以借助一个先序遍历的函数 PreTraverse(BinTree BST, int n,int m),在树BST中查找值为n的节点,此时树的根节点的值为m。若查找成功,则说明值为m的节点是值为n的节点的祖先节点

2.1.4 本题设计的知识点

1.构建二叉搜索树就是一个不断插入节点的过程,插入节点时,可借助递归使节点正确地插入到根节点的左子树或右子树上。而在二叉搜索树中查找特定元素时,可借助迭代,边移动根节点指针边查找特定元素

2.判断一个节点A是否是另一节点B的祖先节点时,可先调用查找函数在二叉搜索树中查找前一个节点A,再以此节点A为根节点进行先序遍历。若先序遍历的过程中某个节点的值等于后一个节点B的值,则说明节点A是节点B的祖先节点

3.构建二叉搜索树时,可用一个数组来存储每次插入的节点的值,这样以后要查找两个节点的最近公共祖先节点时可通过遍历数组,第一个值处于两个节点值之间的数组元素所对应的节点就是要找的最近公共祖先节点


2.2 7-4 整型关键字的散列映射

解题代码




2.2.1 该题的设计思路

  • 数据表达:输入正整数n,表示待插入的关键字总数;输入正整数p,表示散列表的长度,也表示哈希函数除留余数法中的余数。接着输入n个整数,用一个数组Key[]存储所有的关键字,数组类型为关键字类型。用一个数组table存储散列表中的元素,其中每个元素都有两个成员,一个成员data用来存储关键字,另一个成员flag表示散列表中该处的哈希地址是否为空

  • 解法;先初始化table数组,每一个元素的成员flag都置为0,表示散列表中该处的哈希地址为空。接着输入n个待插入的关键字,用数组Key[]存储。每次向散列表中插入关键字时,先调用查找函数Find,若散列表中已有此关键字,则无需插入;否则使用除留余数法先求得关键字的哈希地址,再通过哈希地址处元素的成员flag进行判断此哈希地址是否为空,若为空,关键字直接插入到此处。否则的话再使用线性探测法寻求空的哈希地址进行插入关键字。最后同时遍历 Key数组和 table数组,按照题目要求格式正确输出每一项关键字在散列表中的哈希地址

  • 时间复杂度为O(n*p),n表示关键字总数,p表示散列表的长度

2.2.2 该题的伪代码

int main()
{
	数组table存储散列表中的每个元素,每个元素都有两个成员data和flag
	for (i = 0; i < p; i++)
	{
		table[i].flag = 0;  //散列表长度为p,每项元素的成员flag置为0,表示该处哈希地址为空
	}
	for (i = 0; i < n; i++)
	{
		输入整形关键字,用数组Key存储
		使用除留余数法,将整形关键字的哈希地址赋给变量num
		if (!Find(table, Key[i], p))  //调用Find函数,在哈希表中未查找到关键字Key[i]
		{
			if (table[num].flag == 0)  //哈希表中哈希地址num处为空
				then 置table数组中该元素的成员flag为1,成员data值为关键字Key[i]
			else  //出现哈希冲突
			{
				while (table[num].flag != 0)
				{
					num = (num + 1) % p;  //线性探查法,逐个查找空的哈希地址
				}
				找到空的哈希地址,置table数组中该处元素的成员flag为1,成员data值为关键字Key[i]
			}
		}
		else
			continue;
	}
	for (i = 0; i < n; i++)  //外层循环遍历Key数组
	{
		for (j = 0; j < p; j++)  //内层循环遍历table数组
		{
			if (table[j].flag == 1 && table[j].data == Key[i])  //散列表中查找Key[i]元素成功
				then 按照题目要求正确输出Key[i]元素在散列表中的哈希地址
		}

	}
	return 0;
}

bool Find(Address table[MaxSize], int num, int p)  //在散列表中查找关键字num
{
	for (i = 0; i < p; i++)  //遍历table数组,p表示散列表长度
	{
		if (table[i].flag && table[i].data == num)  
			then 返回true,表示查找成功
	}
	return false;  //查找不成功
}

2.2.3 PTA提交列表

  • 部分正确(15分):当用除留余数法取得某个关键字的哈希地址时,若此哈希地址处已有关键字,则用线性探测法逐个地查找空的哈希地址。但是逐个探查的语句应为 adr=(adr+1)%p,之前我是直接把哈希地址adr的值加一,那样的话若adr已表示散列表的最后一个地址,再加一的话就会超过table数组的边界,无法正确查找到空的哈希地址,所以PTA上显示哈希地址有冲突时答案错误

  • 部分正确(20分):之前的代码运行后,若要插入的n个关键字中有相同的关键字,则在散列表中也会查找空的哈希地址进行关键字的插入,所以PTA上显示有重复关键字时答案错误。后面加了一个类型为bool的函数Find,若在散列表中查找某个关键字成功,则这个关键字无需插入,否则的话按照之前代码的做法把关键字插入到散列表中

2.2.4 本题设计的知识点

1.在散列表中插入某个关键字时,首先要判断散列表中此关键字是否已存在,若存在,则无需插入。否则的话在散列表中查找某个关键字的哈希地址时,可用除留余数法。若得到的哈希地址有冲突,可以用线性探测法解决,即逐个地查找,直到查找到空的哈希地址来存放关键字

2.在表示散列表存储空间的数组中,每个元素都可以加上一个成员flag,用以表示此位置是否已插入了关键字,数组元素的另一成员data则可以用来存放关键字


2.3 7-5(哈希链) 航空公司VIP客户查询

解题代码






2.3.1 该题的设计思路

  • 数据表达:输入正整数n,表示飞行记录的条数,输入正整数k,表示飞行的最低里程,即若某飞行记录的飞行里程小于k的话,就按k计算。接着输入n条飞行记录,每条飞行记录包括每位会员的18位身份证号码和飞行里程,18位身份证号码包括17位数字和最后一位校验码,校验码可取0到9中的任意一个数字或者字符x,本题中若校验码为x,在哈希链的头节点中可把它定义为数字10。并输入正整数m,接着输入m个人的身份证号码,使用拉链法,在对应头节点的哈希链中进行查找相同身份证号的乘客的飞行里程。若无法查找到此人的飞行里程,说明此人不是会员,按照题目要求输出"No Info"

  • 解法;对于每一个会员,都有唯一的身份证号码,使用拉链法来解决哈希冲突。首先要创建11条哈希链,每条哈希链的头节点用一个数组存储,数组下标从0到10,表示每个会员身份证号码中的检验码,其中下标10表示检验码为字符x。随后对于输入的每条飞行记录,首先求头节点在数组中的下标,即检验码。接着调用查找函数,在所得头节点对应的哈希链中进行查找飞行记录中的身份证号,若找到,则更新此会员的飞行里程;否则将此会员的信息使用头插法保存在这条哈希链中。最后对于每个查询人,先根据其身份证号码中的检验号,在数组中查找到对应的头节点,然后在头节点所连的哈希链中对每个节点都进行查询人信息的匹配,若找到该查询人的信息,则输出其飞行里程;否则按照题目要求输出"No Info",即此查询人不是会员

  • 时间复杂度为O(n),n表示飞行记录的条数

2.3.2 该题的伪代码

typedef struct User {  //每位会员信息的结构体定义
    char id[22];  //身份证号码
    int mile;  //飞行里程
}User;
typedef struct HNode {  //哈希链中节点的结构体
    User data;  //存放一位会员的信息
    struct HNode* next;  //指向下一节点的指针
}HNode, * Node;
typedef struct {
    Node first;  //哈希链的头节点
}HashTable;
int main()
{
    CreateHash(ha, n, k);  //调用CreateHash函数,构建哈希表ha
    for (i = 0; i < m; i++)  //m次查询
    {
        使用getchar()语句吸收换行符
        输入查询人身份证号码后,调用Search函数在哈希表ha中查找飞行里程的信息
    }
    return 0;
}

void CreateHash(HashTable ha[], int n, int k)  //参数n表示飞行记录条数,参数k表示最低里程
{
    User user[MaxSize];  //数组类型为User,每一个元素代表一位会员
    for (i = 0; i < m; i++)  //变量m的值为11,每条哈希链的头节点置空
    {
        ha[i].first = NULL;
    }
    输入n个会员的身份证号码和飞行里程,用数组user的元素表示会员,若飞行里程小于最低里程k,则置为k
    调用n次Insert函数,在哈希表ha中存放每位会员的信息
}

void Insert(HashTable ha[], User user)  //在哈希表ha中插入新节点,节点的数据域为user
{
    if (Find(ha, user)) //调用Find函数,哈希表中已存在此节点,无需插入
        then 直接返回
    else
    {
        定位会员user身份证号码中的校验码,作为哈希地址赋给变量adr
        构建新节点ptr,数据域为user
        if (ha[adr].first == NULL)  //哈希表中此条哈希链的头节点为空
            then 节点ptr作为此条哈希链的头节点
        else
            then 使用头插法将节点ptr插入到此条哈希链中
    }
}

bool Find(HashTable ha[], User user)  //在哈希表中查找数据域为user的节点
{
    定位user身份证号码中的校验码,作为哈希地址赋给变量adr
    ptr = ha[adr].first;  //节点ptr作为此哈希链的头节点
    while (ptr != NULL)  //节点不为空时循环
    {
        if (strcmp(ptr->data.id, user.id) == 0)  //哈希链中某节点数据域中的身份证号码与user身份证号码一致
            then 更新此会员的飞行里程,返回true,表示查找成功
        ptr指向下一节点
    }
    return false;  //查找失败
}

void Search(HashTable ha[], char id[22])
{
    定位字符串id的第十八位字符,相当于身份证号码中的校验码,作为哈希地址赋给变量adr
    ptr = ha[adr].first;  //节点ptr作为此哈希链的头节点
    while (ptr != NULL)  //节点不为空时循环
    {
        if (strcmp(ptr->data.id, id) == 0)  //哈希链中某节点数据域中的身份证号码与字符串id一致
            then 输出该节点数据域中的飞行里程,return返回
        ptr指向下一节点
    }
    按题目要求输出"No Info",此查询人不是会员,return返回
}

2.3.3 PTA提交列表

  • 代码编写中开始首先就是不太懂如何区分不同哈希链的头节点,尤其是当查询人身份证号码中的检验码为字符x时。后来是把字符x当作数字10来处理,这样针对11个检验码,在数组中就是用下标0到10来区分不同的头节点,这样在之后哈希链的构建过程中,只要先根据检验码找到对应的头节点,然后在此头节点所连的哈希链中使用头插法插入查询人的信息就好了

  • 构建好11条哈希链后,对于查询人的信息,关注的是身份证号码。先根据身份证号码中的检验码在数组中找到对应的头节点,然后在该头节点所连的哈希链中进行查询人身份证号的匹配,这里使用的是"string.h"头文件中的strcmp函数,即C字符串处理函数,但是这里的头文件不能是"string",查阅资料后发现"string"头文件对应的是新的string 类,而"string.h"头文件对应的是基于char*的字符串处理函数。所以在PTA上我使用了C语言中的strcmp函数,但是头文件为"string",提交后会显示编译错误

2.3.4 本题设计的知识点

1.使用拉链法解决哈希冲突时,关键是每条哈希链的构建。首先要用一个数组存储每条哈希链的头节点,数组下标为关键字对应的哈希地址,之后对于每条哈希链中关键字的插入使用头插法。而进行查找关键字时,先求得关键字的哈希地址,在数组中找到对应的头节点,然后在此头节点所连的哈希链中进行关键字的查找

2.对于字符串比较函数strcmp,它对应的头文件应为"string.h",而"string"头文件对应的是新的string 类,它是C++ 的头文件