<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://qiyu-lu.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://qiyu-lu.github.io/" rel="alternate" type="text/html" /><updated>2026-03-20T12:30:53+00:00</updated><id>https://qiyu-lu.github.io/feed.xml</id><title type="html">qiyu</title><subtitle>Write an awesome description for your new site here. You can edit this line in _config.yml. It will appear in your document head meta (for Google search results) and in your feed.xml site description.</subtitle><author><name>qiyu-lu</name></author><entry><title type="html">力扣热题100刷题-hash-128-最长连续序列</title><link href="https://qiyu-lu.github.io/algorithms/_128_LongestConsecutive/" rel="alternate" type="text/html" title="力扣热题100刷题-hash-128-最长连续序列" /><published>2026-03-20T00:00:00+00:00</published><updated>2026-03-20T00:00:00+00:00</updated><id>https://qiyu-lu.github.io/algorithms/_128_LongestConsecutive</id><content type="html" xml:base="https://qiyu-lu.github.io/algorithms/_128_LongestConsecutive/"><![CDATA[<h1 id="128--最长连续序列">128 + 最长连续序列</h1>

<hr />

<h2 id="思路">思路</h2>

<p>将数组中的所有元素添加到哈希表中，然后遍历哈希表，看以当前数为起点或者以当前数为终点的连续序列的长度，
核心优化点：
假如是以当前遍历的数为起点的，判断哈希集合中是否有以他为起点的连续序列，
那么就可以在遍历时，查找哈希集合中是否有 n-1 这样的数，如果有的话，n
n为起点的就不是最长的，就可以不进行后面的查找操作了，首先的操作是找到起点或者终点，
这样可以节省很多时间。
—</p>

<h2 id="总结">总结</h2>

<p>使用到的几个方法</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//哈希集合</span>
<span class="kt">boolean</span> <span class="nf">add</span><span class="o">(</span><span class="no">E</span> <span class="n">e</span><span class="o">)</span>                        <span class="c1">// 添加元素（若已存在返回 false）</span>
<span class="kt">boolean</span> <span class="nf">contains</span><span class="o">(</span><span class="nc">Object</span> <span class="n">o</span><span class="o">)</span>              <span class="c1">// 是否包含元素</span>
<span class="c1">//遍历</span>
<span class="nc">HashSet</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">set</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HashSet</span><span class="o">&lt;&gt;();</span>
<span class="k">for</span> <span class="o">(</span><span class="nc">String</span> <span class="n">s</span> <span class="o">:</span> <span class="n">set</span><span class="o">){}</span> <span class="c1">//增强 for 循环</span>
<span class="nc">Iterator</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">it</span> <span class="o">=</span> <span class="n">set</span><span class="o">.</span><span class="na">iterator</span><span class="o">();</span><span class="c1">//使用 Iterator（可在遍历中删除元素）</span>
<span class="k">while</span> <span class="o">(</span><span class="n">it</span><span class="o">.</span><span class="na">hasNext</span><span class="o">())</span> <span class="o">{</span>
<span class="nc">String</span> <span class="n">s</span> <span class="o">=</span> <span class="n">it</span><span class="o">.</span><span class="na">next</span><span class="o">();</span>
    <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">s</span><span class="o">);</span>
<span class="o">}</span>
<span class="n">set</span><span class="o">.</span><span class="na">forEach</span><span class="o">(</span><span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">::</span><span class="n">println</span><span class="o">);</span><span class="c1">//使用 Stream（更简洁，但不可改元素和结构）</span>
</code></pre></div></div>]]></content><author><name>qiyu-lu</name></author><category term="Algorithms" /><category term="Algorithms" /><category term="LeetCode" /><summary type="html"><![CDATA[128 + 最长连续序列]]></summary></entry><entry><title type="html">力扣热题100刷题-hash-1-两数之和</title><link href="https://qiyu-lu.github.io/algorithms/_1_TwoSum/" rel="alternate" type="text/html" title="力扣热题100刷题-hash-1-两数之和" /><published>2026-03-20T00:00:00+00:00</published><updated>2026-03-20T00:00:00+00:00</updated><id>https://qiyu-lu.github.io/algorithms/_1_TwoSum</id><content type="html" xml:base="https://qiyu-lu.github.io/algorithms/_1_TwoSum/"><![CDATA[<h1 id="1-两数之和">1 两数之和</h1>

<h2 id="题意">题意</h2>

<p>给定一个整数数组 nums 和一个目标值 target，找出数组中两个数，使它们的和等于 target，并返回它们的下标。</p>

<hr />

<h2 id="思路">思路</h2>

<p>一开始最自然的想法是双重循环，枚举所有两数组合，看是否等于 target。</p>

<p>时间复杂度 O(n^2)，数据量大时会超时。</p>

<p>优化思路：<br />
在遍历数组时，记录已经遍历过的数字。<br />
对于当前数字 x，只需要看 target - x 是否已经出现过。</p>

<p>因此可以使用 HashMap：</p>

<ul>
  <li>key：数组中的值</li>
  <li>value：该值对应的下标</li>
</ul>

<p>遍历一次数组即可解决。</p>

<hr />

<h3 id="易错点--卡点">易错点 / 卡点</h3>

<ul>
  <li>
    <p>第一想法：
双重 for 循环暴力枚举。</p>
  </li>
  <li>
    <p>为什么不行：
时间复杂度 O(n^2)，不满足优化要求。</p>
  </li>
  <li>
    <p>最终解法：
使用 HashMap，在一次遍历中完成查找。</p>
  </li>
  <li>
    <p>容易出错点：</p>
    <ul>
      <li>先判断 complement 是否存在，再 put 当前值。</li>
      <li>注意数组中可能有重复元素。</li>
      <li>返回的是下标，不是数值。</li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="核心思路本质">核心思路（本质）</h3>

<ul>
  <li>空间换时间</li>
  <li>“查找某个值是否存在” → 使用 HashMap</li>
  <li>一边遍历，一边建立映射关系</li>
</ul>

<p>本质是：<br />
把 O(n) 的查找嵌入到一次遍历中，避免双重循环。</p>

<hr />

<h2 id="总结">总结</h2>

<p>看到“找两个数之和等于 target” + “只需返回一组解”， 立刻想到用 HashMap 记录已遍历元素，用空间换时间。</p>]]></content><author><name>qiyu-lu</name></author><category term="Algorithms" /><category term="Algorithms" /><category term="LeetCode" /><summary type="html"><![CDATA[1 两数之和]]></summary></entry><entry><title type="html">力扣热题100刷题-hash-49-字母异位词分组</title><link href="https://qiyu-lu.github.io/algorithms/_49_GroupAnagrams/" rel="alternate" type="text/html" title="力扣热题100刷题-hash-49-字母异位词分组" /><published>2026-03-20T00:00:00+00:00</published><updated>2026-03-20T00:00:00+00:00</updated><id>https://qiyu-lu.github.io/algorithms/_49_GroupAnagrams</id><content type="html" xml:base="https://qiyu-lu.github.io/algorithms/_49_GroupAnagrams/"><![CDATA[<h1 id="49--字母异位词分组">49 + 字母异位词分组</h1>

<hr />

<h2 id="思路">思路</h2>

<p>字母异位词就是一个字符串的字符的不同排列方式，可以使用哈希表，遍历这个字符串，将其中的字符进行排序，
排序后的字符串作为键，为什么不使用字符数组作为键，字符数组作为键的话是它的地址，每一个遍历到的字符串排序后新建的字符数组地址都不一样。
然后符合字母异位词的要求的列表作为值。</p>

<hr />

<hr />

<h3 id="核心思路本质">核心思路（本质）</h3>
<p>（这题的抽象本质是什么？）</p>

<p>例：</p>
<ul>
  <li>右侧第一个更大元素</li>
  <li>子数组求和问题</li>
  <li>树的后序遍历</li>
  <li>二分查找答案</li>
</ul>

<hr />

<h2 id="总结">总结</h2>

<p>总结一下这里使用到的一些方法:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">map</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="n">value</span><span class="o">);</span>       <span class="c1">// 插入/更新</span>
<span class="n">map</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">key</span><span class="o">);</span>              <span class="c1">// 获取value</span>
<span class="n">map</span><span class="o">.</span><span class="na">containsKey</span><span class="o">(</span><span class="n">key</span><span class="o">);</span>      <span class="c1">// 是否包含key</span>
<span class="n">map</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="n">key</span><span class="o">);</span>           <span class="c1">// 删除对应key的键值对</span>
<span class="n">map</span><span class="o">.</span><span class="na">getOrDefault</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="mi">0</span><span class="o">);</span>  <span class="c1">// 不存在时返回默认值</span>
<span class="n">map</span><span class="o">.</span><span class="na">values</span><span class="o">();</span> <span class="c1">// 获取全部的值 返回的正是 Collection 类型</span>
<span class="c1">//然后 ArrayList 提供了接收 Collection 的构造方法</span>
<span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;(</span><span class="n">map</span><span class="o">.</span><span class="na">values</span><span class="o">())</span> <span class="c1">//可以这样</span>
<span class="n">map</span><span class="o">.</span><span class="na">getOrDefault</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="n">defaultValue</span><span class="o">)</span> <span class="c1">//如果 key 存在，就返回对应的 value；如果 key 不存在，就返回你给的默认值。</span>

<span class="c1">//key,要查找的键 如果不存在，则执行第二个参数：lambda表达式函数创建值,然后返回 key 对应的值（现有的或新创建的）</span>
<span class="n">map</span><span class="o">.</span><span class="na">computeIfAbsent</span><span class="o">(</span><span class="no">K</span> <span class="n">key</span><span class="o">,</span> <span class="nc">Function</span><span class="o">&lt;?</span> <span class="kd">super</span> <span class="no">K</span><span class="o">,</span> <span class="o">?</span> <span class="kd">extends</span> <span class="no">V</span><span class="o">&gt;</span> <span class="n">mappingFunction</span><span class="o">)</span>

<span class="n">s</span><span class="o">.</span><span class="na">toCharArray</span><span class="o">()</span> <span class="c1">// 字符串转化为字符数组</span>
<span class="nc">Arrays</span><span class="o">.</span><span class="na">sort</span><span class="o">(</span><span class="n">arr</span><span class="o">)</span> <span class="c1">//对数组进行排序</span>
</code></pre></div></div>]]></content><author><name>qiyu-lu</name></author><category term="Algorithms" /><category term="Algorithms" /><category term="LeetCode" /><summary type="html"><![CDATA[49 + 字母异位词分组]]></summary></entry><entry><title type="html">力扣热题100刷题-slidingWindow-3-无重复字符的最长子串</title><link href="https://qiyu-lu.github.io/algorithms/_3_lengthOfLongestSubstring/" rel="alternate" type="text/html" title="力扣热题100刷题-slidingWindow-3-无重复字符的最长子串" /><published>2026-03-20T00:00:00+00:00</published><updated>2026-03-20T00:00:00+00:00</updated><id>https://qiyu-lu.github.io/algorithms/_3_lengthOfLongestSubstring</id><content type="html" xml:base="https://qiyu-lu.github.io/algorithms/_3_lengthOfLongestSubstring/"><![CDATA[<h1 id="3-无重复字符的最长子串">3. 无重复字符的最长子串</h1>

<h2 id="问题描述">问题描述</h2>

<p>给定一个字符串 <code class="language-plaintext highlighter-rouge">s</code> ，请你找出其中不含有重复字符的 <strong>最长子串</strong> 的长度。</p>

<h2 id="解题思路滑动窗口">解题思路：滑动窗口</h2>

<p>这道题是典型的滑动窗口问题，我们可以用两个指针 <code class="language-plaintext highlighter-rouge">left</code> 和 <code class="language-plaintext highlighter-rouge">right</code> 来维护一个窗口，这个窗口内的子串就是我们正在检查的子串。</p>

<h3 id="核心思想">核心思想</h3>

<ol>
  <li><strong>右指针 <code class="language-plaintext highlighter-rouge">right</code> 扩张</strong>: 不断向右移动 <code class="language-plaintext highlighter-rouge">right</code> 指针，以扩大窗口，将新的字符纳入窗口中。</li>
  <li><strong>检查重复</strong>: 每当一个新的字符 <code class="language-plaintext highlighter-rouge">s[right]</code> 进入窗口时，我们需要判断这个字符是否已经在窗口中存在。
    <ul>
      <li><strong>不重复</strong>: 如果 <code class="language-plaintext highlighter-rouge">s[right]</code> 不在窗口中，那么我们成功地将窗口扩大了一格，当前窗口的长度就是 <code class="language-plaintext highlighter-rouge">right - left + 1</code>，我们更新最大长度。</li>
      <li><strong>重复</strong>: 如果 <code class="language-plaintext highlighter-rouge">s[right]</code> 已经在窗口中了，说明从 <code class="language-plaintext highlighter-rouge">left</code> 到 <code class="language-plaintext highlighter-rouge">right</code> 的这个子串包含了重复字符。此时，我们需要收缩窗口。</li>
    </ul>
  </li>
  <li><strong>左指针 <code class="language-plaintext highlighter-rouge">left</code> 收缩</strong>: 不断向右移动 <code class="language-plaintext highlighter-rouge">left</code> 指针，将 <code class="language-plaintext highlighter-rouge">s[left]</code> 移出窗口，直到窗口内不再包含重复的 <code class="language-plaintext highlighter-rouge">s[right]</code> 字符。然后 <code class="language-plaintext highlighter-rouge">right</code> 指针可以继续扩张。</li>
</ol>

<p>通过这个过程，我们就能遍历所有可能的无重复字符的子串，并找到其中最长的一个。</p>

<h3 id="实现方法">实现方法</h3>

<h4 id="方法一使用-hashset">方法一：使用 HashSet</h4>

<p>这是最直观的实现。我们可以用一个 <code class="language-plaintext highlighter-rouge">HashSet</code> 来存储当前窗口内的所有字符。</p>

<ul>
  <li><strong>扩张窗口</strong>: 当 <code class="language-plaintext highlighter-rouge">right</code> 指针移动时，检查 <code class="language-plaintext highlighter-rouge">s[right]</code> 是否在 <code class="language-plaintext highlighter-rouge">HashSet</code> 中。
    <ul>
      <li>如果不在，就将其加入 <code class="language-plaintext highlighter-rouge">HashSet</code>，然后 <code class="language-plaintext highlighter-rouge">right</code> 指针加一，并更新最大长度 <code class="language-plaintext highlighter-rouge">ans = max(ans, set.size())</code>。</li>
      <li>如果在，说明遇到了重复字符。</li>
    </ul>
  </li>
  <li><strong>收缩窗口</strong>: 当发现重复时，我们从 <code class="language-plaintext highlighter-rouge">HashSet</code> 中移除 <code class="language-plaintext highlighter-rouge">s[left]</code>，然后 <code class="language-plaintext highlighter-rouge">left</code> 指针加一。这个过程一直持续到 <code class="language-plaintext highlighter-rouge">s[right]</code> 可以被安全地加入 <code class="language-plaintext highlighter-rouge">HashSet</code> 为止。</li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">int</span> <span class="nf">lengthOfLongestSubstring</span><span class="o">(</span><span class="nc">String</span> <span class="n">s</span><span class="o">)</span> <span class="o">{</span>
    <span class="kt">char</span><span class="o">[]</span> <span class="n">arr</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="na">toCharArray</span><span class="o">();</span>
    <span class="kt">int</span> <span class="n">n</span> <span class="o">=</span> <span class="n">arr</span><span class="o">.</span><span class="na">length</span><span class="o">;</span>
    <span class="nc">HashSet</span><span class="o">&lt;</span><span class="nc">Character</span><span class="o">&gt;</span> <span class="n">set</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HashSet</span><span class="o">&lt;&gt;();</span>
    <span class="kt">int</span> <span class="n">ans</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
    <span class="kt">int</span> <span class="n">left</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
    <span class="kt">int</span> <span class="n">right</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>

    <span class="k">while</span><span class="o">(</span><span class="n">right</span> <span class="o">&lt;</span> <span class="n">n</span><span class="o">){</span>
        <span class="k">if</span><span class="o">(!</span><span class="n">set</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="n">arr</span><span class="o">[</span><span class="n">right</span><span class="o">])){</span>
            <span class="n">set</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">arr</span><span class="o">[</span><span class="n">right</span><span class="o">]);</span>
            <span class="n">right</span><span class="o">++;</span>
            <span class="c1">// 每次扩大窗口都可能是最长的一次</span>
            <span class="n">ans</span> <span class="o">=</span> <span class="nc">Math</span><span class="o">.</span><span class="na">max</span><span class="o">(</span><span class="n">ans</span><span class="o">,</span> <span class="n">set</span><span class="o">.</span><span class="na">size</span><span class="o">());</span> 
        <span class="o">}</span>
        <span class="k">else</span><span class="o">{</span>
            <span class="c1">// 发现重复，收缩窗口</span>
            <span class="n">set</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="n">arr</span><span class="o">[</span><span class="n">left</span><span class="o">]);</span>
            <span class="n">left</span><span class="o">++;</span>
        <span class="o">}</span>
    <span class="o">}</span>
    <span class="k">return</span> <span class="n">ans</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<h4 id="方法二使用布尔数组优化">方法二：使用布尔数组（优化）</h4>

<p>考虑到题目提示 “s 由英文字母、数字、符号和空格组成”，这些都属于 ASCII 字符集。ASCII 码的范围是 0-127。因此，我们可以用一个大小为 128 的布尔数组 <code class="language-plaintext highlighter-rouge">has[]</code> 来代替 <code class="language-plaintext highlighter-rouge">HashSet</code>，以提高效率。数组的索引就是字符的 ASCII 码。</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">has[char]</code> 为 <code class="language-plaintext highlighter-rouge">true</code> 表示该字符在当前窗口中。</li>
  <li><code class="language-plaintext highlighter-rouge">has[char]</code> 为 <code class="language-plaintext highlighter-rouge">false</code> 表示该字符不在当前窗口中。</li>
</ul>

<p>这种方法的逻辑和 <code class="language-plaintext highlighter-rouge">HashSet</code> 完全一样，但数组操作通常比 <code class="language-plaintext highlighter-rouge">HashSet</code> 的哈希计算和冲突处理要快。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">int</span> <span class="nf">lengthOfLongestSubstringByArray</span><span class="o">(</span><span class="nc">String</span> <span class="n">s</span><span class="o">)</span> <span class="o">{</span>
    <span class="kt">char</span><span class="o">[]</span> <span class="n">arr</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="na">toCharArray</span><span class="o">();</span>
    <span class="kt">int</span> <span class="n">n</span> <span class="o">=</span> <span class="n">arr</span><span class="o">.</span><span class="na">length</span><span class="o">;</span>
    <span class="kt">boolean</span><span class="o">[]</span> <span class="n">has</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">boolean</span><span class="o">[</span><span class="mi">128</span><span class="o">];</span> <span class="c1">// ASCII 字符集</span>
    <span class="kt">int</span> <span class="n">ans</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
    <span class="kt">int</span> <span class="n">left</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>

    <span class="k">for</span><span class="o">(</span><span class="kt">int</span> <span class="n">right</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">right</span> <span class="o">&lt;</span> <span class="n">n</span><span class="o">;</span> <span class="n">right</span><span class="o">++){</span>
        <span class="c1">// 当发现 arr[right] 已经在窗口中时，收缩窗口</span>
        <span class="k">while</span><span class="o">(</span><span class="n">has</span><span class="o">[</span><span class="n">arr</span><span class="o">[</span><span class="n">right</span><span class="o">]]){</span>
            <span class="n">has</span><span class="o">[</span><span class="n">arr</span><span class="o">[</span><span class="n">left</span><span class="o">]]</span> <span class="o">=</span> <span class="kc">false</span><span class="o">;</span> <span class="c1">// 将左边界字符移出窗口</span>
            <span class="n">left</span><span class="o">++;</span>
        <span class="o">}</span>
        <span class="c1">// 此时 arr[right] 可以安全地加入窗口</span>
        <span class="n">has</span><span class="o">[</span><span class="n">arr</span><span class="o">[</span><span class="n">right</span><span class="o">]]</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span>
        <span class="c1">// 更新最大长度</span>
        <span class="n">ans</span> <span class="o">=</span> <span class="nc">Math</span><span class="o">.</span><span class="na">max</span><span class="o">(</span><span class="n">ans</span><span class="o">,</span> <span class="n">right</span> <span class="o">-</span> <span class="n">left</span> <span class="o">+</span> <span class="mi">1</span><span class="o">);</span>
    <span class="o">}</span>
    <span class="k">return</span> <span class="n">ans</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p><em>代码 <code class="language-plaintext highlighter-rouge">_3_lengthOfLongestSubstring.java</code> 中的 <code class="language-plaintext highlighter-rouge">lengthOfLongestSubstringByArrayPlus</code> 方法与此逻辑相同。</em></p>

<h2 id="总结">总结</h2>

<ul>
  <li><strong>问题类型</strong>: 寻找满足条件的 <strong>最长子串/子数组</strong>，通常可以考虑滑动窗口。</li>
  <li><strong>核心数据结构</strong>: 需要一个能快速判断元素是否存在的数据结构来维护窗口内的字符，例如 <code class="language-plaintext highlighter-rouge">HashSet</code> 或 <code class="language-plaintext highlighter-rouge">HashMap</code>。</li>
  <li><strong>优化</strong>: 如果字符集范围有限（如 ASCII），使用数组代替哈希表可以提高性能。</li>
  <li><strong>窗口移动</strong>: 滑动窗口的精髓在于，指针只向前移动，避免了暴力解法中的重复计算，将时间复杂度从 O(N^2) 或 O(N^3) 优化到 O(N)。</li>
</ul>]]></content><author><name>qiyu-lu</name></author><category term="Algorithms" /><category term="Algorithms" /><category term="LeetCode" /><summary type="html"><![CDATA[3. 无重复字符的最长子串]]></summary></entry><entry><title type="html">力扣热题100刷题-slidingWindow-438-找到字符串中所有字母异位词</title><link href="https://qiyu-lu.github.io/algorithms/_438_findAnagrams/" rel="alternate" type="text/html" title="力扣热题100刷题-slidingWindow-438-找到字符串中所有字母异位词" /><published>2026-03-20T00:00:00+00:00</published><updated>2026-03-20T00:00:00+00:00</updated><id>https://qiyu-lu.github.io/algorithms/_438_findAnagrams</id><content type="html" xml:base="https://qiyu-lu.github.io/algorithms/_438_findAnagrams/"><![CDATA[<h1 id="438-找到字符串中所有字母异位词">438. 找到字符串中所有字母异位词</h1>

<h2 id="问题描述">问题描述</h2>

<p>给定两个字符串 <code class="language-plaintext highlighter-rouge">s</code> 和 <code class="language-plaintext highlighter-rouge">p</code>，找到 <code class="language-plaintext highlighter-rouge">s</code> 中所有 <code class="language-plaintext highlighter-rouge">p</code> 的 <strong>异位词</strong> 的子串，并返回这些子串的起始索引。</p>

<p><strong>异位词</strong> 是指由相同字母、相同数量组成的字符串，但顺序可能不同。例如, “abc” 和 “cba” 是异位词。</p>

<h2 id="解题思路">解题思路</h2>

<h3 id="方法一暴力法-brute-force">方法一：暴力法 (Brute Force)</h3>

<p>这是最直接但效率最低的方法，对应 <code class="language-plaintext highlighter-rouge">findAnagramsBF</code> 函数。</p>

<ol>
  <li><strong>遍历</strong>: 从 <code class="language-plaintext highlighter-rouge">s</code> 的每个字符开始，检查其后长度为 <code class="language-plaintext highlighter-rouge">p.length()</code> 的子串。</li>
  <li><strong>排序比较</strong>:
    <ul>
      <li>取出这个子串。</li>
      <li>将子串和字符串 <code class="language-plaintext highlighter-rouge">p</code> 都转换成字符数组。</li>
      <li>对两个数组进行排序。</li>
      <li>如果排序后的数组完全相同，说明它们是异位词，记录下当前子串的起始索引。</li>
    </ul>
  </li>
  <li><strong>复杂度</strong>: 假设 <code class="language-plaintext highlighter-rouge">s</code> 的长度为 <code class="language-plaintext highlighter-rouge">m</code>，<code class="language-plaintext highlighter-rouge">p</code> 的长度为 <code class="language-plaintext highlighter-rouge">n</code>。
    <ul>
      <li>时间复杂度: <code class="language-plaintext highlighter-rouge">O((m-n) * n log n)</code>。因为外层循环 <code class="language-plaintext highlighter-rouge">m-n</code> 次，内部每次都要进行长度为 <code class="language-plaintext highlighter-rouge">n</code> 的字符串排序。</li>
      <li>空间复杂度: <code class="language-plaintext highlighter-rouge">O(n)</code>，用于存储子串的字符数组。</li>
    </ul>
  </li>
</ol>

<p>这个方法在 <code class="language-plaintext highlighter-rouge">s</code> 很大时会非常慢，通常会导致超时。</p>

<h3 id="方法二滑动窗口与字符计数-sliding-window">方法二：滑动窗口与字符计数 (Sliding Window)</h3>

<p>这是解决此类问题的标准、高效方法，对应 <code class="language-plaintext highlighter-rouge">findAnagramsBySwV2</code> 函数。</p>

<p>核心思想是维护一个固定大小的窗口（大小为 <code class="language-plaintext highlighter-rouge">p</code> 的长度），在 <code class="language-plaintext highlighter-rouge">s</code> 上滑动。我们不去比较子串本身，而是比较窗口内子串的 <strong>字符频率</strong> 是否与 <code class="language-plaintext highlighter-rouge">p</code> 的字符频率相同。</p>

<ol>
  <li>
    <p><strong>统计 <code class="language-plaintext highlighter-rouge">p</code> 的频率</strong>: 使用一个大小为 26 的数组 <code class="language-plaintext highlighter-rouge">cnt</code>（因为题目说明是小写英文字母），统计 <code class="language-plaintext highlighter-rouge">p</code> 中每个字符出现的次数。</p>
  </li>
  <li><strong>初始化窗口</strong>:
    <ul>
      <li>在 <code class="language-plaintext highlighter-rouge">s</code> 中建立一个初始窗口，大小与 <code class="language-plaintext highlighter-rouge">p</code> 相同（从索引 0 到 <code class="language-plaintext highlighter-rouge">p.length() - 1</code>）。</li>
      <li>使用另一个数组 <code class="language-plaintext highlighter-rouge">cntTemp</code> 统计这个初始窗口内各字符的频率。</li>
    </ul>
  </li>
  <li><strong>滑动与比较</strong>:
    <ul>
      <li><strong>比较</strong>: 比较 <code class="language-plaintext highlighter-rouge">cnt</code> 和 <code class="language-plaintext highlighter-rouge">cntTemp</code> 两个频率数组是否完全相同。如果相同，则表示当前窗口是一个异位词子串，将窗口的起始索引 <code class="language-plaintext highlighter-rouge">left</code> 加入结果列表。</li>
      <li><strong>滑动</strong>: 将窗口向右移动一格。
        <ul>
          <li><strong>移出</strong>: <code class="language-plaintext highlighter-rouge">left</code> 指针向右移动，所以 <code class="language-plaintext highlighter-rouge">s[left]</code> 字符离开窗口。在 <code class="language-plaintext highlighter-rouge">cntTemp</code> 中将该字符的计数减一。</li>
          <li><strong>移入</strong>: <code class="language-plaintext highlighter-rouge">right</code> 指针向右移动，所以 <code class="language-plaintext highlighter-rouge">s[right]</code> 字符进入窗口。在 <code class="language-plaintext highlighter-rouge">cntTemp</code> 中将该字符的计数加一。</li>
        </ul>
      </li>
      <li>重复 <strong>比较</strong> 和 <strong>滑动</strong> 步骤，直到 <code class="language-plaintext highlighter-rouge">right</code> 指针到达 <code class="language-plaintext highlighter-rouge">s</code> 的末尾。</li>
    </ul>
  </li>
</ol>

<h4 id="代码实现-findanagramsbyswv2">代码实现 (<code class="language-plaintext highlighter-rouge">findAnagramsBySwV2</code>)</h4>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">&gt;</span> <span class="nf">findAnagramsBySwV2</span><span class="o">(</span><span class="nc">String</span> <span class="n">s</span><span class="o">,</span> <span class="nc">String</span> <span class="n">p</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">&gt;</span> <span class="n">ans</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;();</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">s</span><span class="o">.</span><span class="na">length</span><span class="o">()</span> <span class="o">&lt;</span> <span class="n">p</span><span class="o">.</span><span class="na">length</span><span class="o">())</span> <span class="k">return</span> <span class="n">ans</span><span class="o">;</span>

    <span class="kt">int</span> <span class="n">n</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="na">length</span><span class="o">();</span>
    <span class="kt">int</span><span class="o">[]</span> <span class="n">cnt</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[</span><span class="mi">26</span><span class="o">];</span>      <span class="c1">// p 的字符频率</span>
    <span class="kt">int</span><span class="o">[]</span> <span class="n">cntTemp</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[</span><span class="mi">26</span><span class="o">];</span>  <span class="c1">// 窗口的字符频率</span>

    <span class="c1">// 1. 统计 p 的频率，并初始化第一个窗口的频率</span>
    <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">n</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
        <span class="n">cnt</span><span class="o">[</span><span class="n">p</span><span class="o">.</span><span class="na">charAt</span><span class="o">(</span><span class="n">i</span><span class="o">)</span> <span class="o">-</span> <span class="sc">'a'</span><span class="o">]++;</span>
        <span class="n">cntTemp</span><span class="o">[</span><span class="n">s</span><span class="o">.</span><span class="na">charAt</span><span class="o">(</span><span class="n">i</span><span class="o">)</span> <span class="o">-</span> <span class="sc">'a'</span><span class="o">]++;</span>
    <span class="o">}</span>

    <span class="c1">// 2. 检查第一个窗口</span>
    <span class="k">if</span> <span class="o">(</span><span class="nc">Arrays</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">cnt</span><span class="o">,</span> <span class="n">cntTemp</span><span class="o">))</span> <span class="o">{</span>
        <span class="n">ans</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="mi">0</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="c1">// 3. 开始滑动窗口</span>
    <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">left</span> <span class="o">=</span> <span class="mi">1</span><span class="o">;</span> <span class="n">left</span> <span class="o">&lt;=</span> <span class="n">s</span><span class="o">.</span><span class="na">length</span><span class="o">()</span> <span class="o">-</span> <span class="n">n</span><span class="o">;</span> <span class="n">left</span><span class="o">++)</span> <span class="o">{</span>
        <span class="kt">int</span> <span class="n">right</span> <span class="o">=</span> <span class="n">left</span> <span class="o">+</span> <span class="n">n</span> <span class="o">-</span> <span class="mi">1</span><span class="o">;</span>
        
        <span class="c1">// 移出 s[left-1]</span>
        <span class="n">cntTemp</span><span class="o">[</span><span class="n">s</span><span class="o">.</span><span class="na">charAt</span><span class="o">(</span><span class="n">left</span> <span class="o">-</span> <span class="mi">1</span><span class="o">)</span> <span class="o">-</span> <span class="sc">'a'</span><span class="o">]--;</span>
        <span class="c1">// 移入 s[right]</span>
        <span class="n">cntTemp</span><span class="o">[</span><span class="n">s</span><span class="o">.</span><span class="na">charAt</span><span class="o">(</span><span class="n">right</span><span class="o">)</span> <span class="o">-</span> <span class="sc">'a'</span><span class="o">]++;</span>

        <span class="c1">// 再次比较</span>
        <span class="k">if</span> <span class="o">(</span><span class="nc">Arrays</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">cnt</span><span class="o">,</span> <span class="n">cntTemp</span><span class="o">))</span> <span class="o">{</span>
            <span class="n">ans</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">left</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>
    <span class="k">return</span> <span class="n">ans</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p><em>注：你提供的 <code class="language-plaintext highlighter-rouge">findAnagramsBySwV2</code> 在循环的实现上略有不同，但核心思想是一致的。以上版本是另一种常见的、等效的滑动窗口实现。</em></p>

<ol>
  <li><strong>复杂度</strong>:
    <ul>
      <li>时间复杂度: <code class="language-plaintext highlighter-rouge">O(m)</code>。我们只需要遍历一遍字符串 <code class="language-plaintext highlighter-rouge">s</code>。数组比较是 <code class="language-plaintext highlighter-rouge">O(1)</code>（因为数组大小固定为 26）。</li>
      <li>空间复杂度: <code class="language-plaintext highlighter-rouge">O(1)</code>，因为我们只用了两个固定大小的数组。</li>
    </ul>
  </li>
</ol>

<h2 id="总结">总结</h2>

<p>滑动窗口通过复用前一步的计算结果（只更新离开和进入窗口的两个字符），避免了暴力法中大量的重复排序和比较工作，是解决子串/子数组问题的强大技巧。</p>]]></content><author><name>qiyu-lu</name></author><category term="Algorithms" /><category term="Algorithms" /><category term="LeetCode" /><summary type="html"><![CDATA[438. 找到字符串中所有字母异位词]]></summary></entry><entry><title type="html">力扣热题100刷题-subString-560-和为 K 的子数组</title><link href="https://qiyu-lu.github.io/algorithms/_560_SubarraySum/" rel="alternate" type="text/html" title="力扣热题100刷题-subString-560-和为 K 的子数组" /><published>2026-03-20T00:00:00+00:00</published><updated>2026-03-20T00:00:00+00:00</updated><id>https://qiyu-lu.github.io/algorithms/_560_SubarraySum</id><content type="html" xml:base="https://qiyu-lu.github.io/algorithms/_560_SubarraySum/"><![CDATA[<h1 id="560-和为-k-的子数组">560. 和为 K 的子数组</h1>

<h2 id="题意">题意</h2>

<p>给你一个整数数组 <code class="language-plaintext highlighter-rouge">nums</code> 和一个整数 <code class="language-plaintext highlighter-rouge">k</code> ，请你统计并返回 该数组中和为 <code class="language-plaintext highlighter-rouge">k</code> 的<strong>连续子数组</strong>的个数 。</p>

<hr />

<h2 id="思路">思路</h2>

<h3 id="易错点--卡点">易错点 / 卡点</h3>

<ul>
  <li><strong>负数存在</strong>：数组中可能包含负数，这意味着子数组的和不具有单调性。即使当前和已经大于 <code class="language-plaintext highlighter-rouge">k</code>，加上一个负数后仍可能等于 <code class="language-plaintext highlighter-rouge">k</code>。因此，不能简单地用滑动窗口来优化。</li>
  <li>
    <p><strong>O(n^2) 超时</strong>：最直观的暴力解法是枚举所有子数组的起点和终点，计算其和，时间复杂度为 O(n^3) 或优化后的 O(n^2)，在数据量较大时会超时。</p>
  </li>
  <li>
    <p><strong>第一想法：</strong>
通过两层循环枚举所有连续子数组。外层循环固定子数组的右端点 <code class="language-plaintext highlighter-rouge">i</code>，内层循环枚举左端点 <code class="language-plaintext highlighter-rouge">j</code>，计算 <code class="language-plaintext highlighter-rouge">[j, i]</code> 区间的和。</p>
  </li>
  <li>
    <p><strong>为什么不行：</strong>
时间复杂度为 O(n^2)，当 <code class="language-plaintext highlighter-rouge">nums</code> 数组的长度很大时，会导致性能问题，无法通过所有测试用例。</p>
  </li>
  <li><strong>最终解法：前缀和 + 哈希表</strong>
这是解决此类问题的经典优化方法。
    <ol>
      <li>定义 <code class="language-plaintext highlighter-rouge">pre[i]</code> 为 <code class="language-plaintext highlighter-rouge">nums[0..i-1]</code> 的元素和。那么，任意连续子数组 <code class="language-plaintext highlighter-rouge">nums[j..i]</code> 的和就可以表示为 <code class="language-plaintext highlighter-rouge">pre[i+1] - pre[j]</code>。</li>
      <li>我们要求的是 <code class="language-plaintext highlighter-rouge">pre[i+1] - pre[j] == k</code>。</li>
      <li>将这个等式变形为 <code class="language-plaintext highlighter-rouge">pre[j] = pre[i+1] - k</code>。</li>
      <li>这个等式是本题解法的关键。它的含义是：<strong>对于当前遍历到的位置 <code class="language-plaintext highlighter-rouge">i</code>，我们只需要找到在它之前，有多少个位置 <code class="language-plaintext highlighter-rouge">j</code> 的前缀和 <code class="language-plaintext highlighter-rouge">pre[j]</code> 恰好等于 <code class="language-plaintext highlighter-rouge">pre[i+1] - k</code></strong>。</li>
      <li>我们可以使用一个哈希表（HashMap）来记录每个前缀和值出现的次数。哈希表的 <code class="language-plaintext highlighter-rouge">key</code> 是前缀和的值，<code class="language-plaintext highlighter-rouge">value</code> 是该前-缀和值已经出现过的次数。</li>
      <li>我们从左到右遍历数组，计算当前的前缀和 <code class="language-plaintext highlighter-rouge">current_sum</code>。在哈希表中查找 <code class="language-plaintext highlighter-rouge">current_sum - k</code> 是否存在。如果存在，说明找到了若干个满足条件的子数组，将对应的 <code class="language-plaintext highlighter-rouge">value</code> 累加到最终结果 <code class="language-plaintext highlighter-rouge">ans</code> 中。</li>
      <li>查询完后，将当前的前缀和 <code class="language-plaintext highlighter-rouge">current_sum</code> 存入哈希表（或更新其出现次数），供后续的计算使用。</li>
      <li><strong>初始化技巧</strong>：为了处理从索引 0 开始的子数组（即 <code class="language-plaintext highlighter-rouge">pre[j]</code> 中 <code class="language-plaintext highlighter-rouge">j=0</code> 的情况，其前缀和为 0），需要在哈希表中预先放入 <code class="language-plaintext highlighter-rouge">(0, 1)</code>，表示和为 0 的前缀和已经出现过 1 次。</li>
    </ol>
  </li>
</ul>

<hr />

<h3 id="核心思路本质">核心思路（本质）</h3>

<p>本题的本质是将 <strong>“求解定值子数组和”</strong> 的问题，通过 <strong>“前缀和”</strong> 技巧，转化为一个 <strong>“寻找两个前缀和之差为定值”</strong> 的问题。这与 “两数之和” 问题的思想非常相似，因此可以利用 <strong>哈希表</strong> 来高效地进行查找，将时间复杂度从 O(n^2) 优化到 O(n)。</p>

<hr />

<h2 id="总结">总结</h2>
<p>（一句话总结触发器）</p>

<p>看到 “连续子数组和”、“定值 <code class="language-plaintext highlighter-rouge">k</code>” 且包含负数 → <strong>前缀和 + 哈希表</strong>。</p>]]></content><author><name>qiyu-lu</name></author><category term="Algorithms" /><category term="Algorithms" /><category term="LeetCode" /><summary type="html"><![CDATA[560. 和为 K 的子数组]]></summary></entry><entry><title type="html">力扣热题100刷题-twoPointers-11-盛最多水的容器</title><link href="https://qiyu-lu.github.io/algorithms/_11_ContainerWithMostWater/" rel="alternate" type="text/html" title="力扣热题100刷题-twoPointers-11-盛最多水的容器" /><published>2026-03-20T00:00:00+00:00</published><updated>2026-03-20T00:00:00+00:00</updated><id>https://qiyu-lu.github.io/algorithms/_11_ContainerWithMostWater</id><content type="html" xml:base="https://qiyu-lu.github.io/algorithms/_11_ContainerWithMostWater/"><![CDATA[<h1 id="leetcode-11-盛最多水的容器-container-with-most-water">LeetCode 11. 盛最多水的容器 (Container With Most Water)</h1>

<h2 id="题目描述">题目描述</h2>

<p>给定一个非负整数数组 <code class="language-plaintext highlighter-rouge">height</code> ，每个数代表一个点的坐标 <code class="language-plaintext highlighter-rouge">(i, height[i])</code>。在坐标系中画 <code class="language-plaintext highlighter-rouge">n</code> 条垂直线，第 <code class="language-plaintext highlighter-rouge">i</code> 条线的两个端点分别为 <code class="language-plaintext highlighter-rouge">(i, 0)</code> 和 <code class="language-plaintext highlighter-rouge">(i, height[i])</code>。找出其中的两条线，使得它们与 x 轴共同构成的容器可以容纳最多的水。</p>

<h2 id="核心思路双指针法">核心思路：双指针法</h2>

<p>这道题的经典解法是<strong>双指针法</strong>。我们使用两个指针，一个指向数组的开头（<code class="language-plaintext highlighter-rouge">left</code>），另一个指向数组的末尾（<code class="language-plaintext highlighter-rouge">right</code>）。这两个指针代表了容器的两条边。</p>

<p>容器的面积取决于两个因素：</p>
<ol>
  <li><strong>宽度</strong>：两条边之间的距离，即 <code class="language-plaintext highlighter-rouge">right - left</code>。</li>
  <li><strong>高度</strong>：由较短的那条边决定，即 <code class="language-plaintext highlighter-rouge">min(height[left], height[right])</code>。</li>
</ol>

<p>因此，面积 <code class="language-plaintext highlighter-rouge">Area = min(height[left], height[right]) * (right - left)</code>。</p>

<p>我们的目标是让这个面积最大化。</p>

<h2 id="算法步骤">算法步骤</h2>

<ol>
  <li>初始化左指针 <code class="language-plaintext highlighter-rouge">left = 0</code>，右指针 <code class="language-plaintext highlighter-rouge">right = height.length - 1</code>。</li>
  <li>初始化最大面积 <code class="language-plaintext highlighter-rouge">maxArea = 0</code>。</li>
  <li>当 <code class="language-plaintext highlighter-rouge">left &lt; right</code> 时，循环执行以下步骤：
a. 计算当前指针构成的容器面积：<code class="language-plaintext highlighter-rouge">currentArea = min(height[left], height[right]) * (right - left)</code>。
b. 更新最大面积：<code class="language-plaintext highlighter-rouge">maxArea = max(maxArea, currentArea)</code>。
c. <strong>移动指针</strong>：比较 <code class="language-plaintext highlighter-rouge">height[left]</code> 和 <code class="language-plaintext highlighter-rouge">height[right]</code> 的高度。为了找到可能更大的面积，我们选择移动指向<strong>较短</strong>边的那一侧指针。
    <ul>
      <li>如果 <code class="language-plaintext highlighter-rouge">height[left] &lt;= height[right]</code>，则 <code class="language-plaintext highlighter-rouge">left++</code>。</li>
      <li>否则，<code class="language-plaintext highlighter-rouge">right--</code>。</li>
    </ul>
  </li>
  <li>循环结束后，<code class="language-plaintext highlighter-rouge">maxArea</code> 即为所求答案。</li>
</ol>

<h2 id="为什么移动较短的边是正确的正确性证明">为什么移动较短的边是正确的？（正确性证明）</h2>

<p>这是一个贪心策略。我们来证明为什么每次移动较短的边，不会错过最优解。</p>

<p>假设我们当前的左右指针为 <code class="language-plaintext highlighter-rouge">left</code> 和 <code class="language-plaintext highlighter-rouge">right</code>，并且 <code class="language-plaintext highlighter-rouge">height[left] &lt; height[right]</code>。
此时的面积是 <code class="language-plaintext highlighter-rouge">Area = height[left] * (right - left)</code>。</p>

<p>我们有两个选择：</p>
<ol>
  <li>
    <p><strong>移动右指针 <code class="language-plaintext highlighter-rouge">right</code></strong>：<code class="language-plaintext highlighter-rouge">right--</code>。新的宽度变成了 <code class="language-plaintext highlighter-rouge">right - 1 - left</code>，比原来小。新的高度是 <code class="language-plaintext highlighter-rouge">min(height[left], height[right-1])</code>。因为 <code class="language-plaintext highlighter-rouge">height[left]</code> 已经是较短的边，所以新的高度最大也只能是 <code class="language-plaintext highlighter-rouge">height[left]</code>。宽度变小了，高度没有增加，所以新的面积 <code class="language-plaintext highlighter-rouge">Area_new = min(height[left], height[right-1]) * (right - 1 - left) &lt;= height[left] * (right - 1 - left) &lt; Area</code>。因此，移动较高的边（<code class="language-plaintext highlighter-rouge">right</code>）不可能得到更大的面积。</p>
  </li>
  <li>
    <p><strong>移动左指针 <code class="language-plaintext highlighter-rouge">left</code></strong>：<code class="language-plaintext highlighter-rouge">left++</code>。新的宽度 <code class="language-plaintext highlighter-rouge">right - (left + 1)</code> 也变小了。但新的高度 <code class="language-plaintext highlighter-rouge">min(height[left+1], height[right])</code> 可能会变大。如果 <code class="language-plaintext highlighter-rouge">height[left+1]</code> 足够大，就有可能弥补宽度减小的损失，从而得到一个更大的面积。</p>
  </li>
</ol>

<p>因此，在每一步中，我们固定较高的那条边，移动较低的那条边，才有可能找到一个更大的容器面积。这个策略保证了我们朝着面积可能增大的方向探索，最终能够找到全局最优解。</p>

<h2 id="总结">总结</h2>

<p>双指针法是解决此问题的关键。通过从两端向中间收缩，并始终移动较短的板，我们可以在 O(n) 的时间复杂度内有效地找到最大面积。这个方法之所以有效，是因为容器的面积由短板和宽度共同决定，移动短板是唯一可能找到更大面积的策略。</p>]]></content><author><name>qiyu-lu</name></author><category term="Algorithms" /><category term="Algorithms" /><category term="LeetCode" /><summary type="html"><![CDATA[LeetCode 11. 盛最多水的容器 (Container With Most Water)]]></summary></entry><entry><title type="html">力扣热题100刷题-twoPointers-15-三数之和</title><link href="https://qiyu-lu.github.io/algorithms/_15_ThreeSum/" rel="alternate" type="text/html" title="力扣热题100刷题-twoPointers-15-三数之和" /><published>2026-03-20T00:00:00+00:00</published><updated>2026-03-20T00:00:00+00:00</updated><id>https://qiyu-lu.github.io/algorithms/_15_ThreeSum</id><content type="html" xml:base="https://qiyu-lu.github.io/algorithms/_15_ThreeSum/"><![CDATA[<h1 id="leetcode-15-三数之和-3sum">LeetCode 15. 三数之和 (3Sum)</h1>

<h2 id="核心问题">核心问题</h2>
<p>在一个数组中找出所有和为 0 且不重复的三元组。</p>

<h2 id="思路演进如何想到最优解">思路演进：如何想到最优解？</h2>

<p>解决这类问题的通常思路是从暴力解法开始，逐步优化，最终得到高效的解法。</p>

<h3 id="阶段一暴力求解-on">阶段一：暴力求解 (O(n³))</h3>
<p>最直观的想法是穷举所有可能。用三层循环遍历数组，找出所有三个数的组合，判断它们的和是否为 0。</p>
<ul>
  <li><strong>伪代码</strong>:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>for i from 0 to n-1:
  for j from i+1 to n-1:
    for k from j+1 to n-1:
      if nums[i] + nums[j] + nums[k] == 0:
        add (nums[i], nums[j], nums[k]) to result
</code></pre></div>    </div>
  </li>
  <li><strong>问题</strong>:
    <ol>
      <li><strong>时间复杂度太高</strong>: O(n³) 无法通过大多数测试用例。</li>
      <li><strong>重复问题</strong>: 需要额外的数据结构（如 <code class="language-plaintext highlighter-rouge">HashSet</code>）对结果进行去重，增加了复杂性。</li>
    </ol>
  </li>
</ul>

<h3 id="阶段二降维---2sum-问题-on">阶段二：降维 -&gt; “2Sum” 问题 (O(n²))</h3>
<p>为了降低复杂度，我们可以尝试减少一层循环。思路是<strong>“固定一个数，找另外两个数”</strong>。</p>

<p>我们遍历数组，固定第一个数 <code class="language-plaintext highlighter-rouge">nums[i]</code>，那么问题就转化为：在数组的剩余部分中寻找两个数，使它们的和等于 <code class="language-plaintext highlighter-rouge">-nums[i]</code>。这就把“3Sum”问题降维成了我们熟悉的“2Sum”问题。</p>

<p><strong>如何解决这个内部的“2Sum”问题？</strong></p>
<ol>
  <li><strong>哈希表法</strong>: 遍历 <code class="language-plaintext highlighter-rouge">nums[i]</code> 之后的部分，对于每个 <code class="language-plaintext highlighter-rouge">nums[j]</code>，我们去哈希表中查找是否存在 <code class="language-plaintext highlighter-rouge">target - nums[j]</code>（其中 <code class="language-plaintext highlighter-rouge">target = -nums[i]</code>）。这方法可行，整体时间复杂度为 O(n²)，空间复杂度为 O(n)。（这对应了 <code class="language-plaintext highlighter-rouge">threeSumBF</code> 方法的思路）。</li>
  <li><strong>双指针法</strong>: 如果数组是有序的，解决“2Sum”问题会更高效。这就是我们走向最优解的关键一步。</li>
</ol>

<h3 id="阶段三排序--双指针-最优解-on">阶段三：排序 + 双指针 (最优解 O(n²))</h3>
<p>结合“降维”和“有序数组”这两个思想，最终的解法浮出水面。</p>

<ol>
  <li>
    <p><strong>排序 (Sort)</strong>: 首先对整个数组进行排序，时间复杂度 O(n log n)。排序是使用双指针的前提，并且极大地简化了去重操作。</p>
  </li>
  <li>
    <p><strong>遍历与固定 (Outer Loop)</strong>: 遍历排序后的数组，用 <code class="language-plaintext highlighter-rouge">for</code> 循环固定第一个数 <code class="language-plaintext highlighter-rouge">nums[i]</code>。</p>
  </li>
  <li><strong>双指针 (Inner Two Pointers)</strong>: 在 <code class="language-plaintext highlighter-rouge">nums[i]</code> 后面的区间 <code class="language-plaintext highlighter-rouge">[i+1, n-1]</code> 上，设置左指针 <code class="language-plaintext highlighter-rouge">left = i + 1</code> 和右指针 <code class="language-plaintext highlighter-rouge">right = n - 1</code>。
    <ul>
      <li>计算三数之和 <code class="language-plaintext highlighter-rouge">sum = nums[i] + nums[left] + nums[right]</code>。</li>
      <li>如果 <code class="language-plaintext highlighter-rouge">sum == 0</code>，说明找到了一个解。记录下来，然后同时移动 <code class="language-plaintext highlighter-rouge">left</code> 和 <code class="language-plaintext highlighter-rouge">right</code> 指针并<strong>跳过所有重复元素</strong>，继续寻找其他解。</li>
      <li>如果 <code class="language-plaintext highlighter-rouge">sum &lt; 0</code>，说明和太小了，需要增大。因为数组是有序的，所以移动左指针 <code class="language-plaintext highlighter-rouge">left++</code>。</li>
      <li>如果 <code class="language-plaintext highlighter-rouge">sum &gt; 0</code>，说明和太大了，需要减小。移动右指针 <code class="language-plaintext highlighter-rouge">right--</code>。</li>
    </ul>
  </li>
  <li><strong>去重 (Deduplication)</strong>:
    <ul>
      <li><strong>外层循环去重</strong>: 当我们固定 <code class="language-plaintext highlighter-rouge">nums[i]</code> 时，如果 <code class="language-plaintext highlighter-rouge">i &gt; 0</code> 且 <code class="language-plaintext highlighter-rouge">nums[i]</code> 和它前一个数 <code class="language-plaintext highlighter-rouge">nums[i-1]</code> 相等，说明以 <code class="language-plaintext highlighter-rouge">nums[i]</code> 开头能找到的三元组，之前以 <code class="language-plaintext highlighter-rouge">nums[i-1]</code> 开头时肯定都已经找到了。因此可以直接跳过，避免重复计算。</li>
      <li><strong>内层指针去重</strong>: 当 <code class="language-plaintext highlighter-rouge">sum == 0</code> 找到一个解后，需要将 <code class="language-plaintext highlighter-rouge">left</code> 和 <code class="language-plaintext highlighter-rouge">right</code> 指针移动到下一个不重复的位置，以防同一个三元组被多次记录。</li>
    </ul>
  </li>
</ol>

<p>这个“排序 + 双指针”的方案，将时间复杂度降到了 O(n²)，空间复杂度为 O(1)（不考虑结果存储空间），是此问题的最佳解法。</p>

<h2 id="举一反三从-3sum-到-k-sum">举一反三：从 “3Sum” 到 “K-Sum”</h2>

<p>这个“<strong>排序 + N指针</strong>”的思路可以推广到更一般性的“K-Sum”问题。</p>

<ul>
  <li><strong>2Sum (K=2)</strong>:
    <ul>
      <li>哈希表法: O(n) 时间, O(n) 空间。</li>
      <li>排序+双指针法: O(n log n) 时间, O(1) 空间。</li>
    </ul>
  </li>
  <li><strong>3Sum (K=3)</strong>:
    <ul>
      <li>转化为 2Sum: O(n²) 时间。<code class="language-plaintext highlighter-rouge">for + two_pointers</code>。</li>
    </ul>
  </li>
  <li><strong>4Sum (K=4)</strong>:
    <ul>
      <li>转化为 3Sum: O(n³) 时间。<code class="language-plaintext highlighter-rouge">for + 3Sum_logic</code>，即 <code class="language-plaintext highlighter-rouge">for + for + two_pointers</code>。</li>
      <li><strong>通用解法</strong>: 固定第一个数，递归调用 3Sum；或者固定前两个数，在剩余部分做 2Sum。</li>
    </ul>
  </li>
  <li><strong>K-Sum (通用)</strong>:
    <ul>
      <li><strong>递归思路</strong>:
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  function kSum(nums, target, k):
// 提前剪枝
if a few base cases fail, return []
      
if k == 2:
  return twoSum(nums, target) // 双指针解决
        
for i from 0 to n-1:
  // 去重
  if i &gt; 0 and nums[i] == nums[i-1]:
    continue
            
  // 递归调用 (k-1)Sum
  sub_results = (k-1)Sum(nums[i+1:], target - nums[i], k-1)
          
  // 组合结果
  for res in sub_results:
    result.add([nums[i]] + res)
        
return result
</code></pre></div>        </div>
      </li>
      <li>在每层递归中，都需要排序（或利用已排序的数组）和去重逻辑。通过这种递归+双指针的模式，可以将 K-Sum 问题在 O(n^(k-1)) 的时间复杂度内解决。</li>
    </ul>
  </li>
</ul>]]></content><author><name>qiyu-lu</name></author><category term="Algorithms" /><category term="Algorithms" /><category term="LeetCode" /><summary type="html"><![CDATA[LeetCode 15. 三数之和 (3Sum)]]></summary></entry><entry><title type="html">力扣热题100刷题-twoPointers-283-移动零</title><link href="https://qiyu-lu.github.io/algorithms/_283_MoveZeros/" rel="alternate" type="text/html" title="力扣热题100刷题-twoPointers-283-移动零" /><published>2026-03-20T00:00:00+00:00</published><updated>2026-03-20T00:00:00+00:00</updated><id>https://qiyu-lu.github.io/algorithms/_283_MoveZeros</id><content type="html" xml:base="https://qiyu-lu.github.io/algorithms/_283_MoveZeros/"><![CDATA[<h1 id="283移动零">283.移动零</h1>

<h2 id="题意">题意</h2>

<hr />

<h2 id="思路">思路</h2>

<p>我的最初的思路是：对于这个数组：使用双指针，一个指针p1指向0，一个指针p2指向第一个非0，然后找到了之后，将p2的值覆盖p1的值。然后两个同时后移。
终止条件是p2到达数组的末尾，此时将后面的非0数移动到前面了，然后就是要将后面的数变为0，要从p1开始后面的。</p>

<p>思路二：将数组看作栈，一个指针维护栈顶，一个指针维护当前变量，是0跳过，非0压入栈中，然后为了节省空间，可以直接使用原来的数组做栈。
之后将非0的数放入前面之后，将后面的几个变为0.</p>

<p>第一个思路和第二个思路都有一个不足，当数组全是0的话，都需要多次遍历，一次查找，一次变0.</p>

<p>思路三：上面思路的不足是因为非0数前移是覆盖，这个思路采用的是交换，保证两个数组之间维护着一组0，最终当右指针到了数组末尾的话，0就也到了末尾了。</p>

<hr />]]></content><author><name>qiyu-lu</name></author><category term="Algorithms" /><category term="Algorithms" /><category term="LeetCode" /><summary type="html"><![CDATA[283.移动零]]></summary></entry><entry><title type="html">力扣热题100刷题-twoPointers-42-接雨水</title><link href="https://qiyu-lu.github.io/algorithms/_42_TrappinRainWater/" rel="alternate" type="text/html" title="力扣热题100刷题-twoPointers-42-接雨水" /><published>2026-03-20T00:00:00+00:00</published><updated>2026-03-20T00:00:00+00:00</updated><id>https://qiyu-lu.github.io/algorithms/_42_TrappinRainWater</id><content type="html" xml:base="https://qiyu-lu.github.io/algorithms/_42_TrappinRainWater/"><![CDATA[<h1 id="42-接雨水">42. 接雨水</h1>

<h2 id="题意">题意</h2>

<p>给定一个非负整数数组 <code class="language-plaintext highlighter-rouge">height</code> ，每个整数代表一个柱子的高度，柱子的宽度为 1。计算下雨后，这些柱子能接多少雨水。</p>

<h2 id="思路">思路</h2>

<p>这道题是求解一个二维图形中可以容纳多少“水”，核心在于确定每个位置上方能蓄水的高度。</p>

<h3 id="思路一动态规划空间换时间">思路一：动态规划（空间换时间）</h3>

<p>最直观的想法是，对于数组中的每一个位置 <code class="language-plaintext highlighter-rouge">i</code>，它能接的雨水数量取决于其<strong>左侧最高柱子</strong>和<strong>右侧最高柱子</strong>中最矮的那个。具体来说，<code class="language-plaintext highlighter-rouge">i</code> 位置的水位高度是 <code class="language-plaintext highlighter-rouge">min(左侧最高柱子高度, 右侧最高柱子高度)</code>。那么，<code class="language-plaintext highlighter-rouge">i</code> 位置能接的雨水量就是 <code class="language-plaintext highlighter-rouge">水位高度 - height[i]</code>。</p>

<p>但是，每次都去遍历查找左右两侧的最高柱子，时间复杂度会达到 O(n^2)，效率不高。</p>

<p>我们可以优化这个过程。通过两次遍历，预先计算出每个位置的左侧最高和右侧最高。</p>
<ol>
  <li>创建一个数组 <code class="language-plaintext highlighter-rouge">preMax</code>，其中 <code class="language-plaintext highlighter-rouge">preMax[i]</code> 记录从 <code class="language-plaintext highlighter-rouge">0</code> 到 <code class="language-plaintext highlighter-rouge">i</code> 的最高柱子高度。</li>
  <li>创建一个数组 <code class="language-plaintext highlighter-rouge">sufMax</code>，其中 <code class="language-plaintext highlighter-rouge">sufMax[i]</code> 记录从 <code class="language-plaintext highlighter-rouge">i</code> 到 <code class="language-plaintext highlighter-rouge">n-1</code> 的最高柱子高度。</li>
  <li>再次遍历数组，对于每个位置 <code class="language-plaintext highlighter-rouge">i</code>，其能接的雨水量就是 <code class="language-plaintext highlighter-rouge">min(preMax[i], sufMax[i]) - height[i]</code>。</li>
</ol>

<p>这个方法将时间复杂度降到了 O(n)，但需要 O(n) 的额外空间。这对应了代码中的 <code class="language-plaintext highlighter-rouge">trapByArray</code> 方法。</p>

<h3 id="思路二双指针最优解">思路二：双指针（最优解）</h3>

<p>在思路一的基础上，我们思考是否可以优化空间复杂度，去掉额外的 <code class="language-plaintext highlighter-rouge">preMax</code> 和 <code class="language-plaintext highlighter-rouge">sufMax</code> 数组。</p>

<p>我们可以使用两个指针，<code class="language-plaintext highlighter-rouge">left</code> 从数组开头，<code class="language-plaintext highlighter-rouge">right</code> 从数组末尾，相向移动。同时维护两个变量，<code class="language-plaintext highlighter-rouge">preMax</code> 表示 <code class="language-plaintext highlighter-rouge">[0...left]</code> 区间的最高柱子，<code class="language-plaintext highlighter-rouge">sufMax</code> 表示 <code class="language-plaintext highlighter-rouge">[right...n-1]</code> 区间的最高柱子。</p>

<p>在 <code class="language-plaintext highlighter-rouge">left</code> 和 <code class="language-plaintext highlighter-rouge">right</code> 指针相遇之前，循环执行以下逻辑：</p>
<ul>
  <li>比较 <code class="language-plaintext highlighter-rouge">preMax</code> 和 <code class="language-plaintext highlighter-rouge">sufMax</code>。</li>
  <li>如果 <code class="language-plaintext highlighter-rouge">preMax &lt; sufMax</code>：
    <ul>
      <li>这意味着，对于 <code class="language-plaintext highlighter-rouge">left</code> 指针所在的位置，它的左侧最高墙（<code class="language-plaintext highlighter-rouge">preMax</code>）比右侧已经看到的最高墙（<code class="language-plaintext highlighter-rouge">sufMax</code>）要矮。由于 <code class="language-plaintext highlighter-rouge">sufMax</code> 只是 <code class="language-plaintext highlighter-rouge">[right...n-1]</code> 的最大值，整个数组 <code class="language-plaintext highlighter-rouge">[left...n-1]</code> 的真实最大值肯定不小于 <code class="language-plaintext highlighter-rouge">sufMax</code>。因此，<code class="language-plaintext highlighter-rouge">left</code> 位置的水位就由 <code class="language-plaintext highlighter-rouge">preMax</code> 决定了。</li>
      <li>计算 <code class="language-plaintext highlighter-rouge">left</code> 位置的雨水量：<code class="language-plaintext highlighter-rouge">preMax - height[left]</code>，累加到总结果中。</li>
      <li><code class="language-plaintext highlighter-rouge">left</code> 指针右移，并更新 <code class="language-plaintext highlighter-rouge">preMax</code>。</li>
    </ul>
  </li>
  <li>如果 <code class="language-plaintext highlighter-rouge">preMax &gt;= sufMax</code>：
    <ul>
      <li>同理，对于 <code class="language-plaintext highlighter-rouge">right</code> 指针所在的位置，它的右侧最高墙（<code class="language-plaintext highlighter-rouge">sufMax</code>）是瓶颈。<code class="language-plaintext highlighter-rouge">right</code> 位置的水位由 <code class="language-plaintext highlighter-rouge">sufMax</code> 决定。</li>
      <li>计算 <code class="language-plaintext highlighter-rouge">right</code> 位置的雨水量：<code class="language-plaintext highlighter-rouge">sufMax - height[right]</code>，累加到总结果中。</li>
      <li><code class="language-plaintext highlighter-rouge">right</code> 指针左移，并更新 <code class="language-plaintext highlighter-rouge">sufMax</code>。</li>
    </ul>
  </li>
</ul>

<p>这个方法非常巧妙，它在一次遍历中完成了计算，时间复杂度为 O(n)，空间复杂度为 O(1)。这对应了代码中的 <code class="language-plaintext highlighter-rouge">trapByTwoPointers</code> 方法。</p>

<h3 id="思路三单调栈">思路三：单调栈</h3>

<p>这道题也可以用单调栈来解决。栈里存放的是柱子的索引。我们维护一个单调递减栈。</p>
<ul>
  <li>当遍历到的当前柱子高度 <code class="language-plaintext highlighter-rouge">height[i]</code> 小于等于栈顶柱子高度时，将 <code class="language-plaintext highlighter-rouge">i</code> 入栈。</li>
  <li>当 <code class="language-plaintext highlighter-rouge">height[i]</code> 大于栈顶柱子高度时，说明出现了一个“凹”槽，可以蓄水。此时，栈顶元素 <code class="language-plaintext highlighter-rouge">top</code> 就是凹槽的底部。<code class="language-plaintext highlighter-rouge">top</code> 出栈后，新的栈顶元素 <code class="language-plaintext highlighter-rouge">left = st.peek()</code> 就是凹槽的左边界，而当前柱子 <code class="language-plaintext highlighter-rouge">i</code> 就是右边界。</li>
  <li>水的宽度是 <code class="language-plaintext highlighter-rouge">i - left - 1</code>，高度是 <code class="language-plaintext highlighter-rouge">min(height[left], height[i]) - height[top]</code>。</li>
  <li>如此循环，直到栈为空或当前柱子不再高于栈顶。</li>
</ul>

<p>单调栈提供了一个不同的视角，对于求解“最近的最大/最小值”这类问题非常有效。</p>

<h2 id="举一反三">举一反三</h2>

<p>这道题的几种解法都非常经典，体现了算法优化的常见思路。</p>

<ol>
  <li><strong>动态规划/空间换时间</strong>：预计算并存储重复计算的结果。这个思想在很多问题中都有应用，比如斐波那契数列、最长公共子序列等。</li>
  <li><strong>双指针</strong>：特别是相向双指针，常用于有序数组或需要从两端同时处理的问题。它能有效地将 O(n^2) 的暴力搜索优化到 O(n)。
    <ul>
      <li><strong>11. 盛最多水的容器</strong>：同样是双指针从两端向中间收敛，每次移动较短的那一边的指针。</li>
      <li><strong>15. 三数之和</strong>：在排序后，固定一个数，然后用双指针在剩余部分查找另外两个数。</li>
    </ul>
  </li>
  <li><strong>单调栈</strong>：适用于解决“下一个更大元素”、“上一个更小元素”等问题。
    <ul>
      <li><strong>84. 柱状图中最大的矩形</strong>：一个经典的单调栈应用，和本题的单调栈解法有相似之处。</li>
      <li><strong>739. 每日温度</strong>：用单调栈寻找下一个比当天温度高的日子。</li>
    </ul>
  </li>
</ol>

<p>通过本题，可以深刻理解以上几种数据结构和算法思想的巧妙之处，并学会在不同场景下选择合适的方案来优化时间和空间复杂度。</p>]]></content><author><name>qiyu-lu</name></author><category term="Algorithms" /><category term="Algorithms" /><category term="LeetCode" /><summary type="html"><![CDATA[42. 接雨水]]></summary></entry></feed>