小米算法高频题面试:小米Android开发覆盖MIUI系统定制、IoT设备互联。技术栈偏底层,面试官喜欢问系统级原理和性能数据。技术方向偏向MIUI定制、IoT互联。特色专项包括MIUI系统定制与IoT设备开发。今天8道题覆盖小米算法高频题面试核心考点。
Q1:快速排序原理?
分治:选pivot→根据pivot分区(左边小于pivot,右边大于)→递归排左右子数组。平均O(nlogn),最坏O(n²)(已排序数组+最后一个元素做pivot)。优化:随机选pivot或三数取中。
fun quickSort(arr: IntArray, low: Int = 0, high: Int = arr.lastIndex) {
if (low >= high) return
val pivot = partition(arr, low, high)
quickSort(arr, low, pivot - 1); quickSort(arr, pivot + 1, high)
}
深度解析:从复杂度看,分治:选基准 partition 成两半再递归。平均 O(n log n),最坏 O(n²)(已有序且选端点的基准)。。常见变体与延伸包括:三数取中优化基准、堆排序(稳定 O(n log n))、TopK 用快排思想 O(n)。。实战要特别注意边界情况:近乎有序数据要随机化基准,否则退化。。面试官大概率会接着追问:怎么用快排思想在 O(n) 平均时间内找第 K 大?为什么平均是 O(n)?。
追问:为什么实际工程中快排比归并排序更常用?快排空间O(logn)(递归栈),归并O(n)(临时数组)——但快排不稳定。
Q2:两数之和(哈希表一次遍历)?
经典面试题:数组中找两个数使和等于target。暴力O(n²)→哈希表O(n)。关键优化:一次遍历——边遍历边查哈希表,当前元素之前如果有target-nums[i]在哈希表中就找到了。不需要先建哈希表再遍历。
fun twoSum(nums: IntArray, target: Int): IntArray {
val map = HashMap<Int, Int>()
for (i in nums.indices) {
val complement = target - nums[i]
if (complement in map) return intArrayOf(map[complement]!!, i)
map[nums[i]] = i
}
return intArrayOf()
}
追问:为什么不能先建完整哈希表再查找?可以但多一次遍历。一次遍历法每次只查已遍历部分,更高效且避免重复元素覆盖问题。
Q3:有效的括号(栈的应用)?
判断括号字符串是否有效:每对括号必须正确闭合且嵌套正确。用栈:遇到左括号入栈,遇到右括号检查栈顶是否匹配的左括号——匹配则出栈,不匹配或栈空则无效。最后栈为空则有效。时间复杂度O(n),空间O(n)。
fun isValid(s: String): Boolean {
val stack = ArrayDeque<Char>()
val pairs = mapOf(')' to '(', ']' to '[', '}' to '{')
for (c in s) {
if (c in pairs.values) stack.addLast(c)
else if (stack.isEmpty() || stack.removeLast() != pairs[c]) return false
}
return stack.isEmpty()
}
追问:为什么用栈?括号嵌套的LIFO特性——最后打开的括号最先关闭,天然匹配栈的后进先出。
Q4:环形链表检测(快慢指针)?
判断链表是否有环。快慢指针:slow每次走1步,fast每次走2步。如果有环,fast一定会在环中追上slow(相遇)。如果无环,fast会先到达null。时间复杂度O(n),空间O(1)——优于哈希表法的O(n)空间。进阶:找环入口——相遇后slow回到head,slow和fast同速前进,再次相遇点就是环入口。
fun hasCycle(head: ListNode?): Boolean {
var slow = head; var fast = head
while (fast?.next != null) {
slow = slow?.next; fast = fast.next?.next
if (slow == fast) return true
}
return false
}
追问:为什么快慢指针一定能相遇?如果有环,fast比slow每轮多走1步,在环中两者的距离每轮缩小1——一定会追上。
Q5:二叉树的最近公共祖先?
递归:如果root是p或q直接返回root;递归左子树和右子树;如果左右各返回一个(非null)说明p和q分布在两侧→root就是LCA;如果只有一侧返回非null说明p和q都在那一侧→返回那个非null。
fun lca(root: TreeNode?, p: TreeNode, q: TreeNode): TreeNode? {
if (root == null || root == p || root == q) return root
val left = lca(root.left, p, q); val right = lca(root.right, p, q)
return when { left != null && right != null -> root; left != null -> left; else -> right }
}
追问:BST的LCA怎么做?利用BST有序性质——如果两个值都小于root在左子树递归,都大于在右子树递归,否则root就是LCA。
Q6:最大子数组和(Kadane算法)?
找数组中连续子数组的最大和。Kadane算法:遍历数组,维护currentSum和maxSum。currentSum = max(nums[i], currentSum + nums[i])——如果前面累加的和不如当前元素本身大,就重新开始。maxSum = max(maxSum, currentSum)。一次遍历O(n),空间O(1)。
fun maxSubArray(nums: IntArray): Int {
var curSum = nums[0]
var maxSum = nums[0]
for (i in 1 until nums.size) {
curSum = max(nums[i], curSum + nums[i])
maxSum = max(maxSum, curSum)
}
return maxSum
}
追问:Kadane算法的直觉是什么?如果前面的累加是负贡献(让当前元素变小),不如丢弃重新开始——贪心思想。
Q7:合并两个有序链表?
递归:比较两个头节点的大小,小的作为新头,next指向递归合并剩余部分。迭代:用dummy哨兵,每次取两链表中较小的接到结果链表。
fun mergeTwo(l1: ListNode?, l2: ListNode?): ListNode? =
when { l1 == null -> l2; l2 == null -> l1
l1.v < l2.v -> { l1.next = mergeTwo(l1.next, l2); l1 }
else -> { l2.next = mergeTwo(l1, l2.next); l2 } }
追问:合并K个有序链表的思路?用最小堆(O(nlogk))或分治两两合并。
Q8:滑动窗口—无重复字符的最长子串?
双指针left/right维护窗口,HashMap记录字符最后出现位置。right右移:如果当前字符在窗口内出现过,left跳到上次出现位置+1。右移时更新maxLen。时间复杂度O(n)。
fun lengthOfLongestSubstring(s: String): Int { var l = 0; var max = 0
val map = mutableMapOf<Char, Int>()
for (r in s.indices) { val c = s[r]; if (c in map) l = max(l, map[c]!! + 1)
map[c] = r; max = max(max, r - l + 1) }; return max }
追问:滑动窗口的核心思想?用两个指针维护一个满足条件的窗口,右边界扩张,左边界收缩。
下一篇进入小米特色专项——MIUI的GPU Turbo如何在Andro、MIUI如何实现IoT设备互联、MIUI的自由窗口(小窗)实现原理。
觉得有用的同学扣1,你面试被追问最狠的是哪道题?
本系列连载中,关注不迷路,下一篇:小米特色专项面经:MIUI的GPU Turbo如何在Andro、MIUI如何实现IoT设备互联、MIUI的自由窗口(小窗)实现原理
系列简介:Android大厂面经连载,覆盖字节跳动、腾讯、阿里、美团等40+企业,从初级到架构师全岗位覆盖。每篇文章包含真实面试题+详细答案+代码示例,帮你拿到大厂Offer。