微软面试算法题整理

7,564 阅读11分钟

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

关于微软

今年的时候,因为想回江苏发展,去面试了微软苏州,后端开发。

微软的后端语言是C#/C++,这个相对于目前国内主流的Java/Go,市场会小很多,所以想要去微软的小伙伴要做好转语言的准备。

微软的好处是大平台+外企,不必像国内一线互联网那么卷,同时因为在苏州,生活比较舒适,房价压力小于一线城市。

微软苏州这边也有很多组,比如M365,bing,MMD,edge等等,不同组的工作风格会有差异。

面试特点

微软的面试考察内容包括基础知识、项目经验、系统设计、算法题这几块,视部门和岗位不同会稍有差别,其中算法题基本是每轮面试的必考题,语言不限,也是面试的重点。

因为新冠疫情的影响,面试基本都采用teams远程面试,写代码的话可以投屏用IDE写,当然也有面试官会给了链接,在线白板编程。算法题的难度是leetcode的easy或medium级别。

微软面试轮次非常多,有6轮,一般是1轮技术面+4轮技术面+1轮主管面,面试周期也比较长。

从面试难度上讲,我的感觉是,微软苏州的面试难度会低于国内一线互联网大厂。

如果一个部门面试失败了,微软是可以面试别的部门的。我本人共面了三个部门,分别是M365,MMD,edge,面前两个部门的时候,我大概才做了300道算法题,无论是算法面试,还是技术积累,都还不够火候,被刷了。面edge的时候,我已经做了500道算法题了,而且很多题都做了很多遍数,基本对常用的解法都融会贯通,对算法面试也驾轻就熟了,最后的结果是每轮算法题都做出来了,可惜由于背景差异被刷了。

关于算法面试如何准备,可参考我之间写的《算法面试指北》

算法面试题目

两数组和相等的变动最小的次数

两个数组,改变其中一个数,使两个数组的和相等,求最小变动的数目。

举个例子,数组nums1:[1,2,3,4,5,6],nums2:[1,1,2,2,2,2]

要使得它们和相等,需要变动两次,nums1:[1,2,3,4,5,1],nums2:[7,1,2,2,2,2]

这道题我没有在leetcode上找到,这也反映了微软面试官的特点,常见题型出的少,都是一些冷门题,但是能反映出一定的代码能力,尤其是对各种边界情况的处理。

这道题,我面试时用贪心法解出来了。

class Solution:
    def test(self, num1, num2):
        if not num1 and not num2: return 0
        s1 = sum(num1)
        s2 = sum(num2)
        arr = []
        d = abs(s1-s2)
        if d == 0:
            return 0
        if s1 < s2:
            num1, num2 = num2, num1
        for i in num1:
            arr.append(i-1)
        for i in num2:
            arr.append(10-i)
        arr.sort()
        count = 0
        for i in arr[::-1]:
            d -= i
            count += 1
            if d <= 0:
                return count
        return -1

移掉K位数字

给你一个以字符串表示的非负整数 num 和一个整数 k ,移除这个数中的 k 位数字,使得剩下的数字最小。请你以字符串形式返回这个最小的数字。

 
示例 1 :

输入:num = "1432219", k = 3
输出:"1219"
解释:移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219 。
示例 2 :

输入:num = "10200", k = 1
输出:"200"
解释:移掉首位的 1 剩下的数字为 200. 注意输出不能有任何前导零。
示例 3 :

输入:num = "10", k = 2
输出:"0"
解释:从原数字移除所有的数字,剩余为空就是 0

这道题是leetcode上的原题,402. 移掉 K 位数字

这道题要注意时间复杂度,正确思路是贪心+单调栈,时间复杂度O(n)


class Solution:
    def removeKdigits(self, num: str, k: int) -> str:
        numStack = []
        
        # 构建单调递增的数字串
        for digit in num:
            while k and numStack and numStack[-1] > digit:
                numStack.pop()
                k -= 1
        
            numStack.append(digit)
        
        # 如果 K > 0,删除末尾的 K 个字符
        finalStack = numStack[:-k] if k else numStack
        
        # 抹去前导零
        return "".join(finalStack).lstrip('0') or "0"

按出现频数和相对位置重新排序字符串

Input String: 123123412345
Output String: 111222333445

按字母出现的频率重新排序字符串,如果频率一样,则按照相对顺序排序

这道题是leetcode上的1636. 按照频率将数组升序排序 的变体,做这道题的时候,不能想太复杂,要灵活运用字符串和排序的相关API。

我自己针对这道题,研究出了Python的一行解法。

class Solution:
    def interview(self, items):
        return "".join(sorted(list(items), key=lambda x: (items.count(x), -items.index(x)), reverse=True))

是否存在和为K的连续子数组

输入一个数组,如[1,2,3],和一个目标值target

判断是否存在连续的子数组的和为目标值

这道题其实是leetcode上的 325. 和等于 k 的最长子数组长度 的简化版,当时面试官只要我算出是否有解。

类似这种连续子数组的题目,要想到用哈希表+前缀和的思路去做,否则暴力法的时间复杂度会非常夸张。

我这里给的是leetcode上原题的解法。

class Solution:
    def maxSubArrayLen(self, nums: List[int], k: int) -> int:
        dic = {0: -1}
        s = res = 0
        for i in range(len(nums)):
            s += nums[i]
            if s - k in dic:
                res = max(i-dic[s-k], res)
            if s not in dic:
                dic[s] = i
        return res

火柴拼正方形

还记得童话《卖火柴的小女孩》吗?现在,你知道小女孩有多少根火柴,请找出一种能使用所有火柴拼成一个正方形的方法。不能折断火柴,可以把火柴连接起来,并且每根火柴都要用到。

输入为小女孩拥有火柴的数目,每根火柴用其长度表示。输出即为是否能用所有的火柴拼成正方形。

示例 1:

输入: [1,1,2,2,2]
输出: true

解释: 能拼成一个边长为2的正方形,每边两根火柴。

这道题是leetcode上的原题: 473. 火柴拼正方形

我一开始想用贪心去做,后来发现非常麻烦,还好即时反应了过来,改用了回溯。

class Solution:
    def makesquare(self, nums: List[int]) -> bool:
        if not nums: return False
        s = sum(nums)
        # 如果周长不能被4整除,直接返回False
        if s % 4 != 0: return False
        w = s // 4
        # 先将数组从大到小排序,这里利用了贪心的思路,找到解的速度会快很多
        nums.sort(reverse=True)

        def _dfs(index, w_arr):
            # 数组用完了,边长数组必须都为0,否则返回False
            if index == len(nums):
                return all([i==0 for i in w_arr])
            result = False
            for j in range(len(w_arr)):
                # 这里用到了剪枝,如果w_arr连续两个值相同,则可以跳过
                if j > 0 and w_arr[j] == w_arr[j-1]:
                    continue
                # 依次尝试去扣除nums[index]
                if w_arr[j] >= nums[index]:
                    w_arr[j] -= nums[index]
                    if _dfs(index+1, w_arr):
                        return True
                    w_arr[j] += nums[index]
            return result

        return _dfs(0, [w,w,w,w])

两数之和与目标值相差最小

Find minimun abs(val[a] + val[b]) in an integer array.[2, -1000, 0, 33, -15, -100, -1]

这道题是经典的两数之和的变体,类似的还有leetcode上的这道题: 1099. 小于 K 的两数之和

我因为对这类题目做的比较多,轻松用排序+双指针写了出来。

class Solution:
    def interview(self, nums):
        if len(nums) < 2:
            return -1
        nums.sort()
        min_value = float('inf')
        i, j = 0, len(nums) - 1
        while i < j:
            s = nums[i] + nums[j]
            if s == 0:
                return 0
            elif s < 0:
                i += 1
            else:
                j -= 1
            min_value = min(min_value, abs(s))
        return min_value

快速排序

没错,快速排序也是在面试中的常考点,除了微软面试之外,我在其他很多面试都遇到,要掌握快速排序的原理,以及平均复杂度O(nlogn),和最坏时间复杂度O(n^2)

class Solution:
    def sortArray(self, nums):
        n = len(nums)

        def quick(left, right):
            if left >= right:
                return nums
            pivot = left
            i = left
            j = right
            while i < j:
                while i < j and nums[j] > nums[pivot]:
                    j -= 1
                while i < j and nums[i] <= nums[pivot]:
                    i += 1
                nums[i], nums[j] = nums[j], nums[i]
            nums[pivot], nums[j] = nums[j], nums[pivot]
            quick(left, j - 1)
            quick(j + 1, right)
            return nums

        return quick(0, n - 1)

leetcode上也有一道专门的数组排序的题目: 912. 排序数组

x的平方根

实现 int sqrt(int x) 函数。

计算并返回 x 的平方根,其中 x 是非负整数。

由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

示例 1:

输入: 4
输出: 2
示例 2:

输入: 8
输出: 2
说明: 8 的平方根是 2.82842..., 
     由于返回类型是整数,小数部分将被舍去。

这道题非常经典了,leetcode原题: 69. x 的平方根

不过在边界处理上特别容易出错,要多检查一下。

class Solution:
    def mySqrt(self, x: int) -> int:
        l, r, ans = 0, x, -1
        while l <= r:
            mid = (l + r) // 2
            if mid * mid <= x:
                ans = mid
                l = mid + 1
            else:
                r = mid - 1
        return ans

只有一个不同字符的字符串

一组列表['abcd', 'acdb', 'aacd'],存在只替换一个字符后,与另一个元素相等,返回True
否则返回False

这道题也是leetcode原题:1554. 只有一个不同字符的字符串,不过比较冷门。

class Solution:
    def differByOne(self, dict: List[str]) -> bool:
        # 记录遍历过的字符串可能存在的所有不同字符串
        c = set()
        for i in dict:
            for j in range(len(i)):
                s = i[:j] + '.' + i[j + 1:]
                if s in c:
                    return True
                c.add(s)
        return False

最近时刻

给定一个形如 “HH:MM” 表示的时刻,利用当前出现过的数字构造下一个距离当前时间最近的时刻。每个出现数字都可以被无限次使用。

你可以认为给定的字符串一定是合法的。例如,“01:34” 和 “12:09” 是合法的,“1:34” 和 “12:9” 是不合法的。

样例 1:

输入: "19:34"
输出: "19:39"
解释: 利用数字 1, 9, 3, 4 构造出来的最近时刻是 19:39,是 5 分钟之后。结果不是 19:33 因为这个时刻是 23 小时 59 分钟之后。
 

样例 2:

输入: "23:59"
输出: "22:22"
解释: 利用数字 2, 3, 5, 9 构造出来的最近时刻是 22:22。 答案一定是第二天的某一时刻,所以选择可构造的最小时刻。

这道题也是leetcode上原题,不过非常冷门: 681. 最近时刻。当时面试的时候,因为跟面试官用的语言不一样,面试官又看我刷题很多,特地找了一道很少人做过的,又能很考验代码能力,注重边界值处理的题目。

不过做这道题的时候,我已经有500道题的积累了,稍微调试了下,所以测试用例都过了,也受到了面试官好评。

class Solution:
    def nextClosestTime(self, time: str) -> str:
        digit_set = set()
        for c in time:
            if c != ':':
                digit_set.add(c)
        if len(digit_set) == 1:
            return time
        
        origin_minute = int(time[0:2]) * 60 + int(time[3: ])    #time换算成分钟
        candidates = set()

        def backtrace(path: str) -> None:
            if len(path) == 4:
                candidates.add(path)
                return 
            for c in digit_set:
                path += c
                backtrace(path)
                path = path[ :-1]       #回溯。不回溯 内存就爆了
        
        backtrace("")

        res = 0
        first_calc = True
        for s in candidates:
            H = int(s[0:2])
            M = int(s[2:4])
            if 0 <= H < 24 and 0 <= M < 60:
                cur = H * 60 + M        #当前的时间(单位:分钟)
                if cur == origin_minute:    #是time本身,就跳过
                    continue
                if first_calc == True:      #第一次计算
                    res = cur
                    first_calc = False
                else:                       #不是第一次计算
                    cur_diff = (cur + 24*60 - origin_minute) % (24*60)  #比time小的时刻,都是算第二天的
                    res_diff = (res + 24*60 - origin_minute) % (24*60)
                    if cur_diff < res_diff:
                        res = cur 
        H, M = res//60, res%60
        res_s = ""
        if H < 10:
            res_s += '0'
        res_s += str(H)
        res_s += ':'
        if M < 10:
            res_s += '0'
        res_s += str(M)

        return res_s

移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

示例:

输入: [0,1,0,3,12]
输出: [1,3,12,0,0]

当时拿到这道题的时候很开心,因为过于经典,我已经做了不下5遍,轻松搞定。

原题地址:283. 移动零

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        i = 0
        for j in range(len(nums)):
            if nums[j] != 0:
                nums[i], nums[j] = nums[j], nums[i]
                i += 1
        return nums

求日志的百分位数

web服务器日志,格式为:GET /api/test 200 333ms
算日志文件里时间的百分位

这道题严格来讲不算算法题,只是日常工作中的一个场景,我用Python很容易就做出来了

class Solution:
    def interview(self, arr, target):
        # target: 0-1的小数
        length = len(arr)
        index = int(length*target)
        print(index)

        time_arr = []
        for s in arr:
            time = int(s.split()[-1][:-2])
            time_arr.append(time)
        time_arr.sort()
        print(time_arr)
        return time_arr[index]

if __name__ == "__main__":
    arr = ["GET /api/test 200 333ms", "GET /api/test 200 222ms", "GET /api/test 200 200ms"]
    arr1 = ["GET /api/test 200 333ms", "GET /api/test 200 222ms", "GET /api/test 200 20ms", "GET /api/test 200 1ms"]
    a = Solution()
    print(a.test(arr, 0.5))
    print(a.test(arr1, 0.5))
    print(a.test(arr, 0.2))

和为k的长度大于1的所有连续子数组

input: [0,1,5,6,0]  k=6
output: [0,1,5] [1,5] [6,0]
连续 长度至少为2

这道题在leetcode上也有很多类似的题目,比如: 560. 和为K的子数组

因为我针对这种题型专门练过,所以知道用哈希表+前缀和去做,很快就做出来了。

class Solution:
    def interview(self, arr, target):
        hashmap = collections.defaultdict(list)
        hashmap[0].append(-1)
        s = 0
        res = []
        for i in range(len(arr)):
            s += arr[i]
            if s - target in hashmap:
                left_arr = hashmap[s-target]
                for j in left_arr:
                    if i - j > 1:
                        res.append(arr[j+1:i+1])
            hashmap[s].append(i)
        return res

英文转整数表示

201
two hundred and one
2001
two thousand and one
20001
twenty thousand and one
200001
two hundred thousand and one
2000001
two million and one
20 000 001
twenty million and one
200 000 001
two hundred million and one
200 300 001
two hundred million and three hundred thousand and one
two hundred million three hundred thousand and one

这道题不简单,要考虑的东西非常多,我当时为了防止时间不够,所以跟面试官讨论了,只完成我上面列的这些用例。

leetcode上有姊妹题,273. 整数转换英文表示,难度为hard

我下面的解法还可以优化,是我面试中写的。

class Solution:
    def interview(self, s):
        num_map = {
            'one': 1,
            'two': 2,
            'three': 3,
            'hundred': 100,
            'thousand': 1000,
            'million': 1000000,
            'twenty': 20
        }

        res = 0
        cur = 1
        for i in s.split():
            if i != 'and':
                cur *= num_map[i]
            else:
                res += cur
                cur = 1
        res += cur
        return res


if __name__ == "__main__":
    a = Solution()
    print(a.test("two thousand and one"))
    print(a.test("twenty thousand and one"))
    print(a.test("two hundred million and one"))
    print(a.test("two hundred million and three hundred thousand and one"))

面试感悟

我一开始在参加算法面试的时候,特别容易紧张,经常一紧张就导致脑子一片空白,即便是曾经做过的题也想不出解法了。

我后来痛定思痛,觉得还是练的不够多,所以针对常见题型都进行了反复练习,加深理解,并且模拟真实面试场景,白板写一遍,IDE写一遍。

还有一点要注意的是,可以把面试官当合作伙伴,如果一道题实在想不出解法,可以找他要点提示,而不要闷头苦想,不给面试官反馈,这是非常糟糕的。

最后,写完后,主动想测试用例、corner case是个好习惯。

虽然我没拿到微软的offer,但是在准备微软的面试的过程中,我自己对算法的掌握越来越深了,在面其他公司的时候,算法题这块基本难不倒我,最终也拿到了理想的offer。

随着程序员面试越来越卷,算法面试的重要性越来越高,算法不像八股文只要提前背诵就可以了,算法是一项技能,需要日复一日的进行训练,从这个角度上来说,学习算法永远不亏,将来的你,一定会感谢此刻正在苦练算法题的自己。