本文介绍机试中考查的一些非线性数据结构,包括二叉树、二叉排序树、优先队列和散列表等较为高级的数据结构。
一、二叉树
树的结构有诸多变体,它们在各种应用中发挥着重要作用。
作为树的特例的二叉树(Binary Tree),虽然看似简单,但不失一般性。因为已经被证明,任何有根、有序的多叉树,都可通过等价变换转换为二叉树。
也就是说,二叉树是大部分纷繁复杂的树结构的简化版,学好了二叉树,再学习其他树结构时就能做到游刃有余。
二叉树的递归定义:二叉树要么为空,要么由根结点(Root)、左子树(LetSubtree)和右子树(Right Subtree)构成,而左右两个子树又分别是一棵二叉树。于是如果要建立一棵二叉树,那么最简单的方式便是按照二叉树的定义进行递归建树,每个结点的定义如下:
struct TreeNode{
ElementType data;
TreeNode *leftChild;
TreeNode *rightChild;
}
对于二叉树来说,机试中常考的是其各种遍历方法,根据遍历每个结点的左子树 L、结点本身 N、右子树 R 的顺序不同,可将对二树的遍历方法分为前序遍历(NLR)、中序遍历(LNR)和后序遍历(LRN),其中的序是指根结点在何时被访问。在前序、中序和后序这三种遍历方法中,左、右子树的遍历顺序是固定的,区别仅在于访问根结点的顺序不同。
层次遍历:二叉树按照其高度一层层地遍历所有的结点。层次遍历需要借助一个队列来实现。先将二叉树的根结点入队,在队伍非空的情况下访问队首结点,若它有左子树,则将左子树根结点入队:若它有右子树,则将右子树根结点入队。如此往复,直到队列为空为止。
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
typedef int ElementType;
/*
* 二叉树结点定义
*/
typedef struct TreeNode {
ElementType data;
TreeNode *leftChild;
TreeNode *rightChild;
} TreeNode, *BinTree;
/**
* 前序遍历
*/
void preOrder(TreeNode *root);
/**
* 中序遍历
* @param root
*/
void inOrder(TreeNode *root);
/**
* 后序遍历
* @param root
*/
void postOrder(TreeNode *root);
/**
* 层序遍历
* @param root
*/
void levelOrder(TreeNode *root);
/**
* 访问结点
* @param node
*/
void visit(TreeNode *node);
/**
* 二叉树
* @return
*/
int main() {
return 0;
}
void visit(TreeNode *node) {
cout << node->data << " ";
}
void preOrder(TreeNode *root) {
if (root == nullptr) {
return;
}
/*
* 根、左子树、右子树
*/
visit(root);
preOrder(root->leftChild);
preOrder(root->rightChild);
}
void inOrder(TreeNode *root) {
if (root == nullptr) {
return;
}
/*
* 左子树、根、右子树
*/
inOrder(root->leftChild);
visit(root);
inOrder(root->rightChild);
}
void postOrder(TreeNode *root) {
if (root == nullptr) {
return;
}
/*
* 左子树、右子树、根
*/
postOrder(root->leftChild);
postOrder(root->rightChild);
visit(root);
}
void levelOrder(TreeNode *root) {
queue<TreeNode *> queue;
/*
* 根结点不为空,则先将根结点入队
*/
if (root != nullptr) {
queue.push(root);
}
while (!queue.empty()) {
TreeNode *cur = queue.front();
queue.pop();
visit(cur);
/*
* 左子树不为空,则左子树入队
*/
if (cur->leftChild != nullptr) {
queue.push(cur->leftChild);
}
/*
* 右子树不为空,则右子树入队
*/
if (cur->rightChild != nullptr) {
queue.push(cur->rightChild);
}
}
}
1、二叉树遍历–华中科技大学
描述
编一个程序,读入用户输入的一串先序遍历字符串,根据此字符串建立一个二叉树(以指针方式存储)。 例如如下的先序遍历字符串: ABC##DE#G##F### 其中“#”表示的是空格,空格字符代表空树。建立起此二叉树以后,再对二叉树进行中序遍历,输出遍历结果。
输入描述:
输入包括1行字符串,长度不超过100。
输出描述:
可能有多组测试数据,对于每组数据, 输出将输入字符串建立二叉树后中序遍历的序列,每个字符后面都有一个空格。 每个输出结果占一行。
示例1
输入:
abc##de#g##f###
输出:
c b e g d f a
题解:
#include <iostream>
#include <cstdio>
using namespace std;
typedef char ElementType;
struct TreeNode {
ElementType data;
TreeNode *leftChild;
TreeNode *rightChild;
TreeNode(char c) {
this->data = c;
this->leftChild = nullptr;
this->rightChild = nullptr;
}
};
void inOrder(TreeNode *root) {
if (root == nullptr) {
return;
}
/*
* 左子树、根、右子树
*/
inOrder(root->leftChild);
cout << root->data << " ";
inOrder(root->rightChild);
}
/**
* 全局变量,用于记录谦虚遍历序列的索引
*/
int pos = 0;
TreeNode *build(string preOrder) {
char cur = preOrder[pos];
pos++;
if (cur == '#') {
return nullptr;
}
TreeNode *root = new TreeNode(cur);
root->leftChild = build(preOrder);
root->rightChild = build(preOrder);
return root;
}
/**
* 二叉树遍历--华中科技大学
* @return
*/
int main() {
string preOrder;
while (cin >> preOrder) {
TreeNode *root = build(preOrder);
inOrder(root);
cout << endl;
}
return 0;
}
2、二叉树遍历–清华大学
描述
二叉树的前序、中序、后序遍历的定义: 前序遍历:对任一子树,先访问根,然后遍历其左子树,最后遍历其右子树; 中序遍历:对任一子树,先遍历其左子树,然后访问根,最后遍历其右子树; 后序遍历:对任一子树,先遍历其左子树,然后遍历其右子树,最后访问根。 给定一棵二叉树的前序遍历和中序遍历,求其后序遍历(提示:给定前序遍历与中序遍历能够唯一确定后序遍历)。
输入描述:
两个字符串,其长度n均小于等于26。 第一行为前序遍历,第二行为中序遍历。 二叉树中的结点名称以大写字母表示:A,B,C…最多26个结点。
输出描述:
输入样例可能有多组,对于每组测试样例, 输出一行,为后序遍历的字符串。
示例1
输入:
ABC
BAC
FDXEAG
XDEFAG
输出:
BCA
XEDGAF
题解:
本题考查的是给定前序遍历与中序遍历可以唯一地确定一棵二叉树这个知识点。
在前序遍历中,第一个结点必定是二叉树的根结点;而在中序遍历中,该根结点必定可以将中序遍历的序列拆分为两个子序列,前一个子序列就是根结点左子树的中序遍历序列,而后一个子序列就是根结点右子树的中序遍历序列。
如此递归地进行下去,便可以唯一地确定该二叉树。
如果题目给定的是后序遍历与中序遍历,那么也可以唯一地确定一棵二叉树,
后序遍历的最后一个结点就如同前序遍历的第一个结点,必定是二叉树的根结点。该根结点也可以将中序遍历序列拆分为两个子序列。
因此,也可以采用类似的方法递归地建立唯一的一棵二叉树。
如果题目给定的是先序遍历和后序遍历,那就无法唯一地确定一棵二叉树;
但是,如果题目中再附加说明这是一棵满二叉树,即每个结点都只有零个或两个子结点的二叉树,那么只给定先序遍历和后序遍历也可以唯一地确定一棵二叉树。
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
typedef char ElementType;
/*
* 二叉树
*/
struct TreeNode {
ElementType data;
TreeNode *leftChild;
TreeNode *rightChild;
TreeNode(char c) {
this->data = c;
this->leftChild = nullptr;
this->rightChild = nullptr;
}
};
/**
* 访问结点
* @param node
*/
void visit(TreeNode *node);
/**
* 后序遍历
* @param root
*/
void postOrder(TreeNode *root);
/**
* 由先序遍历和中序遍历构建二叉树
* @param preOrder
* @param inOrder
* @return
*/
TreeNode *build(string preOrder, string inOrder) {
if (preOrder.size() == 0) {
return nullptr;
}
/*
* 前序遍历的第一个元素必为根
*/
char cur = preOrder[0];
TreeNode *root = new TreeNode(cur);
int pos = inOrder.find(cur);
/*
* 分别递归构建左子树和右子树
* 对于中序遍历而言,假设中序遍历中根节点的索引为index
* 1. 左子树元素的索引区间是[0, index-1]
* 2. 右子树元素的索引区间是[index+1, inOrder.size()-1]
*
* 对于前序遍历来说,假设中序遍历中根节点的索引为index
* 1. 左子树元素的索引区间是[1, index]
* 2. 右子树元素的索引区间是[index+1, preOrder.size()-1]
*
* 注:此部分可以结合此题的笔记配图进行理解。
* 补充:substr(off, count): 从索引off开始,截取长度为count的字符串
*/
root->leftChild = build(preOrder.substr(1, pos), inOrder.substr(0, pos));
root->rightChild = build(preOrder.substr(pos + 1), inOrder.substr(pos + 1));
return root;
}
/**
* 遍历--清华大学
* @return
*/
int main() {
string preOrder;
string inOrder;
while (cin >> preOrder >> inOrder) {
TreeNode *root = build(preOrder, inOrder);
postOrder(root);
cout << endl;
}
return 0;
}
void postOrder(TreeNode *root) {
if (root == nullptr) {
return;
}
/*
* 先后遍历左子树、右子树、根
*/
postOrder(root->leftChild);
postOrder(root->rightChild);
visit(root);
}
void visit(TreeNode *node) {
cout << node->data;
}
二、二叉排序树
二叉排序树也称二叉搜索树(Binary Search Tree),是一种特殊的二叉树。
一棵非空的二叉排序树具有如下的特征:
- 若左子树非空,则左子树上所有结点关键字的值均小于根结点关键字的值。
- 若右子树非空,则右子树上所有结点关键字的值均大于根结点关键字的值。
- 左右子树本身也是一棵二叉排序树。
如果各个数字插入的顺序不同,那么得到的二叉排序树的形态很可能不同,所以不同的插入顺序对二叉排序树的形态有重要影响。
二叉排序树的特点:左子树结点值 < 根结点值 < 右子树结点值;
因此,如果对二叉排序树进行中序遍历,那么其遍历结果必然是一个升序序列,这也是二叉排序树名称的由来。
通过建立一棵二叉排序树,就能对原本无序的序列进行排序,并实现序列的动态维护。
1、二叉排序树–华中科技大学
描述
二叉排序树,也称为二叉查找树。可以是一颗空树,也可以是一颗具有如下特性的非空二叉树: 1. 若左子树非空,则左子树上所有节点关键字值均不大于根节点的关键字值; 2. 若右子树非空,则右子树上所有节点关键字值均不小于根节点的关键字值; 3. 左、右子树本身也是一颗二叉排序树。 现在给你N个关键字值各不相同的节点,要求你按顺序插入一个初始为空树的二叉排序树中,每次插入后成功后,求相应的父亲节点的关键字值,如果没有父亲节点,则输出-1。
输入描述:
输入包含多组测试数据,每组测试数据两行。 第一行,一个数字N(N<=100),表示待插入的节点数。 第二行,N个互不相同的正整数,表示要顺序插入节点的关键字值,这些值不超过10^8。
输出描述:
输出共N行,每次插入节点后,该节点对应的父亲节点的关键字值。
示例1
输入:
5
2 5 1 3 4
输出:
-1
2
2
5
3
题解:
本题考查的是二叉排序树的建树,给定插入元素的顺序,因此需要通过依次将元素插入二叉排序树来完成建树。
不过本题在建树的基础上,还需要输出父结点的值,若采取递归建树的方式,可不必等树完全建好后再输出每个结点的父结点,而可以在插入时用一个变量记录父结点的值,在建树的同时输出父结点的值。
#include <iostream>
#include <cstdio>
using namespace std;
typedef int ElementType;
struct TreeNode {
ElementType data;
TreeNode *leftChild;
TreeNode *rightChild;
TreeNode(ElementType c) {
this->data = c;
this->leftChild = nullptr;
this->rightChild = nullptr;
}
};
/**
* 插入结点,并输出结点对应的父节点的数据域
* @param root
* @param x
* @return
*/
TreeNode *insert(TreeNode *root, ElementType x, ElementType fatherData);
/**
* 二叉排序树--华中科技大学
* @return
*/
int main() {
int n;
while (cin >> n) {
TreeNode *root = nullptr;
for (int i = 0; i < n; ++i) {
int x;
cin >> x;
/*
* 根节点没有父节点,所以起始传入-1
*/
root = insert(root, x, -1);
}
}
return 0;
}
TreeNode *insert(TreeNode *root, ElementType x, ElementType fatherData) {
if (root == nullptr) {
/*
* 创建新结点
*/
root = new TreeNode(x);
cout << fatherData << endl;
} else if (x < root->data) {
/*
* 插入到左子树,fatherData即是root的data
*/
root->leftChild = insert(root->leftChild, x, root->data);
} else if (x > root->data) {
/*
* 插入到右子树,fatherData即是root的data
*/
root->rightChild = insert(root->rightChild, x, root->data);
}
return root;
}
2、二叉排序树–华中科技大学
描述
输入一系列整数,建立二叉排序树,并进行前序,中序,后序遍历。
输入描述:
输入第一行包括一个整数n(1 <= n <= 100)。 接下来的一行包括n个整数。
输出描述:
可能有多组测试数据,对于每组数据,将题目所给数据建立一个二叉排序树,并对二叉排序树进行前序、中序和后序遍历。 每种遍历结果输出一行。每行最后一个数据之后有一个换行。 输入中可能有重复元素,但是输出的二叉树遍历序列中重复元素不用输出。
示例1
输入:
5
1 6 5 9 8
输出:
1 6 5 9 8
1 5 6 8 9
5 8 9 6 1
题解:
本题考查的是二叉排序树的建树,给定插入元素的顺序,因此需要通过依次将元素插入二叉排序树来完成建树。
建树完成之后,分别输出前序遍历、中序遍历、后序遍历即可。
#include <iostream>
#include <cstdio>
using namespace std;
typedef int ElementType;
struct TreeNode {
ElementType data;
TreeNode *leftChild;
TreeNode *rightChild;
TreeNode(ElementType c) {
this->data = c;
this->leftChild = nullptr;
this->rightChild = nullptr;
}
};
/**
* 前序遍历
*/
void preOrder(TreeNode *root);
/**
* 中序遍历
* @param root
*/
void inOrder(TreeNode *root);
/**
* 后序遍历
* @param root
*/
void postOrder(TreeNode *root);
/**
* 层序遍历
* @param root
*/
void levelOrder(TreeNode *root);
/**
* 访问结点
* @param node
*/
void visit(TreeNode *node);
/**
* 插入结点
* @param root
* @param x
* @return
*/
TreeNode *insert(TreeNode *root, ElementType x);
/**
* 二叉排序树--华中科技大学
* @return
*/
int main() {
int n;
while (cin >> n) {
TreeNode *root = nullptr;
for (int i = 0; i < n; ++i) {
int x;
cin >> x;
root = insert(root, x);
}
preOrder(root);
cout << endl;
inOrder(root);
cout << endl;
postOrder(root);
cout << endl;
}
return 0;
}
void preOrder(TreeNode *root) {
if (root == nullptr) {
return;
}
/*
* 根、左子树、右子树
*/
visit(root);
preOrder(root->leftChild);
preOrder(root->rightChild);
}
void inOrder(TreeNode *root) {
if (root == nullptr) {
return;
}
/*
* 左子树、根、右子树
*/
inOrder(root->leftChild);
visit(root);
inOrder(root->rightChild);
}
void postOrder(TreeNode *root) {
if (root == nullptr) {
return;
}
/*
* 左子树、右子树、根
*/
postOrder(root->leftChild);
postOrder(root->rightChild);
visit(root);
}
void visit(TreeNode *node) {
cout << node->data << " ";
}
TreeNode *insert(TreeNode *root, ElementType x) {
if (root == nullptr) {
/*
* 创建新结点
*/
root = new TreeNode(x);
} else if (x < root->data) {
/*
* 插入到左子树
*/
root->leftChild = insert(root->leftChild, x);
} else if (x > root->data) {
/*
* 插入到右子树
*/
root->rightChild = insert(root->rightChild, x);
} else {
/*
* 题目说重复的元素无需输出,因此重复元素我们不进行插入
*/
}
return root;
}
三、优先队列
前面我们已经学过了“先进先出”规则的队列,可以解决一定的需求。
而优先队列(Priority Queue)是按照事先规定的优先级次序进行动态组织的数据结构。在优先队列中,每个元素都被赋予了优先级,访问元素时,只能访问当前优先队列中优先级最高的元素,即最高级先出(First-In Greast-Out)规则。
1、STL-priority_queue
在正式介绍优先队列的应用之前,先介绍标准库中的优先队列模板。优先队列底层是用二叉堆(默认是大根堆)实现的。
优先队列的本质是堆,但它具有队列的所有操作特性,与普通队列不同的地方就是出队的时候按照优先级顺序出队,这个优先级即大根堆或小根堆的规则(即大的为top优先出队或小的为top优先出队),在队列的基础上加了个堆排序。
以O(log n) 的效率查找一个队列中的最大值或者最小值,其中是最大值还是最小值是根据创建的优先队列的性质来决定的。
1.1、priority_queue的定义
1. 添加头文件
#include <queue>
2. 定义优先队列
priority_queue<type, container, functional> name,其中
typename: 优先队列元素的类型;
container: 保存数据的容器,默认情况下以vector为容器;
functional: 元素的比较方式,默认请款下以operator<为比较方式;
name: 定义优先队列的名字。
Tips:当只有type一个参数时,默认是大根堆,每次输出的对顶元素是优先级最高的元素。
3. 大根堆和小根堆的定义
大根堆:priority_queue<int> bigHeap;
大根堆:priority_queue<int, vector<int>, less<int>> bigHeap;
小根堆:priority_queue<int, vector<int>, greater<int>> smallHeap;
1.2、priority_queue的状态
empty(): 返回当前优先队列是否为空。
size(): 返回当前优先队列元素个数。
1.3、priority_queue的添加或删除
push()和pop()来实现向优先队列中元素的入队和出队操作。
注:底层是对堆的插入和删除。
1.4、priority_queue的访问
由于优先队列对访问元素的限制,优先队列只能通过to()访问当前队列中优先级最高的元素。
1.5、优先队列的应用
应用:
1. 顺序问题
这类问题往往需要根据元素在序列中的大小顺序来进行求解。
例如,输出动态维护序列中的最大值。由于按照优先级依次出队,优先队列能够很好地处理这类问题。
2. 哈夫曼树
在机试中最常考查优先队列的应用便是哈夫曼树(Huffman Tree)。
给定n个带有权值的结点,以他们为叶子结点构造一个带权路径长度和最小的二叉树,该二叉树即为哈夫曼树。
因为,哈夫曼树可能不唯一,所以机试中一般去求解最小带权路径长度。
假设权值小的结点优先级高,结合哈夫曼树的构造过程,利用优先队列可以高效的求优先级最高(即权值最小)的两个结点。
1.6、示例代码
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
priority_queue<int> myPriorityQueue;
/**
* 优先队列示例代码
* @return
*/
int main() {
printf("the size of myPriorityQueue:%d\n", myPriorityQueue.size());
myPriorityQueue.push(20);
myPriorityQueue.push(100);
myPriorityQueue.push(30);
myPriorityQueue.push(50);
printf("the top of myPriorityQueue:%d\n", myPriorityQueue.top());
printf("the size of myPriorityQueue:%d\n", myPriorityQueue.size());
int sum = 0;
while (!myPriorityQueue.empty()) {
printf("%d ", myPriorityQueue.top());
sum += myPriorityQueue.top();
myPriorityQueue.pop();
}
printf("\n");
printf("sum:%d\n", sum);
return 0;
}
the size of myPriorityQueue:0
the top of myPriorityQueue:100
the size of myPriorityQueue:4
100 50 30 20
sum:200
2、复数集合–北京邮电大学
描述:
一个复数(x+iy)集合,两种操作作用在该集合上:
1、Pop 表示读出集合中复数模值最大的那个复数,如集合为空 输出 empty ,不为空就输出最大的那个复数并且从集合中删除那个复数,再输出集合的大小SIZE;
2、Insert a+ib 指令(a,b表示实部和虚部),将a+ib加入到集合中 ,输出集合的大小SIZE; 最开始要读入一个int n,表示接下来的n行每一行都是一条命令。
输入描述:
输入有多组数据。 每组输入一个n(1<=n<=1000),然后再输入n条指令。
输出描述:
根据指令输出结果。 模相等的输出b较小的复数。 a和b都是非负数。
示例1
输入:
3
Pop
Insert 1+i2
Pop
输出:
empty
SIZE = 1
1+i2
SIZE = 0
题解:
始终输出当前队列中模最大的复数,是一道典型的考察优先队列的题目。
本题优先队列中的数据并非内置数据类型,因此我们可以自定义结构体来进行存储复数Complex。
同时,因为程序无法比较自定义结构体的大小,所以我们要重载小于符号,即重定义复数这个结构体的比较关系。
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
/*
* 复数
*/
struct Complex {
int real;
int imag;
Complex(int real, int imag) {
this->real = real;
this->imag = imag;
}
/**
* 重载小于号(优先级递增)
* 如何理解?
* 左 < 右 ===> 右的优先级高于左,两种情况:
* 1. 左右模相等-->模大的优先级高
* 2. 左右模不等-->虚部小的优先级高
* @param c
* @return
*/
bool operator<(Complex c) const {
if (real * real + imag * imag == c.real * c.real + c.imag * c.imag) {
/*
* 复数的模相等,则返回虚部较小者
* 即,虚部较小者,其优先级高
*/
return imag > c.imag;
} else {
/*
* 复数的模不相等,则返回模较大者
* 即,模大者,其优先级高
*/
return real * real + imag * imag < c.real * c.real + c.imag * c.imag;
}
}
};
priority_queue<Complex> myPriorityQueue;
/**
* 复数集合--北京邮电大学
* @return
*/
int main() {
int n;
while (scanf("%d", &n) != EOF) {
string instruction;
cin >> instruction;
if (instruction == "Pop") {
/*
* 指令为Pop
* 判断优先队列是否为空,空则输出empty;否则输出并删除最大复数
*/
if (myPriorityQueue.empty()) {
printf("empty\n");
} else {
Complex cur = myPriorityQueue.top();
myPriorityQueue.pop();
printf("%d+i%d\n", cur.real, cur.imag);
printf("SIZE = %d\n", myPriorityQueue.size());
}
} else {
/*
* 指令为Insert
* 输入实部和虚部,并将复数存入到优先队列中
*/
int real;
int imag;
scanf("%d+i%d", &real, &imag);
myPriorityQueue.push(Complex(real, imag));
printf("SIZE = %d\n", myPriorityQueue.size());
}
}
return 0;
}
3、哈夫曼树–北京邮电大学
描述:
哈夫曼树,第一行输入一个数n,表示叶结点的个数。需要用这些叶结点生成哈夫曼树,根据哈夫曼树的概念,这些结点有权值,即weight,题目需要输出所有结点的值与权值的乘积之和的最小值。
输入描述:
输入有多组数据。 每组第一行输入一个数n,接着输入n个叶节点(叶节点权值不超过100,2<=n<=1000)。
输出描述:
输出权值。
示例1
输入:
5
1 2 2 5 9
输出:
37
题解:
此题是哈夫曼树的经典题目,按照哈夫曼树的建树过程求解即可。
以weight权重为关键字进行优先级排序,每次拿出权值最小的两个结点,所以此题我们要用小根堆。
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
/**
* 哈夫曼树--北京邮电大学
* @return
*/
int main() {
int n;
while (cin >> n) {
/*
* 定义小根堆的优先队列
*/
priority_queue<int, vector<int>, greater<int>> nodes;
for (int i = 0; i < n; ++i) {
int weight;
cin >> weight;
nodes.push(weight);
}
int ans = 0;
/*
* 依次弹出两个权值最小的结点,求其权值之和并放入优先队列中
*/
while (nodes.size() > 1) {
int x = nodes.top();
nodes.pop();
int y = nodes.top();
nodes.pop();
ans += x + y;
nodes.push(x + y);
}
cout << ans << endl;
}
return 0;
}
四、散列表
散列表是一种根据关键字(key) 直接进行访问的数据结构,通过建立关键字和存储位置的直接映射关系(map)便可利用关键码直接访问元素,以加快查找的速度。
由于散列表摒弃了关键码有序,因此在理想情况下可在期望的常数时间内实现所有接口操作。即,就平均时间复杂度的意义而言,这些操作的复杂度都是 O(1)。
同时要注意映射这种思想在机试中的应用。
1、STL-map
C++标准库中提供的映射模板map是一个将关键字(key)和映射值(value)形成一对映射后进行绑定存储的容器。
map 的底层是用红黑树实现的,内部仍然是有序的,其查找效率仍然为 O(logn)。标准库中还有一个无序映射 unordered_map,其底层是用散列表实现的,其期望的查找效率为 O(1)。map和unordered_map的操作几乎一样,下面只介绍 map 的使用方法。
在数据量不大时,map的性能能够满足绝大多数题目的要求,题目中对性能的要求特别高时,只需将map改成unordered_map 即可。
1.1、map的定义
1. 添加头文件
#include <map>
2. 定义map
map<type T1, type T2> name,其中
type T1: 映射关键字的类型
type T2: 映射值得类型
nmae: map的名字
1.2、map的状态
empty(): 返回当前map是否为空。
size(): 返回当前map元素个数。
1.3、map元素的添加或删除
insert()和erase()来实现向map中特定元素的添加和删除操作。
1.4、map元素的访问
1. 由于map内部重载了[]运算符,因此可以通过[key]的方式访问。
2. 可以使用at()进行访问。
3. 还可以通过迭代器进行访问。
1.5、map元素操作
find(): 通过key查找map中的特定值。
clear(): 将映射清空
用函数fid()查找特定元素时,若找到则返回该元素的迭代器,若未找到则返回迭代器end()。
1.6、map迭代器操作
map中常用的迭代器操作有返回映射中首元素迭代器的begin(),以及返回映射尾元素之后一个位置的迭代器end()
1.7、示例代码
#include <iostream>
#include <cstdio>
#include <map>
using namespace std;
map<string, int> myMap;
/**
* map映射示例代码
* @return
*/
int main() {
myMap["Emma"] = 67;
myMap["Benedict"] = 100;
myMap.insert(pair<string, int>("Bob", 72));
myMap.insert(pair<string, int>("Mary", 85));
myMap.insert(pair<string, int>("Alice", 93));
printf("the size of myMap:%d\n", myMap.size());
printf("the score of Benedict:%d\n", myMap["Benedict"]);
printf("the score of Mary:%d\n", myMap.at("Mary"));
myMap.erase("Benedict");
/*
* 定义迭代器
*/
map<string, int>::iterator it;
for (it = myMap.begin(); it != myMap.end(); it++) {
cout << "the score of " << it->first;
cout << ": " << it->first << endl;
}
myMap.clear();
if (myMap.empty()) {
cout << "my map is empty" << endl;
} else {
cout << "my map is not empty" << endl;
}
it = myMap.find("Bob");
if (it != myMap.end()) {
cout << "Bob is found" << endl;
} else {
cout << "Bob is not found" << endl;
}
cout << "the size of myMap: " << myMap.size() << endl;
return 0;
}
the size of myMap:5
the score of Benedict:100
the score of Mary:85
the score of Alice: Alice
the score of Bob: Bob
the score of Emma: Emma
the score of Mary: Mary
my map is empty
Bob is not found
the size of myMap: 0
2、查找学生信息–清华大学
描述:
输入N个学生的信息,然后进行查询。
输入描述:
输入的第一行为N,即学生的个数(N<=1000) 接下来的N行包括N个学生的信息,信息格式如下: 01 李江 男 21 02 刘唐 男 23 03 张军 男 19 04 王娜 女 19 然后输入一个M(M<=10000),接下来会有M行,代表M次查询,每行输入一个学号,格式如下: 02 03 01 04
输出描述:
输出M行,每行包括一个对应于查询的学生的信息。 如果没有对应的学生信息,则输出“No Answer!”
示例1
输入:
4
01 李江 男 21
02 刘唐 男 23
03 张军 男 19
04 王娜 女 19
5
02
03
01
04
03
输出:
02 刘唐 男 23
03 张军 男 19
01 李江 男 21
04 王娜 女 19
03 张军 男 19
题解:
分析:
本题属于查找类题目,但本题若按照线性查找,则肯定会超时;若按照二分查找,则需要对所有元素进行排序。
然而,学生的信息本身并无大小可言,而且本题也不要求将学生的信息进行比较,只需根据学生的学号返回其信息即可。
所以,本题是一道考查映射的典型题目。
其实,映射的思想就是为了提高查找的效率而提出。与一般的查找策略相比,由于映射可以直接通过元素的关键字,得到它所对应的映射值。
所以,本题以学号为关键字key,整个学生的信息为值value。
例如,01 李江 男 21,则关键字key为01,值value为01 李江 男 21。
#include <iostream>
#include <cstdio>
#include <string>
#include <map>
using namespace std;
/**
* 查找学生信息--清华大学
* @return
*/
int main() {
int n;
cin >> n;
/*
* getChar()用于吸收回车换行符
* 因为后面要用getLine()来获取一整行的字符串
*/
getchar();
map<string, string> studentMap;
for (int i = 0; i < n; ++i) {
string studentInfo;
getline(cin, studentInfo);
int pos = studentInfo.find(" ");
string studentId = studentInfo.substr(0, pos);
//studentMap.insert(pair<string, string>(studentId, studentInfo));
studentMap[studentId] = studentInfo;
}
int m;
cin >> m;
for (int i = 0; i < m; ++i) {
string key;
cin >> key;
string ans = studentMap[key];
if (ans == "") {
ans = "No Answer!";
}
cout << ans << endl;
}
return 0;
}
3、魔咒词典–浙江大学
描述
哈利波特在魔法学校的必修课之一就是学习魔咒。据说魔法世界有100000种不同的魔咒,哈利很难全部记住,但是为了对抗强敌,他必须在危急时刻能够调用任何一个需要的魔咒,所以他需要你的帮助。 给你一部魔咒词典。当哈利听到一个魔咒时,你的程序必须告诉他那个魔咒的功能;当哈利需要某个功能但不知道该用什么魔咒时,你的程序要替他找到相应的魔咒。如果他要的魔咒不在词典中,就输出“what?”
输入描述:
首先列出词典中不超过100000条不同的魔咒词条,每条格式为: [魔咒] 对应功能 其中“魔咒”和“对应功能”分别为长度不超过20和80的字符串,字符串中保证不包含字符“[”和“]”,且“]”和后面的字符串之间有且仅有一个空格。词典最后一行以“@END@”结束,这一行不属于词典中的词条。 词典之后的一行包含正整数N(<=1000),随后是N个测试用例。每个测试用例占一行,或者给出“[魔咒]”,或者给出“对应功能”。
输出描述:
每个测试用例的输出占一行,输出魔咒对应的功能,或者功能对应的魔咒。如果魔咒不在词典中,就输出“what?”
示例1
输入:
[expelliarmus] the disarming charm
[rictusempra] send a jet of silver light to hit the enemy
[tarantallegra] control the movement of one's legs
[serpensortia] shoot a snake out of the end of one's wand
[lumos] light the wand
[obliviate] the memory charm
[expecto patronum] send a Patronus to the dementors
[accio] the summoning charm
@END@
4
[lumos]
the summoning charm
[arha]
take me to the sky
输出:
light the wand
accio
what?
what?
题解:
分析:
题目中明确"魔咒"和"功能"不会重复,因此,我们将"魔咒"作为key,"功能"作为value,形成双向映射后存入到map中。
#include <iostream>
#include <cstdio>
#include <map>
#include <string>
using namespace std;
/**
* 魔咒词典--浙江大学
* @return
*/
int main() {
map<string, string> curseMap;
string curse;
while (getline(cin, curse)) {
//getchar();
if (curse == "@END@") {
break;
}
int pos = curse.find("]");
/*
* 补充substr(off, count):截取字符串,从指定位置off开始,并指定的长度count
*/
string instruction = curse.substr(0, pos + 1);
string function = curse.substr(pos + 2);
/*
* 以[魔咒]为key,功能为value
* 以为功能key,[魔咒]为value
*/
curseMap.insert(pair<string, string>(instruction, function));
curseMap.insert(pair<string, string>(function, instruction));
}
int m;
cin >> m;
getchar();
for (int i = 0; i < m; ++i) {
string key;
getline(cin, key);
string ans = curseMap[key];
if (ans == "") {
/*
* [魔咒]和功能都不存在
*/
ans = "what?";
} else if (ans[0] == '[') {
/*
* 功能存在,输出魔咒
*/
ans = ans.substr(1, ans.size() - 2);
}
cout << ans << endl;
}
return 0;
}