DS博客作业05--查找

0.PTA得分截图

1.本周学习总结

1.1 总结查找内容

1.1.1 静态查找的性能指标ASL

  • 顺序查找:顺序查找就是在一个顺序表(表中数据不一定有序)中从第一个数据开始逐一与目标数据比较,直到找到目标数据或遍历完顺序表为止。

    • 对于含有n个数据的顺序查找表,ASL(查找成功)=(1+2+3+……+n)/n=(n+1)/2,ASL(查找失败)=(n+n+n+n+……+n)/n=(n×n)/n=n
  • 二分查找:二分查找也是在顺序表中进行的,但与顺序查找不同的是,二分查找的顺序表要求表中数据有序递增或递减。二分查找每次都是拿顺序表最中间的数据与目标数据进行比较,这里以顺序递增的顺序表为例,如果表最中间的数据比目标数据大(小)的话,则将该数据往左(右)的所有数据当成一个新的顺序表,然后对新的顺序表进行同样的操作直到找到数据或新的顺序表只有一个数据且该数据不等于目标数据为止。如果将二分查找顺序表建成一棵树的话,就叫做二分查的判定树。

    • 对于含有n个数据的二分查找表,ASL(查找成功)=(log₂n+1)-1,ASL(查找失败)=(所有只有左子树或右子树的结点×结点高度+所有叶子结点*结点高度*2)/(只有左子树或右子树的结点数量+叶子结点数量)

1.1.2 动态查找:二叉搜索树

  • 二叉搜索树的构建:对于一组给定的元素(可以是有序的也可以是无序的),拿第一个数据作为根节点,然后拿后面的数据与根节点比较,如果比根节点小(大)的话就插入作为根节点的左(右)孩子,否则做右孩子;如果根节点有左(右)孩子的话,就拿根节点的左(右)孩子做新的根节点与待插入数据进行比较,重复上述步骤直到所有数据都插入到树中。

  • 二叉搜索树的结点插入:对于二叉搜索树的插入其实就是二叉搜索树的构建的分步,即二叉搜索树的构建可以看成是由一步步的插入完成的。操作大致上和上述二叉搜索树的构建相同,故这里就不再过多赘述。(注意:插入的元素一定在叶节点上)具体代码如下:

typedef Position BinTree;
struct TNode{
    ElementType Data;
    BinTree Left;
    BinTree Right;
};
BinTree Insert( BinTree BST, ElementType x )//递归法插入数据
{
    if(!BST){
        BinTree p=(BinTree)malloc(sizeof(BinTree));
        p->Data=x;
        p->Left=p->Right=NULL;
        BST=p;
    }
    else if(x<BST->Data)BST->Left=Insert(BST->Left,x);
    else if(x>BST->Data)BST->Right=Insert(BST->Right,x);
    return BST;
}
  • 二叉搜索树的删除:对于二叉搜索树结点的删除有三种情况被删除的结点是叶子结点,则直接删去该结点,然后其双亲结点中相应的指针域的值改为NULL;被删除的结点只有左子树或右子树,则用其左子树或右子树代替被它,然后其双亲结点的相应的指针域的值改为“指向被删除结点的左子树或右子树”;被删除的结点既有左子树又有右子树,则以其中序遍历前驱(左子树中的最大(右端)结点)或中序遍历后继(右子树中的最小(左端)结点)代替它。然后修改其双亲结点的对应指针域为被删除结点的中序前驱或后继。具体代码如下:
typedef Position BinTree;
struct TNode{
    ElementType Data;
    BinTree Left;
    BinTree Right;
};
BinTree Delete( BinTree BST, ElementType x )
{
    if(!BST){
        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(x==BST->Data){
            if(BST->Left&&BST->Right){  //被删除结点既有左子树又有右子树
                BinTree t=FindMin(BST->Right);//在右子树中找最小结点
                BST->Data=t->Data;
                BST->Right=Delete(BST->Right,BST->Data);
            }
            else {
                if(!BST->Left)BST=BST->Right;//被删除结点只有右子树
                else if(!BST->Right)BST=BST->Left;//被删除结点只有左子树
            }
        }
    }
    return BST;
}

1.1.3 AVL树

  • AVL树的定义:AVL树是一种特殊的二叉搜索树。在二叉树的基础上规定:对于树中的任何一个结点,其左右子树的最大高度差(也叫平衡因子)的绝对值不超过1。由于这一限制条件,所以在对AVL树进行结点插入或删除操作时可能会改变某些结点的平衡因子,可能会导致平衡因子大小超过1,所以这时候就需要进行一次或多次旋转来重新平衡这棵树。下面是四种旋转(调整)做法。

  • LL调整:若在A结点的左子树的左子树上插入新结点,导致A结点的平衡因子的绝对值大于1,则需要进行一次顺时针旋转:将A的左孩子B往右上旋转,作为A的根节点、A结点往右下旋转作为B的右孩子、B结点原本的左子树保留,右子树作为A结点新的左子树。(如果存在多个结点失衡即平衡因子绝对值大于1,则从最下面的那个失衡结点开始调整)具体代码如下:

Node* LL(Node* node){
    Node* Right = node->right;
    Node* Left = Right->left;
    Right->left = node;
    node->right = Left;
    node->UpdateHeight();
    Right->UpdateHeight();
    return Right;
}

  • RR调整:如果在A结点的右子树的右子树上插入新结点,导致A结点的平衡因子的绝对值大于1,则需要进行一次逆时针旋转:A结点的右孩子B往左上旋转作为A结点的根节点、A结点往坐下旋转作为B结点的左孩子、B点原本的右子树保留不变,左子树作为A结点新的右子树。(如果存在多个结点失衡即平衡因子绝对值大于1,则从最下面的那个失衡结点开始调整)具体代码如下:
Node* RR(Node* node){
    Node* Left = node->left;
    Node* Right = Left->right;
    Left->right = node;
    node->left = Right;
    node->UpdateHeight();
    Left->UpdateHeight();
    return Left;   
}

  • LR调整:如果在A结点的左子树的右子树上插入新结点,导致A结点的平衡因子的绝对值大于1,则需要进行两次旋转:针对根节点的左孩子B进行一次RR调整,然后针对根节点A进行一次LL调整。(如果存在多个结点失衡即平衡因子绝对值大于1,则从最下面的那个失衡结点开始调整)具体代码如下:
Node *LR(Node* node){
    node->left = RR(node->left);
    return LL(node);
}

  • RL调整:如果在A结点的右子树的左子树上插入新结点,导致A结点的平衡因子的绝对值大于1,则需要进行两次旋转旋转方法与LR调整对称:针对根节点的右孩子进行一次LL调整,然后针对根节点A进行一次RR调整。(如果存在多个结点失衡即平衡因子绝对值大于1,则从最下面的那个失衡结点开始调整)具体代码如下:
Node *RL(Node* node){
    node->right = LL(node->right);
    return RR(node);
}

1.1.4 B-树和B+树

  • B-树的定义:B-树是一种特殊的排序树,类似于二叉搜索树,但不限于二叉,即一个结点可以有多个孩子,且一个结点中可以有多个数据域,即一个结点可以存储多个数据。对于一棵m阶B-树,每个结点至多可以有m个孩子,m-1个关键字(数据域)、除根节点外,其他结点至少有(m+1)/2个孩子结点,[(m+1)/2]-1个关键字(数据域)、如果根节点不是叶子结点的话,那么根节点至少有2个孩子。结构体定义如下:(上述除法运算所得结果均要去尾即舍去小数部分
#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+树的定义:B+树可以看作是B-树的变形。与B-树不同的是:一棵m阶B+树每个结点至多都可以有m棵子树、有n棵子树的结点有n个关键字(数据域)、除根节点外,每个结点的关键字均包含双亲结点中的一个关键字、所有叶子结点包含整棵树的全部关键字及指向相应记录的指针,也就是说,对叶子结点层进行层序遍历的话,所得结果一定是有序的,可以是递增也可以是递减。结构体定义同B-树,只需要在B-树的基础上视情况修改一下MAXM的值即可。

  • B-树的关键字插入:对于B树的关键字插入操作,不论是B+树还是B-树,关键字的插入都是在叶子结点层进行的。对于一棵m阶B-树,每个结点最多只能有m-1个关键字。如果在某个关键字个数小于m-1的叶子结点插入新的关键字,则不需要进行任何修改;如果该叶子结点在插入新关键字后,关键字个数超过m-1个,则需要进行“结点分裂”:首先要保证插入新的关键字后该结点中的所有关键字仍是有序的,然后取最中间位置的关键字k,若该结点没有双亲结点,即该B-树只有根节点,则将k分裂出来作为新的双亲结点,原本在k左边的所有关键字作为k的左孩子,k右边的所有关键字作为k的右孩子,树的高度增加一层;若该结点有双亲结点,则将k插入到双亲结点中,然后修改对应的指针域。(如果k加入双亲结点后,双亲结点的关键字个数也超过了m-1,则重复上述操作)

  • B-树的关键字删除:B-树的关键字删除正好和插入的考虑相反。对于一棵m阶B-树,删除关键字需要考虑两种情况<1>要删除的关键字所在结点为叶子结点。这里又需要分3种情况:①要删除的关键字所在结点的关键字个数大于[(m+1)/2-1],则直接删去该关键字即可; ②要删除的关键字所在的结点的关键字个数正好等于[(m+1)/2-1],则需要向其兄弟结点“借调”关键字(这里叫抢夺比较贴切,因为借了就不还了),如果向左(右)兄弟借,则该兄弟结点中最大(小)结点上移加入双亲结点,然后双亲结点中小(大)于被删除关键字的最大(小)关键字下移至被删除结点的位置; ③要删除关键字所在结点的关键字个数等于[(m+1)/2-1],且其兄弟结点中的关键字个数也都正好等于[(m+1)/2-1],则需要进行“结点合并”,即将一个兄弟结点及要删除的关键字所在结点还有双亲结点中的一个关键字(该关键字的取法与②相同)合并成一个新的叶子结点(如果合并后双亲结点的关键字个数小于[(m+1)/2-1],则重复上述步骤)。 <2>要删除的结点为非叶子结点:从被删除的关键字的左下(右下)子树“借调”最大(小)关键字来代替被删除的关键字。若被“借调”关键字后的子树关键字个数小于[(m+1)/2-1],若该子树为叶子结点,则按叶子结点的操作方法继续进行,否则重复上述步骤。

1.1.5 散列查找

  • 散列查找的定义:散列查找就是在记录的存储位置和其关键字之间建立一个对应的关系,使每个关键字都对应一个存储位置,即存储位置=f(关键字),这里把这种对应关系f称为散列函数,又称为哈希函数。对于存储地址,有两种选择:①顺序存储也成为哈希表,即用一维数组来存储关键字。 ②链地址法,即用链表的方式来存储关键字,也称作哈希链。

  • 哈希表:也叫做散列表。通过哈希函数,将关键字一一映射到数组中。例如:关键字7,哈希函数为存储地址=key%3,则关键字7的存储地址为数组中的第二个位置,即a[1]。但是不同的关键字根据哈希函数映射后,可能会对应同一存储地址,比如上述的关键字7和关键字10通过上述哈希函数映射后的地址都是a[1],这就叫哈希冲突。因为在实际中,哈希冲突是难以避免的,所以哈希表的设计主要需要解决哈希冲突,这主要与三个因素有关:与装填因子α有关,α=存储的关键字个数/哈希表大小(长度),α越小,冲突的可能性就越小,反之越大;与所采用的哈希函数有关;与解决冲突的方法有关<1>开放定址法:①线性探查法,即在遇到冲突时,冲突位置一直+1直到找到空位置或超过哈希表长为止 ②平方探查法,即在冲突位置探查±i²(1≤i≤哈希表长-1)的位置,直到找到空位置或超过哈希表的尽头为止(表头或表尾); <2>拉链法:即通过链表的方式,将因哈希冲突映射到同一地址的所有关键字用链表链起来(哈希链)。 在插入关键字的过程中,即通过哈希函数映射到地址后,如果没有发生冲突的话,则该关键字的成功探查次数即为1次,否则探查次数等于往前或往后探查次数的总和+1;对于一个给定的关键字,若其不在哈希表中,则其失败的探查次数为通过哈希函数映射后的地址到下一个空位置(0~p-1,p为哈希函数除留余数法中求余的数)所需要探查的数量+1,如果通过哈希函数映射后的地址为空,则失败的探查次数为1次。

pta 7-4
typedef struct node
{
	KeyType key;			//关键字域
	InfoType data;			//其他数据域
	int count;			//探查次数域
} HashTable[MaxSize];		//哈希表类型
void CreateHT(HashTable ha, KeyType x[], int n, int m, int p)  //创建哈希表,x为输入数组,n输入数据个数,m为哈希表长度,这里假设m=p
{
	int adr, t;
	for (int i = 0; i < m; i++)
	{
		ha[i].key = NULLKEY;
	}
	for (int i = 0; i < n; i++)
	{
		if (visited[x[i]] == 1)//如果数据已经插入,即遇到重复数据,则不进行插入操作
		{
			continue;
		}
		adr = x[i] % p;//哈希函数
		if (adr == NULLKEY )
		{
			ha[adr].key = x[i];
			ha[adr].count = 1;
			visited[x[i]] = 1;
		}
		else
		{
			t = 1;//成功探查次数
			while (ha[adr].key != NULLKEY )
			{
				adr = (adr + 1) % m;//线性探查法
				t++;
			}
			ha[adr].key = x[i];
			ha[adr].count = t;
			visited[x[i]] = 1;
		}
	}
}
int SearchHT(HashTable ha, int p, KeyType k)	//在哈希表中查找关键字k,找不到返回-1,找到返回查找地址。
{
	int i = 0, adr;
	adr = k % p;//哈希函数
	uns_count = 1;
	while (ha[adr].key != NULLKEY && ha[adr].key != k)
	{
		adr = (adr + 1) % p;
		uns_count++;//查找失败时探查次数
	}
	if (ha[adr].key == NULLKEY)
	{
		return -1;
	}
	if (ha[adr].key == k)
	{
		return adr;
	}
	else
	{
		return -1;
	}
}

  • 哈希链:用上述拉链法来解决哈希冲突所构造的链表就称为哈希链。对于哈希链的构造,一般采用头插法,有几个数据就有几条链。对于哈希链,查找成功的ASL=(Σ第i层结点数*i)/结点数(i≥1),查找失败的ASL=(Σ第i层的空结点数*i)/链的条数(i≥1)
pta 7-5
#include<iostream>
#include<string>
using namespace std;
#define MaxSize 100005
typedef struct HashNode
{
	string ID;
	struct HashNode* next;
	long long distance;
}HashNode,*HashTable;
void CreatHash(HashTable ht[], long long n,long long k,long long p);
HashTable Find(HashTable ht[],string ID,long long p);
long long ListSize(long long n);
long long Hash(string ID,long long size);
int main()
{
	long long N, K,FNum;
	HashTable ht[MaxSize];
	string ID;
	HashTable t;
	long long size;

	cin >> N >> K;
	size = ListSize(N);
	CreatHash(ht, N, K,size);
	cin >> FNum;
	for (long long i = 0; i < FNum; i++)
	{
		cin >> ID;
		t = Find(ht, ID,size);
		if (t == NULL)
		{
			cout << "No Info" << endl;
		}
		else
		{
			cout << t->distance<<endl;
		}
	}
}
void CreatHash(HashTable ht[], long long n,long long k,long long p)//创建哈希链
{
	long long distance,key;
	int status;
	string ID;
	HashTable t;
	long long i, j;

	for (i = 0; i < p; i++)
	{
		ht[i] = new HashNode;
		ht[i]->next= NULL;
	}
	for (i = 0; i < n; i++)
	{
		cin >> ID >> distance;
		key = Hash(ID,p);
		

		status = 0;
		t = Find(ht, ID, p);

		if (t==NULL)
		{
			//头插法
			HashTable temp = new HashNode;
			temp->next = ht[key]->next;
			temp->ID = ID;
			if (distance < k)
			{
				temp->distance = distance + k;
			}
			else
			{
				temp->distance = distance;
			}
			ht[key]->next = temp;
		}
		else
		{
			(t->distance) += distance;
		}
	}
}
HashTable Find(HashTable ht[], string ID,long long p)//查找关键字
{
	long long key;
	HashTable t;

	key = Hash(ID, p);
	t = ht[key]->next;
	while (t != NULL)
	{
		if (t->ID == ID)
		{
			return t;
		}
		t = t->next;
	}
	return NULL;
}
long long ListSize(long long n)//计算除留余数法的求余数
{
	long long j;
	for (long long i = n;; i++)
	{
		for (j = 2; j * j <= i; j++)
		{
			if (i % j == 0)
			{
				break;
			}
		}
		if (j * j > i)
		{
			return i;
		}
	}
}
long long Hash(string ID, long long size)//哈希函数,用以计算哈希地址
{
	long long key = 0;
	int i;
	for (i = 13; i < 18; i++)
	{
		if (ID[i] == 'x')
		{
			key = (key * 10 + 10) % size;
		}
		else
		{
			key = (key * 10 + ID[i] - '0') % size;
		}
	}
	return key;
}

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

  • 认识:在这信息化时代,查找可以说是十分重要。我们平常使用浏览器查找资料等操作都离不开查找。查找是一个浏览器的基本功能,有了查找,我们可以在短时间内搜索到我们想要的资料。查找还可以用作某些公司的会员记录表、学校的学生名单等,借助查找就可以在短时间内找到对应的客户信息、学生信息等。所以说,查找的实际用途十分广,可以说和我们如今的生活息息相关
  • 学习体会:查找的功能十分强大,种类也很多。在实际运用中需要根据不同的要求和数据信息选择不同的查找方式,否则可能导致查找效率低下;由于在实际运用中,存储数据量往往十分巨大,所以在写代码的时候要注意数组范围或选择动态存储的方式,避免越界;如果使用动态存储的方式存放数据的话,要记得释放动态申请的内存,否则如果程序长时间运行的话可能会导致计算机运行缓慢甚至死机

2.PTA题目介绍

2.1 7-1 是否完全二叉搜索树

2.1.1 该题的设计思路

  • 设计思路:一开始看到这个题目,我的第一想法是利用队列,对树进行层序遍历。遍历过程中,每出队一个结点就判断其是否有左孩子和右孩子,如果有左孩子没有右孩子或都没有的话,就标记status=false,则后续的所有结点都只能是叶子结点,即没有左孩子与右孩子,否则的话就不是完全二叉搜索树。
  • 时间复杂度:O(结点个数)

2.1.2 该题的伪代码

输入结点数n;
定义根节点BinTree T = NULL; 
for(i=0 to n-1)
  输入a[i];
  调用T = Insert(T, a[i])函数将a[i]插入二叉搜索树;
  {
    if(空树)  a[i]直接作为根节点的值创建二叉树;
    else if(a[i]小于结点值)  递归将a[i]判断插入左子树BST->Left = Insert(T->Left, a[i]); 
    else if(a[i]大于结点值)  递归将a[i]判断插入右子树BST->Right = Insert(T->Right, a[i]);
    return T;//程序能运行到这条语句说明a[i]以插入树中或a[i]已存在,则不再重复插入
  }
end for
调用printBSTree(T)函数层序遍历输出二叉搜索树;//这步不重要,不进行详细说明
调用IsComplateTree(T)函数对二叉搜索树进行判断
{
  if(根节点不空)  入队根节点;
  初始化状态status=true;//如果status==false,则后续所有结点都只能是叶子结点
  while(队列不为空)
    出队队头结点t;
    if(t的左孩子不为空)
      if(status==false)     说明前面已经有某个结点只有左孩子没有右孩子或左、右孩子都没有,而且结点t不是叶子结点,不符合要求,return false;
      t的左孩子入队; 
    else  标记status=false;
    if(t的右孩子不为空)
      if(status==false)     说明前面已经有某个结点只有左孩子没有右孩子或左、右孩子都没有,而且结点t不是叶子结点,不符合要求,return false;
      t的右孩子入队;
    else  标记status=false;
  end while
  return true;//运行到这条语句说明所有结点均符合要求
}

2.1.3 解题代码

#include<iostream>
#include<queue>
using namespace std;
typedef int ElementType;
typedef struct TNode* Position;
typedef Position BinTree;
struct TNode {
	ElementType Data;
	BinTree Left;
	BinTree Right;
};
BinTree Insert(BinTree BST, ElementType x);
bool IsComplateTree(BinTree BST);//判断是否是完全二叉树
void printBSTree(BinTree t);//层序遍历二叉树

int main()
{
	int n;
	int a[21];
	BinTree T=NULL;

	cin >> n;
	for (int i = 0; i < n; i++)
	{
		cin >> a[i];
		T=Insert(T, a[i]);
	}
	printBSTree(T);
	if (IsComplateTree(T))
	{
		cout <<endl<< "YES";
	}
	else
	{
		cout <<endl<< "NO";
	}
}

BinTree Insert(BinTree BST, ElementType x)
{
	if (!BST) {
		BinTree p = new TNode;
		p->Data = x;
		p->Left = p->Right = NULL;
		BST = p;
	}
	else if (x > BST->Data)BST->Left = Insert(BST->Left, x);
	else if (x < BST->Data)BST->Right = Insert(BST->Right, x);
	return BST;
}
bool IsComplateTree(BinTree BST)
{
	queue<BinTree> p;
	int count = 0;
	if (BST != NULL)
	{
		p.push(BST);
	}
	bool status = true;
	while (!p.empty())
	{
		BinTree t = p.front();
		p.pop();
		if (t->Left != NULL)
		{
			if (status == false)
			{
				return false;
			}
			p.push(t->Left);
		}
		else
		{
			status = false;
		}
		if (t->Right)
		{
			if (status == false)
			{
				return false;
			}
			p.push(t->Right);
		}
		else
		{
			status = false;
		}
	}
	return true;
}
void printBSTree(BinTree t)
{
	queue<BinTree> tree;
	BinTree p;

	if (t == NULL)
	{
		cout << "NULL";
		return;
	}
	tree.push(t);

	while (!tree.empty())
	{
		p = tree.front();
		tree.pop();
		cout << p->Data;
		if (p->Left)
		{
			tree.push(p->Left);
		}
		if (p->Right)
		{
			tree.push(p->Right);
		}
		if (!tree.empty())
		{
			cout << " ";
		}
	}
}

2.1.4 PTA提交列表

1.在写这题的时候我对完全二叉搜索树的概念已经有些模糊了,所以不得不上网查询一下相关信息重新了解一下完全二叉搜索树:叶子结点只能在倒数第一和倒数第二层,且如果一个结点的度为1的话,那么该结点一定只能有左子树,且如果该层的某一结点只有左子树没有右子树或为叶子结点的话,则该层以该结点往右的所有结点都只能是叶子结点。
2.部分正确:我一开始为了图方便,判断二叉搜索树的过程中,一边判断一边输出,结果导致只能过一个最小N的测试点;经检查才发现,如果输入的树不是完全二叉搜索树的话我这种写法只会输出部分层序遍历的结点数据,而题目要求不管是不是完全二叉搜索树都必须输出该树的层序遍历顺序。于是便另外写了一个函数来输出层序遍历数据。
3-4完全正确:解决完上述问题后,提交通过。

2.1.5 本题设计的知识点

  • 完全二叉树的概念:叶子只能出现在最下面的二层、最下层的叶子一定集中在左部的连续位置、倒数第二层若有叶子结点,一定在右部连续位置、如果结点的度为1 ,则该结点只有左孩子。
  • 层序遍历:借助队列来进行层序遍历,每出队一个结点就将其左孩子和右孩子入队,直到队空为止。
  • 二叉搜索树的创建:用递归对每个待插入的数据进行判断,如果比根节点小的话就将根节点的左孩子做新的根节点进行递归判断;如果比根节点大的话就将根节点的右孩子作为新的根节点进行递归判断;如果根节点为空的话则直接将该数据作为根节点的数据创建根节点。

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

2.2.1 该题的设计思路

  • 设计思路:定义一个全局数组visited[]用来判断数据是否重复插入,初始划visited[]为0。如果visited[key]==0,说明哈希表中不存在关键字key,则插入key,并置visited[key]=1。对于哈希函数的选择,采用除留余数法,为方便计算,求余数p取哈希表长,即哈希函数为adr=key%p,p为哈希表长度。用线性探测法来解决哈希冲突。
  • 时间复杂度:O(1)

2.2.2 该题的伪代码

输入关键字个数n,哈希表长度p;
for(i=0 to n-1) 输入关键字存入数组x[]中;end for
for(i=0 to n-1) 初始化visited[]数组为0; end for
调用CreateHT(ha, x, n, m, p)函数创建哈希表;x为输入数组,n输入数据个数,m为哈希表长度,m=p
{
  for(i=0 to m-1) 初始化哈希表中所有地址的值为空; end for
  for(i=0 to n-1)
    if(待插入关键字x[i]已存在哈希表中即visited[x[i]]==1) continue;
    adr=x[i]%p;//求哈希地址
    if(adr所指地址没有数据)
      将x[i]插入到哈希表ha[adr]中,标记x[i]已插入哈希表visited[x[i]]=1;
    else
      while(adr所在地址不为空)  线性探测法adr=(adr+1)%m;  end while
      将x[i]插入到哈希表ha[adr]中,标记x[i]已插入哈希表visited[x[i]]=1;
  end for
}
for(j=0 to n-1)
调用i = SearchHT(ha, p, x[j])函数查找x[j]在哈希表中的地址;
{
  adr=x[i]%p;//原始的哈希地址
  while(adr所指地址不空&&adr所指地址的关键字不等于x[i]) 线性探测法adr=(adr+1)%p;
  if(adr所指地址的关键字为空)  x[i]不在哈希表中,return -1;
  if(adr所指地址的关键字等于x[i]) 返回x[i]所在地址adr;
}

2.2.3 解题代码

#include<iostream>
#define MaxSize 1009			//定义最大哈希表长度
#define NULLKEY -1			//定义空关键字值
using namespace std;
typedef int KeyType;		//关键字类型
typedef char* InfoType;	//其他数据类型
typedef struct node
{
	KeyType key;			//关键字域
	InfoType data;			//其他数据域
	int count;				//探查次数域
} HashTable[MaxSize];		//哈希表类型
void InsertHT(HashTable ha, int& n, KeyType k, int p);//哈希表插入数据
void CreateHT(HashTable ha, KeyType x[], int n, int m, int p);  //创建哈希表
int SearchHT(HashTable ha, int p, KeyType k);	//在哈希表中查找关键字k
int uns_count = 0;//查找不成功查找次数 
int visited[100000];//用来记录数据是否已经插入
int main()
{
	int n, p, m, i, k;
	cin >> n >> p;
	int x[MaxSize];
	int count = 0;
	HashTable ha;
	for (i = 0; i < n; i++) cin >> x[i];
	for (i = 0; i < n; i++)
	{
		visited[i] = 0;
	}
	m = p;
	CreateHT(ha, x, n, m, p);
	for (int j = 0; j < n; j++)
	{
		uns_count = 0;
		k = x[j];
		i = SearchHT(ha, p, k);
		if (count == 0)
		{
			cout << i;
			count++;
		}
		else
		{
			cout << " " << i;
		}
	}
	return 0;
}

/* 请在这里填写答案 */
void CreateHT(HashTable ha, KeyType x[], int n, int m, int p)  //创建哈希表,x为输入数组,n输入数据个数,m为哈希表长度,这里假设m=p
{
	int adr, t;
	for (int i = 0; i < m; i++)
	{
		ha[i].key = NULLKEY;
	}
	for (int i = 0; i < n; i++)
	{
		if (visited[x[i]] == 1)//如果数据已经插入,即遇到重复数据,则不进行插入操作
		{
			continue;
		}
		adr = x[i] % p;
		if (adr == NULLKEY )
		{
			ha[adr].key = x[i];
			ha[adr].count = 1;
			visited[x[i]] = 1;
		}
		else
		{
			t = 1;
			while (ha[adr].key != NULLKEY )
			{
				adr = (adr + 1) % m;
				t++;
			}
			ha[adr].key = x[i];
			ha[adr].count = t;
			visited[x[i]] = 1;
		}
	}
}
int SearchHT(HashTable ha, int p, KeyType k)	//在哈希表中查找关键字k,找不到返回-1,找到返回查找地址。
{
	int i = 0, adr;
	adr = k % p;
	uns_count = 1;
	while (ha[adr].key != NULLKEY && ha[adr].key != k)
	{
		adr = (adr + 1) % p;
		uns_count++;
	}
	if (ha[adr].key == NULLKEY)
	{
		return -1;
	}
	if (ha[adr].key == k)
	{
		return adr;
	}
	else
	{
		return -1;
	}
}

2.2.4 PTA提交列表

部分正确1-5:这里主要是最后一个测试点最大N随机过不了,段错误。经检查后才发现是我最大哈希表长度MaxSize的值设错了,题目要求最大数据为1000,而我只设了100,修改MaxSize的值后段错误解决。
编译错误:这个是在修改上述问题的过程中不小心按到键盘多打了几个字符下去,导致编译错误。
部分正确7-14:这里还是最后一个测试点最大N过不了,我实在想不出这个测试点是什么意思,于是便上网查了一下相关题目,才发现是因为我没有特别处理插入重复数据的情况,以我原本的写法,如果有重复数据的话不会直接跳过,而是会继续插入到哈希表中,导致重复数据时重复插入;新增了一个全局数组来对每个数据的插入情况进行判断后,问题解决。
答案正确15-16:解决完上述问题后,提交通过。

2.2.5 本题设计的知识点

  • 哈希表的构建:由于这题已经给定了哈希表长度,所以不需要额外去求最合适的哈希表长度。否则一般情况下哈希表的长度为大于数据个数的最小素数;对于哈希冲突的解决方法,本题采用开放定址法,也可以选用拉链法
  • 哈希表关键字的查找:对于关键字的查找,由于本题只要求输出所有关键字的地址,所以直接对有所关键字进行哈希函数查找地址或线性探测法查找即可;否则,如果待查找的关键字不存在哈希表中的话,用线性探测法探查到空地址时就代表该关键字不存在哈希表中。
  • 对于重复数据的插入:可以选择定义一个全局数组来判断,也可以将哈希函数查找到的地址的关键字与待插入关键字比较,如果相同的话则不进行插入。

2.3 7-6(自建倒排索引表) 基于词频的文件相似度

2.3.1 该题的设计思路

  • 设计思路:利用set集合来存放文档单词,这样可以保证不会插入重复数据;由于输入的文档数据可能会有换行,所以用getline()来一行一行输入数据;对于一行输入的数据,用string来存放,对单词的处理则是遍历string字符串,用isalpha()函数来排除非字母字符,先遍历该行字符串,遍历到非字母字符时停止,则该非字母前、单词首字母后的所有字符即为该单词;为方便后续对公共单词的判断,将所有单词一律转换为大写;对于公共单词的判断,通过遍历set集合,用string库中的count函数来判断一个文档中的单词是否存在于另一个文档中。
  • 时间复杂度:O(n²)

2.3.2 该题的伪代码

定义set文档集合set<string> doc[MaxN];
输入文档数N;
getchar()吸收回车符;
for(i=1 to N)
  while(循环输入数据)
    getline(cin,line)输入一行数据;
    if(输入的数据line==“#”)  文档数据输入完毕,break;
    for(j=0 to 当前行数据的长度line.size();)//j为单词首字母位置
      k=j;//k用来记录单词尾字母位置
      while(k小于当前行数据长度line.size()&&line[k]为字母)  k++; end while
      if(单词长度大于等于3即k-j≥3)
        if(单词长度小于10) 
          调用word = line.substr(j, k - j)提取主串中的单词;
          for(l=0 to 单词长度word.size()-1) 将word[l]转换为大写字母word[l] -= 'a' - 'A';
        else  
          调用word = line.substr(j, 10)提取主串中的单词;
          for(l=0 to 单词长度word.size()-1) 将word[l]转换为大写字母word[l] -= 'a' - 'A';
        将单词word插入到文档i中doc[i].insert(word);
      while(k小于当前行数据长度&&line[k]不为字母)  滤去非字母字符k++; end while
      修改下一个单词首字母位置 j=k;
    end for
  end while
end for
输入待查询次数m;
while(m--)
  输入待查询文档p、q;
  定义公共词汇量s=0;
  for(遍历文档p中的所有单词it)
    if(文档q中存在单词it) 公共词汇量s++;
  end for
  两个文档的总词汇量t=doc[p].size()+doc[q].size();
  两个文档的相似度=s/(t-s);
end while

2.3.3 解题代码

#include<iostream>
#include<map>
#include<string>
#include<cctype>
#include<set>
#include<stdio.h>
#include<string.h>
#define MaxN 105
using namespace std;

int main()
{
	int N, i, j, k;
	string line, word;//line为每行输入的内容,word为单个单词内容
	set<string> doc[MaxN];//文档合集
	cin >> N;
	getchar();//吸收回车符
	for (i = 1; i <= N; i++)
	{
		while (1)
		{
			getline(cin, line);
			if (line == "#")
			{
				break;
			}
			for (j = 0; j < line.size();)
			{
				k = j;
				while (k < line.size() && isalpha(line[k]))//计算单词长度
				{
					k++;
				}
				if (k - j >= 3)//j为单词首字母地址,k为单词尾字母地址+1
				{
					if (k - j < 10)
					{
						word = line.substr(j, k - j);//提取主串中的单词
						//一律将小写字母转为大写字母
						for (int l = 0; l < word.size(); l++)
						{
							if (word[l] >= 'a' && word[i] <= 'z')
							{
								word[l] -= 'a' - 'A';
							}
						}
					}
					else
					{
						word = line.substr(j, 10);//提取主串中的单词
						//一律将小写字母转为大写字母
						for (int l = 0; l < word.size(); l++)
						{
							if (word[l] >= 'a' && word[i] <= 'z')
							{
								word[l] -= 'a' - 'A';
							}
						}
					}
					doc[i].insert(word);
				}
				while (k < line.size() && !isalpha(line[k]))//滤去非字母字符
				{
					k++;
				}
				j = k;//下一个单词的首字母地址
			}
		}
	}
	int m, p, q;
	cin >> m;
	while (m--)
	{
		cin >> p >> q;
		int s = 0;//公共词汇量
		int t;
		for (string it : doc[p])//对set容器中元素的遍历,it为p文档中的单词
		{
			if (doc[q].count(it))//判断q文档中是否存在p文档中的单词it
			{
				s++;
			}
		}
		t = doc[p].size() + doc[q].size();//两个文档的总词汇量
		double result;
		result = s * 100.0 / (t - s);
		printf("%.1f%%\n", result);
	}
	return 0;
}

2.3.4 PTA提交列表

1.我第一眼看到这题的时候一点头绪都没有,连倒排索引表是什么,怎么建倒排索引表都不知道,只好结合课件,上网查询相关资料;了解了相关资料后,发现这题只是基础的倒排索引表,不需要考虑一个文档中出现重复单词,所以可以用set库解决;而且这题不需要记录单词在文档中出现的位置,所以可以省去很多麻烦。
2.在写的过程中,不知道要怎么判断一个单词的起止位置,怎么把单词从主串中抠出来。于是便想到了string库中的函数,上网查询string库中的函数,发现substr可以从string字符串中截取子串,于是便使用substr来从主串中提取单词。
3.对于字母与非字母的判断,我一开始是打算直接几条if语句来判断字符line[k]是不是大写或小写字母,但觉得这样做有点麻烦,于是便上网查询判断字母与非字母的做法,发现cctype库中有一个isalpha函数可以直接用来判断字母与非字母,于是便改用该函数判断字母与非字母。
4.还有就是要怎么处理相同单词不同大小写,我一开始是打算将一个单词的所有不同大小写情况全部写入文档中,但写的过程中发现,这样子要增加很多代码量,且时间复杂度也高。于是就一律将所有单词都转换为大写字母后再存入文档中。
5.部分正确1-3:解决完上述问题后,最后一个最大N,M的测试点还是过不了。一开始以为是我最大值MaxN设置错了,于是便增大MaxN,发现还是解决不了。仔细看发现是答案错误,不是运行超时。才疏学浅,反复检查多次后还是找不出问题所在,只得作罢,待日后再想办法解决。

2.3.5 本题设计的知识点

  • 倒排索引表的构建:由于本题不需要考虑一个文档中存在多个重复单词,所以只需要用set集合来存放文档数据即可;否则可以定义一个二维数组string a[][]动态数组vettor<string>a[]来存放文档信息。
  • 对于字母与非字母的判断:如果是C语言的话,可以在头文件中加入<ctype.h>,如果是C++的话就在头文件中加入<cctype>,然后用库中的函数isalpha即可快速判断字符是不是字母。
  • 对于相同单词不同大小写的情况,这里可以统一转换为大写或小写,方便后续判断。
  • 单词的提取:要从主串中提取子串单词,可以使用string库中的substr(子串起始位置,子串结尾位置)来提取单词,但需要知道单词的首字母位置和尾字母位置,所以需要加一条循环语句来找单词的尾字母位置。
  • 对于相似单词的判断:可以用string库中的count函数来判断该单词在文档中出现过几次。因为本题不考虑一个文档中存在多个重复单词,所以doc[q].count[it]只会有两个值,如果是1的话说明文档q中存在单词it;如果是0的话说明文档q中不存在单词it。
posted @ 2020-05-24 20:01  Kevin。  阅读(281)  评论(0编辑  收藏  举报