高频算法及数据结构面试题目汇总 (2)

134 阅读3分钟

高频算法及数据结构面试题目汇总(2)

作者:光火

邮箱:victor_b_zhang@163.com

系列文章:高频算法及数据结构面试题目汇总 (1)

只出现一次的数字

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素 (算法应具备线性复杂度)

  • 本题有一种非常巧妙的做法,就是使用异或。异或运算有三个基本性质:

    • 异或操作满足交换律和结合律
    • 任何数和自身进行异或,结果为 0
    • 任何数和 0 进行异或,结果仍为自身
  • 对于本题而言,假设输入的规模为 2n+12n + 1,其中有 nn 个数出现了两次。记这 nn 个数字为 a1,...,ana_1, ..., a_n,则根据异或运算满足交换律和结合律,这 2n+12n + 1 个数的异或结果总可以转换成 (a1a1)(a2a2)...(anan)an+1(a_1 \oplus a_1 ) \oplus (a_2 \oplus a_2) ... \oplus(a_n \oplus a_n) \oplus a_{n+1} 的形式。再依据性质二及性质三,可知该式的结果为 00...an+1=an+10 \oplus 0 ... \oplus a_{n+1} = a_{n+1}

  • 因此,利用异或运算遍历数组即可得到最终答案

int singleNumber(vector<int>& nums) {
    int rtn = 0;
    for(int num: nums) {
        rtn ^= num;
    }
    return rtn;
}

搜索连通块

小白和他的朋友在周末相约去召唤师峡谷踏青。他们发现召唤师峡谷的地图由一块一块的格子组成,有的格子上是草丛,有的则是空地。草丛可以通过上下左右 4 个方向扩展其他草丛形成一片草地,任何一片草地中的格子都是草丛,并且所有格子间相互连通。倘若用 # 代表草丛,S 代表空地,下面的峡谷就有 2 片草地

##SSSS##\#\# SS \\ SS\#\#

处于同一片草地的 2 个人可以互相看到,而在空地则看不到草里面的人。现在他们发现有一个朋友不见了,需要每个人负责一片草地,分头寻找,那么他们至少需要多少人呢

Input:Input:

  • 第一行输入 m,n(1m,n100)m, n \quad (1 \leq m, n \leq 100) 代表峡谷的尺寸
  • 接下来输入 mm 行字符串表示峡谷的地形

Output:Output:

  • 输出至少需要多少人
#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

这是一道情景题,故事背景如下:$&*@(...#^

反正就算略去情景,也完全不影响题意就是了。输入数据包含多个测试实例,每个测试实例的第一行为一个整数n(n100)n(n \leq 100),表示你喜欢观看的节目总数。然后是 nn 行数据,每行包括两个值 TisT_isTieT_ie (1in)(1 \leq i \leq n),分别表示第 ii 个节目的开始及结束时间,为简化问题,每个时间都用一个正整数表示。对于每个测例,输出最多完整观看的节目总数

  • 本题是非常经典的活动选择问题,它有一个贪心结论:最早结束的活动一定可以成为最优解的一部分。证明过程如下:

    • 假设 aa 是所有活动中最早结束的那个,而 bb 则是最优解中最早结束的活动
    • 倘若 a=ba = b,结论显然成立
    • 倘若 aba \not = b,则说明 bb 的结束时间一定晚于 aa 的结束时间,且早于最优解中其他活动的开始时间。那么将 bb 替换为 aa,同样不会产生冲突,即替换后的方案也是最优解
  • 在明确了这一点后,就可以采用贪心选择策略。我们首先按照结束时间对各个节目进行排序,然后选择最早结束的那个。此时,在满足不与已选节目存在时间冲突的前提下,还是可以选择余下的节目中最早结束的那个,证明思路和上文完全一样

#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;
}

矩阵置零

给定一个 m×nm \times n 的矩阵,倘若某元素为零,则将其所在行和所在列全部置为零。请使用原地算法。

Input:Input:

[111101111]\left[ \begin{matrix} 1 & 1 & 1 \\ 1 & 0 & 1 \\ 1 & 1 & 1 \end{matrix} \right]

Output:Output:

[101000101]\left[ \begin{matrix} 1 & 0 & 1 \\ 0 & 0 & 0 \\ 1 & 0 & 1 \end{matrix} \right]
  • 对于本题而言,我们很容易想到暴力解法,即首先遍历整个矩阵,利用两个数组记录哪一行和哪一列出现了零元。随后再遍历这两个数组,将矩阵对应的行、列置零
  • 那是否存在不需要额外空间的方法呢?答案是有的,不过这需要我们将矩阵的首行、首列作为一个标记,具体代码如下:
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] 升。你从其中的一个加油站出发,开始时油箱为空。 给定两个整数数组 gascost ,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则保证它是唯一的

Input:Input:

gas=[1,2,3,4,5]gas = [1,2,3,4,5], cost=[3,4,5,1,2]cost = [3,4,5,1,2]

Output:Output:

33

DetailsDetails

  • 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 (超时)了
  • 上述暴力算法就类似于传统的字符串匹配,那学过 KMPBM 算法的读者都知道,字符串每次失配其实都提供了一定的信息,我们没有必要每次只挪动一个格子
  • 本题也是一样的,我们要尝试利用失败经验。具体来说,如果从 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();
	}
}

无向图判树

给定一个用邻接表存储的无向图,判断它是否为一棵树

  • 对于一个无向图而言,倘若它是一个包含 nn 个节点且边数为 n1n - 1连通子图,则它就是一棵树。因此,我们可以采用 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;
}