Android 面试算法完整手册

7 阅读22分钟

Android 面试算法完整手册(64 题)

定位:面向 Android/客户端面试的算法速查与复习手册。
特点:每题包含「易记方式、思路、步骤、复杂度、可抄写代码骨架」。
适用:面试前突击、平时刷题复盘、口述思路训练。

1 链表类

1.1 反转链表

易记方式三指针反转|触发词:prev/cur/next|警报:先存 next 再改指针。

思想:迭代反转整条链表。用三个引用:prev(已反转部分的头)、cur(当前结点)、nxt(暂存后继,避免断链后丢失)。

步骤

  1. 初始化 prev=nullcur=head
  2. cur 非空:nxt=cur.nextcur.next=prev,然后 prev=curcur=nxt
  3. 结束时 prev 即为新头结点。

时间复杂度:O(n),n 为链表长度

空间复杂度:O(1),仅用常数个引用。

fun reverseList(head: ListNode?): ListNode? {
        var prev: ListNode? = null
        var cur = head
        while (cur != null) {
            val nxt = cur.next
            cur.next = prev
            prev = cur
            cur = nxt
        }
        return prev
    }

1.2 反转链表 II(区间反转)

易记方式dummy 头插|触发词:pre/start/then|警报:left=1 必须有 dummy

思想:在「虚拟头结点 dummy」后定位到第 left-1 个结点作为 pre,对 [left,right] 区间做多次「头插」,把区间内结点逐个插到 pre 之后。

步骤

  1. dummy.next=headpredummyleft-1 步。
  2. start 为区间首结点,then 为待头插的下一个结点,循环 right-left 次:把 then 移到 pre.next 之前。

时间复杂度:O(n)

空间复杂度:O(1)。

fun reverseBetween(head: ListNode?, left: Int, right: Int): ListNode? {
        val dummy = ListNode(0).apply { next = head }
        var pre: ListNode? = dummy
        repeat(left - 1) { pre = pre!!.next }
        val start = pre!!.next
        var then = start?.next
        repeat(right - left) {
            start!!.next = then!!.next
            then.next = pre!!.next
            pre.next = then
            then = start.next
        }
        return dummy.next
    }

1.3 环形链表 II(入环点)

易记方式相遇再重逢|触发词:快慢指针|警报:先判 fast/fast.next

思想Floyd 判环——快指针每次走 2 步、慢指针走 1 步,若有环必相遇;无环则快指针先到达链表尾。求环入口:相遇后,一指针从 head 出发、一指针从相遇点出发,同速前进,相遇处即为入环点(数学可证路程相等)。

步骤

  1. slow=fast=head,循环直到 fastfast.next 为空(无环)或 slow==fast(有环)。
  2. 若无环返回 null;若有环,p=headpslow 同步前移直到 p===slow,返回 p

时间复杂度:O(n)

空间复杂度:O(1)。

fun detectCycle(head: ListNode?): ListNode? {
        var slow = head
        var fast = head
        while (fast?.next != null) {
            slow = slow?.next
            fast = fast.next?.next
            if (slow === fast) break
        }
        if (fast?.next == null) return null
        var p = head
        while (p !== slow) {
            p = p?.next
            slow = slow?.next
        }
        return p
    }

1.4 链表的中间结点

易记方式快二慢一|触发词:fast/slow|警报:偶数长度题意取哪一个中点。

思想:快慢指针:fast 每次走两步、slow 走一步,当 fast 到尾时,slow 在链表中点(偶数个结点时取第二个中点)。

步骤

  1. slow=fast=head
  2. while (fast?.next!=null)slow=slow.nextfast=fast.next.next
  3. 返回 slow

时间复杂度:O(n)

空间复杂度:O(1)。

fun middleNode(head: ListNode?): ListNode? {
        var slow = head
        var fast = head
        while (fast?.next != null) {
            slow = slow?.next
            fast = fast.next?.next
        }
        return slow
    }

1.5 合并两个有序链表

易记方式归并接小|触发词:a/b/tail|警报:最后别忘接剩余链。

思想:两链表元素均已有序,用双指针比较当前头结点值,较小者接到结果链尾(merge,归并的一个子过程,不是完整排序算法)。

步骤

  1. dummy
  2. ab 指向两链头
  3. while 两者非空,选较小者接尾
  4. 最后接上剩余一段。

时间复杂度:O(n+m)

空间复杂度:O(1)。

fun mergeTwoLists(l1: ListNode?, l2: ListNode?): ListNode? {
        val dummy = ListNode(0)
        var t = dummy
        var a = l1
        var b = l2
        while (a != null && b != null) {
            if (a.`val` <= b.`val`) {
                t.next = a
                a = a.next
            } else {
                t.next = b
                b = b.next
            }
            t = t.next!!
        }
        t.next = a ?: b
        return dummy.next
    }

1.6 相交链表

易记方式A+B 等于 B+A|触发词:换头双指针|警报:比较引用不是值。

思想:两链表相交则尾部共用。指针 a 从 A 出发走到尾后从 B 头继续,b 对称;总路程相同,若有交点必在交点相遇(否则同到 null)。

步骤

  1. a=headAb=headB
  2. while (a!==b)a = a?.next ?: headBb = b?.next ?: headA
  3. 返回 a

时间复杂度:O(n+m)

空间复杂度:O(1)。

fun getIntersectionNode(headA: ListNode?, headB: ListNode?): ListNode? {
        var a = headA
        var b = headB
        while (a !== b) {
            a = if (a == null) headB else a.next
            b = if (b == null) headA else b.next
        }
        return a
    }

1.7 奇偶链表

易记方式奇偶分链再接|触发词:odd/even/evenHead|警报:处理空链和单节点。

思想:保持相对顺序:奇数位结点串成一条链,偶数位串成另一条,再把奇链尾接偶链头。下标从 1 开始计「奇」「偶」。

步骤

  1. odd 指向第 1 个结点,evenHead 为第 2 个结点;若只有 0/1 个结点直接返回。
  2. 反复:odd.next = even.nextodd 前进;even.next = odd.nexteven 前进,直到偶链无后继。
  3. odd.next = evenHead,返回原头。

时间复杂度:O(n)

空间复杂度:O(1)。

fun oddEvenList(head: ListNode?): ListNode? {
        if (head == null || head.next == null) return head
        var odd: ListNode = head
        val evenHead = head.next!!
        var even: ListNode = evenHead
        while (even.next != null) {
            odd.next = even.next
            odd = odd.next!!
            even.next = odd.next
            even = even.next!!
        }
        odd.next = evenHead
        return head
    }

2 栈与队列类

2.1 有效的括号

易记方式左进右配|触发词:括号栈|警报:右括号前先判空栈。

思想:左括号入栈;遇到右括号必须与栈顶左括号类型匹配,否则非法。

步骤

  1. 遍历字符
  2. ( [ { 入栈
  3. ) ] } 时若栈空或栈顶不匹配则失败
  4. 结束栈须为空。

时间复杂度:O(n)

空间复杂度:O(n)。

import java.util.ArrayDeque

fun isValidParentheses(s: String): Boolean {
        val st = ArrayDeque<Char>()
        val map = mapOf(')' to '(', ']' to '[', '}' to '{')
        for (c in s) {
            when (c) {
                '(', '[', '{' -> st.addLast(c)
                else -> {
                    if (st.isEmpty() || st.removeLast() != map[c]) return false
                }
            }
        }
        return st.isEmpty()
    }

2.2 每日温度

易记方式单调栈找右大|触发词:从右向左|警报:栈里存下标不存值。

思想:对每个位置找「右侧第一个更大温度」的距离。从右往左扫,单调栈存下标,栈底到栈顶温度递减;当前温度会弹出栈里所有不大于它的下标。

步骤

  1. in-10:弹出 <=T[i] 的栈顶
  2. 答案为栈顶与 i 的距离(无则为 0)
  3. i 入栈。

时间复杂度:O(n)

空间复杂度:O(n)。

import java.util.ArrayDeque

fun dailyTemperatures(temperatures: IntArray): IntArray {
        val n = temperatures.size
        val ans = IntArray(n)
        val st = ArrayDeque<Int>()
        for (i in n - 1 downTo 0) {
            while (st.isNotEmpty() && temperatures[st.last()] <= temperatures[i]) {
                st.removeLast()
            }
            ans[i] = if (st.isEmpty()) 0 else st.last() - i
            st.addLast(i)
        }
        return ans
    }

2.3 接雨水

易记方式短板先算水|触发词:leftMax/rightMax|警报:更新最大值与结算顺序别反。

思想:每个位置存水量 = min(左侧最高, 右侧最高) - 当前高度。双指针从两端向中间,维护左右最大高度,先处理较短一侧(短板效应)。

步骤

  1. l=0r=n-1,维护 leftMaxrightMax
  2. 每次较短边更新对应侧贡献或更新最大高度。

时间复杂度:O(n)

空间复杂度:O(1)。

fun trap(height: IntArray): Int {
        var l = 0
        var r = height.size - 1
        var leftMax = 0
        var rightMax = 0
        var water = 0
        while (l < r) {
            if (height[l] < height[r]) {
                if (height[l] >= leftMax) leftMax = height[l] else water += leftMax - height[l]
                l++
            } else {
                if (height[r] >= rightMax) rightMax = height[r] else water += rightMax - height[r]
                r--
            }
        }
        return water
    }

2.4 用栈实现队列

易记方式in 进 out 出|触发词:双栈模拟队列|警报:仅在 out 为空时倒栈。

思想:两个栈:inSt 负责入队,outSt 负责出队;出队时若 outSt 空,把 inSt 全部倒入 outSt(顺序反转后满足 FIFO)。

步骤

  1. pushinSt
  2. pop/peek 前调用 pour()
  3. outSt 弹出。

时间复杂度:均摊 O(1)

空间复杂度:O(n)。

import java.util.ArrayDeque

class MyQueue {
        private val inSt = ArrayDeque<Int>()
        private val outSt = ArrayDeque<Int>()
        private fun pour() {
            if (outSt.isEmpty()) while (inSt.isNotEmpty()) outSt.addLast(inSt.removeLast())
        }
        fun push(x: Int) = inSt.addLast(x)
        fun pop(): Int {
            pour()
            return outSt.removeLast()
        }
        fun peek(): Int {
            pour()
            return outSt.last()
        }
        fun empty(): Boolean = inSt.isEmpty() && outSt.isEmpty()
    }

2.5 用队列实现栈

易记方式新元素旋到首|触发词:单队列模拟栈|警报:push 后旋转 size-1 次。

思想:一个队列模拟栈:栈顶应在队首;每次 push 新元素后,把前面所有元素依次从队尾重新入队,使新元素到队首。

步骤addLast(x) 后执行 size-1 次「队首出队并 addLast」。

时间复杂度push 为 O(n),pop/peek 为 O(1)

空间复杂度:O(n)。

import java.util.ArrayDeque

class MyStack {
        private val q = ArrayDeque<Int>()
        fun push(x: Int) {
            q.addLast(x)
            repeat(q.size - 1) { q.addLast(q.removeFirst()) }
        }
        fun pop(): Int = q.removeFirst()
        fun top(): Int = q.first()
        fun empty(): Boolean = q.isEmpty()
    }

3 二叉树类

3.1 二叉树的最大深度

易记方式高度递归式|触发词:1+max(左,右)|警报:空节点返回 0。

思想:树高 = 1 + max(左子树高, 右子树高);空结点高度为 0。

步骤

  1. root==null 返回 0
  2. 否则返回 1 + max(maxDepth(left), maxDepth(right))

时间复杂度:O(n)

空间复杂度:O(h),h 为树高(递归栈)。

fun maxDepth(root: TreeNode?): Int {
        if (root == null) return 0
        return 1 + maxOf(maxDepth(root.left), maxDepth(root.right))
    }

3.2 二叉树前序遍历(迭代)

易记方式前序入栈即访|触发词:左链入栈|警报:访问时机在入栈前后别混。

思想:前序顺序为「根–左–右」。迭代:沿左链一路访问并入栈,弹栈后处理右子树。

步骤

  1. cur=root
  2. while cur!=null 或栈非空:沿左走并访问
  3. 弹栈取结点并转向其右孩子。

时间复杂度:O(n)

空间复杂度:O(h)。

import java.util.ArrayDeque

fun preorderTraversal(root: TreeNode?): List<Int> {
        val ans = mutableListOf<Int>()
        val st = ArrayDeque<TreeNode>()
        var cur = root
        while (cur != null || st.isNotEmpty()) {
            while (cur != null) {
                ans.add(cur.`val`)
                st.addLast(cur)
                cur = cur.left
            }
            cur = st.removeLast().right
        }
        return ans
    }

3.3 二叉树中序遍历(迭代)

易记方式中序出栈再访|触发词:左压右转|警报:访问在弹栈后。

思想:中序为「左–根–右」。对 BST 而言,中序遍历得到升序序列。

步骤

  1. 左链入栈直到空
  2. 弹栈访问结点
  3. 转右子树,重复。

时间复杂度:O(n)

空间复杂度:O(h)。

import java.util.ArrayDeque

fun inorderTraversal(root: TreeNode?): List<Int> {
        val ans = mutableListOf<Int>()
        val st = ArrayDeque<TreeNode>()
        var cur = root
        while (cur != null || st.isNotEmpty()) {
            while (cur != null) {
                st.addLast(cur)
                cur = cur.left
            }
            cur = st.removeLast()
            ans.add(cur.`val`)
            cur = cur.right
        }
        return ans
    }

3.4 二叉树层序遍历

易记方式层序看 size|触发词:队列分层|警报:每层循环次数固定为当层 size。

思想BFS:队列中依次存放当前层的结点;每层处理前记录队列长度,即该层结点个数。

步骤

  1. 根入队
  2. while 队列非空:弹出当前层 size 个结点,子结点入队。

时间复杂度:O(n)

空间复杂度:O(n)(队列最多约 n/2 个结点)。

import java.util.ArrayDeque

fun levelOrder(root: TreeNode?): List<List<Int>> {
        val res = mutableListOf<List<Int>>()
        if (root == null) return res
        val q = ArrayDeque<TreeNode>()
        q.add(root)
        while (q.isNotEmpty()) {
            val level = mutableListOf<Int>()
            repeat(q.size) {
                val n = q.removeFirst()
                level.add(n.`val`)
                n.left?.let { q.addLast(it) }
                n.right?.let { q.addLast(it) }
            }
            res.add(level)
        }
        return res
    }

3.5 对称二叉树

易记方式镜像交叉比|触发词:a.leftb.right|警报:一空一非空立即 false。

思想:对称 = 左子树与右子树镜像:左左与右右比、左右与右左比。

步骤

  1. 递归 mirror(a,b):皆空则真
  2. 一空则假
  3. 值相等且 mirror(a.left,b.right)mirror(a.right,b.left)

时间复杂度:O(n)

空间复杂度:O(h)。

fun isSymmetric(root: TreeNode?): Boolean {
        fun mirror(a: TreeNode?, b: TreeNode?): Boolean {
            if (a == null && b == null) return true
            if (a == null || b == null) return false
            return a.`val` == b.`val` && mirror(a.left, b.right) && mirror(a.right, b.left)
        }
        return root == null || mirror(root.left, root.right)
    }

3.6 路径总和

易记方式目标一路减|触发词:递归传剩余和|警报:必须在叶子节点判等。

思想:从根到叶的路径和需等于 targetSum;递归时减去当前结点值,到叶子判断是否为 0。

步骤

  1. 空结点返回 false
  2. 叶子结点判断 val==targetSum
  3. 否则递归左右子树,目标和减去当前值。

时间复杂度:O(n)

空间复杂度:O(h)。

fun hasPathSum(root: TreeNode?, targetSum: Int): Boolean {
        if (root == null) return false
        if (root.left == null && root.right == null) return root.`val` == targetSum
        return hasPathSum(root.left, targetSum - root.`val`) ||
            hasPathSum(root.right, targetSum - root.`val`)
    }

3.7 二叉树的最近公共祖先

易记方式LCA 后序汇合|触发词:左右子树结果|警报:当前命中 p/q 直接返回。

思想LCA:后序遍历。若 pq 分别在左右子树,则当前根为 LCA;若只在一边,则向上返回那一边找到的结点。本题实现适用于一般二叉树(非 BST 专用写法)。

步骤

  1. 若当前为 null 或等于 p/q 则返回当前
  2. 左、右递归
  3. 若左右均非空则返回根,否则返回非空一侧。

时间复杂度:O(n)

空间复杂度:O(h)。

说明:若题目限定为 BST 的 LCA,可利用大小关系从根向下走,时间可写为 O(h) 且实现更短。

fun lowestCommonAncestor(root: TreeNode?, p: TreeNode, q: TreeNode): TreeNode? {
        if (root == null || root === p || root === q) return root
        val left = lowestCommonAncestor(root.left, p, q)
        val right = lowestCommonAncestor(root.right, p, q)
        return when {
            left != null && right != null -> root
            else -> left ?: right
        }
    }

3.8 验证二叉搜索树

易记方式中序严格增|触发词:prev 前驱值|警报:必须是严格大于。

思想BST 的中序遍历为严格递增序列;用 prev 保存中序前一个值,当前值须大于 prev

步骤

  1. 中序 DFS:先左子树
  2. 再比较 prev 与当前值
  3. 更新 prev
  4. 再右子树。

时间复杂度:O(n)

空间复杂度:O(h)。

fun isValidBST(root: TreeNode?): Boolean {
        var prev: Int? = null
        fun dfs(n: TreeNode?): Boolean {
            if (n == null) return true
            if (!dfs(n.left)) return false
            if (prev != null && n.`val` <= prev!!) return false
            prev = n.`val`
            return dfs(n.right)
        }
        return dfs(root)
    }

3.9 二叉树中的最大路径和

易记方式全局看拐点|触发词:bestmaxGain|警报:向上只返单边贡献。

思想:路径可起止于任意结点,但沿父子边。递归函数返回「从该结点向下延伸的最大链和(可为负则取 0 不接)」;全局维护「经过当前结点」的折线路径 = 左链 + 右链 + 结点值。

步骤

  1. maxGain(n):空返回 0;左、右子贡献为 max(0, maxGain(子))
  2. n.val + left + right 更新全局答案。
  3. 向上返回 n.val + max(left, right)

时间复杂度:O(n)

空间复杂度:O(h)。

fun maxPathSum(root: TreeNode?): Int {
        var best = Int.MIN_VALUE
        fun maxGain(n: TreeNode?): Int {
            if (n == null) return 0
            val left = maxOf(0, maxGain(n.left))
            val right = maxOf(0, maxGain(n.right))
            best = maxOf(best, n.`val` + left + right)
            return n.`val` + maxOf(left, right)
        }
        maxGain(root)
        return best
    }

3.10 二叉树的右视图

易记方式每层取最后|触发词:层序 idx==sz-1|警报:先记录层大小再遍历。

思想二叉树右视图:从右侧看每层只露出最右结点。层次遍历(BFS)每一层从左到右出队时,该层最后一个出队的结点值即为右视图在该层的一个值。

步骤

  1. 根入队;当队列非空时,记本层大小 sz
  2. 出队 sz 次:第 sz-1 个结点的值加入结果;子结点入队。
  3. 重复直到队列为空。

时间复杂度:O(n)

空间复杂度:O(n)。

import java.util.ArrayDeque

fun rightSideView(root: TreeNode?): List<Int> {
        val res = mutableListOf<Int>()
        if (root == null) return res
        val q = ArrayDeque<TreeNode>()
        q.add(root)
        while (q.isNotEmpty()) {
            val sz = q.size
            repeat(sz) { idx ->
                val n = q.removeFirst()
                if (idx == sz - 1) res.add(n.`val`)
                n.left?.let { q.addLast(it) }
                n.right?.let { q.addLast(it) }
            }
        }
        return res
    }

3.11 二叉树展开为链表(前序)

易记方式左最右接原右|触发词:pred.right=cur.right|警报:记得清空 cur.left

思想:将树按前序拉成单链表(顺序:根–左–右),原地用指针完成(类似 Morris:有左子树时把左子树最右结点接到原右子树前)。

步骤

  1. 当前结点有左子树:找左子树最右结点 pred
  2. pred.right = 原右子树当前.right = 左子树;清空左子树。
  3. 当前沿 right 走到下一个结点,直到空。

时间复杂度:O(n)

空间复杂度:O(1) 额外空间。

fun flattenBinaryTreeToLinkedList(root: TreeNode?) {
        var cur = root
        while (cur != null) {
            if (cur.left != null) {
                var pred = cur.left
                while (pred!!.right != null) pred = pred.right
                pred.right = cur.right
                cur.right = cur.left
                cur.left = null
            }
            cur = cur.right
        }
    }

4 图论与并查集

4.1 岛屿数量

易记方式见1就淹没|触发词:DFS 染色|警报:会原地修改 grid

思想:遇到陆地 '1' 就从此格 DFS 四向扩展,把连通陆地标成水,避免重复计数;连通块个数即答案。

步骤

  1. 二重循环枚举格
  2. 若为 '1'count++dfs 将连通 '1''0'

时间复杂度:O(mn)

空间复杂度:O(mn) 为递归栈最坏情况。

说明:本实现会原地修改输入 grid(把访问过的 '1' 置为 '0')。若不允许改输入,可改为 visited 数组。

fun numIslands(grid: Array<CharArray>): Int {
        if (grid.isEmpty()) return 0
        var count = 0
        val m = grid.size
        val n = grid[0].size
        fun dfs(i: Int, j: Int) {
            if (i !in 0 until m || j !in 0 until n || grid[i][j] != '1') return
            grid[i][j] = '0'
            dfs(i + 1, j)
            dfs(i - 1, j)
            dfs(i, j + 1)
            dfs(i, j - 1)
        }
        for (i in 0 until m) for (j in 0 until n) {
            if (grid[i][j] == '1') {
                count++
                dfs(i, j)
            }
        }
        return count
    }

4.2 课程表(拓扑排序)

易记方式入度0开修|触发词:拓扑队列|警报:最终检查 taken==numCourses

思想:课程 a 依赖 b 表示边 b→a。若图为 DAG,可拓扑排序修完所有课;用入度:入度为 0 的课可先修,修完后删边,新入度为 0 的继续入队。

步骤

  1. 建邻接表与入度数组
  2. 入度 0 入队
  3. taken 计数
  4. BFS 减少后继入度
  5. taken==课程数 则无环。

时间复杂度:O(V+E)

空间复杂度:O(V+E)。

import java.util.ArrayDeque

fun canFinish(numCourses: Int, prerequisites: Array<IntArray>): Boolean {
        val g = Array(numCourses) { mutableListOf<Int>() }
        val indeg = IntArray(numCourses)
        for (e in prerequisites) {
            g[e[1]].add(e[0])
            indeg[e[0]]++
        }
        val q = ArrayDeque<Int>()
        indeg.forEachIndexed { i, d -> if (d == 0) q.add(i) }
        var taken = 0
        while (q.isNotEmpty()) {
            val u = q.removeFirst()
            taken++
            for (v in g[u]) {
                indeg[v]--
                if (indeg[v] == 0) q.add(v)
            }
        }
        return taken == numCourses
    }

4.3 无权图最短路径(边数,BFS)

易记方式首到即最短|触发词:无权图 BFS|警报:用 dist=-1 防重复。

思想:边权均为 1 时,BFS 第一次到达终点的层数即最短边数。

步骤

  1. dist[start]=0,起点入队
  2. 弹出 u 时遍历邻居,未访问则 dist[v]=dist[u]+1 并入队
  3. 到达 target 返回 dist

时间复杂度:O(V+E)

空间复杂度:O(V)。

import java.util.ArrayDeque

fun shortestPathEdges(adj: List<List<Int>>, start: Int, target: Int): Int {
        val n = adj.size
        val dist = IntArray(n) { -1 }
        val q = ArrayDeque<Int>()
        dist[start] = 0
        q.add(start)
        while (q.isNotEmpty()) {
            val u = q.removeFirst()
            if (u == target) return dist[u]
            for (v in adj[u]) {
                if (dist[v] == -1) {
                    dist[v] = dist[u] + 1
                    q.add(v)
                }
            }
        }
        return -1
    }

4.4 并查集

易记方式认祖宗再并|触发词:路径压缩+按秩|警报:先判根是否相同。

思想:用数组 parent 表示森林,Find 查集合代表元(根),Union 合并两集合。

步骤

  1. find(x):若父不是自己则递归并路径压缩
  2. union:两树根不同则按秩把矮树挂高树下。

时间复杂度find/union 均摊约 O(α(n)),α 为反阿克曼函数,可视为很小常数

空间复杂度:O(n)。

class UnionFind(n: Int) {
    private val parent = IntArray(n) { it }
    private val rank = IntArray(n)

    fun find(x: Int): Int {
        if (parent[x] != x) parent[x] = find(parent[x])
        return parent[x]
    }

    fun union(a: Int, b: Int): Boolean {
        var ra = find(a)
        var rb = find(b)
        if (ra == rb) return false
        if (rank[ra] < rank[rb]) ra = rb.also { rb = ra }
        parent[rb] = ra
        if (rank[ra] == rank[rb]) rank[ra]++
        return true
    }
}

5 双指针与滑动窗口

5.1 盛最多水的容器

易记方式只动短板|触发词:双指针对撞|警报:移动长板通常无收益。

思想:对撞双指针,短板决定盛水高度;向内移动较短一侧,才有可能增大面积。

步骤

  1. l=0r=n-1best=0
  2. l<r:面积 = min(height[l],height[r]) * (r-l),更新 best
  3. height[l] < height[r]l++,否则 r--(移动较短边)。

时间复杂度:O(n)

空间复杂度:O(1)。

fun maxArea(height: IntArray): Int {
        var l = 0
        var r = height.size - 1
        var best = 0
        while (l < r) {
            val h = minOf(height[l], height[r])
            best = maxOf(best, h * (r - l))
            if (height[l] < height[r]) l++ else r--
        }
        return best
    }

5.2 无重复字符的最长子串

易记方式重复就缩左|触发词:窗口频次|警报:数组计数版本默认 ASCII。

思想:滑动窗口内每种字符至多出现一次;右端扩窗,若出现重复则左端收缩直到合法,维护最大窗口长度。

步骤

  1. left=0,用数组或 Map 统计窗口内字符频次。
  2. right 遍历:加入 s[right]while 当前字符重复则左移并减少计数。
  3. right-left+1 更新答案。

时间复杂度:O(n)

空间复杂度:O(字符集大小),本实现用 IntArray(128)(按 ASCII);若要支持完整 Unicode,建议改为 HashMap<Char, Int>

fun lengthOfLongestSubstring(s: String): Int {
        val cnt = IntArray(128)
        var left = 0
        var best = 0
        for (right in s.indices) {
            cnt[s[right].code]++
            while (cnt[s[right].code] > 1) {
                cnt[s[left].code]--
                left++
            }
            best = maxOf(best, right - left + 1)
        }
        return best
    }

5.3 移动零

易记方式快找慢写|触发词:fast/slow|警报:保持相对顺序。

思想:非零元素前移,保持相对顺序;fast 扫描,slow 指向下一个应写入非 0 的位置。

步骤

  1. slow=0
  2. for fast in indices:若 nums[fast]!=0 则与 nums[slow] 交换,slow++

时间复杂度:O(n)

空间复杂度:O(1)。

fun moveZeroes(nums: IntArray) {
        var slow = 0
        for (fast in nums.indices) {
            if (nums[fast] != 0) {
                val t = nums[slow]
                nums[slow] = nums[fast]
                nums[fast] = t
                slow++
            }
        }
    }

5.4 最小覆盖子串

易记方式先满足后压缩|触发词:valid==requiredKinds|警报:缩窗时别忘回退计数。

思想need 表示字符串 t 中各字符需求量,window 表示当前窗口内计数;当窗口覆盖 t 的全部字符种类及数量时,尝试收缩左边界以得到最短子串。

步骤

  1. 统计 t 的字符需求,得到需满足的「种类数」requiredKinds
  2. right 右扩,更新 windowvalid(已满足需求的种类数)。
  3. valid == requiredKinds 时循环:更新最短窗口起点与长度;左移 left 并更新状态。

时间复杂度:O(|s|+|t|)

空间复杂度:O(字符集)。

说明:本实现同样基于 ASCII 计数数组(IntArray(128));若字符集更大可改 HashMap 版本。

fun minWindow(s: String, t: String): String {
        if (t.isEmpty()) return ""
        val need = IntArray(128)
        for (c in t) need[c.code]++
        val requiredKinds = need.count { it > 0 }
        val window = IntArray(128)
        var valid = 0
        var left = 0
        var start = 0
        var len = Int.MAX_VALUE
        for (right in s.indices) {
            val c = s[right].code
            if (need[c] > 0) {
                window[c]++
                if (window[c] == need[c]) valid++
            }
            while (valid == requiredKinds) {
                if (right - left + 1 < len) {
                    len = right - left + 1
                    start = left
                }
                val lc = s[left].code
                if (need[lc] > 0) {
                    if (window[lc] == need[lc]) valid--
                    window[lc]--
                }
                left++
            }
        }
        return if (len == Int.MAX_VALUE) "" else s.substring(start, start + len)
    }

5.5 滑动窗口最大值

易记方式队列三件事|触发词:弹尾加尾踢头|警报:先踢过期再取队首。

思想:长度为 k 的窗口在数组上滑动,求每个窗口的最大值。用**双端队列(Deque)**维护「候选下标」,队列中对应值从队首到队尾单调递减;队首即为当前窗口最大值下标。窗口滑动时,移除队尾不大于新元素的值(它们不可能成为最大值)。

步骤

  1. 遍历下标 i:队尾弹出所有 nums[队尾] <= nums[i]
  2. i 入队;若队首下标已超出窗口左端则出队。
  3. i >= k-1 起,每个窗口取队首对应值为答案。

时间复杂度:O(n),每个下标最多入队、出队一次

空间复杂度:O(k),队列最多存 k 个下标。

import java.util.ArrayDeque

fun maxSlidingWindow(nums: IntArray, k: Int): IntArray {
        if (nums.isEmpty()) return intArrayOf()
        require(k in 1..nums.size) { "k must be in 1..${nums.size}" }
        val dq = ArrayDeque<Int>()
        val n = nums.size
        val ans = IntArray(n - k + 1)
        var ai = 0
        for (i in nums.indices) {
            while (dq.isNotEmpty() && nums[dq.last()] <= nums[i]) dq.removeLast()
            dq.addLast(i)
            while (dq.first() <= i - k) dq.removeFirst()
            if (i >= k - 1) ans[ai++] = nums[dq.first()]
        }
        return ans
    }

5.6 除自身以外数组的乘积

易记方式前缀后缀各一遍|触发词:ans*= 后缀积|警报:禁止除法题设。

思想:结果第 i 位 = 左侧所有数乘积 × 右侧所有数乘积。先从左到右扫一遍得「左侧前缀积」,再从右到左乘上「右侧后缀积」,无需除法。

步骤

  1. ans[i] 先存 prefix[i]nums[0]nums[i-1] 的积)。
  2. 用变量 suffix 从右向左累乘,并 ans[i] *= suffix

时间复杂度:O(n)

空间复杂度:O(1) 除输出数组外。

fun productExceptSelf(nums: IntArray): IntArray {
        val n = nums.size
        if (n == 0) return intArrayOf()
        val ans = IntArray(n)
        ans[0] = 1
        for (i in 1 until n) ans[i] = ans[i - 1] * nums[i - 1]
        var suffix = 1
        for (i in n - 1 downTo 0) {
            ans[i] *= suffix
            suffix *= nums[i]
        }
        return ans
    }

6 矩阵与螺旋遍历

6.1 螺旋矩阵

易记方式四边走收边界|触发词:top/bottom/left/right|警报:每收一边都要判交叉。

思想:从外向内一圈一圈走:顶行从左到右、右列上到下、底行右到左、左列下到上;每走完一边收缩边界 t,b,l,r,直到交叉。

步骤

  1. 初始化上 t=0、下 b=m-1、左 l=0、右 r=n-1
  2. 顶行 j in l..r 收集;t++;若 t>b 结束。
  3. 右列 i in t..b 收集;r--;若 l>r 结束。
  4. 底行 jrlb--;若 t>b 结束。
  5. 左列 ibtl++

时间复杂度:O(mn)

空间复杂度:O(1) 除结果列表外。

fun spiralOrder(matrix: Array<IntArray>): List<Int> {
        val res = mutableListOf<Int>()
        if (matrix.isEmpty() || matrix[0].isEmpty()) return res
        var t = 0
        var b = matrix.lastIndex
        var l = 0
        var r = matrix[0].lastIndex
        while (t <= b && l <= r) {
            for (j in l..r) res.add(matrix[t][j])
            t++
            if (t > b) break
            for (i in t..b) res.add(matrix[i][r])
            r--
            if (l > r) break
            for (j in r downTo l) res.add(matrix[b][j])
            b--
            if (t > b) break
            for (i in b downTo t) res.add(matrix[i][l])
            l++
        }
        return res
    }

7 动态规划

7.1 爬楼梯

易记方式斐波那契同款|触发词:f(i-1)+f(i-2)|警报:n<=2 先返回。

思想:到达第 i 级台阶的方法数 = 从 i-1 跨一步 + 从 i-2 跨两步(斐波那契递推)。

步骤

  1. dp[i]=dp[i-1]+dp[i-2]
  2. 边界 dp[1]=1dp[2]=2
  3. 用两变量滚动。

时间复杂度:O(n)

空间复杂度:O(1)。

fun climbStairs(n: Int): Int {
        if (n <= 2) return n
        var a = 1
        var b = 2
        repeat(n - 2) {
            val c = a + b
            a = b
            b = c
        }
        return b
    }

7.2 打家劫舍

易记方式偷或不偷|触发词:max(prev1, prev2+x)|警报:滚动变量更新顺序。

思想:相邻房屋不能同时偷;偷第 i 家则收益 = nums[i] + 前 i-2 家最优;不偷则继承 i-1 家最优。

步骤

  1. 滚动变量 prev2prev1 表示前两家最优
  2. cur = max(prev1, prev2+nums[i])

时间复杂度:O(n)

空间复杂度:O(1)。

fun rob(nums: IntArray): Int {
        var prev2 = 0
        var prev1 = 0
        for (x in nums) {
            val cur = maxOf(prev1, prev2 + x)
            prev2 = prev1
            prev1 = cur
        }
        return prev1
    }

7.3 最长公共子序列

易记方式等看左上+1|触发词:LCS 二维表|警报:dp 下标是前缀长度。

思想LCS:若两串末尾字符相同,长度 = 两串各去掉末尾后的 LCS +1;否则取「删 A 尾」与「删 B 尾」两种情况的较大值。

步骤

  1. 二维表 dp[i][j] 表示 text1[0..i)text2[0..j) 的 LCS 长度
  2. 填表。

时间复杂度:O(mn)

空间复杂度:O(mn)。

fun longestCommonSubsequence(text1: String, text2: String): Int {
        val m = text1.length
        val n = text2.length
        val dp = Array(m + 1) { IntArray(n + 1) }
        for (i in 1..m) {
            for (j in 1..n) {
                dp[i][j] = if (text1[i - 1] == text2[j - 1]) {
                    dp[i - 1][j - 1] + 1
                } else {
                    maxOf(dp[i - 1][j], dp[i][j - 1])
                }
            }
        }
        return dp[m][n]
    }

7.4 0-1 背包

易记方式01背包倒序j|触发词:容量倒序|警报:正序会重复选同物品。

思想0-1 背包:每件物品最多用一次。dp[j] 表示容量为 j 时能达到的最大价值;内层容量逆序更新,保证每件只用一次。

步骤:对每个物品 ijcapacity 降到 weights[i],尝试 dp[j] = max(dp[j], dp[j-w]+v)

时间复杂度:O(n·capacity)

空间复杂度:O(capacity)。

fun knapsack01(weights: IntArray, values: IntArray, capacity: Int): Int {
        val dp = IntArray(capacity + 1)
        for (i in weights.indices) {
            for (j in capacity downTo weights[i]) {
                dp[j] = maxOf(dp[j], dp[j - weights[i]] + values[i])
            }
        }
        return dp[capacity]
    }

7.5 编辑距离

易记方式删插替三选一|触发词:编辑距离 dp[i][j]|警报:首行首列初始化。

思想:三种操作:删、插、替换;dp[i][j] 表示 word1[0..i) 变成 word2[0..j) 的最少操作数。

步骤

  1. 初始化首行首列
  2. word1[i-1]==word2[j-1]dp[i][j]=dp[i-1][j-1]
  3. 否则 1+min(删,插,替)

时间复杂度:O(mn)

空间复杂度:O(mn)。

fun minDistance(word1: String, word2: String): Int {
        val m = word1.length
        val n = word2.length
        val dp = Array(m + 1) { IntArray(n + 1) }
        for (i in 0..m) dp[i][0] = i
        for (j in 0..n) dp[0][j] = j
        for (i in 1..m) {
            for (j in 1..n) {
                dp[i][j] = if (word1[i - 1] == word2[j - 1]) {
                    dp[i - 1][j - 1]
                } else {
                    1 + minOf(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
                }
            }
        }
        return dp[m][n]
    }

7.6 零钱兑换

易记方式完全背包正序x|触发词:零钱兑换|警报:无解返回 -1

思想完全背包(每种硬币无限枚):凑成金额 amount 的最少硬币数。dp[x] = min(dp[x], dp[x-c]+1)

步骤

  1. dp[0]=0,其余初值为无穷大
  2. 外层遍历硬币,内层 x 从小到大(完全背包顺序)。

时间复杂度:O(amount·硬币种类数)

空间复杂度:O(amount)。

fun coinChange(coins: IntArray, amount: Int): Int {
        val dp = IntArray(amount + 1) { amount + 1 }
        dp[0] = 0
        for (c in coins) {
            for (x in c..amount) {
                dp[x] = minOf(dp[x], dp[x - c] + 1)
            }
        }
        return if (dp[amount] > amount) -1 else dp[amount]
    }

7.7 最长递增子序列

易记方式tails 二分替换|触发词:首个 >=x|警报:替换不是拼接原序列。

思想LIS(Longest Increasing Subsequence,最长递增子序列)——维护数组 tailstails[k] 表示长度为 k+1 的递增子序列的最小末尾值;tails 必递增,对新元素二分插入位置,长度即 tails.size

步骤

  1. 遍历 x:在 tails 中二分第一个 >=x 的位置替换
  2. 若比末尾都大则追加。

时间复杂度:O(n log n)

空间复杂度:O(n)。

fun lengthOfLIS(nums: IntArray): Int {
        val tails = mutableListOf<Int>()
        for (x in nums) {
            var lo = 0
            var hi = tails.size
            while (lo < hi) {
                val mid = (lo + hi) / 2
                if (tails[mid] < x) lo = mid + 1 else hi = mid
            }
            if (lo == tails.size) tails.add(x) else tails[lo] = x
        }
        return tails.size
    }

7.8 乘积最大子数组

易记方式双状态同维护|触发词:maxP/minP|警报:负数会交换角色。

思想:子数组乘积可能因负数变号,同时维护「以当前结尾的最大积」和「最小积」(负数×负数可能变最大)。

步骤

  1. 遍历 nums[i]:用 maxPminP 表示以 i-1 结尾的最大/最小积
  2. 更新 maxP = max(x, maxP*x, minP*x),对称更新 minP

时间复杂度:O(n)

空间复杂度:O(1)。

fun maxProduct(nums: IntArray): Int {
        var maxP = nums[0]
        var minP = nums[0]
        var ans = nums[0]
        for (i in 1 until nums.size) {
            val x = nums[i]
            val t = maxP
            maxP = maxOf(x, maxP * x, minP * x)
            minP = minOf(x, t * x, minP * x)
            ans = maxOf(ans, maxP)
        }
        return ans
    }

8 贪心

8.1 无重叠区间(最少删除区间数)

易记方式结束早优先|触发词:按 end 排序|警报:别把“删除数”与“保留数”混。

思想:结束越早越给后面留空位,按 end 排序后贪心选不重叠区间。

步骤

  1. 排序
  2. lastEnd 初值极小
  3. 若 start>=lastEnd 则选该区间并更新 lastEnd。

时间复杂度:O(n log n)

空间复杂度:O(1) 不计排序额外空间。

fun eraseOverlapIntervals(intervals: Array<Interval>): Int {
        if (intervals.isEmpty()) return 0
        intervals.sortBy { it.end }
        var lastEnd = Int.MIN_VALUE
        var keep = 0
        for (iv in intervals) {
            if (iv.start >= lastEnd) {
                keep++
                lastEnd = iv.end
            }
        }
        return intervals.size - keep
    }

8.2 跳跃游戏

易记方式维护最远 reach|触发词:贪心覆盖范围|警报:i>reach 立即 false。

思想:维护最远能跳到的位置,遍历下标若超出则失败。

步骤

  1. reach=0
  2. i in 0..n-1,若 i>reach 返回 false
  3. reach=max(reach,i+nums[i])。

时间复杂度:O(n)

空间复杂度:O(1)。

fun canJump(nums: IntArray): Boolean {
        var reach = 0
        for (i in nums.indices) {
            if (i > reach) return false
            reach = maxOf(reach, i + nums[i])
            if (reach >= nums.lastIndex) return true
        }
        return true
    }

8.3 跳跃游戏 II

易记方式按层扩边界|触发词:end/farthest|警报:默认题目可达终点。

思想:同 canJump,在可达前提下求最少步数:层序 BFS 思想,每步扩展当前最远边界。

步骤

  1. end 为当前步最远
  2. next 为下一步最远
  3. 到达 end 时步数+1 并换边界。

时间复杂度:O(n)

空间复杂度:O(1)。

说明:该写法默认题目保证「一定能到达最后一个位置」(与 LeetCode 45 设定一致)。

fun jump(nums: IntArray): Int {
        if (nums.size <= 1) return 0
        var steps = 0
        var end = 0
        var farthest = 0
        for (i in 0 until nums.lastIndex) {
            farthest = maxOf(farthest, i + nums[i])
            if (i == end) {
                steps++
                end = farthest
            }
        }
        return steps
    }

8.4 合并区间

易记方式先排后并|触发词:与结果尾比较|警报:sortBy 会原地改输入。

思想:区间按 start 排序后,若当前区间与结果中上一段重叠(当前 start ≤ 上一段 end),则合并为一段;否则新开一段。

步骤

  1. start 升序排序。
  2. 结果列表先放入第一个区间。
  3. 从第二个开始:若与最后一项重叠则合并 end,否则追加。

时间复杂度:O(n log n)

空间复杂度:O(n)。

说明:实现中 intervals.sortBy原地排序,会改变输入数组顺序;若业务不允许改输入,请先拷贝再排序。

fun mergeIntervals(intervals: Array<Interval>): Array<Interval> {
        if (intervals.isEmpty()) return intervals
        intervals.sortBy { it.start }
        val out = mutableListOf(intervals[0])
        for (i in 1 until intervals.size) {
            val cur = intervals[i]
            val last = out.last()
            if (cur.start <= last.end) {
                out[out.lastIndex] = Interval(last.start, maxOf(last.end, cur.end))
            } else {
                out.add(cur)
            }
        }
        return out.toTypedArray()
    }

9 回溯

9.1 子集

易记方式选或不选|触发词:子集 DFS 树|警报:收集路径用拷贝。

思想:每个元素选或不选,生成所有子集。

步骤

  1. dfs(start):若 start==n 收集路径
  2. dfs(start+1) 不选
  3. 选 nums[start] 再 dfs(start+1) 再移除。

时间复杂度:O(n·2^n)

空间复杂度:O(n) 递归栈。

fun subsets(nums: IntArray): List<List<Int>> {
        val ans = mutableListOf<List<Int>>()
        val path = mutableListOf<Int>()
        fun dfs(start: Int) {
            if (start == nums.size) {
                ans.add(path.toList())
                return
            }
            dfs(start + 1)
            path.add(nums[start])
            dfs(start + 1)
            path.removeAt(path.lastIndex)
        }
        dfs(0)
        return ans
    }

9.2 全排列

易记方式路径+used|触发词:全排列回溯|警报:回溯要成对撤销。

思想:全排列,用布尔数组标记已用元素。

步骤

  1. path 长度 n 时入答案
  2. 否则枚举 i,未用则标记、加入、dfs、撤销。

时间复杂度:O(n!·n)

空间复杂度:O(n)。

fun permute(nums: IntArray): List<List<Int>> {
        val ans = mutableListOf<List<Int>>()
        val used = BooleanArray(nums.size)
        val path = mutableListOf<Int>()
        fun dfs() {
            if (path.size == nums.size) {
                ans.add(path.toList())
                return
            }
            for (i in nums.indices) {
                if (used[i]) continue
                used[i] = true
                path.add(nums[i])
                dfs()
                path.removeAt(path.lastIndex)
                used[i] = false
            }
        }
        dfs()
        return ans
    }

9.3 组合

易记方式start防重剪枝|触发词:组合回溯|警报:上界剪枝避免无效分支。

思想:组合无顺序,从 start 开始选,避免 1,2 与 2,1 重复。

步骤

  1. path.size==k 收集
  2. 从 index 到 n-1 尝试加入并 dfs(i+1)。

时间复杂度:O(C(n,k)·k)

空间复杂度:O(k)。

fun combine(n: Int, k: Int): List<List<Int>> {
        val ans = mutableListOf<List<Int>>()
        val path = mutableListOf<Int>()
        fun dfs(start: Int) {
            if (path.size == k) {
                ans.add(path.toList())
                return
            }
            // 剪枝:若剩余可选数字不足以补满 k,后续分支无需继续。
            val need = k - path.size
            val end = n - need + 1
            for (i in start..end) {
                path.add(i)
                dfs(i + 1)
                path.removeAt(path.lastIndex)
            }
        }
        dfs(1)
        return ans
    }

9.4 N 皇后

易记方式行列对角约束|触发词:cols/diag1/diag2|警报:回溯要清理标记。

思想:每行放一个皇后,列与两对角线用集合剪枝。

步骤

  1. row 从 0 到 n-1,尝试每列
  2. 合法则记录并 dfs(row+1)
  3. 回溯清除标记。

时间复杂度:近似 O(n!)

空间复杂度:O(n)。

fun solveNQueens(n: Int): List<List<String>> {
        val ans = mutableListOf<List<String>>()
        val cols = mutableSetOf<Int>()
        val diag1 = mutableSetOf<Int>()
        val diag2 = mutableSetOf<Int>()
        val board = Array(n) { CharArray(n) { '.' } }
        fun dfs(row: Int) {
            if (row == n) {
                ans.add(board.map { it.concatToString("") })
                return
            }
            for (col in 0 until n) {
                if (col in cols || row - col in diag1 || row + col in diag2) continue
                cols.add(col)
                diag1.add(row - col)
                diag2.add(row + col)
                board[row][col] = 'Q'
                dfs(row + 1)
                board[row][col] = '.'
                cols.remove(col)
                diag1.remove(row - col)
                diag2.remove(row + col)
            }
        }
        dfs(0)
        return ans
    }

10 位运算与数学

10.1 最大公约数(辗转相除)

易记方式辗转取余到0|触发词:gcd(a,b)=gcd(b,a%b)|警报:先取绝对值。

思想GCD(Greatest Common Divisor,最大公约数)——辗转相除:gcd(a,b)=gcd(b, a mod b),直到除数为 0。

步骤

  1. while b!=0(a,b)=(b, a%b)
  2. 返回 a

时间复杂度:O(log min(a,b))

空间复杂度:O(1)。

fun gcd(a: Int, b: Int): Int {
        var x = kotlin.math.abs(a)
        var y = kotlin.math.abs(b)
        while (y != 0) {
            val t = x % y
            x = y
            y = t
        }
        return x
    }

10.2 快速幂取模

易记方式指数按位拆|触发词:快速幂|警报:每步都要取模防溢出。

思想快速幂:将指数按二进制展开,exp 最低位为 1 时把当前 base 乘入答案;base 每步平方并对 mod 取余。

步骤

  1. res=1
  2. while exp>0:若 exp 为奇数 res=res*base%mod
  3. base=base*base%mod
  4. exp 右移一位。

时间复杂度:O(log exp)

空间复杂度:O(1)。

fun powMod(base: Long, exp: Long, mod: Long): Long {
        var b = base % mod
        var e = exp
        var res = 1L
        while (e > 0) {
            if (e and 1L == 1L) res = (res * b) % mod
            b = (b * b) % mod
            e = e shr 1
        }
        return res
    }

10.3 位 1 的个数(汉明重量)

易记方式每次消最低1|触发词:n&(n-1)|警报:注意无符号统计语义。

思想n & (n-1) 会消掉 n 的二进制最低位的 1;循环直到 n=0,次数即 1 的个数(汉明重量)。

步骤while n!=0n &= n-1,计数加一。

时间复杂度:O(1) 的位数(32 次以内)

空间复杂度:O(1)。

fun hammingWeight(n: Int): Int {
        var x = n.toLong() and 0xFFFFFFFFL
        var c = 0
        while (x != 0L) {
            x = x and (x - 1)
            c++
        }
        return c
    }

10.4 2 的幂

易记方式正数且单1位|触发词:n&(n-1)==0|警报:先判 n>0

思想:2 的幂在二进制中只有一位为 1,且 n>0;即 n & (n-1) == 0

步骤:判断 n>0 && (n and (n-1))==0

时间复杂度:O(1)

空间复杂度:O(1)。

fun isPowerOfTwo(n: Int): Boolean = n > 0 && n and (n - 1) == 0
}

11 Trie(前缀树)

11.1 Trie 插入单词

易记方式边走边建|触发词:Trie insert|警报:本实现仅支持 a-z

思想:从根沿字符向下走,无则新建子结点;单词末尾标记 isEnd = true

步骤

  1. cur=root
  2. 对每个字符 c:索引 c-'a',移动或创建
  3. 最后 isEnd=true

时间复杂度:O(L),L 为单词长

空间复杂度:O(L) 新增结点。

fun insert(word: String) {
        var cur = root
        for (c in word) {
            val i = idx(c)
            if (cur.next[i] == null) cur.next[i] = Node()
            cur = cur.next[i]!!
        }
        cur.isEnd = true
    }

11.2 Trie 搜索单词

易记方式路通且终点旗|触发词:Trie search|警报:非 a-z 直接 false。

思想:完整匹配单词须走到对应结点且 isEnd 为真。

步骤

  1. 沿字符走子结点
  2. 缺失则 false
  3. 结束看 isEnd

时间复杂度:O(L)

空间复杂度:O(1) 辅助空间。

fun search(word: String): Boolean {
        var cur: Node? = root
        for (c in word) {
            val i = if (c in 'a'..'z') c - 'a' else return false
            cur = cur?.next?.get(i) ?: return false
        }
        return cur.isEnd
    }

11.3 Trie 前缀查询

易记方式只看路通|触发词:Trie prefix|警报:前缀不要求 isEnd

思想:只需是某单词前缀,不要求 isEnd

步骤:沿前缀走,缺失则 false。

时间复杂度:O(L)

空间复杂度:O(1)。

fun startsWith(prefix: String): Boolean {
        var cur: Node? = root
        for (c in prefix) {
            val i = if (c in 'a'..'z') c - 'a' else return false
            cur = cur?.next?.get(i) ?: return false
        }
        return true
    }

12 字符串与哈希

12.1 字母异位词分组

易记方式同字母同签名|触发词:排序串作 key|警报:更快可用 26 计数签名。

思想:异位词排序后字母序相同。用「排序后的字符串」作 HashMap 的键,值为该组所有原串。

步骤

  1. 遍历每个字符串,转为字符数组排序后拼接为键 key
  2. map[key] 追加当前串。
  3. 返回 map 所有 value 组成的列表。

时间复杂度:O(n · k log k),k 为最长串长

空间复杂度:O(nk)。

可选优化:若字符集固定为 26 个小写字母,可用「26 维计数签名」做键,常见写法时间可降为 O(n·k)。

fun groupAnagrams(strs: Array<String>): List<List<String>> {
        val map = mutableMapOf<String, MutableList<String>>()
        for (s in strs) {
            val key = s.toCharArray().sorted().joinToString("")
            map.getOrPut(key) { mutableListOf() }.add(s)
        }
        return map.values.toList()
    }

12.2 最长连续序列

易记方式只从起点扩展|触发词:x-1 不在集合|警报:避免重复扩展。

思想:最长连续序列长度只与「每个数能否作为序列起点」有关:若 x-1 在集合中,则 x 不是起点。对每个起点 x 不断 x+1 直到不在集合。

步骤

  1. 所有数放入 HashSet
  2. 对每个 x,若 x-1 在集合则跳过。
  3. 否则从 x 开始连续向后计数,更新全局最大长度。

时间复杂度:O(n)

空间复杂度:O(n)。

fun longestConsecutive(nums: IntArray): Int {
        val set = nums.toHashSet()
        var best = 0
        for (x in set) {
            if (x - 1 in set) continue
            var y = x
            var len = 0
            while (y in set) {
                len++
                y++
            }
            best = maxOf(best, len)
        }
        return best
    }

13 堆与 Top K

13.1 数组中的第 K 个最大元素

易记方式小顶堆保K大|触发词:堆顶最弱候选|警报:k 需在合法范围内。

思想:求第 K 大元素:用小顶堆维护当前「最大的 K 个」里的最小值(堆顶);堆大小超过 K 就弹出堆顶。

步骤

  1. 遍历 nums:堆未满则加入
  2. 否则若当前值大于堆顶则弹出堆顶再入堆。

时间复杂度:O(n log K)

空间复杂度:O(K)。

说明:当前实现对非法参数做了显式校验:k 必须在 1..nums.size

import java.util.PriorityQueue

fun findKthLargest(nums: IntArray, k: Int): Int {
        require(k in 1..nums.size) { "k must be in 1..${nums.size}" }
        val pq = PriorityQueue<Int>()
        for (x in nums) {
            if (pq.size < k) {
                pq.offer(x)
            } else if (x > pq.peek()) {
                pq.poll()
                pq.offer(x)
            }
        }
        return pq.peek()
    }

13.2 前 K 个高频元素

易记方式频次表+小顶堆|触发词:按频率比较|警报:结果顺序通常不保证。

思想:统计每个数出现次数,按频率建大小为 K 的小顶堆(堆顶为 K 个里频率最低的);超过 K 就弹出。

步骤

  1. groupingBy 得频次表
  2. 遍历 distinct 数值入堆
  3. 最后堆中即为前 K 个高频(次序任意)。

时间复杂度:O(n log K)

空间复杂度:O(U),U 为不同元素个数。

import java.util.PriorityQueue

fun topKFrequent(nums: IntArray, k: Int): IntArray {
        val freq = nums.groupingBy { it }.eachCount()
        val pq = PriorityQueue<Int>(compareBy { freq[it]!! })
        for (num in freq.keys) {
            pq.offer(num)
            if (pq.size > k) pq.poll()
        }
        return IntArray(pq.size) { pq.poll() }
    }

13.3 合并 K 个升序链表

易记方式K路归并|触发词:最小堆弹头补 next|警报:先过滤空链表头。

思想K 路归并:每次从 K 个链表头里取最小值,用小顶堆按结点值排序;弹出最小后将其 next 入堆。

步骤

  1. 非空头指针入堆
  2. while 堆非空:弹最小接在结果尾,若该结点有后继则后继入堆。

时间复杂度:O(N log K),N 为总结点数

空间复杂度:O(K)。

import java.util.PriorityQueue

fun mergeKLists(lists: Array<LinkedListAlgorithms.ListNode?>): LinkedListAlgorithms.ListNode? {
        val dummy = LinkedListAlgorithms.ListNode(0)
        var tail: LinkedListAlgorithms.ListNode = dummy
        val pq = PriorityQueue<LinkedListAlgorithms.ListNode>(compareBy { it.`val` })
        for (h in lists) {
            if (h != null) pq.offer(h)
        }
        while (pq.isNotEmpty()) {
            val n = pq.poll()!!
            tail.next = n
            tail = n
            n.next?.let { pq.offer(it) }
        }
        tail.next = null
        return dummy.next
    }

14 LRU 缓存

LRU(Least Recently Used,最近最少使用) 是一种缓存淘汰策略(eviction policy):容量有上限时,若还要写入新数据且空间已满,必须淘汰一条旧数据;LRU 淘汰的是「距离最近一次被访问(读或写)最久」的那条。

直观理解:为每个键维护「最后使用时间」;时间最旧者优先被踢出。本实现用双向链表表示从「最近使用」到「最久未使用」的顺序:靠近哨兵头结点 head 一侧为最近使用(MRU,Most Recently Used),靠近哨兵尾 tail 一侧为最久未使用tail.prev 即为下次要淘汰的 LRU 结点。

实现上:HashMap<Int, Node> 按键 O(1) 找到链表结点;双向链表支持 O(1) 删除与插入(把结点移到头部表示「刚用过」)。getput 命中或更新后都要把结点移到头部;put 新增导致超出容量时,删除 tail 前驱结点并从 map 中移除对应键。面试中也可答 LinkedHashMap 按访问顺序 + removeEldestEntry 的思路,本质相同。

14.1 LRU get

易记方式读命中就提头|触发词:LRU get|警报:未命中返回 -1

思想:命中则把结点移到头侧(标记为最近使用 MRU);未命中返回 -1。

步骤

  1. map 取结点
  2. removeNodeaddToHead
  3. 返回 val

时间复杂度:O(1)

空间复杂度:O(capacity)。

fun get(key: Int): Int {
        val n = map[key] ?: return -1
        removeNode(n)
        addToHead(n)
        return n.`val`
    }

14.2 LRU put

易记方式写命中提头,超容删尾|触发词:tail.prev|警报:删链后同步删 map

思想:键已存在则更新值并移到头侧;否则新建结点插头。若 map.size 超过 capacity,删除 tail.prev(LRU)map.remove

步骤

  1. keymap 中则更新并调整位置
  2. 否则新建
  3. 超容量删 LRU 结点。

时间复杂度:O(1)

空间复杂度:O(capacity)。

fun put(key: Int, value: Int) {
        map[key]?.let { n ->
            n.`val` = value
            removeNode(n)
            addToHead(n)
            return
        }
        val node = Node(key, value)
        map[key] = node
        addToHead(node)
        if (map.size > capacity) {
            val lru = tail.prev!!
            removeNode(lru)
            map.remove(lru.key)
        }
    }

复习建议(面试前)

  1. 每天抽 8~10 题,只做「30 秒口述 + 2 分钟骨架默写」。
  2. 重点记录错因:边界漏判、指针顺序错误、复杂度说错。
  3. 优先复盘错题,不重复刷已经稳定掌握的题。

说明

  • 本文用于算法学习与面试训练,题解与代码均为整理后的教学表达。
  • 若你要“单文件直接运行”,建议额外补齐 ListNodeTreeNodeLRU 完整类定义与测试入口。