DS博客作业05--查找

0.PTA得分截图

1.本周学习总结

1.1 查找表相关名词解释

  • 静态查找表:在查找表中只做查找操作,而不改动表中数据元素。

  • 动态查找表:在查找表中做查找操作的同时进行插入数据或者删除数据的操作。

  • ASL:查找成功时,查找的关键字和查找表中的数据元素中进行过比较的个数的平均值,即为平均查找长度(Average Search Length)。

Pi 为第 i 个数据元素被查找的概率,通常认为概率都是相同的,即都为 i/n ;Ci 表示在查找到第 i 个数据元素之前已进行过比较的次数。

1.2 静态查找表

1.2.1 顺序查找

  • 查找原理:从线性表中的一端开始,逐个与关键字做比较,如果匹配成功,则为查找成功;若直到另一端也没匹配成功即为查找失败。

  • 查找的性能指标

    ASL(成功):(n+1)/2

    ASL(不成功):n

  • 代码实现:

int SeqSearch(SeqList 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;             //找到返回位置
}

1.2.2 二分查找

  • 查找原理:先对查找表进行排序,然后让指针 low 和 high 分别指向查找表的第一个元素和最后一个元素,指针 mid 指向处于 low 和 high 指针中间位置的元素。

查找时,若被查找元素大于中间元素,low 指针不动,high 指针应改指向 mid 指针左边第一个元素;同理,若被查找元素小于中间元素,high 指针不动,low 指针应

改指向 mid 指针右边第一个元素;若 mid 指针指向的元素等于被查找元素即匹配成功;若 low 指针与 high 指针出现逻辑上越界,即为匹配失败。(求得 mid 的位

置不是整数,需做取整操作。)

  • 图解:对查找表{5,13,19,21,37,56,64,75,80,88,92}采用折半查找 21 :

由于 21 < 56,low 指针不动,high 指针应改指向 mid 指针左边第一个元素,如下图

由于 19 < 21, high 指针不动,low 指针应改指向 mid 指针右边第一个元素,如下图

mid 指针指向的元素等于被查找元素即匹配成功。

  • 查找的性能指标

    ASL(成功):log2(n+1)-1

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

  • 代码实现:

int Search(SSTable *ST,keyType key)
{
    int low=1;//初始状态 low 指针指向第一个关键字
    int high=ST->length;//high 指向最后一个关键字
    int mid;
    while (low<=high) 
    {
        mid=(low+high)/2;//int 本身为整形,所以,mid 每次为取整的整数
        if (ST->elem[mid].key==key)//如果 mid 指向的同要查找的相等,返回 mid 所指向的位置
            return mid;
        else if(ST->elem[mid].key>key)//如果mid指向的关键字较大,则更新 high 指针的位置
            high=mid-1;
        //反之,则更新 low 指针的位置
        else
            low=mid+1;
    }
    return 0;
}
  • 缺点:只适用于用顺序存储结构表示的有序表,因为当查找表使用链式存储结构表示时,排序和查找操作的实现都异常繁琐。

  • 优点:与顺序查找相对比,折半查找的效率要高。

1.2 二叉搜索树

1.2.1 二叉搜索树的原理

  • 查找原理:以二叉树来组织,若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点

的值;它的左、右子树也分别为二叉排序树,如下图所示

1.2.2 二叉搜索树的构建与插入

  • 结构体定义:
struct TNode
{
    ElementType Data;
    BinTree Left;
    BinTree Right;
};
  • 代码实现
BinTree Insert(BinTree BST, ElementType X)
{
	if (BST == NULL)
	{
        BST=(BinTree)malloc(sizeof(struct TNode));
		BST->Data = X;
		BST->Left = BST->Right = NULL;
	}
	else
	{
		if (X > BST->Data)
			BST->Right = Insert(BST->Right, X);
		else
			BST->Left = Insert(BST->Left, X);
	}
	return BST;
}

1.2.3 二叉搜索树的删除

  • 代码实现
BinTree Delete(BinTree BST, ElementType X)
{
	Position Tmp;
	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)//左右孩子都有 
			{
				Tmp = FindMin(BST->Right);
				BST->Data = Tmp->Data;
				BST->Right = Delete(BST->Right, BST->Data);
			}
			else //左右孩子不都有 
			{
				Tmp = BST;
				if (BST->Left==NULL)//只有右孩子 
					BST = BST->Right;
				else//只有左孩子
					BST = BST->Left;
				free(Tmp);
			}
		}
	}
	return BST;
}

1.2.4 二叉搜索树的查找

  • 代码实现
Position Find(BinTree BST, ElementType X)
{
	if (BST == NULL)
		return NULL;
	else
	{
		if (X < BST->Data)
			return Find(BST->Left, X);
		else if (X > BST->Data)
			return Find(BST->Right, X);
		else
			return BST;
	}
}

1.2.5 二叉搜索树的ASL

ASL(成功):每个结点的深度相加除以结点个数,即 (11+22+33+34+25+16)/12=3.5

ASL(不成功):可先补齐二叉树(如下图),每个补齐的结点的根结点的深度相加除以补齐结点的个数,即(12+33+44+35+2*6)/13=4.15

1.3 AVL树

1.3.1 AVL树相关概念

  • ACL树,又称平衡二叉树。有两大特点:一,每棵子树中的左子树和右子树的深度差不能超过 1;二,二叉树中每棵子树都要求是平衡二叉树。如下图

  • 平衡因子:每个结点都有其各自的平衡因子,表示的就是其左子树深度同右子树深度的差,平衡二叉树中各结点平衡因子的取值只可能是:0、1 和 -1。

  • 平衡调整:在一棵AVL树中插入一个新结点,有可能造成失衡,此时必须重新调整树的结构,使之恢复平衡,该过程即为平衡旋转。

  • 时间复杂度:使用平衡二叉树进行查找操作的时间复杂度为O(logn)。

1.3.2 RR平衡旋转

  • 原理:若在以结点 a 的左子树为根结点的左子树上插入结点,致使以 a 为根结点的子树失去平衡,则只需进行一次向右的顺时针旋转:

    ①A的右孩子B左上旋转作为A的根节点

    ②节点左下旋转称为B的左孩子

    ③B原左子树称为A右子树

  • 图解

1.3.3 LL平衡旋转

  • 原理:若在以结点 a 的右子树为根结点的左子树上插入结点,致使以 a 为根结点的子树失去平衡,则只需进行一次向左的逆时针旋转:

    ①A的左孩子B右上旋转作为A的根节点

    ②A节点右下旋转称为B的右孩子

    ③B原右子树称为A左子树

  • 图解

1.3.4 LR平衡旋转

  • 原理:若在以结点 a 的左子树为根结点的右子树上插入结点,致使以 a 为根结点的子树失去平衡,则需要进行两次旋转(先左旋后右旋)操作:

    ①C向上旋转到A的位置,A作为C右孩子

    ②C原左孩子作为B的右孩子

    ③C原右孩子作为A的左孩子

  • 图解

1.3.4 RL平衡旋转

  • 原理:若在以结点 a 的右子树为根结点的左子树上插入结点,致使以 a 为根结点的子树失去平衡,则需要进行两次旋转(先右旋后左旋)操作:

    ①C向上旋转到A的位置,A作为C左孩子

    ②C原左孩子作为A的右孩子

    ③3原右孩子作为B的左孩子

  • 图解

1.4 B-树

1.4.1 B-树的相关概念及特性

  • B-树是一种多路搜索树,即一个节点可放多个关键字。

  • 根结点至少有两个子女。

  • 所有的叶子结点都位于同一层。

  • 每个非根节点所包含的关键字个数 j 满足:┌m/2┐ - 1 <= j <= m - 1。

  • 内部子树个数 k 满足:┌m/2┐ <= k <= m。

1.4.2 B-树的插入

  • 在一棵建好的ASL树上进行插入操作时,关键字插入的位置必定在叶子结点层,于是便有以下两种情况:

    ①该结点的关键字个数n<m-1,正常插入,不修改原树。

    ②该结点的关键字个数 n=m-1,非正常插入,需进行“结点分裂”。

  • 结点分裂

如果没有双亲结点,新建一个双亲结点,树的高度增加一层。

如果有双亲结点,将ki插入到双亲结点中。

例:

1.4.3 B-树的删除

  • 在非叶子结点层上删除关键字k。

    找到要删除的关键字所在结点,假设所删关键字为非终端结点中Ki,则可用指针Ki所指子树中最小或最大关键字Y代替Ki,然后在相应终端结点中删去Y,这样就转为删除

    在叶子结点层上的关键字的问题了。

  • 在叶子结点层上删除关键字k。

    ①假如叶子结点的关键字个数大于Min,即删去该关键字后该结点仍满足B树的定义,则可直接删去该关键字。例:

②假如b结点的关键字个数等于Min,但可以从兄弟结点的情况,例:

③假如b结点的关键字个数等于Min,但不可以从兄弟结点的情况,例:

1.5 B+树

*1.5.1 m阶B+树的特性

  • B+ 树是一种树数据结构,是一个n叉树,每个节点通常有多个孩子,一颗B+树包含根节点、内部节点和叶子节点。

  • B+ 树元素自底向上插入。

  • 根结点或者没有子树,或者至少有两棵子树。

  • 除根结点外,其他每个分支结点至少有ceil(m / 2)棵子树,最多有m个子树。

  • 有n棵子树的结点恰好有n个关键字。

  • 所有叶子结点包含全部关键字及指向相应记录的指针,而且叶子结点按关键字大小顺序链接。并将所有叶子结点链接起来。

  • 所有分支结点(可看成是索引的索引)中仅包含它的各个子结点(即下级索引的索引块)中最大关键字及指向子结点的指针。

1.5.2 B+树与B-树的区别

  • 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。

  • 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。

1.5.3 B+树的应用

  • B+ 树通常用于数据库和操作系统的文件系统中。

1.6 哈希表

1.6.1 哈希表的相关概念

  • 哈希表的建立同函数类似,查找记录时使用的关键字相当于函数的自变量,然后经过一个公式中,求出一个因变量,这个因变量就是表示记录存储的哈希地址。

  • 哈希地址:哈希地址只是表示在查找表中的存储位置,而不是实际的物理存储位置。

  • 哈希冲突:对于两个关键字分别为ki和kj(i≠j)的记录,有ki≠kj,但h(ki)=h(kj),又称同义词冲突,在哈希表存储结构的存储中,哈希冲突难以避免。

  • 装填因子:装填因子α=存储的记录个数/哈希表的大小,α越小,冲突的可能性就越小; α越大(最大可取1),冲突的可能性就越大。

  • 非同义词冲突:哈希函数值不相同的两个记录争夺同一个后继哈希地址,即堆积(或聚集)现象。

  • 探查次数:确定哈希地址的次数。

1.6.2 哈希函数的构造

  • 直接定址法:以关键字k本身或关键字加上某个数值常量作为哈希地址的方法,公式:H(key)=a * key + b

  • 除留余数法:若已知整个哈希表的最大长度 m,可以取一个不大于 m 的数 p,然后对该关键字 key 做取余运算,公式:H(key)= key % p。

  • 数字分析法: 如果关键字由多位字符或者数字组成,就可以考虑抽取其中的 2 位或者多位作为该关键字对应的哈希地址,在取法上尽量选择变化较多的位,避免冲突发生。

  • 平方取中法:对关键字做平方操作,取中间得几位作为哈希地址。

  • 折叠法:将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址。此方法适合关键字位数较多的情况。

  • 随机数法:是取关键字的一个随机函数值作为它的哈希地址,公式:H(key)=random(key),此方法适用于关键字长度不等的情况。

1.6.3 哈希冲突的解决办法

  • 开放定址法

    ①线性探测法:H(key)=(H(key)+ d)MOD m(其中 m 为哈希表的表长,d 为一个增量),d=1,2,3,…,m-1

    ②平方探测法:d0=h(k),di=(d0 ± i^2) mod m (1≤i≤m-1),可以避免出现堆积现象,不能探查到哈希表上的所有单元,但至少能探查到一半单元。

  • 拉链法:即建哈希链,下文展开叙述。

1.6.4 哈希表的相关AVL计算

  • ASL(成功):(2+1+1+1+1+4+1+1+1+1+1)/11=1.364

  • ASL(不成功):(2+1+10+9+8+7+6+5+4+3+2+1+3)/13=4.692

1.6.5 哈希表代码实现

  • 结构体定义
typedef struct node
{
	KeyType key;			//关键字域
	InfoType data;			//其他数据域
	int count;				//探查次数域
} HashTable[MaxSize];		//哈希表类型
  • 创建哈希表
void CreateHT(HashTable ha, KeyType x[], int n, int m, int p)  //创建哈希表
{
	int i;
	int count = 0;
	for (i = 0; i < m; i++)
		ha[i].count = 0;
	for (i = 0; i < n; i++)
		InsertHT(ha, count, x[i], p);
}
  • 往哈希表插入数据
void InsertHT(HashTable ha, int& n, KeyType k, int p)//哈希表插入数据
{
	n++;
	int loc;
	loc = k % p;
	int count = 0;
	if (ha[loc].count == 0)
	{
		ha[loc].key = k;
		ha[loc].count = 1;
	}
	else
	{
		while (ha[loc].count != 0)
		{
			count++;
			loc = (loc + 1) % p;
		}
		count++;
		ha[loc].key = k;
		ha[loc].count = count;
	}
}
  • 哈希表查找
int SearchHT(HashTable ha, int p, KeyType k)	//在哈希表中查找关键字k
{
	int i;
	int loc;
	loc = k % p;
	if (ha[loc].key == 0)
		return -1;
	else
	{
		if (ha[loc].key == k)
			return loc;
		else
		{
			uns_count++;
			while (1)
			{
				loc = (loc + 1) % p;
				uns_count++;
				if (ha[loc].count == 0)
					return -1;
				if (ha[loc].key == k)
					return loc;
			}
		}
	}
}

1.7 哈希链

1.7.1 哈希链的相关概念

  • 概念:把所有的同义词用单链表链接起来。

  • 例:关键字集合:(16,74,60,43,54,90,46,31,29,88,77)

1.7.2 哈希链的相关AVL计算

  • ASL(成功):各结点的层数之和除以结点数,即(19+22)/11=1.182。

  • ASL(不成功):探寻到各有效单链表的NULL所需的比较次数之和除以链表长度,即(17+22)/13=0.846。

1.7.3 哈希链的代码实现

  • 结构体定义
typedef struct node
{
	char id[20];
	int miles;
	struct node* next;
}*List;
typedef struct tb
{
	int Tablesize;
	List* list;
}*HashList;
  • 建链
HashList Init(int size)
{
	HashList H = (HashList)malloc(sizeof(tb));
	H->Tablesize = NextPrim(size);//取哈希链长度
	H->list = (List*)malloc(sizeof(List) * H->Tablesize);
	for (int i = 0; i < H->Tablesize; i++)
	{
		H->list[i] = (List)malloc(sizeof(node));
		H->list[i]->next = NULL;
	}
	return H;
}
  • 取哈希链长度
int NextPrim(int size)
{
	int j;
	for (int i = size;; i++)
	{
		for (j = 2; j * j <= i; j++)
			if (i % j == 0)
				break;
		if (j * j > i)
			return i;
	}
}
  • 查找
List Find(char key[], HashList H)
{
	List t = H->list[Hash(key, H->Tablesize)];
	List p = t->next;
	while (p != NULL && strcmp(key, p->id))
		p = p->next;
	return p;
}
  • 插入
void Insert(char key[], int miles, HashList H)
{
	List t = H->list[Hash(key, H->Tablesize)];
	List f = Find(key, H);
	if (f == NULL)//该位置为空
	{
		List tmp = (List)malloc(sizeof(node));
		tmp->miles = miles;
		strcpy(tmp->id, key);
		tmp->next = t->next;
		t->next = tmp;
	}
	else
		f->miles = f->miles + miles;
}

1.8 对查找的认识及学习体会

有了前面链表与树知识的铺垫,对各种查找算法的理解还是有帮助的。以前也接触过查找方面的内容,比如折半法、顺序查找、建哈希表,知识点方面较为熟悉,学起来较为轻

松,但通过本章内容的学习还是极大拓展了我的知识面,比如就建哈希表而言,就有多种哈希函数的构造方法以及拉链法,通过PTA的编程练习我还了解到还有一种高效的查找

算法:倒排索引表,虽然有些难懂,但终究是能攻克的。数据化时代,如何实现对数据的快速灵活运用,数据的查找首当其冲,学好这方面内容必然会大有帮助。

2.PTA题目介绍

2.1 是否完全二叉搜索树

2.1.1 该题的设计思路

  • 思路:层序遍历二叉搜索树,统计符合完全二叉搜索树条件的结点,与总结点数做比较,若相同即为完全二叉搜索树,不相同则不是。所以关键就是确定结

    点的判断条件:根据二叉树结点的左右孩子个数可以把结点分成四种情况:①双满、②双空、③右空和④左空(如下表),层次遍历时,先记录该结点的子树情况,然后遍

    历到下一个结点时就可以结合前一个结点的情况判断是否符合条件。

  • 时间复杂度:需要遍历所有结点,所以为O(n)。

2.1.2 伪代码

void CheckTree(BinTree BST, int N)
{
	queue<BinTree> Q;
	BinTree temp;
	int flag = 0;//统计合法结点数量
	int front_flag = 0;//0表示根结点,1表示双满的结点,2表示双空的结点,3表示右空结点,4表示左空结点
	开始层序遍历
	{
		取队首赋予temp
		if 是双满的根结点 或 前结点双满且当前结点也双满 then
			标记该结点为1,并加入统计
		end if
		if 前结点不是左空 且 当前结点为双空 then
			标记该结点为2,并加入统计
		end if
		if 是右空的根结点 或 前结点双满且当前结点右空 then
			标记该结点为3,并加入统计
                end if
		if 当前结点左空
			标记该结点为4
                end if
		输出层序遍历结果
	}
	判断flag与总结点数N是否相同
}

2.1.3 代码实现

2.1.4 PTA提交列表

  • 部分正确:两个部分正确都是相同原因——对完全二叉搜索树的概念理解模糊,以下为错误代码:

当时只考虑双满与双空的情况,忽视了右空也能构成完全二叉搜索树的情况,如下图

  • 解决方法:重写判断条件。

2.1.5 本题设计的知识点

  • 叶子结点为右空时也可能是完全二叉树。

2.2 二叉搜索树的最近公共祖先

2.2.1 该题的设计思路

  • 思路:建完树后,先对U、V进行一次查找判断,只有二者都存在的情况才进入找祖先函数。根据二叉树的性质,根结点即为祖先,若U或V刚好等于根结点,则U或V就是祖先;

    若根结点介于二者之间,则该根结点即为U、V公共祖先;若这以上三个条件都不符合,就说明U、V都大于或都小于根结点的值,之后进入相应的子树再次重复以上操作即可。

  • 时间复杂度:取最坏情况,O(n)。

2.2.2 伪代码

void GetLCA(BSTree BST, int U, int V)
{
	if U是根结点 then
	      U是V祖先,输出,退出函数
	end if
	if V是根结点 then
	      V是U祖先,输出,退出函数
	end if
	if 根结点的值介于U与V之间 then
	      找到共同祖先,输出,退出函数
	end if
	if U 大于根结点的值 then
		根的右子树进入递归
        end if
	if U 小于根结点的值 then
		根的左子树进入递归
        end if
}

2.2.3 代码

2.2.4 PTA提交列表

  • 部分正确:“最大单边树,U和V都不在。卡不用二分法的”和“最大单边树,底部分叉”两个测试点显示“运行超时”。经检查,是主函数的构造有问题,以下为错误代码:
for (i = 0; i < N; i++)
{
	cin >> X;
	调用建树函数
}

由于最大结点数为10000,即会出现调用10000次建树函数的情况,导致运行超时。

  • 解决:改写建树函数和主函数的调用结构。

  • 段错误:建树函数中少写了递归出口语句,导致建树失败。

  • 补充该语句,如下图。

2.2.5 本题设计的知识点

  • 非递归查找函数
BSTree Find(BSTree 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;
}

2.3 整型关键字的散列映射

2.3.1 该题的设计思路

  • 思路:依题意,用除留余数法定义的散列函数求出哈希地址,用线性探测法解决哈希冲突。

  • 时间复杂度:无冲突时时间复杂度是O(1),一般是O(c),c为哈希关键字冲突时查找的平均长度。

2.3.2 伪代码

int Get_loc(int temp, int p)  //创建哈希表
{
	temp除p取余赋予loc
	while 发生哈希冲突且不是重复数据
		loc加一
        end while
	temp存入hash[loc % p]
	返回loc%p
}

2.3.3 代码

2.3.4 PTA提交列表

  • 部分正确:“最大N随机”测试点显示“答案错误”。以下为错误代码

while语句的判断条件错误,例:当输入的关键字为12 22 22 61,p为5 时,按题意正确的哈希表应如下表所示

当while语句的判断条件为while (hash[loc] != 0)时,得出的哈希表如下表所示

即当关键字数据为有两个连续重复数据且该数据的查找次数大于一时,用while (hash[loc] != 0)就会出错,因为它没有过滤掉重复的数据,只是机械地为第二个重复数据找空位置。

  • 解决方法:改写while语句的判断条件。

2.3.5 本题设计的知识点

  • 大于1000的最小质数为1009

  • 哈希表遇到重复数据时要按题意来处理,不能套用老路子。

posted @ 2020-05-24 16:11  甘津津  阅读(241)  评论(0编辑  收藏  举报