高频算法及数据结构面试题目汇总(2)
作者:光火
系列文章:高频算法及数据结构面试题目汇总 (1)
只出现一次的数字
- 题干:只出现一次的数字
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素 (算法应具备线性复杂度)
-
本题有一种非常巧妙的做法,就是使用异或。异或运算有三个基本性质:
- 异或操作满足交换律和结合律
- 任何数和自身进行异或,结果为
0
- 任何数和
0
进行异或,结果仍为自身
-
对于本题而言,假设输入的规模为 ,其中有 个数出现了两次。记这 个数字为 ,则根据异或运算满足交换律和结合律,这 个数的异或结果总可以转换成 的形式。再依据性质二及性质三,可知该式的结果为
-
因此,利用异或运算遍历数组即可得到最终答案
int singleNumber(vector<int>& nums) {
int rtn = 0;
for(int num: nums) {
rtn ^= num;
}
return rtn;
}
搜索连通块
- 题干:连通块问题
小白和他的朋友在周末相约去召唤师峡谷踏青。他们发现召唤师峡谷的地图由一块一块的格子组成,有的格子上是草丛,有的则是空地。草丛可以通过上下左右 4 个方向扩展其他草丛形成一片草地,任何一片草地中的格子都是草丛,并且所有格子间相互连通。倘若用
#
代表草丛,S
代表空地,下面的峡谷就有 2 片草地
处于同一片草地的 2 个人可以互相看到,而在空地则看不到草里面的人。现在他们发现有一个朋友不见了,需要每个人负责一片草地,分头寻找,那么他们至少需要多少人呢
- 第一行输入 代表峡谷的尺寸
- 接下来输入 行字符串表示峡谷的地形
- 输出至少需要多少人
#include <vector>
#include <cstring>
#include <iostream>
using namespace std;
#define N 110
int m = 0, n = 0;
int nxt[4][2] = { {0, 1}, {1, 0}, {-1, 0}, {0, -1} };
bool inside(int x, int y) {
return x >= 0 && x < m && y >= 0 && y < n;
}
void dfs(char grass[N][N], bool visit[N][N], int x, int y) {
for (int i = 0; i < 4; i++) {
int x_next = x + nxt[i][0];
int y_next = y + nxt[i][1];
if (inside(x_next, y_next) && grass[x_next][y_next] == '#' && !visit[x_next][y_next]) {
visit[x_next][y_next] = true;
dfs(grass, visit, x_next, y_next);
}
}
}
int main() {
cin >> m >> n;
int count = 0;
char grass[N][N];
bool visit[N][N];
memset(visit, false, sizeof(visit));
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
cin >> grass[i][j];
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
if (grass[i][j] == '#' && !visit[i][j]) {
count ++;
visit[i][j] = true;
dfs(grass, visit, i, j);
}
cout << count << '\n';
return 0;
}
- 本题是一道经典的连通块计数问题。题干中的“草地”就相当于连通块,草地内部相互连通,但不同的草地之间却为空地所分隔
- 在保证访问不越界的前提下,一次
dfs
搜索可以确定一块草地的范围。那么,为了统计地图中的草地总数,我们就需要遍历整张地图,并多次调用dfs
,这也是main
函数中二层循环的含义。当所有的格子均被访问后,计数量count
即为最终结果 - 由于草丛之间可以通过上下左右四个方向相互连通,因此我们在
dfs
函数内部需要依次枚举这四种选择,查看对应的位置上是否也为草丛。倘若为是,则说明两者同处一块草地,只需要一名同学查看即可 - 或许有人会说,一共6块草丛,我找了5块没有找到,那么“失踪”的那位同志肯定躲在剩余的那片里了,因此结果应当返回
count - 1
。这样理解也没什么问题,但重点还是掌握知识,至于返回什么,根据题目样例而定吧
今年暑假不 AC
- 题干:今年暑假不 AC
这是一道情景题,故事背景如下:$&*@(...#^
反正就算略去情景,也完全不影响题意就是了。输入数据包含多个测试实例,每个测试实例的第一行为一个整数,表示你喜欢观看的节目总数。然后是 行数据,每行包括两个值 , ,分别表示第 个节目的开始及结束时间,为简化问题,每个时间都用一个正整数表示。对于每个测例,输出最多能完整观看的节目总数
-
本题是非常经典的活动选择问题,它有一个贪心结论:最早结束的活动一定可以成为最优解的一部分。证明过程如下:
- 假设 是所有活动中最早结束的那个,而 则是最优解中最早结束的活动
- 倘若 ,结论显然成立
- 倘若 ,则说明 的结束时间一定晚于 的结束时间,且早于最优解中其他活动的开始时间。那么将 替换为 ,同样不会产生冲突,即替换后的方案也是最优解
-
在明确了这一点后,就可以采用贪心选择策略。我们首先按照结束时间对各个节目进行排序,然后选择最早结束的那个。此时,在满足不与已选节目存在时间冲突的前提下,还是可以选择余下的节目中最早结束的那个,证明思路和上文完全一样
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
struct program {
int begin;
int end;
} schedule[110];
int compare(program a, program b) {
return a.end < b.end;
}
int main() {
int n = 0;
scanf("%d", &n);
memset(schedule, 0, sizeof(schedule));
for (int i = 0; i < n; i++) {
scanf("%d%d", &schedule[i].begin, &schedule[i].end);
}
sort(schedule, schedule + n, compare);
int time = schedule[0].end, count = 1;
for (int i = 1; i < n; i++) {
if (schedule[i].begin >= time) {
count++;
time = schedule[i].end;
}
}
printf("%d\n", count);
return 0;
}
矩阵置零
给定一个 的矩阵,倘若某元素为零,则将其所在行和所在列全部置为零。请使用原地算法。
- 对于本题而言,我们很容易想到暴力解法,即首先遍历整个矩阵,利用两个数组记录哪一行和哪一列出现了零元。随后再遍历这两个数组,将矩阵对应的行、列置零
- 那是否存在不需要额外空间的方法呢?答案是有的,不过这需要我们将矩阵的首行、首列作为一个标记,具体代码如下:
void setZeros(vector<vector<int>>& base, int m, int n) {
bool row = false;
bool column = false;
// 判断第一行有无 0
for(int i = 0; i < n; i ++)
if(base[0][i] == 0)
row = true;
// 判断第一列有无 0
for(int i = 0; i < m; i ++)
if(base[i][0] == 0)
column = true;
// 遍历首行、首列外的所有元素
for(int i = 1; i < m; i ++)
for(int j = 1; j < n; j ++)
if(base[i][j] == 0) {
base[0][j] = 0;
base[i][0] = 0;
}
// 遍历第一行, 如果有 0, 则将对应的列全部置 0
for(int i = 1; i < n; i ++)
if(base[0][i] == 0)
for(int j = 0; j < m; j ++)
base[j][i] = 0;
// 遍历第一列, 如果有 0, 则将对应的行全部置 0
for(int i = 1; i < m; i ++)
if(base[i][0] == 0)
for(int j = 0; j < m; j ++)
base[i][j] = 0;
// 如果为真, 将第一行元素全部置为 0
if(row)
for(int i = 0; i < n; i ++)
base[0][i] = 0;
// 如果为真, 将第一列元素全部置为 0
if(column)
for(int i = 0; i < m; i ++)
base[i][0] = 0;
}
加油站问题
- 题干:加油站
在一条环路上有
n
个加油站,其中第i
个加油站有汽油gas[i]
升。 你有一辆油箱容量无限的的汽车,从第i
个加油站开往第i+1
个加油站需要消耗汽油cost[i]
升。你从其中的一个加油站出发,开始时油箱为空。 给定两个整数数组gas
和cost
,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回-1
。如果存在解,则保证它是唯一的
,
- 从
3
号加油站(索引为3
处)出发,可获得4
升汽油。此时油箱有= 0 + 4 = 4
升汽油 - 开往
4
号加油站,此时油箱有4 - 1 + 5 = 8
升汽油 - 开往
0
号加油站,此时油箱有8 - 2 + 1 = 7
升汽油 - 开往
1
号加油站,此时油箱有7 - 3 + 2 = 6
升汽油 - 开往
2
号加油站,此时油箱有6 - 4 + 3 = 5
升汽油 - 开往
3
号加油站,你需要消耗5
升汽油,正好足够你返回到3
号加油站。 因此,3
可为起始索引
- 相信在读完样例的一瞬间,各位就已经构思好了本题的暴力解法。我们可以依次枚举每个加油站,计算从当前站点出发,能否绕环路行驶一周。倘若能,则返回对应的编号,否则就尝试下一个加油站。至于环路的处理,取余就好啦,没什么困难的,然后本题就
TLE
(超时)了 - 上述暴力算法就类似于传统的字符串匹配,那学过
KMP
和BM
算法的读者都知道,字符串每次失配其实都提供了一定的信息,我们没有必要每次只挪动一个格子 - 本题也是一样的,我们要尝试利用失败经验。具体来说,如果从
x
出发, 最远可以到达的加油站为y
, 那么从x, y
之间任意一个加油站出发, 都没法到达y
之后的加油站 - 直观理解一下:假设从
x
加油站出发经过z
最远能到达y
,那么从z
直接出发,是不可能到达y
的下一站的。因为从x
出发到z
时,起码还有剩余的油,这都到不了y
的下一站,那直接从z
出发,刚开始是没有存油的,所以更不可能到达y
的下一站
// 所以首先检查第 0 个加油站, 并试图判断能否环绕一周; 如果不能,就从第一个无法到达的加油站开始继续检查
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int i = 0;
int n = gas.size();
while(i < n) {
int count = 0;
int gas_sum = 0;
int cost_sum = 0;
while(count < n) {
int j = (i + count) % n;
gas_sum += gas[j];
cost_sum += cost[j];
if(cost_sum > gas_sum) {
break;
}
count ++;
}
if(count == n) {
return i;
}else {
i = i + count + 1;
}
}
return -1;
}
苹果分堆
100
个苹果分成几堆,可以保证在不拆散堆的情况下,就能拼出1 ~ 100
- 本题实际上是一道进制转换类题目。我们可以将这
100
个苹果按2
的次方不断划分成若干堆,直至不够。又因为二进制可以完整表示出1 - 100
,那么对于一个给定的数,我们计算出它在二进制下的表示,如果某位的值为1
,则取对应堆,否则不取。值得一提的是,这种思维很有用,甚至在背包问题中也广泛出现 # 动态规划进阶(2) - 背包问题
void convert(int number, int base) {
if(number <= 0) return;
if(base < 2 || base > 16) return;
stack<char> container;
static char digit[] = {'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
while(number > 0) {
int remain = (int)(number % base);
container.push(digit[remain]);
number /= base;
}
while(!container.empty()) {
cout << container.top();
container.pop();
}
}
无向图判树
给定一个用邻接表存储的无向图,判断它是否为一棵树
- 对于一个无向图而言,倘若它是一个包含 个节点且边数为 的连通子图,则它就是一棵树。因此,我们可以采用
dfs
,对图的节点和边进行计数。倘若一次dfs
就能遍历到全部节点,则说明图连通。另外,由于邻接表在存储无向图时,每条边均被存储了两次,因此在if
判断时需要乘以二
void dfs(Graph& graph, int current, int& count_node, int& count_edge, vector<int>& visited) {
count_node ++;
visited[current] = true;
for(Node* node = graph.nodes[current].first; node; node = node->next) {
count_edge ++;
if(!visited[node->node_id])
dfs(graph, node->id, count_node, count_edge, visited);
}
}
bool isTree(Graph& graph) {
vector<int> visited(graph.node_number, false);
int count_node = 0, count_edge = 0;
dfs(graph, 0, count_node, count_edge, visited);
if(count_node == graph.node_number && count_edge = 2 * (graph.node_number - 1))
return true;
else
return false;
}