第四次实验课
注:这是重庆大学2022级陈自郁班的第二次实验课题目内容,用于实验课后的纠错反思,不可在课上抄袭
7-1 求同年同月同日生的最大人数
古人说过,相逢便是缘。十年修得同船渡,百年修得共枕眠,是缘分;同年同月同日同时出生的人能够碰到一起也是缘分;本题目是从众多已知生日信息中,求出同年同月同日生的最大人数及其出生日期。
输入格式:
在第一行中输入n,表示有n个出生日期,接下来的n行,每行输入一个出生日期,格式yyyy-mm-dd
输出格式:
第一行输出同年同月同日生的最大人数
第二行以后,每行按日期升序输出一个满足同年同月同日生最大人数的日期。
输入样例:
在这里给出一组输入。例如:
8
19820105
20031105
20001231
19820105
20031105
19820105
20010206
20031105
输出样例:
上述出生日期中,19820105出现了3次,20031105也出现了3次,先输出最大次数3,然后按日期升序输出各个日期
3
19820105
20031105
思路:
定义一个map用来存储每个日期出现的次数,然后定义一个vector,用来存储满足同年同月同日生最大人数的日期,通过循环map中的每个键值对,找出最大的计数并存储到vector中,其中我使用的是迭代器iterator,注意:迭代器
(*it).first
会得到key,(*it).second
会得到value。 然后通过sort函数排序,最后输出即可。
代码如下:
#include <iostream>
#include <map>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
// 定义一个map,用来存储每个日期出现的次数
map<string, int> count;
// 定义一个变量n,用来输入出生日期的个数
int n;
// 从标准输入中读取n的值
cin >> n;
// 定义一个循环,从0到n-1,每次循环读取一个出生日期,并更新map中的计数
for (int i = 0; i < n; i++) {
// 定义一个字符串变量date,用来存储一个出生日期
string date;
// 从标准输入中读取date的值
cin >> date;
// 如果map中已经有这个日期,就把它的计数加一
if (count.count(date)) {
count[date]++;
}
// 否则,就把这个日期插入到map中,并把它的计数设为一
else {
count[date] = 1;
}
}
// 定义一个变量max,用来存储同年同月同日生的最大人数,初始值为0
int max = 0;
// 定义一个vector,用来存储满足同年同月同日生最大人数的日期
vector<string> dates;
// 遍历map中的每个键值对,找出最大的计数,并把对应的日期存入vector中
for (auto it = count.begin(); it != count.end(); it++) {
// 如果当前的计数大于max,就更新max,并清空vector,把当前的日期插入到vector中
if (it->second > max) {
max = it->second;
dates.clear();
dates.push_back(it->first);
}
// 如果当前的计数等于max,就把当前的日期插入到vector中
else if (it->second == max) {
dates.push_back(it->first);
}
}
// 对vector中的日期进行升序排序
sort(dates.begin(), dates.end());
// 输出同年同月同日生的最大人数
cout << max << endl;
// 输出满足同年同月同日生最大人数的日期,每行一个
for (auto date : dates) {
cout << date << endl;
}
// 返回0,表示程序正常结束
return 0;
}
7-2 寻找签到题(二叉排序树)
在ACM程序设计竞赛赛场,当某个队伍AC(正确解答)一道题目后就会在其前面升起1个彩色气球。而且每种颜色的气球只能用在一道题目上,所以不同颜色的气球不能相互替代。在某次比赛中,有1道最简单的题目(签到题),显然该题是被AC最多的。已知比赛过程中已送出的气球数量以及每个气球的颜色,请判断哪道题是签到题。若有多道题目的已送气球数相同,则认为字典序最小的颜色对应着签到题。
输入格式:
首先输入一个正整数T,表示测试数据的组数,然后是T组测试数据。每组测试先输入一个整数n(1≤n<1000000),代表已经送出的气球总数,然后输入n个已送出气球的颜色(由长度不超过6且不包含空格的英文字母组成),数据之间间隔一个空格。注意,统计时,忽略气球颜色的大小写。
输出格式:
对于每组测试,在一行上输出一个由小写字母构成的字符串,表示签到题对应的气球颜色。
输入样例:
2
5 RED Red Blue Green REd
5 Red Blue Orange BluE REd
输出样例:
red
blue
思路:
考察二叉排序树的插入与遍历,先建立一颗二叉排序树,在这个二叉排序树的结构体中有气球的颜色和数量,先通过气球的颜色的字典序插入,这时会按气球颜色的字典序排好,然后遍历时,通过中序遍历,先往左子树遍历,这时会首先找到字典序小的,在遍历的同时比较节点数量大小,找到最大数量的节点,这时就可以同时比较气球颜色的字典序和气球数量。
代码如下:
#include <iostream>
#include <string>
using namespace std;
// 定义二叉排序树的结构体
struct BSTNode {
string color; // 气球的颜色
int count; // 气球的数量
BSTNode* left; // 左子树
BSTNode* right; // 右子树
BSTNode(string c) : color(c), count(1), left(NULL), right(NULL) {} // 构造函数
};
// 定义插入函数
void insert(BSTNode*& root, string c) {
if (root == NULL) { // 如果根节点为空,就创建一个新节点
root = new BSTNode(c);
} else if (c < root->color) { // 如果气球的颜色字典序小于当前节点的颜色,就插入到左子树
insert(root->left, c);
} else if (c > root->color) { // 如果气球的颜色字典序大于当前节点的颜色,就插入到右子树
insert(root->right, c);
} else { // 如果气球的颜色和当前节点的颜色相同,就将当前节点的数量加一
root->count++;
}
}
// 定义遍历函数
void traverse(BSTNode* root, string& ans, int& maxCount) {
if (root == NULL) return; // 如果根节点为空,就返回
traverse(root->left, ans, maxCount); // 遍历左子树
if (root->count > maxCount) { // 如果当前节点的数量大于最大数量,就更新答案和最大数量
ans = root->color;
maxCount = root->count;
} else if (root->count == maxCount && root->color < ans) { // 如果当前节点的数量等于最大数量,且当前节点的颜色字典序小于答案,就更新答案
ans = root->color;
}
traverse(root->right, ans, maxCount); // 遍历右子树
}
// 定义主函数
int main() {
int T; // 测试数据的组数
cin >> T;
while (T--) {
int n; // 气球的总数
cin >> n;
BSTNode* root = NULL; // 创建一个空的二叉排序树
while (n--) {
string c; // 气球的颜色
cin >> c;
for (char& ch : c) { // 将气球的颜色转换为小写
ch = tolower(ch);
}
insert(root, c); // 将气球的颜色和数量插入到二叉排序树中
}
string ans = ""; // 签到题对应的气球颜色
int maxCount = 0; // 签到题被AC的次数
traverse(root, ans, maxCount); // 从二叉排序树中找出答案
cout << ans << endl; // 输出答案
}
return 0;
}
7-3 航空公司VIP客户查询
不少航空公司都会提供优惠的会员服务,当某顾客飞行里程累积达到一定数量后,可以使用里程积分直接兑换奖励机票或奖励升舱等服务。现给定某航空公司全体会员的飞行记录,要求实现根据身份证号码快速查询会员里程积分的功能。
输入格式:
输入首先给出两个正整数N(≤105)和K(≤500)。其中K是最低里程,即为照顾乘坐短程航班的会员,航空公司还会将航程低于K公里的航班也按K公里累积。随后N行,每行给出一条飞行记录。飞行记录的输入格式为:18位身份证号码(空格)飞行里程
。其中身份证号码由17位数字加最后一位校验码组成,校验码的取值范围为0~9和x共11个符号;飞行里程单位为公里,是(0, 15 000]区间内的整数。然后给出一个正整数M(≤105),随后给出M行查询人的身份证号码。
输出格式:
对每个查询人,给出其当前的里程累积值。如果该人不是会员,则输出No Info
。每个查询结果占一行。
输入样例:
4 500
330106199010080419 499
110108198403100012 15000
120104195510156021 800
330106199010080419 1
4
120104195510156021
110108198403100012
330106199010080419
33010619901008041x
输出样例:
800
15000
1000
No Info
思路:
这道题考察哈希表,我们直接用STL库中的unordered_map(注意不要使用map,会超时),unordered_map是由哈希表实现的,我们可以直接用,定义一个unordered_map,然后用这个哈希表记录身份证号和对应的里程数,然后就是简单的coding问题了。
代码如下:
#include <unordered_map>
#include <iostream>
#include <string>
using namespace std;
int main() {
unordered_map<string, int> a;
int n, m, l, k;
string b;
cin>>n>>k;
for (int i = 0; i < n; i++) {
cin >> b >> l;
if (l > k) //如果里程数大于k,则直接记录
a[b] += l;
else //如果里程数小于k,则记录k
a[b] += k;
}
cin>>m;
for (int i = 0; i < m; i++) {
cin >> b;
if (a.count(b)) { //判断是否是会员
cout << a[b] << endl;
}
else {
cout << "No Info" << endl;
}
}
return 0;
}
7-4 平均查找长度之线性再散列
对于指定长度为N的整数数组,存储于指定长度为M的散列表中,使用指定的散列函数(使用简单的模素数P运算),若使用线性再散列(这里指的是:若冲突找下一个空位置)处理冲突,编程计算查找成功时平均查找长度和查找失败时的平均查找长度。
输入样例:
第一行三个整数,表示待存储的元素个数N,拟存储的散列表长度M和散列函数模除的素数P。第二行是N个以空格分隔的整数。
9 13 11
47 7 29 11 9 84 54 20 30
输出样例:
分别在两行中输出查找成功时平均查找长度和查找失败时的平均查找长度。
注意,请表达成总查找长度除以总查找次数的未约分形式(如:查找成功有10次,总共查找了30次,成功的平均查找长度为:30/10)。
23/9
41/11
思路:
借鉴了这个博客的代码
代码如下:
// 假设P是一个素数
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
int N, M, P;
int sum1 = 0, sum2 = 0;
// 散列表单元类型
struct Cell {
int data; // 存放元素
bool empty; // 单元状态,true表示空,false表示有合法元素
Cell() : data(0), empty(true) {} // 默认构造函数,初始化为空单元
};
// 散列表类型
class HashTable {
private:
int tableSize; // 表的最大长度
vector<Cell> cells; // 存放散列单元数据的向量
public:
// 构造函数,根据给定的表大小创建一个空的散列表
HashTable(int size) : tableSize(size), cells(size) {}
// 散列函数,根据给定的键值计算散列位置
int hash(int key) {
int d = 1, temp;
temp = key % P; // 确定key该放入的地址
if (temp < 0) { // 如果key为负数
temp = (temp + d) % tableSize;
d++;
}
return temp;
}
// 查找函数,根据给定的键值在散列表中查找对应的位置
int find(int key) {
int currentPos, newPos, d = 1;
int cNum = 0; // 记录冲突次数
newPos = currentPos = hash(key); // 初始散列位置
// 当该位置的单元非空,并且不是要找的元素时,发生冲突
while (!cells[newPos].empty && cells[newPos].data != key) {
newPos = (currentPos + d) % tableSize; // 线性探索解决冲突
d++;
cNum++;
}
sum1 += (cNum + 1); // (每个插入元素的冲突次数+1)的总和=所有元素查找成功总次数
return newPos; // 此时newPos或者是key的位置,或者是一个空单元的位置(表示找不到)
}
// 插入函数,根据给定的键值在散列表中插入一个元素
bool insert(int key) {
int pos = find(key); // 先检测key是否已经存在
if (cells[pos].empty) {
// 如果这个单元没有被占,说明key可以插入在此
cells[pos].empty = false;
cells[pos].data = key;
return true;
}
else // 键值已经存在
return false;
}
// 统计查找失败时的总查找次数
void check_fail() {
int i, j, count;
for (i = 0; i < P; i++) {
// 注意这里是从0到P,而不是从0到tableSize,也就是说只需统计散列函数能直接映射到的位置
if (cells[i].empty)
sum2 = sum2 + 1; // 如果查找位置为空则一次查找就可以确定查找位置为空
if (!cells[i].empty) {
// 如果查找位置不为空要从该元素位置开始查找到元素位置为空的位置才能确定查找失败
count = 0;
for (j = i; j < tableSize; j++) { // 注意这里是在tableSize长度的散列表中查找
count++;
if (cells[j].empty)
break;
if (j == tableSize - 1)
// 若j达到tableSize - 1,让j置0,因为j是从i开始的i前面的元素还没统计
j = -1; // 因为进入for循环的时候j++可以使j置0所以这里让j=-1
}
sum2 += count; // 统计查找次数
}
}
}
};
int main() {
int i, j;
int key;
unique_ptr<HashTable> H; // 使用智能指针管理散列表对象的内存
cin >> N >> M >> P;
H = make_unique<HashTable>(M); // 创建一个大小为M的散列表
for (i = 0; i < N; i++) {
cin >> key;
H->insert(key); // 插入元素
}
H->check_fail(); // 统计查找失败次数
cout << sum1 << "/" << N << endl;
cout << sum2 << "/" << P << endl;
return 0;
}
7-5 二叉搜索树的最近公共祖先
给定一棵二叉搜索树的先序遍历序列,要求你找出任意两结点的最近公共祖先结点(简称 LCA)。
输入格式:
输入的第一行给出两个正整数:待查询的结点对数 M(≤ 1 000)和二叉搜索树中结点个数 N(≤ 10 000)。随后一行给出 N 个不同的整数,为二叉搜索树的先序遍历序列。最后 M 行,每行给出一对整数键值 U 和 V。所有键值都在整型int范围内。
输出格式:
对每一对给定的 U 和 V,如果找到 A
是它们的最近公共祖先结点的键值,则在一行中输出 LCA of U and V is A.
。但如果 U 和 V 中的一个结点是另一个结点的祖先,则在一行中输出 X is an ancestor of Y.
,其中 X
是那个祖先结点的键值,Y
是另一个键值。如果 二叉搜索树中找不到以 U 或 V 为键值的结点,则输出 ERROR: U is not found.
或者 ERROR: V is not found.
,或者 ERROR: U and V are not found.
。
输入样例:
6 8
6 3 1 2 5 4 8 7
2 5
8 7
1 9
12 -3
0 8
99 99
输出样例:
LCA of 2 and 5 is 3.
8 is an ancestor of 7.
ERROR: 9 is not found.
ERROR: 12 and -3 are not found.
ERROR: 0 is not found.
ERROR: 99 and 99 are not found.
思路:
先通过先序遍历,构建一颗二叉搜索树,找二叉搜索树的最近公共祖先,其本质还是找二叉树的最近公共祖先,分为两种情况,(1) 如果根结点是其中一个结点,直接返回。(2) 如果左右子树都不为空,说明根结点是最近公共祖先。其他为coding问题。
代码如下:
#include <iostream>
#include <vector>
using namespace std;
// 定义二叉搜索树的结点结构
struct TreeNode {
int val; // 结点的键值
TreeNode* left; // 左子结点
TreeNode* right; // 右子结点
TreeNode(int x) : val(x), left(NULL), right(NULL) {} // 构造函数
};
// 根据先序遍历序列,递归地构建一棵二叉搜索树
TreeNode* buildTree(vector<int>& preorder, int low, int high) {
if (low > high) return NULL; // 递归终止条件
TreeNode* root = new TreeNode(preorder[low]); // 创建根结点
int mid = low + 1; // 找到左右子树的分界点
while (mid <= high && preorder[mid] < root->val) mid++;
root->left = buildTree(preorder, low + 1, mid - 1); // 构建左子树
root->right = buildTree(preorder, mid, high); // 构建右子树
return root; // 返回根结点
}
// 在二叉搜索树中,查找两个结点的最近公共祖先
TreeNode* findLCA(TreeNode* root, int u, int v) {
if (root == NULL) return NULL; // 递归终止条件
if (root->val == u || root->val == v) return root; // 如果根结点是其中一个结点,直接返回
TreeNode* left = findLCA(root->left, u, v); // 在左子树中查找
TreeNode* right = findLCA(root->right, u, v); // 在右子树中查找
if (left != NULL && right != NULL) return root; // 如果左右子树都不为空,说明根结点是最近公共祖先
if (left == NULL) return right; // 如果左子树为空,返回右子树的结果
else return left; // 如果右子树为空,返回左子树的结果
}
// 判断一个结点是否在二叉搜索树中
bool isFound(TreeNode* root, int x) {
if (root == NULL) return false; // 递归终止条件
if (root->val == x) return true; // 如果找到了,返回真
if (root->val > x) return isFound(root->left, x); // 如果键值大于x,往左子树查找
else return isFound(root->right, x); // 如果键值小于x,往右子树查找
}
int main() {
int M, N; // 待查询的结点对数和二叉搜索树中结点个数
cin >> M >> N;
vector<int> preorder(N); // 先序遍历序列
for (int i = 0; i < N; i++) {
cin >> preorder[i];
}
TreeNode* root = buildTree(preorder, 0, N - 1); // 构建二叉搜索树
for (int i = 0; i < M; i++) {
int u, v; // 待查询的两个结点
cin >> u >> v;
bool hasu = isFound(root, u); // 判断u是否在树中
bool hasv = isFound(root, v); // 判断v是否在树中
if (!hasu && !hasv) { // 如果都不在,输出错误信息
cout << "ERROR: " << u << " and " << v << " are not found." << endl;
}
else if (!hasu) { // 如果u不在,输出错误信息
cout << "ERROR: " << u << " is not found." << endl;
}
else if (!hasv) { // 如果v不在,输出错误信息
cout << "ERROR: " << v << " is not found." << endl;
}
else { // 如果都在,查找最近公共祖先
TreeNode* lca = findLCA(root, u, v);
if (lca->val == u) { // 如果u是v的祖先,输出信息
cout << u << " is an ancestor of " << v << "." << endl;
}
else if (lca->val == v) { // 如果v是u的祖先,输出信息
cout << v << " is an ancestor of " << u << "." << endl;
}
else { // 如果u和v有其他的公共祖先,输出信息
cout << "LCA of " << u << " and " << v << " is " << lca->val << "." << endl;
}
}
}
return 0;
}