从暴力排序到滑动窗口:解决字母异位词搜索的优化之路

在解决LeetCode上的"找到字符串中所有字母异位词"问题时,我们往往从直观的解法入手,但很快会遇到性能瓶颈。本文将带你从最初的暴力解法出发,逐步优化到高效的滑动窗口解法,深入理解算法优化的思考过程。

问题描述

给定两个字符串 sp,我们需要在 s 中找到所有 p 的字母异位词的起始索引。字母异位词指字母相同但排列不同的字符串。

示例

makefile 复制代码
输入: s = "cbaebabacd", p = "abc"
输出: [0, 6]
解释:
起始索引 0 的子串是 "cba",是 "abc" 的异位词。
起始索引 6 的子串是 "bac",是 "abc" 的异位词。

解法一:暴力排序法(超时)

初始思路

刚开始正好看了split和join函数,用split转列表,再列表排序,用join改回来字符最后比较就行,我说这题不是秒杀吗?结果我是小丑了。

最直观的想法是将 p 排序,然后在 s 中截取所有长度为 p.length 的子串,排序后与排序后的 p 比较。

javascript 复制代码
var findAnagrams = function(s, p) {
    let res = [];
    let i = 0;
    //转列表排序,在转回来
    p = p.split('').sort().join('');
    while(i < s.length) {
    //取与p相同长度字符相比较
        let temp = s.slice(i, i + p.length).split('').sort().join('');
        if(temp === p) {
            res.push(i);
        }
        i++;
    }
    return res;
};

复杂度分析

一开始还在为只有一个循环沾沾自喜,我忘了函数也有mlog m的时间复杂度,再乘以for的n,那就有点high了,运行结果自然就是超时了。

  • 时间复杂度 :O(n * m log m),其中n是s的长度,m是p的长度
    • 对每个子串进行排序需要O(m log m)
    • 共有n个子串需要检查
  • 空间复杂度:O(m),用于存储排序后的子串

缺陷分析

当字符串较长时(如s长度10^5,p长度10^4),这种解法会超时,因为排序操作在循环内重复执行,效率太低。

解法二:滑动窗口+字符计数(优化解法)

优化思路

我们可以使用滑动窗口和字符计数的方法来优化:

  1. 统计 p 中每个字符的出现次数
  2. 维护一个与 p 长度相同的滑动窗口
  3. 统计窗口内字符的出现次数
  4. 比较窗口内字符计数与 p 的字符计数是否一致

优化后的代码

javascript 复制代码
var findAnagrams = function(s, p) {
    const res = [];
    const pLen = p.length;
    const sLen = s.length;
    
    if (sLen < pLen) return res;
    
    // 初始化p的字符计数
    const pCount = new Array(26).fill(0);
    for (let char of p) {
        pCount[char.charCodeAt() - 'a'.charCodeAt()]++;
    }
    
    // 初始化滑动窗口的字符计数
    const windowCount = new Array(26).fill(0);
    for (let i = 0; i < pLen; i++) {
        windowCount[s.charCodeAt(i) - 'a'.charCodeAt()]++;
    }
    
    // 比较初始窗口
    if (arraysEqual(windowCount, pCount)) {
        res.push(0);
    }
    
    // 滑动窗口
    for (let i = pLen; i < sLen; i++) {
        // 移除左边界的字符
        windowCount[s.charCodeAt(i - pLen) - 'a'.charCodeAt()]--;
        // 添加右边界的字符
        windowCount[s.charCodeAt(i) - 'a'.charCodeAt()]++;
        
        // 比较当前窗口
        if (arraysEqual(windowCount, pCount)) {
            res.push(i - pLen + 1);
        }
    }
    
    return res;
};

// 辅助函数:比较两个数组是否相等
function arraysEqual(a, b) {
    for (let i = 0; i < a.length; i++) {
        if (a[i] !== b[i]) return false;
    }
    return true;
}

复杂度分析

这里循环虽然多,但都是自己玩自己的,乘不到一块去,复杂度只有n。

  • 时间复杂度 :O(n),其中n是s的长度
    • 只需要遍历s一次
    • 每次窗口滑动是常数时间操作
  • 空间复杂度 :O(1)
    • 只使用了固定大小的计数数组(26个字母)

优化点分析

  1. 避免重复排序:使用字符计数代替排序
  2. 滑动窗口技巧:每次只更新两个字符的计数
  3. 常数时间比较:通过预计算p的字符计数

两种解法的对比

特性 暴力排序法 滑动窗口法
时间复杂度 O(n * m log m) O(n)
空间复杂度 O(m) O(1)
适用数据规模 小规模数据 大规模数据
核心操作 排序比较 字符计数比较
性能表现 容易超时 高效稳定

关键思路演进

  1. 从比较内容到比较特征:从直接比较字符串内容,转变为比较字符出现次数这一特征
  2. 从重新计算到增量更新:利用滑动窗口避免重复计算,只更新变化的字符计数
  3. 从O(m)比较到O(1)比较:通过预计算将每次比较的时间复杂度降低到常数

总结

通过这个问题的解决过程,我们可以学到:

  1. 直观解法往往不是最优解,需要考虑时间复杂度
  2. 字符串问题中,字符计数是常用的优化手段
  3. 滑动窗口技巧能有效减少重复计算
  4. 算法优化常常需要从问题特征入手,寻找更高效的比较方式

掌握这种从暴力解法到优化解法的思考过程,对提升算法能力大有裨益。在实际面试中,即使先提出暴力解法,也能展示出逐步优化的思维能力,这也是面试官看重的。

相关推荐
kidding7231 分钟前
微信小程序怎么分包步骤(包括怎么主包跳转到分包)
前端·微信小程序·前端开发·分包·wx.navigateto·subpackages
微学AI16 分钟前
详细介绍:MCP(大模型上下文协议)的架构与组件,以及MCP的开发实践
前端·人工智能·深度学习·架构·llm·mcp
wuqingshun31415922 分钟前
蓝桥杯 10.拉马车
数据结构·c++·算法·职场和发展·蓝桥杯·深度优先
Java知识库26 分钟前
Java BIO、NIO、AIO、Netty面试题(已整理全套PDF版本)
java·开发语言·jvm·面试·程序员
liangshanbo12151 小时前
CSS 包含块
前端·css
Mitchell_C1 小时前
语义化 HTML (Semantic HTML)
前端·html
倒霉男孩1 小时前
CSS文本属性
前端·css
晚风3081 小时前
前端
前端
JiangJiang1 小时前
🚀 Vue 人如何玩转 React 自定义 Hook?从 Mixins 到 Hook 的华丽转身
前端·react.js·面试
鱼樱前端1 小时前
让人头痛的原型和原型链知识
前端·javascript