☰
滑动窗口与动态规划:解决最长无重复子串与股票买卖问题
2026/11/4 7:50:59 网站建设 项目流程

1. 最长无重复字符子串问题解析

1.1 问题描述与直观理解

给定一个由ASCII字符组成的字符串s,我们需要找到其中最长的连续子串,该子串中不能包含重复字符。例如对于字符串"abcabcbb",最长无重复字符子串是"abc",长度为3。

这个问题看似简单,但要在O(n)时间复杂度内解决需要一些技巧。最直观的暴力解法是检查所有可能的子串,判断是否有重复字符,然后记录最大长度。这种方法的时间复杂度是O(n²),对于长字符串效率太低。

1.2 滑动窗口算法原理

滑动窗口算法是解决这类子串/子数组问题的利器。其核心思想是维护一个窗口[l,r],通过调整窗口边界来寻找符合条件的子串。

具体到这个题目:

  1. 我们使用两个指针l和r表示窗口的左右边界
  2. 使用哈希表记录每个字符最后一次出现的位置
  3. 当遇到重复字符时,将左边界l移动到max(l, 重复字符上次出现位置+1)
  4. 每次移动右边界r时,更新最大长度

这种方法的精妙之处在于它保证了每个字符最多被访问两次(一次加入窗口,一次移出窗口),因此时间复杂度是严格的O(n)。

1.3 代码实现与逐行解析

class Solution { public: int lengthOfLongestSubstring(string &s) { unordered_map<char, int> lastPos; // 记录字符最后出现的位置 int maxLen = 0; for(int r = 0, l = 0; r < s.size(); r++) { // 如果当前字符已经存在于窗口中,更新左边界 if(lastPos.find(s[r]) != lastPos.end()) { l = max(l, lastPos[s[r]] + 1); } // 更新最大长度 maxLen = max(maxLen, r - l + 1); // 记录当前字符的位置 lastPos[s[r]] = r; } return maxLen; } };

关键点解析:

  • lastPos哈希表存储每个字符最后出现的位置
  • 当发现重复字符时,l = max(l, lastPos[s[r]] + 1)确保窗口不会回退
  • maxLen始终记录着当前找到的最大长度

1.4 算法优化与变种

对于ASCII字符集,我们可以用固定大小的数组替代哈希表,进一步提升效率:

int lengthOfLongestSubstring(string s) { vector<int> charIndex(128, -1); // ASCII字符共128个 int maxLen = 0, l = 0; for(int r = 0; r < s.size(); r++) { if(charIndex[s[r]] >= l) { l = charIndex[s[r]] + 1; } charIndex[s[r]] = r; maxLen = max(maxLen, r - l + 1); } return maxLen; }

这种实现将时间复杂度常数因子降得更低,实际运行速度更快。

1.5 常见错误与调试技巧

  1. 边界条件处理:空字符串、全相同字符的字符串等特殊情况需要测试
  2. 窗口左边界回退:确保左边界不会向左移动,只能向右移动
  3. 哈希表更新时机:必须在判断重复之后更新字符位置
  4. 初始值设置:l和maxLen初始值应为0

调试时可以打印窗口变化过程:

cout << "Window [" << l << "," << r << "]: " << s.substr(l, r-l+1) << endl;

2. 带冷却期的股票买卖问题解析

2.1 问题描述与状态定义

给定一个股票价格数组prices,我们可以进行多次买卖,但有如下限制:

  1. 卖出后必须冷却一天(不能立即买入)
  2. 任何时候最多持有一股

我们需要找出可以获得的最大利润。

这个问题比常规的股票买卖问题更复杂,因为存在状态转移的限制。我们需要明确定义每天可能处于的状态:

  1. 持有(hold):当天持有股票
  2. 卖出(sold):当天卖出股票
  3. 冷却(cool):当天处于冷却期(前一天卖出了股票)

2.2 动态规划状态转移分析

我们可以用动态规划来解决这个问题,定义三个状态数组:

  • hold[i]:第i天结束时持有股票的最大利润
  • sold[i]:第i天结束时刚卖出股票的最大利润
  • cool[i]:第i天结束时处于冷却期的最大利润

状态转移方程:

  1. hold[i]可以来自:

    • 前一天就持有股票,今天继续持有:hold[i-1]
    • 前一天是冷却期,今天买入股票:cool[i-1] - prices[i]
  2. sold[i]只能来自:

    • 前一天持有股票,今天卖出:hold[i-1] + prices[i]
  3. cool[i]可以来自:

    • 前一天就是冷却期,今天继续冷却:cool[i-1]
    • 前一天卖出股票,今天必须冷却:sold[i-1]

2.3 两种视角的代码实现

2.3.1 现金视角实现
class Solution { public: int maxProfit(vector<int>& prices) { if(prices.empty()) return 0; int n = prices.size(); vector<int> hold(n), sold(n), cool(n); // 初始状态 hold[0] = -prices[0]; // 第一天买入 sold[0] = 0; // 第一天不能卖出 cool[0] = 0; // 第一天不能冷却 for(int i = 1; i < n; i++) { hold[i] = max(hold[i-1], cool[i-1] - prices[i]); sold[i] = hold[i-1] + prices[i]; cool[i] = max(cool[i-1], sold[i-1]); } return max(sold[n-1], cool[n-1]); } };
2.3.2 资产视角实现
class Solution { public: int maxProfit(vector<int>& prices) { if(prices.empty()) return 0; int n = prices.size(); vector<int> hold(n), sold(n), cool(n); // 初始状态 hold[0] = 0; // 初始资产为0 sold[0] = 0; // 第一天不能卖出 cool[0] = 0; // 第一天不能冷却 for(int i = 1; i < n; i++) { // 持有股票:要么继续持有,要么从冷却期买入 hold[i] = max(hold[i-1] + prices[i] - prices[i-1], cool[i-1]); // 卖出股票:必须从持有状态卖出 sold[i] = hold[i-1] + prices[i] - prices[i-1]; // 冷却期:要么继续冷却,要么来自卖出 cool[i] = max(cool[i-1], sold[i-1]); } return max(sold[n-1], cool[n-1]); } };

2.4 状态压缩优化

注意到每个状态只依赖于前一个状态,我们可以将空间复杂度从O(n)优化到O(1):

int maxProfit(vector<int>& prices) { int hold = INT_MIN, sold = 0, cool = 0; for(int price : prices) { int prevHold = hold; int prevSold = sold; int prevCool = cool; hold = max(prevHold, prevCool - price); sold = prevHold + price; cool = max(prevCool, prevSold); } return max(sold, cool); }

2.5 实际应用与变种问题

这类问题在实际量化交易中有广泛应用。理解状态转移对于设计交易策略至关重要。类似的问题变种包括:

  1. 交易手续费:每次交易收取固定费用
  2. 交易次数限制:最多进行k次交易
  3. 多重冷却期:卖出后需要冷却多日

3. 算法实战经验分享

3.1 滑动窗口算法的应用场景

滑动窗口算法不仅适用于最长无重复子串问题,还适用于:

  • 最小覆盖子串
  • 长度最小的子数组
  • 最多包含K个不同字符的子串
  • 字符串排列

关键识别特征是需要在数组/字符串中寻找满足特定条件的连续子序列。

3.2 动态规划状态设计的技巧

对于复杂的动态规划问题,设计合适的状态是关键。我的经验是:

  1. 明确问题的所有可能状态
  2. 确定状态之间的转移条件
  3. 考虑初始状态和边界条件
  4. 尝试用状态机表示转移关系
  5. 必要时画出状态转移图

3.3 调试复杂算法的实用方法

  1. 打印中间状态:在关键步骤打印变量值
  2. 小规模测试:先用简单例子验证
  3. 边界测试:空输入、单元素输入等
  4. 可视化工具:使用调试器逐步执行
  5. 对比验证:用暴力解法验证正确性

3.4 性能优化实践

在实际编码面试中,除了正确性,还需要考虑:

  1. 时间复杂度分析
  2. 空间复杂度优化
  3. 常数因子优化(如用数组替代哈希表)
  4. 避免不必要的计算
  5. 利用问题特性进行剪枝

对于最长无重复子串问题,我实测发现数组实现比哈希表实现快约30%。在极端情况下(如超长字符串),这种优化可能决定是否通过测试用例。

4. 常见问题解答

4.1 最长无重复子串问题

Q:为什么滑动窗口算法的时间复杂度是O(n)?A:每个字符最多被访问两次(一次加入窗口,一次移出窗口),所以总操作次数是2n,时间复杂度为O(n)。

Q:如何处理Unicode字符而不仅是ASCII?A:可以将哈希表改为支持Unicode的类型,如unordered_map<wchar_t, int>,算法逻辑不变。

Q:如果需要输出最长子串本身而不仅是长度怎么办?A:在更新最大长度时,同时记录窗口的起始和结束位置,最后用substr提取即可。

4.2 带冷却期的股票买卖问题

Q:为什么最后的结果是max(sold[n-1], cool[n-1])?A:因为最后一天持有股票没有意义(无法获利),所以只考虑卖出或冷却状态。

Q:如何扩展这个问题到允许最多k次交易?A:需要增加一个维度表示交易次数,状态转移会更复杂,但基本思路类似。

Q:现金视角和资产视角有什么区别?A:现金视角跟踪实际现金流动,资产视角跟踪相对收益。现金视角更直观,资产视角有时能简化计算。

Q:如何处理交易手续费的情况?A:在买入或卖出时扣除手续费即可,例如sold[i] = hold[i-1] + prices[i] - fee。

在实际面试中,理解这些算法的核心思想比死记硬背代码更重要。我建议多练习类似的题目,培养对问题模式的敏感度和状态设计的能力。对于动态规划问题,画出状态转移图往往能帮助理清思路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询