常用的算法技巧

常见的算法技巧分类

1、指针 / 索引技巧

  • 双指针(Two Pointers)

    • 用途:处理数组、链表中的遍历、搜索、排序相关问题,通过两个指针协同工作减少时间复杂度。
    • 常见场景:
      • 有序数组的两数之和(左右指针向中间靠拢);
      • 移除元素(快慢指针,快指针遍历、慢指针记录有效元素);
      • 反转数组 / 字符串(首尾指针交换)。
  • 滑动窗口(Sliding Window)

    • 用途:处理字符串或数组中 “连续子序列” 问题(如最长 / 最短子串、子数组和)。
    • 核心:用左右指针维护一个 “窗口”,根据条件动态调整窗口大小,避免重复计算。
    • 示例:无重复字符的最长子串、最小覆盖子串。
  • 快慢指针(Floyd's Tortoise and Hare)

    • 用途:检测链表环、寻找环的入口、找到链表中点等。
    • 原理:快指针每次走 2 步,慢指针每次走 1 步,利用速度差判断环或定位特定节点。

2、哈希表 / 集合技巧

  • 哈希表(Hash Table)
    • 用途:快速查找、存储键值对关系,将时间复杂度从 O (n) 降至 O (1)(平均情况)。
    • 常见场景:
      • 两数之和(用哈希表存储已遍历元素的索引);
      • 统计元素出现频率(如多数元素问题);
      • 判断元素是否存在(如存在重复元素)。
  • 哈希集合(Hash Set)
    • 用途:去重、快速判断元素是否存在(如环形链表中判断节点是否访问过)。

3、递归与回溯

  • 递归(Recursion)

    • 用途:将复杂问题分解为子问题,适合解决具有递归结构的问题(如树、图的遍历)。
    • 示例:二叉树的前 / 中 / 后序遍历、斐波那契数列(需优化重复计算)。
  • 回溯(Backtracking)

    • 用途:解决 “排列、组合、子集、切割、棋盘” 等需要穷举所有可能的问题,通过 “尝试 - 撤销 - 再尝试” 剪枝无效路径。
    • 示例:全排列、组合总和、N 皇后问题。

4、分治算法(Divide and Conquer)

  • 核心:将问题拆分为多个子问题,分别解决后合并结果。
  • 示例:
    • 归并排序、快速排序(拆分数组后排序合并);
    • 求最大子数组和(分治 + 递归);
    • 二叉树的最近公共祖先(拆分左右子树查找)。

5、贪心算法(Greedy)

  • 核心:每次选择局部最优解,最终得到全局最优解(需证明问题满足 “贪心选择性质”)。
  • 示例:
    • 区间调度问题(选择结束最早的区间);
    • 零钱兑换(优先用最大面额硬币,限于特定币种);
    • 跳跃游戏(每次跳最远的位置)。

6、动态规划(Dynamic Programming, DP)

  • 核心:通过存储子问题的解(DP 表),避免重复计算,解决具有 “重叠子问题” 和 “最优子结构” 的问题。
  • 常见场景:
    • 斐波那契数列(用数组存储中间结果);
    • 最长递增子序列(LIS)、最长公共子序列(LCS);
    • 背包问题(0-1 背包、完全背包);
    • 打家劫舍(状态转移方程:dp[i] = max(dp[i-1], dp[i-2] + nums[i]))。

7、位运算技巧

  • 用途:高效处理整数的二进制表示,解决与 “状态压缩、奇偶性、数值计算” 相关的问题。
  • 常见操作:
    • 判断奇偶(n & 1);
    • 清零最低位的 1(n & (n-1),用于统计 1 的个数);
    • 交换两个数(a = a ^ b; b = a ^ b; a = a ^ b);
    • 子集枚举(用二进制位表示元素是否选中)。

8、其他实用技巧

  • 前缀和与后缀和

    • 用途:快速计算数组中任意子数组的和(前缀和prefix[i] = prefix[i-1] + nums[i])。
    • 示例:和为 K 的子数组、二维矩阵的子矩阵和。
  • 单调栈(Monotonic Stack)

    • 用途:解决 “下一个更大 / 更小元素”“柱状图中最大的矩形” 等问题,栈内元素保持单调递增 / 递减。
  • 并查集(Union-Find/Disjoint Set)

    • 用途:处理 “动态连通性” 问题(如判断图中两点是否连通、合并集合),优化后时间复杂度接近 O (1)。
    • 示例:岛屿数量(合并相邻陆地)、冗余连接。

这些技巧的核心是 “针对特定问题场景,选择合适的工具降低复杂度”。实际应用中,很多问题需要多种技巧结合(如滑动窗口 + 哈希表、动态规划 + 贪心),需通过练习熟悉其适用场景。

常见的算法技巧示例

1. 双指针(Two Pointers)

核心思想:使用两个指针(通常在数组或链表中)从不同位置(如头部、尾部、相邻位置)出发,根据条件移动指针,高效解决问题。

适用场景:有序数组去重、两数之和、反转数组等。

示例:有序数组去重(原地删除重复元素)
public int removeDuplicates(int[] nums) {
    if (nums.length == 0) return 0;
    int slow = 0; // 慢指针:指向不重复元素的最后一个位置
    for (int fast = 1; fast < nums.length; fast++) {
        if (nums[fast] != nums[slow]) {
            slow++; // 只有当元素不同时,慢指针才移动
            nums[slow] = nums[fast]; // 覆盖重复元素
        }
    }
    return slow + 1; // 有效长度
}

2. 哈希表(Hash Table)

核心思想:利用哈希函数将键映射到存储位置,实现 O (1) 平均时间复杂度的插入、查询和删除,常用于快速查找、去重或计数。

适用场景:两数之和、判断重复元素、字符频次统计等。

示例:两数之和(返回数组中两数之和为目标值的索引)
public int[] twoSum(int[] nums, int target) {
    Map<Integer, Integer> map = new HashMap<>();
    for (int i = 0; i < nums.length; i++) {
        int complement = target - nums[i];
        if (map.containsKey(complement)) {
            return new int[]{map.get(complement), i}; // 找到互补元素
        }
        map.put(nums[i], i); // 存储元素与索引
    }
    throw new IllegalArgumentException("No solution");
}

3. 二分查找(Binary Search)

核心思想:在有序数组中,通过不断将目标值与中间元素比较,缩小查找范围(每次排除一半元素)。

适用场景:查找目标值、寻找插入位置、求峰值等。

示例:二分查找目标值
public int search(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2; // 避免溢出
        if (nums[mid] == target) {
            return mid;
        } else if (nums[mid] < target) {
            left = mid + 1; // 目标在右半部分
        } else {
            right = mid - 1; // 目标在左半部分
        }
    }
    return -1; // 未找到
}

4. 前缀和(Prefix Sum)

核心思想:预处理数组,计算前i个元素的和(前缀和数组),快速求解任意子数组的和(时间复杂度从O(n)降为O(1))。

适用场景:子数组和问题、区间和查询等。

示例:求子数组和等于 k 的次数
public int subarraySum(int[] nums, int k) {
    int count = 0;
    int prefixSum = 0;
    Map<Integer, Integer> map = new HashMap<>();
    map.put(0, 1); // 前缀和为0的情况(子数组从0开始)
    for (int num : nums) {
        prefixSum += num;
        // 若存在前缀和 = prefixSum - k,则说明中间子数组和为k
        count += map.getOrDefault(prefixSum - k, 0);
        map.put(prefixSum, map.getOrDefault(prefixSum, 0) + 1);
    }
    return count;
}

5. 滑动窗口(Sliding Window)

核心思想:维护一个 “窗口”(子数组或子字符串),通过移动窗口的左右边界,动态调整窗口范围,解决连续子序列问题。

适用场景:最长无重复子串、最小覆盖子串、子数组和等。

示例:长度最小的子数组(找到和≥target 的最短连续子数组)
public int minSubArrayLen(int target, int[] nums) {
    int left = 0; // 窗口左边界
    int sum = 0;
    int minLen = Integer.MAX_VALUE;
    for (int right = 0; right < nums.length; right++) {
        sum += nums[right]; // 右边界右移,扩大窗口
        // 当窗口和≥target时,尝试缩小左边界
        while (sum >= target) {
            minLen = Math.min(minLen, right - left + 1);
            sum -= nums[left];
            left++;
        }
    }
    return minLen == Integer.MAX_VALUE ? 0 : minLen;
}

6. 树的遍历技巧(DFS/BFS)

树的遍历是处理树结构的基础,常见的有深度优先搜索(DFS)和广度优先搜索(BFS),衍生出前序、中序、后序遍历(DFS)和层序遍历(BFS)。

示例 1:二叉树的层序遍历(BFS)
public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> result = new ArrayList<>();
    if (root == null) return result;
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(root);
    while (!queue.isEmpty()) {
        int levelSize = queue.size(); // 当前层的节点数
        List<Integer> level = new ArrayList<>();
        for (int i = 0; i < levelSize; i++) {
            TreeNode node = queue.poll();
            level.add(node.val);
            if (node.left != null) queue.add(node.left);
            if (node.right != null) queue.add(node.right);
        }
        result.add(level); // 加入当前层的结果
    }
    return result;
}
示例 2:二叉树的后序遍历(DFS,递归)
public List<Integer> postorderTraversal(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    dfs(root, result);
    return result;
}

private void dfs(TreeNode node, List<Integer> result) {
    if (node == null) return;
    dfs(node.left, result);   // 左
    dfs(node.right, result);  // 右
    result.add(node.val);     // 根
}

7. 位运算(Bit Manipulation)

核心思想:直接操作二进制位,利用位运算的特性(如与、或、异或、左移、右移)解决问题,效率极高(时间 / 空间复杂度低)。

适用场景:二进制计算、去重(如数组中只出现一次的数字)、状态压缩等。

示例:数组中只出现一次的数字(其他数字均出现两次)
public int singleNumber(int[] nums) {
    int result = 0;
    for (int num : nums) {
        result ^= num; // 异或:a^a=0,0^a=a,且满足交换律
    }
    return result; // 最终结果为只出现一次的数字
}

8. 贪心算法(Greedy Algorithm)

核心思想:每次选择当前最优解(局部最优),最终期望得到全局最优解(需证明问题具有 “贪心选择性质”)。

适用场景:区间调度、霍夫曼编码、零钱兑换(特定条件下)等。

示例:无重叠区间(选择最多不重叠的区间)
public int eraseOverlapIntervals(int[][] intervals) {
    if (intervals.length == 0) return 0;
    // 按区间结束时间排序
    Arrays.sort(intervals, (a, b) -> a[1] - b[1]);
    int count = 1; // 至少选择一个区间
    int end = intervals[0][1];
    for (int[] interval : intervals) {
        int start = interval[0];
        if (start >= end) { // 不重叠,选择当前区间
            count++;
            end = interval[1];
        }
    }
    return intervals.length - count; // 需删除的区间数
}

9. 递归与记忆化(Recursion + Memoization)

核心思想:通过递归将问题分解为子问题,同时用缓存(如哈希表)存储已解决的子问题结果,避免重复计算(本质是动态规划的 “自顶向下” 实现)。适用场景:斐波那契数列、爬楼梯、零钱兑换等具有重叠子问题的场景。

示例:斐波那契数列(记忆化优化)
public int fib(int n) {
    Map<Integer, Integer> memo = new HashMap<>();
    return helper(n, memo);
}

private int helper(int n, Map<Integer, Integer> memo) {
    if (n <= 1) return n;
    if (memo.containsKey(n)) return memo.get(n); // 直接返回缓存结果
    int res = helper(n - 1, memo) + helper(n - 2, memo);
    memo.put(n, res); // 缓存子问题结果
    return res;
}

10. 堆(Heap)/ 优先队列(Priority Queue)

核心思想:利用堆的特性(如大顶堆 / 小顶堆)快速获取极值(最大值 / 最小值),常用于动态维护 Top K 元素或处理带权重的调度问题。

适用场景:前 K 个高频元素、数据流中的中位数、合并 K 个有序链表等。

示例:前 K 个高频元素
public int[] topKFrequent(int[] nums, int k) {
    // 1. 统计元素频次
    Map<Integer, Integer> freqMap = new HashMap<>();
    for (int num : nums) {
        freqMap.put(num, freqMap.getOrDefault(num, 0) + 1);
    }
    // 2. 小顶堆:只保留前k个高频元素(堆顶为当前最小的高频元素)
    PriorityQueue<Map.Entry<Integer, Integer>> heap = new PriorityQueue<>(
        (a, b) -> a.getValue() - b.getValue()
    );
    for (Map.Entry<Integer, Integer> entry : freqMap.entrySet()) {
        heap.add(entry);
        if (heap.size() > k) {
            heap.poll(); // 超过k个则移除最小的
        }
    }
    // 3. 提取结果
    int[] result = new int[k];
    for (int i = k - 1; i >= 0; i--) {
        result[i] = heap.poll().getKey();
    }
    return result;
}

11. 动态规划(Dynamic Programming, DP)

核心思想:将复杂问题分解为重叠子问题,通过存储子问题的解(DP 表)避免重复计算,从底向上推导最终结果。

适用场景:斐波那契数列、最长递增子序列、背包问题、编辑距离等。

示例:爬楼梯(每次可爬 1 或 2 阶,求到第 n 阶的方法数)
public int climbStairs(int n) {
    if (n <= 2) return n;
    int[] dp = new int[n + 1];
    dp[1] = 1;
    dp[2] = 2;
    for (int i = 3; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2]; // 递推公式:到i阶 = 到i-1阶+1步 或 到i-2阶+2步
    }
    return dp[n];
}

12. 单调栈(Monotonic Stack)

核心思想:维护一个栈内元素单调递增或递减的栈,用于高效解决 “下一个更大 / 更小元素” 等问题,避免暴力遍历。

适用场景:每日温度、下一个更大元素、柱状图中最大的矩形等。

示例:下一个更大元素 I(nums1 是 nums2 的子集,求 nums1 中每个元素在 nums2 中的下一个更大元素)
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
    Map<Integer, Integer> nextGreater = new HashMap<>();
    Stack<Integer> stack = new Stack<>(); // 单调递减栈
    for (int num : nums2) {
        // 当栈顶元素小于当前元素时,当前元素是栈顶元素的下一个更大元素
        while (!stack.isEmpty() && stack.peek() < num) {
            nextGreater.put(stack.pop(), num);
        }
        stack.push(num);
    }
    // 处理nums1
    int[] result = new int[nums1.length];
    for (int i = 0; i < nums1.length; i++) {
        result[i] = nextGreater.getOrDefault(nums1[i], -1);
    }
    return result;
}

13. 回溯法(Backtracking)

核心思想:通过递归尝试所有可能的解,当发现当前路径无效时,回溯到上一步继续尝试其他路径(“试错 + 回退”)。

适用场景:排列组合、子集、全排列、数独求解等。

示例:生成所有子集
public List<List<Integer>> subsets(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    backtrack(result, new ArrayList<>(), nums, 0);
    return result;
}

private void backtrack(List<List<Integer>> result, List<Integer> temp, int[] nums, int start) {
    result.add(new ArrayList<>(temp)); // 加入当前子集
    for (int i = start; i < nums.length; i++) {
        temp.add(nums[i]); // 选择当前元素
        backtrack(result, temp, nums, i + 1); // 递归(下一个元素从i+1开始,避免重复)
        temp.remove(temp.size() - 1); // 回溯(移除最后一个元素)
    }
}

14. 分治法(Divide and Conquer)

核心思想:将问题分解为规模更小的子问题,递归解决子问题后合并结果,适合处理具有 “分治特性” 的问题(如问题可拆分、子问题独立、合并简单)。

适用场景:归并排序、快速排序、求最大子数组和、二叉树问题等。

示例:求最大子数组和(LeetCode 53)
public int maxSubArray(int[] nums) {
    return divide(nums, 0, nums.length - 1);
}

private int divide(int[] nums, int left, int right) {
    if (left == right) return nums[left]; // 基线条件:单个元素
    int mid = left + (right - left) / 2;
    // 分:左半部分最大子数组和、右半部分最大子数组和
    int leftMax = divide(nums, left, mid);
    int rightMax = divide(nums, mid + 1, right);
    // 合:跨越中点的最大子数组和
    int crossMax = cross(nums, left, mid, right);
    return Math.max(Math.max(leftMax, rightMax), crossMax);
}

private int cross(int[] nums, int left, int mid, int right) {
    int leftSum = Integer.MIN_VALUE;
    int sum = 0;
    for (int i = mid; i >= left; i--) { // 从中间向左扩展
        sum += nums[i];
        leftSum = Math.max(leftSum, sum);
    }
    int rightSum = Integer.MIN_VALUE;
    sum = 0;
    for (int i = mid + 1; i <= right; i++) { // 从中间向右扩展
        sum += nums[i];
        rightSum = Math.max(rightSum, sum);
    }
    return leftSum + rightSum;
}

15. 并查集(Union-Find / Disjoint Set)

核心思想:高效管理和合并 “集合”,支持快速查询两个元素是否在同一集合、合并两个集合(路径压缩和按秩合并优化后,时间复杂度接近O(1))。适用场景:连通分量问题(如岛屿数量)、朋友圈、判断图中是否有环等。

示例:岛屿数量(统计连通的陆地数量)
public int numIslands(char[][] grid) {
    if (grid == null || grid.length == 0) return 0;
    int rows = grid.length;
    int cols = grid[0].length;
    UnionFind uf = new UnionFind(grid);
    // 方向数组:右、下
    int[][] dirs = {{0, 1}, {1, 0}};
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            if (grid[i][j] == '1') {
                for (int[] dir : dirs) {
                    int x = i + dir[0];
                    int y = j + dir[1];
                    if (x < rows && y < cols && grid[x][y] == '1') {
                        uf.union(i * cols + j, x * cols + y); // 合并相邻陆地
                    }
                }
            }
        }
    }
    return uf.count;
}

// 并查集实现
class UnionFind {
    int[] parent;
    int count; // 连通分量数量

    public UnionFind(char[][] grid) {
        int rows = grid.length;
        int cols = grid[0].length;
        parent = new int[rows * cols];
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (grid[i][j] == '1') {
                    parent[i * cols + j] = i * cols + j; // 自身为父节点
                    count++; // 初始每个陆地是独立分量
                }
            }
        }
    }

    // 查找根节点(路径压缩)
    public int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }

    // 合并两个集合(按秩合并,可选)
    public void union(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);
        if (rootX != rootY) {
            parent[rootY] = rootX;
            count--; // 合并后分量数减1
        }
    }
}

16. 拓扑排序(Topological Sort)

核心思想:针对有向无环图(DAG),按照依赖关系(如 “先修课” 必须在 “后修课” 之前)将节点排序,确保所有前驱节点都排在后继节点之前。

适用场景:课程安排、任务调度、依赖关系处理等。

示例:课程表(判断是否能完成所有课程,即图中无环)
public boolean canFinish(int numCourses, int[][] prerequisites) {
    // 1. 构建邻接表和入度数组
    List<List<Integer>> adj = new ArrayList<>();
    int[] inDegree = new int[numCourses];
    for (int i = 0; i < numCourses; i++) {
        adj.add(new ArrayList<>());
    }
    for (int[] p : prerequisites) {
        int course = p[0];
        int pre = p[1];
        adj.get(pre).add(course); // 前驱 -> 后继
        inDegree[course]++; // 后继入度+1
    }
    // 2. 入度为0的节点入队(无依赖的课程)
    Queue<Integer> queue = new LinkedList<>();
    for (int i = 0; i < numCourses; i++) {
        if (inDegree[i] == 0) {
            queue.add(i);
        }
    }
    // 3. 拓扑排序
    int count = 0;
    while (!queue.isEmpty()) {
        int curr = queue.poll();
        count++; // 完成一门课程
        for (int next : adj.get(curr)) {
            inDegree[next]--; // 依赖减少
            if (inDegree[next] == 0) { // 无依赖时入队
                queue.add(next);
            }
        }
    }
    return count == numCourses; // 所有课程都能完成则无环
}

17. 前缀树(Trie)

核心思想:一种多叉树结构,用于存储字符串集合,支持高效的前缀查找、插入和删除操作(时间复杂度与字符串长度相关)。

适用场景:自动补全、拼写检查、前缀匹配等。

示例:实现前缀树
class TrieNode {
    boolean isEnd; // 是否为单词结尾
    TrieNode[] children; // 26个小写字母

    public TrieNode() {
        isEnd = false;
        children = new TrieNode[26];
    }
}

class Trie {
    private TrieNode root;

    public Trie() {
        root = new TrieNode();
    }

    // 插入单词
    public void insert(String word) {
        TrieNode node = root;
        for (char c : word.toCharArray()) {
            int index = c - 'a';
            if (node.children[index] == null) {
                node.children[index] = new TrieNode();
            }
            node = node.children[index];
        }
        node.isEnd = true;
    }

    // 查找单词是否存在
    public boolean search(String word) {
        TrieNode node = root;
        for (char c : word.toCharArray()) {
            int index = c - 'a';
            if (node.children[index] == null) {
                return false;
            }
            node = node.children[index];
        }
        return node.isEnd;
    }

    // 查找是否有以prefix为前缀的单词
    public boolean startsWith(String prefix) {
        TrieNode node = root;
        for (char c : prefix.toCharArray()) {
            int index = c - 'a';
            if (node.children[index] == null) {
                return false;
            }
            node = node.children[index];
        }
        return true;
    }
}

18. 滑动窗口 + 单调队列(处理滑动窗口极值)

核心思想:结合滑动窗口和单调队列(维护窗口内元素的单调性),高效求解滑动窗口中的最大值 / 最小值(时间复杂度O(n))。

适用场景:滑动窗口最大值、滑动窗口最小值。

示例:滑动窗口最大值
public int[] maxSlidingWindow(int[] nums, int k) {
    if (nums == null || nums.length == 0) return new int[0];
    int[] result = new int[nums.length - k + 1];
    Deque<Integer> deque = new LinkedList<>(); // 单调递减队列(存储索引)
    for (int i = 0; i < nums.length; i++) {
        // 移除窗口外的元素(索引 <= i - k)
        while (!deque.isEmpty() && deque.peekFirst() <= i - k) {
            deque.pollFirst();
        }
        // 移除队列中比当前元素小的元素(它们不可能是最大值)
        while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
            deque.pollLast();
        }
        deque.addLast(i);
        // 窗口形成后,队首即为当前窗口最大值
        if (i >= k - 1) {
            result[i - k + 1] = nums[deque.peekFirst()];
        }
    }
    return result;
}

19. Morris 遍历(树的常数空间遍历)

核心思想:通过修改树的指针(临时创建前驱节点的右指针指向当前节点),实现 O (1) 空间复杂度的树遍历(无需栈或递归栈)。适用场景:在空间受限的情况下遍历树。

示例:二叉树的中序 Morris 遍历
public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    TreeNode curr = root;
    TreeNode prev = null;
    while (curr != null) {
        if (curr.left == null) {
            // 左子树为空,直接访问当前节点
            result.add(curr.val);
            curr = curr.right;
        } else {
            // 找左子树的最右节点(前驱节点)
            prev = curr.left;
            while (prev.right != null && prev.right != curr) {
                prev = prev.right;
            }
            if (prev.right == null) {
                // 首次访问,建立前驱节点到当前节点的指针
                prev.right = curr;
                curr = curr.left;
            } else {
                // 已访问过,恢复指针并访问当前节点
                prev.right = null;
                result.add(curr.val);
                curr = curr.right;
            }
        }
    }
    return result;
}

20. 字符串匹配(KMP 算法)

核心思想:通过预处理模式串得到 “部分匹配表(next 数组)”,在字符串匹配时跳过不必要的比较,将时间复杂度从O(n*m)优化到O(n+m)n为主串长度,m为模式串长度)。

适用场景:字符串查找(如在主串中找模式串的位置)。

示例:实现 strStr ()(在主串中找模式串首次出现的位置)
public int strStr(String haystack, String needle) {
    if (needle.isEmpty()) return 0;
    int n = haystack.length();
    int m = needle.length();
    if (m > n) return -1;

    // 构建next数组(部分匹配表)
    int[] next = new int[m];
    for (int i = 1, j = 0; i < m; i++) {
        while (j > 0 && needle.charAt(i) != needle.charAt(j)) {
            j = next[j - 1]; // 回退j
        }
        if (needle.charAt(i) == needle.charAt(j)) {
            j++;
        }
        next[i] = j;
    }

    // KMP匹配
    for (int i = 0, j = 0; i < n; i++) {
        while (j > 0 && haystack.charAt(i) != needle.charAt(j)) {
            j = next[j - 1]; // 利用next数组回退j
        }
        if (haystack.charAt(i) == needle.charAt(j)) {
            j++;
        }
        if (j == m) { // 匹配成功
            return i - m + 1;
        }
    }
    return -1;
}

难易程度

  • 入门级:双指针、哈希表、二分查找;
  • 进阶级:前缀和、滑动窗口、树的遍历、位运算、贪心、堆;
  • 高手级:动态规划、单调栈、回溯法、分治法、并查集、拓扑排序;
  • 专家级:前缀树、滑动窗口 + 单调队列、Morris 遍历、KMP 算法。

常用频率

  1. 双指针(Two Pointers)
  2. 哈希表(Hash Table)
  3. 滑动窗口(Sliding Window)
  4. 二分查找(Binary Search)
  5. 动态规划(DP)
  6. 树的遍历(DFS/BFS)
  7. 递归与记忆化
  8. 贪心算法
  9. 堆(Priority Queue)
  10. 前缀和(Prefix Sum)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值