19个基本的算法面试问题 *

最好的算法开发人员可以回答的全部来源的基本问题. 在我们社区的推动下,我们鼓励专家提交问题并提供反馈.

现在就雇佣一个顶尖的算法开发者
Toptal logo是顶级自由软件开发人员的专属网络吗, designers, 金融专家, 产品经理, 和世界上的项目经理. 顶级公司雇佣Toptal自由职业者来完成他们最重要的项目.

面试问题

1.

什么是分而治之算法? 描述它们是如何工作的. 你能举一些常见的例子来说明使用这种方法的问题类型吗?

View answer

分而治之算法是解决涉及几个基本步骤的问题的范例. 首先,我们将问题分成更小的部分,并独立解决每个部分. 一旦我们解决了所有的问题, 我们将所有得到的较小的解决方案组合成一个综合的综合解决方案.

This process can be performed recursively; that is, 如果有必要,每个“子问题”本身可以被细分成更小的部分.. 这种问题的递归分解会一直执行,直到每个单独的问题都小到可以相对简单地解决为止.

一些很适合这种方法的常见问题示例是二分搜索, 排序算法(e).g., Merge Sort, Quicksort), 计算复杂数学运算的优化(求幂), FFT, Strassen算法), and others.

2.

如何最优地计算p^k, k是一个非负整数? 解决方案的复杂性是多少?

View answer

首先,我们提一下平凡解的复杂度是0 (k). 这个问题可以通过平方和乘法来解决.

我们知道p^k = p^x * p^y如果x+y=k. 我们还知道p^k = (p^a)^b如果a*b=k.

对于偶数k, 选择a = 2 b = k/2, 得到p^k = (p^2)^(k/2), 是否会将所需的乘法次数减少近一半. 对于奇数k, 选择x = 1和y=k-1会导致y是偶数, 然后我们可以简单地重复处理偶数的过程. 这允许我们定义一个递归函数:

函数pow(基数,指数)
	如果指数== 0
		RETURN 1
	如果指数是偶数
		RETURN(基数*基数,指数/ 2)
	ELSE
		返回基数*基数(基数*基数,(指数- 1)/ 2)
	END IF

这个解的复杂度为O(log k).

3.

如何插入排序,堆排序,快速排序和合并排序工作?

View answer

Insertion sort 按顺序获取数组的元素, 并在当前点左侧维护一个已排序的子数组. 它是通过取一个元素来实现的, 找到它在排序数组中的正确位置, 然后把后面的元素都移动1, 为要插入的元素留下空格.

Heapsort 首先构建一个Max堆. 二进制最大堆是一个几乎完全的二叉树,其中每个父节点大于或等于其子节点. 堆与原始数组元素存储在相同的内存中. 一旦堆形成,它就会完全替换数组. After that, 我们取走第一个元素, 恢复堆属性, 从而将堆大小减少1, 然后将Max元素放在内存的末尾. 这个过程一直重复,直到我们清空堆, 导致最小的元素被放在首位, 下面的元素依次变大.

Quicksort 是否通过将数组的第一个(最左边)元素作为枢轴点来执行. 然后将其与后面的每个元素进行比较. 当我们找到一个更小的,我们把它移到左边. 通过将该元素与枢轴点后的第一个元素交换来快速执行移动, 然后把这个枢轴点和它后面的元素交换. 在遍历整个数组之后, 我们取主轴左边的所有点然后对子数组进行快速排序, 对主元右边的所有点都做同样的处理. 一直递归,直到到达长度为0-1个元素的子数组.

Merge sort 将给定数组递归对半. 一旦子数组达到很小的长度,就开始合并. 合并取两个相邻子数组之间最小的元素,并重复该步骤,直到取走所有元素, 产生一个排序的子数组. 在相邻的子数组对上重复这个过程,直到我们到达起始数组, but sorted.

在这里查看这些排序算法的可视化: www.排序算法.com

申请加入Toptal的发展网络

并享受可靠、稳定、远程 自由算法开发者职位

申请成为自由职业者
4.

插入排序、快速排序、堆排序和归并排序的主要优点是什么? 讨论时间和内存复杂度的最佳、平均和最坏情况.

View answer

Insertion sort 平均和最差运行时间为O(n^2),最佳运行时间为O(n). 它不需要任何额外的缓冲区,所以空间复杂度是0 (1). 由于复杂度的常数因子非常低,它在对极短的数组进行排序时非常有效. 它还非常擅长对已经“几乎”排序过的数组进行排序. 一个常见的用途是对元素进行了一些小更新的数组进行重新排序.

其他三种算法的最佳和平均运行复杂度为O(n log n). Heapsort and Mergesort 即使在最坏的情况下也要保持这种复杂性, 而快速排序的最差性能为O(n^2).

Quicksort 是否对所提供的资料敏感. 在不使用随机枢轴的情况下,它使用O(n^2)时间对一个完整排序的数组进行排序. 而是通过将随机的未排序元素与第一个元素交换, 然后分类, 算法对数据变得不那么敏感,否则会导致最坏情况的行为(例如.g. 已经排序的数组). 尽管它的复杂度并不比 Heapsort or Merge sort, 它的执行速度有一个非常低的常数因子, 在处理大量随机数据时,这通常会给它带来速度优势.

Heapsort 具有可靠的时间复杂度并且不需要任何额外的缓冲空间. As a result, 它在需要可靠速度超过最佳平均运行时间的软件中很有用, 和/或具有有限的内存来操作数据. Thus, 具有实时需求和内存限制的系统从该算法中获益最多.

Merge sort 有一个比堆排序小得多的常数因子, 但需要O(n)缓冲区空间来存储中间数据, 这很贵. 它的主要卖点是它是稳定的,相比之下,堆排序不是. 此外,它的实现是非常并行化的.

5.

什么是哈希表,它的每个操作的平均情况和最坏情况的时间是多少? 我们如何使用这个结构在字典中找到所有的字谜?

View answer

哈希表是一种数据结构,用于将值映射到任意类型的键. 哈希表由一定长度的桶的顺序枚举数组组成. 我们通过使用哈希函数将每个可能的键分配给一个桶,哈希函数对于任何给定的键返回一个整数(桶的索引). 同一个bucket可以分配多个key, so all the (key, 值)对存储在各自桶内的列表中.

选择正确的哈希函数会对性能产生很大的影响. 对于我们想要存储的数据集来说,一个好的散列函数将导致很少出现不同键的散列. 即使访问, 插入和删除在最坏情况下的时间复杂度为O(N)(其中N是哈希表中元素的个数), 实际上我们的平均时间复杂度是0 (1).

在字典中找到所有的字谜, 我们只需要把所有有相同字母的单词分组. So, 如果我们将单词映射到表示其排序字母的字符串, 我们可以使用排序后的字母作为键,将单词分组到列表中.

函数find_anagrams(单词)
	word_groups = HashTable
	逐字逐句
		word_groups.get_or_default(排序(词),[]).push(word)
	END FOR
	anagrams =列表
	对于关键字,在word_groups中的值
		anagrams.push(value)
	END FOR
	返回字谜

哈希表存储映射到字符串的列表. For each word, 我们将其添加到相应的键的列表中, 或者做一个新的清单,然后把它添加进去. On average, 对于长度小于或等于L的N个单词的字典, 该算法的平均时间复杂度为O(N L log L)。.

6.

给出一个长度为N的数值数组. 我们需要设计一个函数来查找数组中所有正数的反数. 描述解决最优最差情况和最优平均情况性能的方法, respectively.

View answer

让我们首先设计一个具有最优最坏情况时间的方法.

我们需要比较数字,看看它们在数组中是否有相反的数. 比较所有数的平凡解的一致时间为O(N^2). 涉及的大量比较应该建议尝试建立一个全顺序运算符,允许我们使用排序来解决问题. 如果定义一个比较运算符,将一个数的所有实例放在它的所有相反数的实例后面, 我们会有一对连续的相反的数对于数组中每个有它的对号数的数.

我们想要实现的一个例子是:

数组:-7 4 -3 2 2 -8 -2 3 3 3 7 -2 3 -2
排序:-2 -2 -2 2 -3 3 -3 4 -7 7 -8

经过特殊的排序方法,我们可以看到, we have [-2, 2], [-3, 3] and [-7, 7]连续发生一次的组合. 实现这种比较很简单,可以这样实现.

函数比较(a, b)
	IF a != b and a != -b
		RETURN abs(a) 

如果数字不相等或相反, 我们按绝对值排序, but if they are, 我们根据它们的标志来分类. 最后,基于此的解决方案现在非常简单:

函数find_numbers_with_opposites(数字)
	answer = List
	Sorted_numbers = sort_by(number, compare)
	FOR n IN [1..sorted_numbers.length()]
		IF sorted_numbers[n] > 0 AND sorted_numbers[n - 1] == -sorted_numbers[n]
			answer.push(n)
		END IF
	END FOR
	RETURN answer

这个实现在最坏情况下的运行时复杂度为O(N log N), 排序算法是瓶颈.

使用哈希表可以实现O(N)的最佳平均情况时间复杂度. 我们将数字映射到它们的绝对值,并检查它们的对立面是否已经在哈希表中.

函数find_numbers_with_opposites(数字)
	table = HashTable
	answer = List
	以数求数
		IF number == 0
			CONTINUE
		END IF
		键= abs(数字)
		IF键不在表中
			Table [key] = number
		如果表[key] = -number
			answer.push(key)
			table[key] = 0
		END IF
	END FOR

我们将表中的值更改为永远不会等于数组中的任何数字的值,这样我们就不会从重复匹配中返回重复的结果.

所有哈希表操作的平均时间复杂度为0(1)。, 我们的复杂度是执行N次操作的结果.

7.

一般来说,你会如何描述动态规划? As an example, 如何使用这种方法找到两个数组中元素的最长公共子序列的长度?

View answer

动态规划是解决优化问题的一种范式. 它包括寻找中间子问题的解, 哪些可以存储和重用以解决实际问题. 动态规划是解决困难问题的最佳方法,一旦我们知道了该问题的一个稍微简单的实例——中间子问题的解,这些问题就会变得微不足道. 动态规划解决方案最好由递归关系表示,并且易于实现.

如果中间子问题不重叠,那么我们就有一个分而治之的例子.

查找两个数组之间的最长公共子序列(LCS)是使用动态规划的一个经典示例. 设数组长度为M和N,并存储为变量a[0:M]和b[0:N]. Let’s use L[p, q] to mark the length of the LCS for subarrays a[0:p] and b[0:q]; that is, L[p, q] == LCS(a[0:p]), b[0:q]). 我们也把矩阵L[p]形象化, Q]看起来就像一对“香蕉”和“凉鞋”.

p/q01 B2 A3 N4 A5 N6 A7 S
000000000
1 S00000001
2 A00111111
3 N00122222
4 D00122222
5 A00123333
6 L00123333

如果p或q为0,那么L[p, q] = 0因为我们有一个空子数组. 所有其他字段都有一个简单的规则连接它们- L[p], Q]等于以下选项的最大值:

  • L[p - 1, q] - LCS没有改变,我们只是在数组a中增加了一个字母来实现L[p, q]
  • L[p, q - 1] -类似于数组b
  • L[p - 1, Q - 1]+1 -给a和b加上相同的字母, 当然,这不是每个领域都能做到的

如果你再看一下这张表, 你可以看到,数字总是等于它的上邻数或左邻数的最大值, 除非该字段中的值相等, 在这种情况下,它们将最大值增加1. 因此,用下面的算法给出了这个问题的解.

函数lcs(a, b)
	M = a.length()
	N = b.length()
	L =矩阵[M + 1, N + 1]
	FOR i IN [0..M]
		L[i, 0] = 0
	END FOR
	FOR i IN [0..N]
		L[0, i] = 0
	END FOR
	FOR i IN [1..M]
		FOR j IN [1..N]
			L[i, j] = max(L[i, j], L[i, j-1])
			如果a[i-1] == b[j-1]
				L[i, j] = max(L[i, j], L[i-1, j-1] + 1)
			END IF
		END FOR
	END FOR
	RETURN L[M, N]

这个解的时间复杂度是0 (MxN)

8.

设计一个算法,找出通过跳1遍历N米的方法个数, 2, 3, 4, 或者5米长. 假设N可以是一个很大的数. 结果的复杂性是什么?

View answer

我们可以用动态规划来解决这个问题. 我们用n[k]来表示到达距离k的方法个数. 这个距离可以通过从前面5个距离中的一个跳跃到达. 因此,到达这个距离的方法数等于到达前面5个距离的方法数之和:

N [k] = N [k-1] + N [k-2] + N [k-3] + N [k-4] + N [k-5]

解决方案是一个简单的for循环.

函数方法(N)
	Array n[N+1]
	n[0] = 1
	FOR k IN [1..N]
		n[k] = 0
		FOR d IN [1..min(5, k)+1]
			N [k] += N [k - d]
		END FOR
	END FOR
	RETURN n[N]

这个解的时间复杂度为0 (N). 但是,我们可以有更好的表现. 给定的和可以表示为由1组成的1x5矩阵乘以由先前元素组成的5x1矩阵. 如果我们用同样的方法进行平移,我们可以得到关系B[k] = A * B[k-1],其中:

B[k] =
	[n[k-4]	]
	[n[k-3]	]
	[n[k-2]	]
	[n[k-1]	]
	[n[k]	]
A =
	[0	1	0	0	0]
	[0	0	1	0	0]
	[0	0	0	1	0]
	[0	0	0	0	1]
	[1	1	1	1	1]

如果B[0] =[0 0 0 0 1] ',则[0 0 0 0 1]* B[k] = n[k]. 现在,由于B[k] = A * B[k-1] B[k] = A^T * B[0]. With that, 我们问题的解可以表示为关系n[n] = [0 0 0 0 1] * a ^ n * [0 0 0 0 1] '. 如果我们使用前面提到的最优方法来计算功率(A), N), 这个解的时间复杂度是0 (log N). 我们必须记住这个复杂度有一个很高的常数, 因为矩阵乘法需要时间. 但是,对于足够大的N,这个解是最优的.

9.

什么是红黑树和b树? 它们各自的最佳用例是什么?

View answer

红黑树和b树都是平衡搜索树,可用于在其上定义比较运算符的项. 它们允许最低限度的操作, maximum, predecessor, successor, insert, and delete, 在O(log N)时间内(N为元素数). Thus, 它们可用于实现地图, priority queue, 或者数据库的索引, 举几个例子.

红黑树是对二叉搜索树的改进. 二叉搜索树使用二叉树来执行命名操作, 但是树的深度是不受控制的,因此操作可能会比预期花费更多的时间. 红黑树通过将树中的所有节点标记为红色或黑色来解决这个问题, 并设定如何处理节点之间某些位置的规则. 不用讲太多细节, 此方法保证最长分支的长度不超过最短分支的两倍, 所以每个分支都小于2*log_base2(N).

这是实现有序映射和优先级队列的理想结构. b树分为K- 2k分支(对于给定的数字K),而不是2个, 这是典型的二叉树. 除此之外,它们的行为和二叉搜索树很相似. 这样做的好处是减少了访问操作, 当数据存储在二级存储器或远程位置时,哪个特别有用. This way, 我们可以请求更大块的数据, 当我们处理完之前的请求时, 我们的新请求可以处理了. 这种结构经常用于实现数据库, 因为它们有很多二级存储访问.

10.

你得到一个MxN布尔值矩阵,表示一个棋盘的空闲(True)或占用(False)字段. 求最大的自由场正方形的大小.

View answer

具有True值的字段单独表示1x1正方形. 如果一个字段的左边有空闲字段, top, and top-left, 然后是2x2正方形的右下角. 如果这三个域都是5x5正方形的右下角, 然后是重叠, 加上所查询的字段是空闲的, 形成一个6x6的正方形. 我们可以用这个逻辑来解决这个问题. That is, 我们定义size[x]的关系, Y = min(size[x-1], y], size[x, y-1], size[x-1, Y-1]) + 1. 对于一个已占用的字段,我们可以设置size[x,y] = 0,它将完美地符合我们的递归关系. 现在,size[x, y]代表的是场地位于右下角的最大正方形. 追踪达到的最大数量将给我们问题的答案.

函数square_size(董事会)
	M = board.height()
	N = board.width()
	size = Matrix[M + 1, N + 1]
	FOR i IN [0..M]
		size[i, 0] = 0
	END FOR
	FOR i IN [0..N]
		size[0, i] = 0
	END FOR
	answer = 0
	FOR i IN [0..M]
		FOR j IN [0..N]
			大小(i + 1, j + 1) = max(大小(i, j + 1),大小(i + 1, j),大小(i, j)) + 1
			Answer = max(Answer, size[i+1, j+1])
		END FOR
	END FOR
	RETURN answer
11.

什么是Dijkstra和Prim算法,它们是如何实现的? 斐波那契堆和它们有什么关系?

View answer

Dijkstra是一种寻找单源最短路径的算法. Prim是一种寻找最小生成树的算法.

这两种算法都有类似的实现. 我们将演示如何计算距离和生成树的大小, 但解的形状很容易推导出来.

函数dijkstra(graph, start_point, end_point)
	heap = MinHeap
	数组[图].node_count()]
	heap.添加(连接(start_point, 0))
	WHILE !heap.empty()
		n = heap.pop()
		IF visited[n.point]
			CONTINUE
		END IF
		IF n.Point == end_point
			RETURN n.distance
		END IF
		visited[n.point] = true
		图中的孩子[n].point].children()
			IF !visited[child.point]
				heap.add(连接(孩子.point, n.距离+孩子.distance)
			END IF
		END FOR
	END WHILE
	返回错误("没有找到路径")

函数的(图)
	heap = MinHeap
	tree_size = 0
	tree_count = 0
	数组[图].node_count()]
	heap.add(连接(0,0))
	WHILE !heap.empty()
		n = heap.pop()
		IF visited[n.point]
			CONTINUE
		END IF
		tree_size += n.distance
		tree_count += 1
		visited[n.point] = true
		图中的孩子[n].point].children()
			IF !visited[child.point]
				heap.add(连接(孩子.point, child.distance)
			END IF
		END FOR
	END WHILE
	IF tree_count != graph.node_count()
		返回错误(“图形未连接”)
	ELSE
		返回tree_size
	END IF

如果你仔细观察, 您将看到唯一的区别(除了返回数据计算)是添加到堆中的数据:而Dijkstra累加距离, 普里姆只是利用树枝的距离. 这两种算法通过从MinHeap中获取价格最小的分支来形成树. In Dijkstra, 离起始点最近的点价格最小, 而在普里姆,离母点最近的点价格最小.

使用普通的二进制堆,我们不能阻止添加重复项. 因此,堆中添加的项目可以和图中边的数量一样多. 如果V表示顶点的数量, E是边的个数, 那么复杂度就是O((E+V) log V).

瓶颈是这样一个事实,在最坏的情况下,我们将在某个点将所有边添加到堆中. 多条边可以指向一个顶点, 所以所有指向那个顶点的边都会在访问检查中被丢弃. To prevent that, 我们可以允许堆直接访问该元素, 如果更好的话,更新边缘, 然后heapify来修复订单的任何更改. 这个操作的复杂度是O(log V). 在斐波那契堆中,此操作的复杂度为0 (1). 因此,通过使用斐波那契堆,这种复杂性可以降低到O(E + V log V)。.

12.

寻找单源最短路径的Bellman-Ford算法是什么? 与Dijkstra相比,它的主要优势是什么?

View answer

Bellman-Ford算法通过反复放松距离找到单源最短路径,直到没有更多的距离可以放松. 放松距离是通过检查中间点是否提供比当前选择的路径更好的路径来完成的. 经过多次迭代后,其数量略小于节点数, 我们可以检查解是否是最优的. 如果不是,就会有一个负边的循环,它会提供无限长的更好的路径.

该算法比Dijkstra算法有优势,因为它可以处理带有负边的图, 而Dijkstra仅限于非负的. 它唯一的限制是具有整体负路径的循环图, 但这意味着没有有限解.

让我们再次实现寻找距离,而不是它所代表的实际路径:

函数bellman_ford(graph, start_node, end_node)
	dist =数组[图].node_count()]
	FOR n IN [0..graph.node_count()]
		Dist [n] =∞
	END FOR
	updated = False
	FOR x in [0..graph.node_count()]
		updated = false
		FOR n in [0..graph.node_count()]
			对于图[n]中的p.parents()
				New_distance = dist[p ..point] + p.distance
				IF dist[n] > new_distance
					Dist [n] = new_distance
					updated = true
				END IF
			END FOR
		END FOR
		IF !updated
			BREAK
		END IF
	END FOR
	IF updated
		返回错误("包含负循环,不可解")
	如果dist[end_node] == infinity
		返回错误("开始和结束节点之间没有连接")
	ELSE
		返回dist [end_node]
	END IF

该算法的复杂度为O(V * E),在大多数情况下比Dijkstra算法要慢. 所以这个算法应该只用在我们期望存在负边的情况下.

13.

What is A*, 它的实现细节是什么, 在向目标遍历图时,它的优点和缺点是什么?

View answer

A*是一种不试图找到最优解的寻路算法, 但只是试图快速找到解决方案,而不是在图中不重要的部分徘徊太多.

它通过使用一种启发式方法来逼近一个节点到目标节点的距离. 这是最简单的解释图,表示空间中的路径网格. 如果我们的目标是找到从a点到B点的路径, 我们可以将启发式设置为被查询点到B点的欧氏距离, 按选定的因子进行缩放.

这个启发式是通过将它加到我们到起点的距离来使用的. 除此之外,实现的其余部分与Dijkstra相同.

该算法的主要优点是平均运行时间短. 主要的缺点是它找不到最优解, 但是任何符合启发式的解.

14.

给我们一个数字数组. 我们怎么求某个子数组的和呢? 我们如何查询任意次数的子数组的和呢? 如果我们希望能够在求和查询之间更新数组, 那么最优解决方案是什么呢? 每个解决方案的预处理和查询复杂度是多少.

View answer

为了便于标记,让我们将数组的长度表示为N.

第一个问题包括计算数组的和. 没有预处理,我们只做一个复杂度为0 (N)的求和运算.

第二个问题需要多次计算和. 因此,执行预处理以降低每个查询的复杂性是明智的. 让我们替换为数组a[0:N]创建一个子数组s[0:N+1],即:

s[0] = 0
FOR k in [1..N+1]
	S [k] = S [k-1] + a[k-1]
END FOR

现在每个元素k存储了a[0:k]的和. 查询子数组a[p:q]的和。, 取到q s[q]的所有元素的和,减去p s[p]之前的所有元素的和, 这就是subsum(p, Q) = s[Q] - s[p]

此方法的预处理需要O(N)时间,但执行每个查询需要O(1)时间.

最难的问题是对任意数量的数据更新和查询作出响应. 首先,让我们看看之前的解决方案. 第一个解决方案的插入复杂度为0(1),查询复杂度为0 (N). 第二个解决方案正好相反,O(N)个插入和O(1)个查询. 对于一般情况,这两种方法都不是理想的. 理想情况下,我们希望实现这两个操作的低复杂度.

A 芬威克树(或二叉索引树) 是解决这个问题的理想方法吗. 我们维护一个数组树[0:N+1]的值, 其中每N项存储和(a[M:N]), 其中M等于N,将其二进制表示中最不重要的1替换为0. So for example, N = 19 = b10011, M = 18=b10010; N = 20 = b10100, M=16 = b10000. 现在我们可以很容易地计算和,通过跟踪M直到0. 更新是在相反的方向进行的.

A = Array[N]
树=数组[N+1]

函数之和(结束)
	result = 0
	WHILE end > 0
		Result += tree[end]
		last_one = end & -end
		end -= last_one
	END WHILE
	RETURN result

函数更新(index, value)
	增量= value - a[索引]
	WHILE index < tree.length()
		树[索引]+=增量
		Last_one =索引 & -index
		索引+= last_one
	END WHILE

函数查询(p, q)
	RETURN sum(q) - sum(p)

这两种操作都需要O(log N)复杂度,这比之前的任何一种方法都要好.

15.

您需要设计一个调度程序来调度一组任务. 许多任务在运行自己之前需要等待一些其他任务完成. 我们可以使用什么算法来设计时间表?我们将如何实现它?

View answer

我们需要做的是拓扑排序. 我们将所有任务依赖关系的图连接起来. 然后我们标记每个节点的依赖项数量,并将零依赖项的节点添加到队列中. 当我们从队列中取出节点时,我们从它的所有子队列中删除了依赖项. 当节点达到零依赖关系时,我们将它们添加到队列中.

算法执行后, 如果列表的元素没有任务的数量那么多, 我们有循环依赖. 否则,我们有一个解决方案:

函数的时间表(节点)
	dependencies =数组[节点].length()]
	FOR节点中的节点
		FOR子节点.children
			依赖项[child] += 1
		END FOR
	END FOR
	queue = Queue
	solution = List
	FOR n IN [0..nodes.length()]
		IF依赖项[n] == 0
			queue.push(n)
		END FOR
	END FOR
	WHILE !queue.empty()
		node = queue.pop()
		solution.push(node)
		FOR n IN nodes[节点].children
			依赖关系[n] -= 1
			IF依赖项[n] == 0
				queue.push(n)
			END IF
		END FOR
	END WHILE
	IF solution.length() != nodes.length()
		返回错误("问题包含循环依赖项")
	ELSE
		RETURN solution
	END IF
16.

给出了一个非常大的静态字符串键集, 以及每个键的数据. 我们需要创建一个允许我们快速更新和访问这些数据的数据结构, 即使在最坏的情况下也需要恒定的时间. 我们如何解决这个问题?

View answer

我们把键的个数标记为N. 这里的问题是一个完美散列的问题. 我们采用与普通HashTable类似的方法, 但不是将冲突存储在列表中, 我们将它们存储在第二个HashTable中. 我们选择主哈希函数,直到所有桶中的元素数量相对较少. 之后,在有K个键的桶中,我们放置有K^2个桶的哈希表. 尽管这看起来会导致较高的内存复杂度,但预期的内存复杂度是0 (N)。. 通过选择这种大小的哈希表,我们有50%的概率发生碰撞. 这使得选择不会导致冲突的散列函数变得容易.

17.

确定两个矩形是否相交.

  1. 当矩形由宽度定义时,给出一种算法来解决这个问题, height, and (x, Y)它们左上角的坐标.
  2. 给出另一种算法,其中矩形由其宽度定义, height, and (x, Y)它们中心的坐标.

你的算法在边缘情况下的行为是什么?

View answer
  1. 你只需要检查一个矩形是不是完全在右边, left, 另一个的顶部或底部:
RETURN !(rect1.x > rect2.x + rect2.width
      || rect1.x  + rect1.width < rect2.x
      || rect1.y > rect2.y + rect2.height
      || rect1.y + rect1.height < rect2.y);
  1. 计算两个中心在x和y上的距离,并将其与它们的宽度和高度之和的一半进行比较, respectively:
返回abs(通过rect1.x - rect2.x) <= (rect1.height + rect2.height) / 2;

唯一的边缘情况是其中一个矩形完全包含在另一个矩形内, 由于“相交”一词在这种情况下不一定是非常明确的行为. 两种算法都认为这是一个交集, 如果这不是我们需要的, 要拆除这个箱子还需要额外检查几次.

18.

您有一组由StartDate和EndDate表示的日期间隔. 如何有效地计算它们所覆盖的最长时间?

时间复杂度是多少?

View answer
函数MaxTimespan(开始日期,结束日期)
   排序(startdate可以EndDates)
   ActStartDate =开始日期[1]
   ActEndDate = EndDates[1]
   ActTimespan = ActEndDate - ActStartDate
   FOR i in 2..StartDates.Length
      如果StartDates[i]介于ActStartDate和acenddate之间
         ActEndDate = MAX(ActEndDate, EndDates[i])
         ActTimespan = MAX(ActTimespan, ActEndDate - ActStartDate)
      ELSE
         ActStartDate =开始日期[i]
         ActEndDate = EndDates[i]
      END IF
   END FOR 
   返回ActTimespan

总体时间复杂度为O(NlogN),因为:

  1. 按开始日期对间隔进行排序. 时间复杂度为O(NlogN).

  2. 将第一个区间作为实际范围. 循环时间间隔和当前StartDate是否在实际范围内, 如果需要,扩展实际范围的EndDate,如果需要,扩展到目前为止所达到的最大时间跨度. 否则,使用当前间隔作为新的实际范围. 时间复杂度为O(N).

19.

您的任务是选择将主服务器连接到由N个路由器组成的网络的最佳路由. 路由器以最少要求的N-1根导线连接成树形结构, 对于每个路由器,我们知道连接到它的设备(不是路由器)需要信息的数据速率. 该信息需求表示每个路由器上的负载,如果该路由器没有被选择承载主路由器. 确定我们需要将主路由器连接到哪个路由器,以便最大限度地减少各个线路上的拥塞.

View answer

首先,我们为每个路由器(connections variable). 然后我们标记每个路由器未处理的连接数, 并将具有一个连接的所有路由器添加到处理队列中. 在路由器树中,这些是叶节点. 我们从每个叶节点开始,并在此过程中修剪树,忽略已修剪的节点. 出站数据表示如果没有主机服务器,路由器需要发送到剩余分支的数据. 总体负载减去出站数据是路由器将从剩余分支接收的数据,如果它托管主服务器. 然后可以使用for循环来查找剩余的分支, 将出站数据添加到其内流, 修剪树枝, 如果另一个路由器被添加到队列中,检查它是否成为叶节点. 最后,我们检索拥塞最小的节点. 在整个算法中 congestion 数组中的变量, 如果该路由器拥有服务器,则每个元素表示负载最多的分支上的负载.

该算法运行的时间复杂度为0 (N),这是我们所能得到的最高效率. 解决方案的伪代码如下:

函数place_server(load_router_wires)
	Overall_load = sum(loads)
	Router_count = loads.length()
	连接数=数组[router_count]
	拥塞=数组[router_count]
	对于router_wires中的分支
		连接(分支.first].push(branch.second)
		连接(分支.second].push(branch.first)
	END FOR
	connections_left = Array[router_count]
	[router_count]
	queries = Queue
	FOR n IN [0..router_count]
		Connections_left [n] = connections[n].length()
		如果connections_left[n] == 1
			queries.push(n)
		END IF
	END FOR
	WHILE !queries.empty()
		id = queries.pop()
		如果connections_left [n] != 1
			CONTINUE
		END IF
		Connections_left [n] = 0
		Outbound_data =流入[id] +负载[id]
		IF overall_load - outbound_data > congestion[id]
			拥塞[id] = overall_loadoutbound_data
		END IF
		FOR connection IN connections[id]
			如果connections_left[connection] == 0
				CONTINUE
			END IF
			流入[连接]+= outbound_data
			IF outbound_data > congestion[connection]
				拥塞[connection] = outbound_data
			END IF
			Connections_left [connection] -= 1
			如果connections_left[connection] == 1
				queries.push(连接)
			END IF
		END FOR
	END WHILE
	返回minimum_index(堵塞)

面试不仅仅是棘手的技术问题, 所以这些只是作为一个指南. 并不是每一个值得雇佣的“A”候选人都能回答所有的问题, 回答所有问题也不能保证成为A级考生. 一天结束的时候, 招聘仍然是一门艺术,一门科学,需要大量的工作.

Why Toptal

厌倦了面试候选人? 不知道该问什么才能让你得到一份好工作?

让Toptal为你找到最合适的人.

现在就雇佣一个顶尖的算法开发者

我们的独家算法开发者网络

希望找到一份算法开发人员的工作?

让Toptal为你找到合适的工作.

作为算法开发人员申请

工作机会从我们的网络

提出面试问题

提交的问题和答案将被审查和编辑, 并可能会或可能不会选择张贴, 由Toptal全权决定, LLC.

*所有字段均为必填项

寻找算法开发人员?

Looking for 算法开发人员? 看看Toptal的算法开发者.

Eric Freiling

自由算法开发人员

United StatesToptal Member Since 2018年9月20日

Eric是圣地亚哥的一名高级数据科学家, 在他现在的职业生涯之前, 在国防工业工作了六年. 他拥有强大的学术背景,最终获得数学硕士学位和电气工程博士学位. 他从事的一些领域是算法开发, 信号/图像处理, 还有机器学习.

Show More

Drazen Zaric

自由算法开发人员

SerbiaToptal Member Since June 22, 2019

Drazen是一名数据科学家和数据工程师,在构建分析基础设施方面拥有超过7年的经验, 机器学习, 数据分析. 他在大数据、机器学习和web开发方面拥有丰富的经验. 他曾与Slack和微软(Microsoft)等知名公司合作, 创造了数百万用户使用的产品.

Show More

Cosmin Rusu

自由算法开发人员

SwitzerlandToptal Member Since 二零一七年十二月六日

科斯明在亚马逊工作, Google, 和苹果的高度可扩展性, 分布式系统, 和专注于NLP的机器学习(Siri团队). 他在一家名为Kuende的初创公司工作,在谷歌云上编写Scala和Go微服务. Cosmin在C/ c++高级算法和数据结构方面有很强的背景,对机器学习非常有热情.

Show More

Toptal连接 Top 3% 世界各地的自由职业人才.

加入Toptal社区.

Learn more