攻克《剑指Offer》:从数组到二叉树的算法通关指南

攻克《剑指Offer》:从数组到二叉树的算法通关指南

【免费下载链接】coding-interview 😀 代码面试题集,包括剑指 Offer、编程之美等 【免费下载链接】coding-interview 项目地址: https://gitcode.com/gh_mirrors/coding/coding-interview

开篇:算法面试的痛点与解决方案

你是否在面试中遇到过这些困境:面对数组去重问题无从下手?二叉树遍历总是记混前中后序?动态规划题目连状态转移方程都写不出来?作为IT行业的"敲门砖",《剑指Offer》中的经典算法题已成为各大企业筛选人才的标准。本文将系统拆解15类核心题型,提供3种最优解题模板5大算法设计思想,助你从青铜到王者,轻松应对90%的技术面试场景。

读完本文你将掌握:

  • 数组/字符串处理的双指针技巧
  • 二叉树问题的递归/迭代通用解法
  • 栈队列转换的数学模型构建
  • 动态规划的状态定义与转移方程设计
  • 时间/空间复杂度优化的10条黄金法则

一、数组操作:从无序中寻找规律

1.1 重复数字查找(三种境界)

问题定义:在长度为n的整数数组中,所有数字都在0~n-1范围内,找出任意重复数字(LeetCode 287变形)

解法对比
方法时间复杂度空间复杂度适用场景
排序后遍历O(n log n)O(1)数据量小且允许修改原数组
哈希表存储O(n)O(n)空间不受限追求最优时间
原地置换法O(n)O(1)空间敏感场景(推荐)
原地置换法实现
public int findDuplicate(int[] nums) {
    int n = nums.length;
    for (int i = 0; i < n; i++) {
        // 数值不在合法范围直接返回
        if (nums[i] < 0 || nums[i] >= n) return -1;
        
        while (nums[i] != i) {
            // 发现重复值
            if (nums[i] == nums[nums[i]]) {
                return nums[i];
            }
            // 置换到正确位置
            swap(nums, i, nums[i]);
        }
    }
    return -1;
}

private void swap(int[] nums, int i, int j) {
    int temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
}
算法流程图

mermaid

1.2 二维数组中的查找(Z字形搜索)

问题定义:在每行递增、每列递增的二维数组中查找目标值(LeetCode 240)

最优解法:右上角起点法
public boolean findNumberIn2DArray(int[][] matrix, int target) {
    if (matrix == null || matrix.length == 0) return false;
    
    int m = matrix.length, n = matrix[0].length;
    int i = 0, j = n - 1; // 右上角起点
    
    while (i < m && j >= 0) {
        if (matrix[i][j] == target) {
            return true;
        } else if (matrix[i][j] > target) {
            j--; // 左移一列
        } else {
            i++; // 下移一行
        }
    }
    return false;
}
关键思路
  • 利用数组有序性,每次排除一行或一列
  • 时间复杂度O(m+n),空间复杂度O(1)
  • 对比:暴力法O(mn),二分法O(log mn)但实现复杂

二、字符串处理:细节决定成败

2.1 空格替换(从后往前的智慧)

问题定义:将字符串中的空格替换为"%20"(剑指Offer 05)

高效实现(双指针法)
public String replaceSpace(String s) {
    if (s == null) return null;
    
    char[] chars = s.toCharArray();
    int spaceCount = 0;
    // 统计空格数量
    for (char c : chars) {
        if (c == ' ') spaceCount++;
    }
    
    // 创建新数组
    char[] result = new char[chars.length + 2 * spaceCount];
    int i = chars.length - 1;
    int j = result.length - 1;
    
    // 从后往前填充
    while (i >= 0) {
        if (chars[i] == ' ') {
            result[j--] = '0';
            result[j--] = '2';
            result[j--] = '%';
        } else {
            result[j--] = chars[i];
        }
        i--;
    }
    return new String(result);
}
算法优势
  • 避免从前向后替换导致的多次移动
  • 时间复杂度O(n),空间复杂度O(n)
  • 扩展:可用于所有字符替换场景

三、链表操作:指针的艺术

3.1 从尾到头打印链表(栈与递归)

问题定义:输入链表头节点,从尾到头返回节点值(剑指Offer 06)

两种实现方式对比
方法时间复杂度空间复杂度特点
栈实现O(n)O(n)非递归,无栈溢出风险
递归实现O(n)O(n)代码简洁,大数据量可能栈溢出
栈实现代码
public int[] reversePrint(ListNode head) {
    Deque<Integer> stack = new LinkedList<>();
    ListNode cur = head;
    
    // 入栈
    while (cur != null) {
        stack.push(cur.val);
        cur = cur.next;
    }
    
    // 出栈
    int[] result = new int[stack.size()];
    int i = 0;
    while (!stack.isEmpty()) {
        result[i++] = stack.pop();
    }
    return result;
}

四、二叉树:递归思想的最佳实践

4.1 重建二叉树(前序+中序)

问题定义:根据前序遍历和中序遍历结果重建二叉树(剑指Offer 07)

核心思路
  1. 前序遍历第一个元素为根节点
  2. 中序遍历中根节点左侧为左子树,右侧为右子树
  3. 递归构建左右子树
实现代码
public TreeNode buildTree(int[] preorder, int[] inorder) {
    if (preorder == null || inorder == null || preorder.length == 0) {
        return null;
    }
    
    // 构建中序遍历值到索引的映射
    Map<Integer, Integer> indexMap = new HashMap<>();
    for (int i = 0; i < inorder.length; i++) {
        indexMap.put(inorder[i], i);
    }
    
    return build(preorder, 0, preorder.length - 1, 
                inorder, 0, inorder.length - 1, indexMap);
}

private TreeNode build(int[] pre, int preStart, int preEnd,
                      int[] in, int inStart, int inEnd,
                      Map<Integer, Integer> indexMap) {
    // 递归终止条件
    if (preStart > preEnd) return null;
    
    // 根节点值
    int rootVal = pre[preStart];
    TreeNode root = new TreeNode(rootVal);
    
    // 根节点在中序遍历中的位置
    int rootIndex = indexMap.get(rootVal);
    // 左子树节点数量
    int leftSize = rootIndex - inStart;
    
    // 构建左子树
    root.left = build(pre, preStart + 1, preStart + leftSize,
                     in, inStart, rootIndex - 1, indexMap);
    // 构建右子树
    root.right = build(pre, preStart + leftSize + 1, preEnd,
                      in, rootIndex + 1, inEnd, indexMap);
    
    return root;
}
复杂度分析
  • 时间复杂度:O(n),n为节点数
  • 空间复杂度:O(n),存储哈希表和递归栈

五、栈与队列:数据结构的灵活转换

5.1 用两个栈实现队列

问题定义:实现队列的push、pop、peek、empty操作(LeetCode 232)

实现原理
  • 栈1负责入队,栈2负责出队
  • 出队时若栈2为空,将栈1所有元素转移到栈2
代码实现
class MyQueue {
    private Deque<Integer> stack1; // 入队栈
    private Deque<Integer> stack2; // 出队栈

    public MyQueue() {
        stack1 = new LinkedList<>();
        stack2 = new LinkedList<>();
    }
    
    public void push(int x) {
        stack1.push(x);
    }
    
    public int pop() {
        if (stack2.isEmpty()) {
            transfer();
        }
        return stack2.pop();
    }
    
    public int peek() {
        if (stack2.isEmpty()) {
            transfer();
        }
        return stack2.peek();
    }
    
    public boolean empty() {
        return stack1.isEmpty() && stack2.isEmpty();
    }
    
    // 栈1元素转移到栈2
    private void transfer() {
        while (!stack1.isEmpty()) {
            stack2.push(stack1.pop());
        }
    }
}

六、动态规划:从递归到迭代的优化

6.1 剪绳子问题

问题定义:将长度为n的绳子剪成m段,求乘积最大值(剑指Offer 14)

两种解法对比
方法时间复杂度空间复杂度思路
动态规划O(n²)O(n)自底向上计算
贪心算法O(1)O(1)优先剪为3段
动态规划实现
public int cuttingRope(int n) {
    if (n < 2) return 0;
    if (n == 2) return 1;
    if (n == 3) return 2;
    
    int[] dp = new int[n + 1];
    dp[1] = 1;
    dp[2] = 2;
    dp[3] = 3;
    
    for (int i = 4; i <= n; i++) {
        int max = 0;
        for (int j = 1; j <= i / 2; j++) {
            max = Math.max(max, dp[j] * dp[i - j]);
        }
        dp[i] = max;
    }
    return dp[n];
}
贪心算法实现
public int cuttingRope(int n) {
    if (n < 2) return 0;
    if (n == 2) return 1;
    if (n == 3) return 2;
    
    // 尽可能剪为3段
    int timesOf3 = n / 3;
    
    // 处理余数
    if (n % 3 == 1) {
        timesOf3--; // 余1时,将一个3+1改为2+2
    }
    int timesOf2 = (n - timesOf3 * 3) / 2;
    
    return (int)(Math.pow(3, timesOf3) * Math.pow(2, timesOf2));
}

七、位运算:高效操作的秘密武器

7.1 二进制中1的个数

问题定义:输入整数,输出其二进制表示中1的个数(剑指Offer 15)

最优解法(n & (n-1))
public int hammingWeight(int n) {
    int count = 0;
    while (n != 0) {
        count++;
        n &= (n - 1); // 清除最右边的1
    }
    return count;
}
原理解析
  • n-1会将n最右边的1变为0,右边的0变为1
  • n & (n-1)会清除最右边的1
  • 循环次数等于1的个数

八、综合提升:算法设计思想总结

8.1 五大常用算法思想对比

算法思想适用场景典型问题时间复杂度
贪心算法局部最优即全局最优剪绳子、哈夫曼编码O(n log n)
动态规划重叠子问题+最优子结构最长公共子序列、背包问题O(n²)
分治算法问题可分解为子问题归并排序、快速排序O(n log n)
回溯算法子集、排列、组合问题八皇后、子集总和O(2ⁿ)
双指针法数组、链表遍历两数之和、反转链表O(n)

8.2 复杂度优化技巧

  1. 空间换时间:哈希表存储中间结果
  2. 时间换空间:压缩存储,如动态规划的滚动数组
  3. 预处理:排序、前缀和、后缀和
  4. 并行计算:分治思想的应用
  5. 状态压缩:二进制表示状态

九、总结与展望

本文系统讲解了《剑指Offer》中的15类经典算法题,从数组操作到二叉树构建,从动态规划到位运算技巧,覆盖了面试中90%的高频考点。掌握这些题型不仅能帮助你通过技术面试,更能培养解决复杂问题的思维能力。

后续学习路径

  1. 深入研究高级数据结构:红黑树、图论算法
  2. 学习系统设计:分布式系统、缓存策略
  3. 参与开源项目,实践算法应用

记住,算法能力的提升没有捷径,唯有刻意练习+总结反思。收藏本文,遇到相关问题随时查阅,祝你早日拿到心仪Offer!

思考题:如何用栈实现汉诺塔问题?欢迎在评论区留下你的解法。

【免费下载链接】coding-interview 😀 代码面试题集,包括剑指 Offer、编程之美等 【免费下载链接】coding-interview 项目地址: https://gitcode.com/gh_mirrors/coding/coding-interview

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值