刚刷了3道某大厂的机试题,居然满分过了

67 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第28天,点击查看活动详情

导读:刷惯了LeetCode,近日体验了一下牛客网的在线编程系统,这里记录一次某大厂的3道机试题实录,最后居然是满分通过。题目不难,但有一定借鉴意义!

图片

一共3道题,其中前两题难度1星,第三题难度2星,题目难度不大,但都比较经典。话不多说,直接看题!

01 平衡二叉树+后序遍历

这是一道考察二叉树相关操作的题目,主要考点为平衡二叉树的性质和二叉树的前、中、后序遍历。另外,受牛客网编程OJ系统的限制,给定题目的输入并非是一颗构建好的树节点,而只提供了一串序列数值。要求以后序遍历的顺序输出二叉树中的非叶子节点。

图片

例如,在上图中,输入为1-2-3-4-5-6-7-8-9-10-11-12,标准输出应为4-5-2-6-3-1。

解决这一问题,实际上可拆解为以下3个子问题:

  • 根据给定输入数值序列构建一颗平衡二叉树;

  • 获取平衡二叉树中的非叶子节点部分;

  • 后序遍历完成这部分节点的数值输出

对于第一个小问题,实际相当于完成二叉树层序遍历的过程:第一层构建1个节点,第二层构建2个节点,第三层4个节点……,直至用完所有的输入数值。这其实也暗含了平衡二叉树的一个性质:在平衡二叉树中,对于编号为i(i从0开始)的父节点,其左右子节点的编号分别为2i+1和2i+2。想到这里,那么在确定了平衡二叉树中节点个数的情况下,很显然的就知道了有子节点的个数,即:

平衡二叉树中非叶子节点有N//2个,其中N为总节点个数,//为整除

进而,第二个小问题实际上可轻松实现,即首先将输入序列中的数值列表砍半,而后根据层序遍历构建一颗完整的平衡二叉树,得到就是原题中要求的非叶子节点部分。

最后,在得到非叶子节点构成的二叉树的基础上,简单通过递归完成后序遍历输出即可。

给出示例代码如下:

class TreeNode:
    # 定义树节点
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None

def fun(nums):
    def buildAVLTree(nums):  # 根据输入序列构建平衡二叉树
        head = TreeNode(nums[0])
        nodes = [head]
        i, n = 1, len(nums)
        while i < n:  # 层序遍历
            for node in nodes:
                if i < n:
                    node.left = TreeNode(nums[i])
                if i+1 < n:
                    node.right = TreeNode(nums[i+1])
                i += 2
            nodes = [n for node in nodes for n in (node.left, node.right) if n]
        return head

    def postOrder(head):  # 以递归形式完成后序遍历
        return postOrder(head.left) + postOrder(head.right) + [head.val] if head else []

    if len(nums) < 2:
        return nums
    head = buildAVLTree(nums[:len(nums)//2])  # 提取一半,作为非叶子节点构建平衡二叉树
    return postOrder(head)

# 测试样例
nums = [123456789101112]
print(fun(nums))
# [4, 5, 2, 6, 3, 1]

02 双指针+滑动窗口

第二道题目大意是:给定正整数序列和一个目标数值,求正整数序列中加和等于目标数值的最长连续子序列。例如给定正整数序列[1, 2, 3, 4, 2]和目标数值6,由于仅有1+2+3=6和4+2=6两个子序列满足条件,所以最长子序列的长度为3。当不存在目标结果时,返回-1。

值得指出,这里需要注意题目中限定了输入序列是正整数序列,后续有重要应用!

在刚拿到题目看到最长子序列时,由于平日里做惯了最长公共子序列、最长上升子序列等经典动态规划题目,所以直觉想到的也是用动态规划(动态规划仍然是内心深处的软肋,总会心有余悸)。按照动态规划的套路,结合这道题的要求,首先初始化一个N×N大小的结果矩阵,其中第i行第j列的取值代表原输入序列中从i到j的子序列的加和。按照这一定义的数据结构,动态规划转移方程其实就很显然的为:

# 记输入序列为nums[N],动态规划结果矩阵为dp[N][N],则状态转移方程为:
dp[i][j+1] = dp[i][j] + nums[j+1]

显然,这个状态转移方程真的是太简单了,简单到毫无动态规划的味道!自然,按照动态规划中常用的空间优化策略,这个N×N的矩阵可以精简成1×N的单行列表,即仅需记录当前行的实时累加结果即可。再进一步地,发现这个结果仅在相邻两个元素之间产生依赖和传递,进而1×N的结果矩阵可进一步精简为一个标量记录当前结果即可。空间优化完毕。时间效率方面,这里需要两层循环,其中外层i:0->N-1,内层j:i->N-1,是一个O(n2)的时间复杂度。未经深入思考,在提交代码之后居然通过了所有案例!代码如下:

def fun(nums, target):
    max_len = -1
    for i in range(len(nums)):
        sum_ = 0
        for j in range(i, len(nums)):
            sum_ += nums[j]
            if sum_ == target:
                max_len = max(max_len, j - i + 1)
    return max_len

# 测试样例
nums = [12342]
target = 6
print(fun(nums, target))
# 3

当然,这是一个奏效的解决方案,空间复杂度上也是一个最优的版本。然而,时间复杂度上其实可以进一步优化,个中玄机就在于题目中明确指出输入序列是一个正整数。既然是正整数,那么就意味着固定子序列起点i,在不断移动右端点的过程中子序列的加和是严格递增的过程。换言之,对于当前一个固定长度的子序列,若求和小于目标值,则需将右端点j向右移动;若求和大于目标值,此时继续移动右端点是不可行的,而需移动左端点i->i+1,进而实现一个弹性移动的滑动窗口,直至当前求和等于目标结果。每个数值最多遍历2次,进而实现了时间复杂度的降维,即O(n)。给出示例代码如下:

def fun(nums, target):
    max_len = -1
    l = r = 0
    sum_ = nums[0]
    while r < len(nums):
        if sum_ < target:  # 当前累和小于目标值,移动右端点,并增加新右端点数值
            r += 1
            if r >= len(nums):
                break
            sum_ += nums[r]
        elif sum_ > target:  # 当前累和大于目标值,移动左端点,并减去原左端点数值
            sum_ -= nums[l]
            l += 1
        else:  # 找到一组结果,更新最大长度,同时移动左右端点,并减去原左端点数值,增加新右端点数值
            max_len = max(max_len, r - l + 1)
            sum_ -= nums[l]
            l += 1
            r += 1
            if r >= len(nums):
                break
            sum_ += nums[r]
    return max_len

# 测试样例
nums = [12342]
target = 6
print(fun(nums, target))
# 3

03 广度优先遍历

第三道题目是一个标准的广度优先遍历,虽然标记2星,但实际上难度与前两题大致相当,个人在实际作答中也是一遍出炉未进行任何深入思考和调试。

题目大意描述如下:给定仅包含0或1两类数值的N×N矩阵,其中0代表健康细胞,1代表病毒细胞,病毒细胞每分钟向周边相邻细胞进行扩散(不含对角线相邻),求解给定数值下需经过多长时间扩散到所有细胞。若原始均为健康细胞或均为病毒细胞,则返回-1。例如在下图中,需要2分钟即可完成四周向中心点的扩散传播,即返回结果为2。

图片

实际上,这种题目在LeetCode中有很多相似题目,基本都是一个套路:广度优先。同时,由于涉及到矩阵坐标的四周传播,所以仅需一个迭代坐标列表即可。

由于本题过于经典和常规,下面直接给出示例代码:

def fun(nums):
    def isValid(x, y):  # 判断给定坐标是否在合法范围
        return 0 <= x < len(nums) and 0 <= y < len(nums)

    locs = [(i, j) for i in range(len(nums)) for j in range(len(nums)) if nums[i][j] == 1]  # 记录初始状态下1的坐标列表
    if len(locs) == 0 or len(locs) == len(nums) ** 2:  # 初始即为全0或全1
        return -1
    directions = [(01), (0, -1), (10), (-10)]  # 模拟4个方向传播
    res = 0
    while locs:
        temp = []
        for i, j in locs:
            for dx, dy in directions:  # 对当前最新发现的病毒细胞进行遍历4个方向
                x = i + dx
                y = j + dy
                if isValid(x, y) and nums[x][y] == 0:  # 发现周边的健康细胞,记录坐标到新列表中并将其置为1
                    nums[x][y] = 1
                    temp.append((x, y))
        if temp:  # 若实际发生传播,结果+1
            res += 1
        locs = temp
    return res

# 测试样例
nums = [[101], [000], [101]]
print(fun(nums))
# 2

通过这3道经典题型,你是否有所收获呢?