DS博客作业05--查找
0.PTA得分截图
1.本周学习总结
1.1总结查找内容
1.1.1查找的性能指标ASL
- ASL即平均查找长度,在査找的过程中,一次査找的长度是指需要比较的关键字次数,而平均査找长度则是所有査找过程中进行关键字的比较次数的平均值。
- 数学定义:
其中n为查找表中元素个数,Pi为查找第i个元素的概率,通常假设每个元素查找概率相同,Pi=1/n,Ci是找到第i个元素的比较次数。
1.1.2静态查找算法
顺序查找:
-
思路:从表的一端开始,顺序扫描线性表,依次将扫描到的关键字和给定值k相比较,若当前扫描到的关键字与k相等,则查找成功;若扫描结束后,仍未找到关键字等于k的记录,则查找失败。
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;//找到返回逻辑序号i+1
}
二分查找:
- 思路:二分查找也称为折半查找,要求线性表中的节点必须己按关键字值的递增或递减顺序排列。
- 步骤:
1.首先将给定值 key 与表中中间位置元素的关键字比较,
2.若相等,则査找成功,返回该元素的存储位置;
3.若不等,则根据key与关键字的大小关系判断查找前部分或后部分
ASL成功=log(n+1)-1
ASL不成功=log(n+1)
循环代码:
int BinSearch(SeqList R,int n,KeyType 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;
}
递归代码:
int BinSearch1(SeqList R,int low,int high,KeyType k)
{ int mid;
if (low<=high) //查找区间存在一个及以上元素
{ mid=(low+high)/2; //求中间位置
if (R[mid].key==k) //查找成功返回其逻辑序号mid+1
return mid+1;
if (R[mid].key>k) //在R[low..mid-1]中递归查找
BinSearch1(R,low,mid-1,k);
else //在R[mid+1..high]中递归查找
BinSearch1(R,mid+1,high,k); }
}
else
return 0;
}
1.1.3动态查找算法
二叉搜索树
- 定义:二叉搜索树,也称有序二叉树,排序二叉树,是指一棵空树或者具有下列性质的二叉树:
若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
任意节点的左、右子树也分别为二叉查找树。
没有键值相等的节点。
结构体定义
typedef struct node
{ KeyType key; //关键字项
InfoType data; //其他数据域
struct node *lchild,*rchild; //左右孩子指针
} BSTNode,*BSTree;
创建二叉搜索树
- 代码:
BSTNode *CreatBST(KeyType A[],int n) //返回树根指针
{ BSTNode *bt=NULL; //初始时bt为空树
int i=0;
while (i<n)
{
InsertBST(bt,A[i]); //将A[i]插入二叉排序树T中
i++;
}
return bt; //返回建立的二叉排序树的根指针
}
插入二叉搜索树
- 代码:
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;
}
查找二叉搜索树
- 代码:
循环算法
BSTNode *SearchBST1(BSTNode *bt,KeyType 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
}
递归算法
BSTNode *SearchBST(BSTNode *bt,KeyType k)
{ if (bt==NULL || bt->key==k)
return bt;
if (k<bt->key) return SearchBST(bt->lchild,k);
else return SearchBST(bt->rchild,k);
}
删除节点
- 思路:
分为如下情况:
1)被删除的节点是叶子节点:直接删去该节点,其双亲节点中相应指针域的值改为“空”
2)被删除的节点只有左子树或者只有右子树:用其左子树或者右子树代替它其双亲节点的相应指针域的值改为 “指向被删除节点的左子树或右子树”。
3)被删除的节点既有左子树,也有右子树:以其前驱替代之,然后再删除该前驱节点。前驱是左子树中最大的节点。也可以用其后继替代之,然后再删除该后继节点。后继是右子树中最小的节点。 - 代码:
int DeleteBST(BSTree &bt,KeyType k)
{ if (bt==NULL) return 0; //空树删除失败
else
{ if (k<bt->key) return DeleteBST(bt->lchild,k);
//递归在左子树中删除为k的节点
else if (k>bt->key) return DeleteBST(bt->rchild,k);
//递归在右子树中删除为k的节点
else
{ Delete(bt); //删除*bt节点
return 1;
}
}
}
void Delete(BSTreee &p) //从二叉排序树中删除*p节点
{ BSTNode *q;
if (p->rchild==NULL) //*p节点没有右子树的情况
{
q=p; p=p->lchild;delete q;
}
else if (p->lchild==NULL) //*p节点没有左子树
{
q=p; p=p->rchild;delete q;
}
else Delete1(p,p->lchild);
//*p节点既有左子树又有右子树的情况
}
//即有左子树又有右子树的删除算法
void Delete1(BSTNode *p,BSTNode *&r)
//被删节点:p,p的左子树节点:r
{ BSTNode *q;
if (r->rchild!=NULL)
Delete1(p,r->rchild); //递归找最右下节点
else //找到了最右下节点*r
{ //将*r的关键字值赋给*p
p->key=r->key;
q=r; r=r->lchild;delete q;
}
}
1.1.4AVL树
- 定义:
左、右子树是平衡二叉树;
所有结点的左、右子树深度之差(平衡因子)的绝对值≤1
最坏情况下的时间也均为O(log2n) - 结构体定义:
typedef struct node //记录类型
{ KeyType key; //关键字项
int bf; //增加的平衡因子
InfoType data; //其他数据域
struct node *lchild,*rchild;//左右孩子指针
} BSTNode;
- 调整方法
- 1.LL调整
判断:若在A的左子树的左子树上插入结点,使A失衡,平衡因子从-1变为-2,需要进行一次顺时针旋转。
步骤:
1.A的左孩子B右上旋转作为A的根节点
2.A节点右下旋转称为B的右孩子
3.B原右子树称为A左子树
图解:
- 2.RR调整
判断:若在A的右子树的右子树上插入结点,使A的平衡因子从1增加至2,需要进行一次逆时针旋转
步骤:
1.A的右孩子C左上旋转作为A的根节点
2.A节点左下旋转称为C的左孩子
3.C原左子树称为A右子树
图解:
- 3.LR调整
判断:若在A的左子树的右子树上插入结点,使A的平衡因子从-1增加至-2,(以插入的结点E为旋转轴),先C进行逆时针旋转,A再顺时针旋转。
步骤:
1.E向上旋转到A的位置,A作为E右孩子
2.E原左孩子作为B的右孩子
3.E原右孩子作为A的左孩子
图解:
- 4.RL调整
判断:若在A的右子树的左子树上插入结点,使A的平衡因子从1增加至2,(以插入的结点D为旋转轴),先进行顺时针旋转,再逆时针旋转。
步骤:
1.D向上旋转到A的位置,A作为D左孩子
2.D原左孩子作为A的右孩子
3.D原右孩子作为C的左孩子
图解:
1.1.5B-树和B+树
B-树:
- 定义:
一棵m阶B-树或者是一棵空树,或者是满足下列要求的m叉树:
每个节点至多m个孩子节点(至多有m-1个关键字)
除根节点外,其他节点至少有[m/2]个孩子节点(即至少有[m/2]-1个关键字)
若根节点不是叶子节点,根节点至少两个孩子节点 - m阶B-树结点特点:
非根结点:孩子个数最小: m/2, 最大:m
非根结点:关键字个数:最小: m/2 -1 , 最大:m-1
根节点至少2个孩子:2--m - B-树特点:
1.B-树是所有结点的平衡因子均等于0的多路查找树。所有外部结点都在同一层上。
2.在计算B-树的高度时,需要计入最底层的外部结点
3.外部结点就是失败结点,指向它的指针为空,不含有任何信息,是虚设的。一棵B树中总有n个关键字,则外部结点个数为n+1。 - B-树基本操作:
结构体:
#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;
查找结果返回类型:
typedef struct {
BTNode *pt; // 指向找到的结点的指针
int i; // 1..m-1,在结点中的关键字序号
int tag; // 标志查找成功(=1)或失败(=0)
} Result; // 在B树的查找结果类型
查找:
伪代码:
在一棵B-树上顺序查找关键字为k的方法为:
将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[n]
则沿着指针ptr[n]所指的子树继续查找。
插入:
在查找不成功之后,需进行插入。关键字插入的位置必定在叶子结点层,有下列几种情况:
该结点的关键字个数n<m-1,不修改指针;
该结点的关键字个数 n=m-1,则需进行“结点分裂”
结点分裂:
1.如果没有双亲结点,新建一个双亲结点,树的高度增加一层。
2.如果有双亲结点,将ki插入到双亲结点中。
图解:
删除:
和插入的考虑相反,
结点中关键字的个数>[m/2]-1,直接删除
结点中关键字的个数=[m/2]-1
要从其左(或右)兄弟结点“借调”关键字
若其左和右兄弟结点均无关键字可借(结点中只有最少量的关键字),则必须进行结点的“合并”。
- 删除关键字k分两种情况:
(1)在非叶子结点上删除关键字ki
从pi子树节点借调最大或最小关键字key代替ki。
pi子树中删除key
若子树节点关键字个数< m/2-1,重复步骤1
若删除关键字为叶子结点层,按叶子结点删除操作法
(2)在B树的叶子结点b上删除关键字共有以下3种情况:
1.假如b结点的关键字个数大于Min,说明删去该关键字后该结点仍满足B树的定义,则可直接删去该关键字。
2.假如b结点的关键字个数等于Min,说明删去关键字后该结点将不满足B树的定义。若可以从兄弟结点借。
图解:
兄弟结点最小关键字上移双亲结点
双亲结点大于删除关键字的关键字下移删除结点
3.b结点的关键字个数等Min,兄弟节点关键字个数也等于Min
图解:
删除关键字
兄弟节点及删除关键字节点、双亲结点中分割二者关键字合并一个新叶子结点
若双亲结点关键字个数<=Min,重复2
B+树
-
定义:索引文件组织中,经常使用B-树的变形B+树。 B+树是大型索引文件的标准组织方式。
-
特点:
- 每个分支节点至多有m棵子树。
- 根节点或者没有子树,或者至少有两棵子树
- 除根节点,其他每个分支节点至少有m/2棵子树
- 有n棵子树的节点有n个关键字。
- 所有叶子节点包含全部关键字及指向相应记录的指针
叶子节点按关键字大小顺序链接
叶子节点是直接指向数据文件中的记录。 - 所有分支节点(可看成是分块索引的索引表)
包含子节点最大关键字及指向子节点的指针。
-
B+树的查找
直接从最小关键字开始进行顺序查找所有叶节点链接成的线性链表。
从B+树的根节点出发一直找到叶节点为止。
1.1.6散列查找
哈希表:
- 定义:又称散列表,是除顺序表存储结构、链接表存储结构和索引表存储结构之外的又一种存储线性表的存储结构。
哈希表是一种存储结构,它并非适合任何情况,主要适合记录的关键字与存储地址存在某种函数关系的数据。
哈希表与哈希地址 - 哈希函数:把关键字为ki的对象存放在相应的哈希地址中
- 哈希表:存储数据记录的长度为m(m≥n)的连续内存单元
哈希冲突 - 对于两个关键字分别为ki和kj(i≠j)的记录,有ki≠kj,但h(ki)=h(kj)。把这种现象叫做哈希冲突(同义词冲突)。
在哈希表存储结构的存储中,哈希冲突是很难避免的。
哈希表设计
哈希表设计主要需要解决哈希冲突。实际中哈希冲突是难以避免的,主要与3个因素有关:
- 哈希表长度
- 与装填因子有关。
- 装填因子α=存储的记录个数/哈希表的大小=n/m
- α越小,冲突可能性就越小;α越大(最大可取1),冲突的可能性就越大。控制在0.6~0.9的范围内
- 与所采用的哈希函数有关。
- 与解决冲突方法有关。
哈希函数构造方法 - 直接定址法
- 直接定址法是以关键字k本身或关键字加上某个数值常量c作为哈希地址的方法。
- 优点:简单,均匀,不会产生冲突;
- 缺点:关键字分布不连续将造成内存单元的大量浪费
- 数学函数:h(k)=h+c;
- 数字分析法:
- 适合于所有关键字值都已知的情况,并需要对关键字中每一位的取值分布情况进行分析。
- 除留余数法
- 除留余数法是用关键字k除以某个不大于哈希表长度m的数p所得的余数作为哈希地址的方法。
- 数学函数:h(k)=h%p;(p多为质点)
哈希冲突解决方法
- 开放定址法
- 定义:开放地址法就是指:一旦发生了冲突就去寻找下一个空的哈希地址,只要哈希表足够大,空的散列地址总能找到,并将记录存入。
- 公式:
Hi=(H(key) + Di) mod m (i = 1,2,3,….,k k<=m-1)(线性探查法)
Hi=(H(key) +/- Di^2) mod m (i = 1,2,3,….,k k<=m-1)(平方探查法) - 平方探查法是一种较好的处理冲突的方法,可以避免出现堆积现象。它的缺点是不能探查到哈希表上的所有单元,但至少能探查到一半单元。
- 拉链法
- 拉链法是把所有的同义词用单链表链接起来的方法。
ASL
- 拉链法是把所有的同义词用单链表链接起来的方法。
- ASL成功=∑每个关键字比较次数/关键字个数
- ASL不成功=∑关键字比较到空结点所需的次数/p
哈希表ASL举例:
哈希链ASL举例:
哈希表
- 哈希表结构体:
#define MaxSize 100
#define NULLKEY -1
#define DELKEY -2
typedef char *InfoType;
typedef struct
{
int key; // 关键字域
InfoType data; //其他数据域
int count; //探查次数
}HashTable[MaxSize];
- 哈希表创建:
void CreateHT(HashTable ha, KeyType x[], int n, int m, int p)
{ //代码实现
int i;
for (i = 0;i < m;i++) //初始化
{
ha[i].key = NULLKEY;
ha[i].count = 0;
ha[i].data = NULL;
}
for (i = 0;i < n;i++) //插入关键字
InsertHT(ha, n, x[i], p);
}
- 哈希表插入:
int InsertHT(HashTable ha,int p,int k,int &n)
{ //伪代码
计算k的哈希地址adr=k%p;
若ha[adr]=k,则已存在k,不需要插入
while(ha[adr]不为空 或ha[adr]不等删除标记 )
{
线性探查k的位置。即adr=adr+1;
计算探查次数count
}
ha[adr]=k;
哈希表长度增1
}
int InsertHT(HashTable ha,int p,int k,int &n)
{
int adr,i;
adr=k % p;
if(adr==NULLKEY || adr==DELKEY) //地址为空,可插入数据
{
ha[adr].key=k;
ha[adr].count=1;
}
else
{
i=1;
while(ha[adr].key!=NULLKEY && ha[adr].key!=DELKEY)
{
adr=(adr+1) % m;
i++;
} //查找插入位置
ha[adr].key=k;ha[adr].count=i; //找到插入位置
}
n++;
}
- 哈希表查找:
int SearchHT(HashTable ha,int p,int k)
{
int i=0,adr;
adr=k % p;
while(ha[adr].key!=NULLKEY && ha[adr].key!=k)
adr=(adr+1) % m;//探查下一个地址
if(ha[adr].key==NULLKEY) return -1;//地址为空,找不到
if(ha[adr].key==k) return adr; //找到关键字k
else return -1;
}
- 哈希表删除:
int DeleteHT(HashTable ha,int p,int k,int &n)
{
int adr;
adr=SearchHT(ha,p,k);
if(adr!=-1)
{
ha[adr].key=DELKEY; //逻辑删除,该元素贴删除标记
n--;
return 1;
}
else return 0;
} //查找删除元素,找到话,不能真正删除,做个删除标记DELKEY。
哈希链
- 哈希链结构体
typedef struct HashNode {
int key;
struct HashNode *next;
}HashNode, *HashTable;
HashTable ht[MAX];
- 哈希链创建
void CreateHashChain(HashChain HC[], int n, int k)
{
int i;
for (i = 0;i < HashSize;i++)
{
HC[i] = new HashNode;
HC[i]->next = NULL;
}
for (i = 0;i < n;i++) Insert(HC, k);
}
- 哈希链插入
void Insert(HashChain HC[], int k)
{
string ID;
int distance;
int index;
HashChain node;
HashChain p;
ID.resize(MAXSIZE);
scanf("%s%d", &ID[0], &distance);
if (distance < k) distance = k;
index = GetID(ID) % HashSize;
p = HC[index]->next;
while (p != NULL)
{
if (p->ID == ID) break;
else p = p->next;
}
if (p == NULL)
{
node = new HashNode;
node->ID = ID;
node->flight_distance = distance;
node->next = HC[index]->next;
HC[index]->next = node;
}
else p->flight_distance += distance;
}
- 哈希链查找
void Search(HashChain HC[], string ID)
{
int index;
HashChain p;
index = GetID(ID) % HashSize;
p = HC[index]->next;
while (p != NULL)
{
if (p->ID == ID)
{
printf("%d", p->flight_distance);
return;
}
p = p->next;
}
printf("No Info");
}
1.2谈谈你对查找的认识及学习体会。
本章学习的难点不在于查找方式,而是对哈希表,哈希链与各种树的操作的学习,拓展了我们的学习面,且在面对查找问题的时候,有着更多的选择类别与解决方式。本章的难度比较大,极大考验我们面对大数据的时候能否完成对算法的简易,而且对于很多的函数操作都需要自己一步步测试才能明白算法的全部流程。
2.PTA题目介绍
2.1二叉搜索树的最近公共祖先
2.1.1该题的设计思路
题目要求创建二叉树并搜索两个节点的最近公共祖先,且会出现查找节点不存在的情况需要判断。
设计思路:
由于题目数据量较大,需要使用根据先序遍历创建二叉搜索树,使用正常的依次创建会导致代码的超时,且增加结构体数据为记录该节点的父亲节点
建树方法图示:
面对所有二叉树数据都使用map函数记录,当查找的数据未被记录时,可以取消查找二叉树而直接输出该数没找到,减少时间复杂度
判断查找树是否为彼此的祖宗节点时,分别查找两节点的位置,后判断这两节点的子节点是否出现另一个节点
最后查找其中一个节点的位置,依次移动到他的父亲节点位置并判断是否找到另一个节点,第一个找到的节点为最近祖宗节点
时间复杂度:O(n^2)
2.1.2该题的伪代码:
树结构
typedef struct node
{
node* left;
node* right;
node* father;
int data;
}Node, *VNode;
map<int int>记录二叉树数据,出现过的设为1
出入结点对数与结点个数
for i=0 to 结点个数
输入二叉树数据,并将该数据的map值设为1
以图示方式建树,并在建表中if 某节点的左节点或右节点存在,则将他的左节点或右节点的父亲节点设为自己
建树代码为:

for i=0 to 结点对数
输入结点对u,v
由map函数判断
if 结点对不都在二叉树上
由不在二叉树上的节点与结点个数进行输出
else
遍历树查找节点u
if 节点u的子节点中找到v /*查找方式为基本的比较该节点与查找节点大小来进入其左节点或又节点直到找到
输出u是v的最近祖先
else
遍历树查找节点v
if 节点v的子节点中找到u
输出v是u的最近祖先
else
循环遍历v节点的父亲节点,直到在父亲节点的子节点中找到u节点后返回父亲节点。
2.1.3PTA提交列表
代码截图:
提交列表截图:
部分正确在于运行超时,一开始使用的是依次输入数据后判断大小一步步的构建二叉树,且并没有使用map函数而是直接查找结点对并根据全局变量flag判断是否在树中找到过这些节点。
之后使用了map函数减少了查找的时间,却依旧在最后3个点运行超时,最后是学习了网络上的建二叉搜索树的方法才通过。
2.1.4本题设计的知识点
1:map容器的使用,简便查找的流程
2:新的建树方法,根据二叉搜索树的先序遍历序列建树
3;二叉树的查找与结点祖先的查找
2.2整型关键字的散列映射
2.2.1该题的设计思路
本题要求使用求余法与线性探测法建立哈希表,且存在特殊点,即出现相同的数字并不会被记录到哈希表并直接输出之前相同数字的位置。
先通过求余找到该关键字本应在的位置,如果该位置的存在数字且不为这关键字则查找下一位置知道找到空位置或该位置的数字与关键字相同后输出该位置
时间复杂度O(n^2)
2.2.2该题的伪代码
输入待插关键字个数N与散列表的长度P
for i=0 to N
输入待插关键字x
计算待查关键字应存放地址m=x/P
if m位置的不存在关键字
m位置的关键字设为x
else
while m位置存在关键字且该关键字不为x
if m==P-1
m=0
else
m++
m位置关键字设为x
输出m
2.2.3PTA提交列表
代码截图:
提交列表截图:
一开始不知道重复数字不会再次编入哈希表,一直只通过第一点,之后查代码的时候看到了这一点,便使用map容器记录该数字的地址并判断是否出现过,如果出现过则直接提起map容器中的值并输出,但vs输出没错,pta缺过不了。
之后去掉了map容器,而是通过线性探测时判断是否有关键字与插入字重复,如果重复则取消探测并覆盖关键字后输出地址,与map容器的结果一致缺只有这种方式能通过pta。
2.2.4本题设计的知识点
1.哈希表的初始化,除留求余法建立哈希表
2.线性探测法处理哈希冲突
3.哈希表对重复数据不再插入
2.3航空公司VIP客户查询
2.3.1该题的设计思路
题目要求由身份证关键字建立哈希链,将小于最小历程的数改为最小历程,后根据查找身份证输出历程
设计思路:由身份证最后一个位安排哈希表,由map容器记录历程,并判断查找的身份证是否是贵宾
时间复杂度O(n)
2.3.2该题的伪代码
map<string,int>记录身份证对面的历程
输入飞行记录N与最小历程K
for i=0 to N
输入身份证s与历程load
if load<K
load=K
根据身份证s最后一位判断插入的哈希链
mp[s]=mp[s]+load
输入查找数M
for i=0 to M
if !mp[s]
输出No Info
else
输出mp[s]
2.3.3PTA提交列表
代码截图:
提交列表截图:
该题的做法与要求存在偏差,一开始没使用map容器,且只根据身份证最后一位来建立哈希链,导致一条链存在的数据过多,在数据过大的时候,查找便会导致超时,之后使用map容器记录身份证后查找花费的时间减少,依旧运行超时,我干脆删掉了建哈希链的步骤,直接使用map容器便通过了,之后便想如何改进建链步骤才能顺利通过,之后将重复数据不进行哈希链的创建便通过了,这样让我不会导致之前重复身份证建立的链,但重复数据的历程叠加却不会记录在哈希链中,且也没进行哈希链的查找
2.3.4本题设计的知识点
1.大数据下对哈希链的建立
2.哈希链的查找
3.头插法插入数据