[{"data":1,"prerenderedAt":1587},["ShallowReactive",2],{"\u002F2026\u002Fleetcode-1-100":3,"surround-\u002F2026\u002Fleetcode-1-100":1581},{"id":4,"title":5,"body":6,"categories":1535,"date":1537,"description":1538,"draft":1539,"extension":1508,"image":1540,"meta":1541,"navigation":1563,"path":1564,"permalink":1565,"published":1565,"readingTime":1566,"recommend":1565,"references":1565,"seo":1571,"sitemap":1572,"stem":1573,"tags":1574,"type":1578,"updated":1579,"__hash__":1580},"content\u002Fposts\u002F2026\u002FLeetcode 1 - 100.md","Leetcode 1 - 100",{"type":7,"value":8,"toc":1512},"minimark",[9],[10,11,12,13,17,35,39,43,56,68,71,74,81,87,100,111,115,124,139,142,145,161,174,178,189,196,200,209,220,227,233,236,246,250,262,269,273,282,292,295,303,312,324,327,346,350,362,369,373,382,385,388,403,406,409,415,422,433,437,448,455,459,468,471,482,488,495,510,513,516,530,534,544,551,555,564,571,574,577,592,599,610,613,626,630,641,648,652,661,668,671,686,689,695,699,710,717,721,730,733,749,769,772,778,782,792,799,803,812,819,831,834,837,856,859,865,869,891,898,902,911,914,921,924,945,951,964,968,979,986,990,999,1002,1008,1018,1026,1029,1062,1068,1074,1078,1088,1095,1099,1108,1119,1122,1144,1147,1153,1175,1178,1184,1188,1198,1205,1209,1218,1221,1229,1235,1250,1266,1269,1275,1279,1289,1296,1300,1309,1312,1320,1326,1329,1335,1339,1350,1357,1361,1370,1373,1376,1391,1397,1401,1411,1418,1422,1431,1434,1455,1461,1464,1470,1474,1486,1493,1496,1503],"quote",{},"先写题，再写清楚思路",[14,15,16],"h2",{"id":16},"这页怎么记",[18,19,22],"alert",{"title":20,"type":21},"记录方式","info",[23,24,25,29,32],"ul",{},[26,27,28],"li",{},"语言只写 C++",[26,30,31],{},"每题只保留当前最顺手的一种做法",[26,33,34],{},"尽量记住触发这个做法的关键词，而不是死背代码",[36,37,38],"p",{},"现在这页更像一本持续追加的题解手账。不是追求一题多解，也不是为了把每道题讲成教程，而是把当下最顺的思路钉下来，下次回来看一眼就能重新进入状态。",[14,40,42],{"id":41},"_1-两数之和","1. 两数之和",[36,44,45,52,55],{},[46,47,51],"a",{"href":48,"rel":49},"https:\u002F\u002Fleetcode.cn\u002Fproblems\u002Ftwo-sum\u002Fdescription\u002F",[50],"nofollow","题目链接",[53,54],"br",{},"\n关键词：哈希表 \u002F 一次遍历 \u002F 边走边查",[36,57,58,59,63,64,67],{},"这题的核心其实很直接：在遍历到当前位置 ",[60,61,62],"code",{"code":62},"j"," 时，判断前面是否已经出现过 ",[60,65,66],{"code":66},"target - nums[j]","。如果出现过，就说明答案已经形成；如果没有，就把当前值和下标记进哈希表，继续往后找。",[36,69,70],{},"也就是说，哈希表里存的是“我前面见过什么”，而不是“我现在要配谁”。这一点会自然导出正确的遍历顺序：必须先查，再存。因为题目要找的是两个不同位置的数，如果先把当前位置塞进去，就有机会把自己和自己错误配对。",[36,72,73],{},"因此这一题真正需要记住的，不是“要用哈希表”，而是“把查找历史信息这件事压进一次遍历里”。",[18,75,78],{"title":76,"type":77},"这题最容易写错在哪","question",[36,79,80],{},"通常不是哈希表本身，而是顺序。先查后存，才能保证找到的是前面已经出现过的那个数。",[36,82,83],{},[84,85,86],"strong",{},"复杂度",[23,88,89,95],{},[26,90,91,92],{},"时间 ",[60,93,94],{"code":94},"O(n)",[26,96,97,98],{},"空间 ",[60,99,94],{"code":94},[101,102,109],"pre",{"className":103,"code":105,"filename":106,"language":107,"meta":108},[104],"language-cpp","class Solution {\npublic:\n    vector\u003Cint> twoSum(vector\u003Cint>& nums, int target) {\n        unordered_map\u003Cint, int> mp;\n        int n = nums.size();\n        for (int j = 0; j \u003C n; j++) {\n            if (mp.count(target - nums[j])) {\n                return {mp[target - nums[j]], j};\n            }\n            mp[nums[j]] = j;\n        }\n        return {};\n    }\n};\n","two-sum.cpp","cpp","",[60,110,105],{"__ignoreMap":108},[14,112,114],{"id":113},"_2-两数相加","2. 两数相加",[36,116,117,121,123],{},[46,118,51],{"href":119,"rel":120},"https:\u002F\u002Fleetcode.cn\u002Fproblems\u002Fadd-two-numbers\u002Fdescription\u002F",[50],[53,122],{},"\n关键词：链表 \u002F 模拟 \u002F 虚拟头结点 \u002F 进位",[36,125,126,127,130,131,134,135,138],{},"这题本质上就是把小学竖式加法搬到链表上。每一轮从 ",[60,128,129],{"code":129},"l1","、",[60,132,133],{"code":133},"l2"," 各取一位，再加上上一轮留下来的进位 ",[60,136,137],{"code":137},"carry","，得到当前位的结果和下一轮的进位。",[36,140,141],{},"所以整个过程的关键不在于链表有多复杂，而在于状态是否维护完整。这里需要一路往后带着三个信息：当前结果链表接到哪里、当前两条输入链表走到哪里、当前还有没有进位。",[36,143,144],{},"一旦你接受这是一个“带状态的逐位模拟”问题，虚拟头结点就几乎是自然选择。它负责把结果链表的拼接过程变得稳定，不需要在每次新建节点时反复判断头结点是否为空。",[36,146,147,148,150,151,153,154,130,156,130,158,160],{},"最后循环条件也很重要：不是只要 ",[60,149,129],{"code":129}," 或 ",[60,152,133],{"code":133}," 没走完才继续，而是只要 ",[60,155,129],{"code":129},[60,157,133],{"code":133},[60,159,137],{"code":137}," 三者之一还没清空，循环就不能结束。",[18,162,164],{"title":163,"type":77},"这题最容易漏掉什么",[36,165,166,167,170,171,173],{},"最容易漏的是最后一位进位。比如 ",[60,168,169],{"code":169},"5 + 5"," 这种情况，链表都走完了，但 ",[60,172,137],{"code":137}," 还没处理完，必须再补一个节点。",[36,175,176],{},[84,177,86],{},[23,179,180,185],{},[26,181,91,182],{},[60,183,184],{"code":184},"O(max(m, n))",[26,186,97,187],{},[60,188,184],{"code":184},[101,190,194],{"className":191,"code":192,"filename":193,"language":107,"meta":108},[104],"class Solution {\npublic:\n    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {\n        ListNode dummy;\n        ListNode* cur = &dummy;\n        int carry = 0;\n        while (l1 || l2 || carry) {\n            int sum = carry;\n            sum += l1 ? l1->val : 0;\n            sum += l2 ? l2->val : 0;\n            cur->next = new ListNode(sum % 10);\n            cur = cur->next;\n            carry = sum \u002F 10;\n            l1 = l1 ? l1->next : nullptr;\n            l2 = l2 ? l2->next : nullptr;\n        }\n        return dummy.next;\n    }\n};\n","add-two-numbers.cpp",[60,195,192],{"__ignoreMap":108},[14,197,199],{"id":198},"_3-无重复字符的最长子串","3. 无重复字符的最长子串",[36,201,202,206,208],{},[46,203,51],{"href":204,"rel":205},"https:\u002F\u002Fleetcode.cn\u002Fproblems\u002Flongest-substring-without-repeating-characters\u002Fdescription\u002F",[50],[53,207],{},"\n关键词：滑动窗口 \u002F 哈希表 \u002F 左端点推进",[36,210,211,212,215,216,219],{},"这题的本质是维护一个始终“不含重复字符”的窗口。右端点 ",[60,213,214],{"code":214},"r"," 不断向右扩张，而左端点 ",[60,217,218],{"code":218},"l"," 只在窗口失去合法性的时候向前跳。",[36,221,222,223,226],{},"关键在于，左端点不是一点一点地挪，而是可以直接跳到重复字符上一次出现位置的下一位。因为一旦知道 ",[60,224,225],{"code":225},"s[r]"," 之前出现在哪里，那么在那个位置之前的所有起点都不可能让当前窗口重新合法，继续慢慢试没有意义。",[36,228,229,230,232],{},"于是问题就变成了两件事：用数组或哈希表记录每个字符最近一次出现的位置，以及在字符重复时，立刻把 ",[60,231,218],{"code":218}," 更新到合法位置。",[36,234,235],{},"这一题很适合拿来理解滑动窗口里“窗口移动不是匀速”的感觉。右端点每次都前进一步，但左端点有时不动，有时直接跨一大段。",[18,237,239],{"title":238,"type":77},"这题最关键的一步",[36,240,241,242,245],{},"不是判断有没有重复，而是当重复发生时，左端点要跳到 ",[60,243,244],{"code":244},"上一次出现位置 + 1","，并且这个位置还必须在当前窗口内部。",[36,247,248],{},[84,249,86],{},[23,251,252,256],{},[26,253,91,254],{},[60,255,94],{"code":94},[26,257,97,258,261],{},[60,259,260],{"code":260},"O(字符集大小)","，这里按 ASCII 记",[101,263,267],{"className":264,"code":265,"filename":266,"language":107,"meta":108},[104],"class Solution {\npublic:\n    int lengthOfLongestSubstring(string s) {\n        vector\u003Cint> mp(128, -1);\n        int n = s.size();\n        int ans = 0;\n        for (int l = 0, r = 0; r \u003C n; r++) {\n            if (mp[s[r]] >= l) {\n                l = mp[s[r]] + 1;\n            }\n            mp[s[r]] = r;\n            ans = max(ans, r - l + 1);\n        }\n        return ans;\n    }\n};\n","longest-substring-without-repeating-characters.cpp",[60,268,265],{"__ignoreMap":108},[14,270,272],{"id":271},"_4-寻找两个正序数组的中位数","4. 寻找两个正序数组的中位数",[36,274,275,279,281],{},[46,276,51],{"href":277,"rel":278},"https:\u002F\u002Fleetcode.cn\u002Fproblems\u002Fmedian-of-two-sorted-arrays\u002Fdescription\u002F",[50],[53,280],{},"\n关键词：二分 \u002F 划分数组 \u002F 中位数",[36,283,284,285,288,289,291],{},"这题第一次看很唬人，但想通之后核心就一句话：在两个有序数组里，找到一对划分点 ",[60,286,287],{"code":287},"i"," 和 ",[60,290,62],{"code":62},"，让左边总数等于右边，或者只多一个。",[36,293,294],{},"于是就有公式：",[101,296,301],{"className":297,"code":299,"language":300,"meta":108},[298],"language-text","i + j = (m + n + 1) \u002F 2\n","text",[60,302,299],{"__ignoreMap":108},[36,304,305,306,308,309,311],{},"有了这个式子，只要二分其中一个数组的划分点 ",[60,307,287],{"code":287},"，另一个数组的 ",[60,310,62],{"code":62}," 就跟着确定了。接下来要检查的是划分是否合法，也就是：",[23,313,314,319],{},[26,315,316],{},[60,317,318],{"code":318},"A[i - 1] \u003C= B[j]",[26,320,321],{},[60,322,323],{"code":323},"B[j - 1] \u003C= A[i]",[36,325,326],{},"一旦这两个条件成立，说明中位数已经被我们夹在划分线附近了。剩下只是根据总长度奇偶，决定取左边最大值，还是左右中间值的平均数。",[18,328,330],{"title":329,"type":77},"这题最容易卡在哪",[36,331,332,333,130,336,130,339,130,342,345],{},"不是公式本身，而是边界。",[60,334,335],{"code":335},"i == 0",[60,337,338],{"code":338},"i == m",[60,340,341],{"code":341},"j == 0",[60,343,344],{"code":344},"j == n"," 这些情况一定要单独兜住，不然二分很容易在数组边缘炸掉。",[36,347,348],{},[84,349,86],{},[23,351,352,357],{},[26,353,91,354],{},[60,355,356],{"code":356},"O(log(min(m, n)))",[26,358,97,359],{},[60,360,361],{"code":361},"O(1)",[101,363,367],{"className":364,"code":365,"filename":366,"language":107,"meta":108},[104],"class Solution {\npublic:\n    double findMedianSortedArrays(vector\u003Cint>& nums1, vector\u003Cint>& nums2) {\n        if (nums1.size() > nums2.size()) {\n            return findMedianSortedArrays(nums2, nums1);\n        }\n        int m = nums1.size(), n = nums2.size();\n        int l = 0, r = m;\n        while (l \u003C= r) {\n            int i = l + ((r - l) >> 1);\n            int j = (m + n + 1) \u002F 2 - i;\n\n            int a1 = i == 0 ? INT_MIN : nums1[i - 1];\n            int a2 = i == m ? INT_MAX : nums1[i];\n            int b1 = j == 0 ? INT_MIN : nums2[j - 1];\n            int b2 = j == n ? INT_MAX : nums2[j];\n\n            if (a1 \u003C= b2 && b1 \u003C= a2) {\n                if ((m + n) % 2 == 0) {\n                    return (max(a1, b1) + min(a2, b2)) \u002F 2.0;\n                } else {\n                    return max(a1, b1) * 1.0;\n                }\n            } else if (a1 > b2) {\n                r = i - 1;\n            } else {\n                l = i + 1;\n            }\n        }\n        return 0.0;\n    }\n};\n","median-of-two-sorted-arrays.cpp",[60,368,365],{"__ignoreMap":108},[14,370,372],{"id":371},"_5-最长回文子串","5. 最长回文子串",[36,374,375,379,381],{},[46,376,51],{"href":377,"rel":378},"https:\u002F\u002Fleetcode.cn\u002Fproblems\u002Flongest-palindromic-substring\u002Fdescription\u002F",[50],[53,380],{},"\n关键词：中心扩展 \u002F 奇偶讨论 \u002F 回文半径",[36,383,384],{},"这题最顺手的做法是中心扩展。因为回文串的定义本身就带着对称性，所以不如直接枚举“回文中心”，再向两边扩展。",[36,386,387],{},"不过这里要同时考虑两种中心：",[23,389,390,397],{},[26,391,392,393,396],{},"奇数长度回文，以 ",[60,394,395],{"code":395},"(i, i)"," 为中心",[26,398,399,400,396],{},"偶数长度回文，以 ",[60,401,402],{"code":402},"(i, i + 1)",[36,404,405],{},"对每个位置都分别做这两次扩展，就能得到以该位置为轴的最长回文长度。然后从中取更大的那个，用它去更新全局答案。",[36,407,408],{},"这一题真正需要记住的不是“回文怎么判”，而是扩展结束之后，如何从中心位置和回文长度反推出子串起点。这里用的是：",[101,410,413],{"className":411,"code":412,"language":300,"meta":108},[298],"start = i - (len - 1) \u002F 2\n",[60,414,412],{"__ignoreMap":108},[36,416,417,418,421],{},"其中 ",[60,419,420],{"code":420},"len"," 是当前中心能扩出的最长回文长度。这个式子同时兼容奇偶两种情况，所以写起来比较顺。",[18,423,425],{"title":424,"type":77},"为什么扩展函数返回 r - l - 1",[36,426,427,428,288,430,432],{},"因为循环退出时，",[60,429,218],{"code":218},[60,431,214],{"code":214}," 已经各自多走了一步，最后那一轮比较失败了，所以真实回文长度要把这多出来的两格扣掉。",[36,434,435],{},[84,436,86],{},[23,438,439,444],{},[26,440,91,441],{},[60,442,443],{"code":443},"O(n^2)",[26,445,97,446],{},[60,447,361],{"code":361},[101,449,453],{"className":450,"code":451,"filename":452,"language":107,"meta":108},[104],"class Solution {\npublic:\n    int expand(const string& s, int l, int r) {\n        int n = s.size();\n        while (l >= 0 && r \u003C n && s[l] == s[r]) {\n            l--;\n            r++;\n        }\n        return r - l - 1;\n    }\n\n    string longestPalindrome(string s) {\n        int n = s.size();\n        int start = 0, maxLen = 0;\n        for (int i = 0; i \u003C n; i++) {\n            int len1 = expand(s, i, i);\n            int len2 = expand(s, i, i + 1);\n            int len = max(len1, len2);\n            if (len > maxLen) {\n                maxLen = len;\n                start = i - (len - 1) \u002F 2;\n            }\n        }\n        return s.substr(start, maxLen);\n    }\n};\n","longest-palindromic-substring.cpp",[60,454,451],{"__ignoreMap":108},[14,456,458],{"id":457},"_6-z字形变换","6. Z字形变换",[36,460,461,465,467],{},[46,462,51],{"href":463,"rel":464},"https:\u002F\u002Fleetcode.cn\u002Fproblems\u002Fzigzag-conversion\u002Fdescription\u002F",[50],[53,466],{},"\n关键词：模拟 \u002F 方向切换 \u002F 按行收集",[36,469,470],{},"这题第一眼容易被题目里的“Z 字形”三个字吓住，但它本质上不是数学题，而是一个纯模拟问题。只要你接受“字符是按行来回走”的过程，后面就只是把过程还原出来。",[36,472,473,474,477,478,481],{},"比如 ",[60,475,476],{"code":476},"PAYPALISHIRING"," 在 ",[60,479,480],{"code":480},"numRows = 3"," 时，字符会在三行之间这样来回移动：",[101,483,486],{"className":484,"code":485,"language":300,"meta":108},[298],"P   A   H   N\nA P L S I I G\nY   I   R\n\nPAHNAPLSIIGYIR\n",[60,487,485],{"__ignoreMap":108},[36,489,490,491,494],{},"如果把每个字符所在的行号单独写出来，就会发现它其实是在 ",[60,492,493],{"code":493},"0 -> 1 -> 2 -> 1 -> 0"," 之间循环折返。于是最直接的做法就是：",[23,496,497,504,507],{},[26,498,499,500,503],{},"准备 ",[60,501,502],{"code":502},"numRows"," 个字符串",[26,505,506],{},"维护当前字符应该落在哪一行",[26,508,509],{},"维护当前方向是向下还是向上",[36,511,512],{},"每读到一个字符，就把它放进当前行；当走到最顶行或最底行时，切换方向。等所有字符都处理完，再把每一行按顺序拼接起来，就是最终答案。",[36,514,515],{},"这题真正值得记住的是“方向切换发生在边界”，而不是去死记某个下标规律。",[18,517,519],{"title":518,"type":77},"哪些情况可以直接返回原串",[36,520,521,522,525,526,529],{},"当 ",[60,523,524],{"code":524},"numRows \u003C= 1"," 时只有一行，或者当 ",[60,527,528],{"code":528},"numRows >= s.size()"," 时还没来得及折返就已经放完了，顺序都不会变化。",[36,531,532],{},[84,533,86],{},[23,535,536,540],{},[26,537,91,538],{},[60,539,94],{"code":94},[26,541,97,542],{},[60,543,94],{"code":94},[101,545,549],{"className":546,"code":547,"filename":548,"language":107,"meta":108},[104],"class Solution {\npublic:\n    string convert(string s, int numRows) {\n        int& n = numRows;\n        if (n \u003C= 1 || n >= s.size()) return s;\n        vector\u003Cstring> rows(n);\n        int row = 0;\n        int dir = -1;\n        for (char c : s) {\n            rows[row] += c;\n            if (row == 0 || row == n - 1) dir = -dir;\n            row += dir;\n        }\n        string ans;\n        for (auto& line : rows) {\n            ans += move(line);\n        }\n        return ans;\n    }\n};\n","zigzag-conversion.cpp",[60,550,547],{"__ignoreMap":108},[14,552,554],{"id":553},"_9-回文数","9. 回文数",[36,556,557,561,563],{},[46,558,51],{"href":559,"rel":560},"https:\u002F\u002Fleetcode.cn\u002Fproblems\u002Fpalindrome-number\u002Fdescription\u002F",[50],[53,562],{},"\n关键词：反转一半 \u002F 边界过滤 \u002F 整数回文",[36,565,566,567,570],{},"这题如果把整数整个反转出来，再去和原数比较，思路当然能走通，但很容易碰到溢出问题。比如接近 ",[60,568,569],{"code":569},"INT_MAX"," 的数字，一旦完整反转，结果就可能超界。",[36,572,573],{},"更顺手的办法是只反转一半数字。因为回文的左右两侧本来就是镜像关系，只要把后半段反转出来，再和前半段比较就够了。",[36,575,576],{},"这个思路要先做一轮边界过滤：",[23,578,579,582],{},[26,580,581],{},"负数不可能是回文",[26,583,584,585,588,589,591],{},"末位是 ",[60,586,587],{"code":587},"0"," 但本身又不是 ",[60,590,587],{"code":587}," 的数也不可能是回文，因为整数最高位不能是前导零",[36,593,594,595,598],{},"之后就不断把原数的末位搬到 ",[60,596,597],{"code":597},"rev"," 上，直到原数前半段长度不再大于反转后的后半段。最后分奇偶讨论：",[23,600,601,604],{},[26,602,603],{},"偶数位时，前后两半应该完全相等",[26,605,606,607],{},"奇数位时，反转出来的那一半会多带一个中间位，所以比较 ",[60,608,609],{"code":609},"x == rev \u002F 10",[36,611,612],{},"这题真正漂亮的地方就在于，它把“完整反转”这个危险动作缩成了“只反转一半”。",[18,614,616],{"title":615,"type":77},"为什么循环条件是 x > rev",[36,617,618,619,621,622,625],{},"因为我们只需要把后半段反转出来。一旦 ",[60,620,597],{"code":597}," 的位数追上甚至超过 ",[60,623,624],{"code":624},"x","，说明一半已经处理完了，再继续就不是“反转一半”了。",[36,627,628],{},[84,629,86],{},[23,631,632,637],{},[26,633,91,634],{},[60,635,636],{"code":636},"O(log10(x))",[26,638,97,639],{},[60,640,361],{"code":361},[101,642,646],{"className":643,"code":644,"filename":645,"language":107,"meta":108},[104],"class Solution {\npublic:\n    bool isPalindrome(int x) {\n        if (x == 0) return true;\n        if (x \u003C 0 || x % 10 == 0) return false;\n        int rev = 0;\n        while (x > rev) {\n            rev = rev * 10 + x % 10;\n            x \u002F= 10;\n        }\n        return x == rev || x == rev \u002F 10;\n    }\n};\n","palindrome-number.cpp",[60,647,644],{"__ignoreMap":108},[14,649,651],{"id":650},"_15-三数之和","15. 三数之和",[36,653,654,658,660],{},[46,655,51],{"href":656,"rel":657},"https:\u002F\u002Fleetcode.cn\u002Fproblems\u002F3sum\u002Fdescription\u002F",[50],[53,659],{},"\n关键词：排序 \u002F 双指针 \u002F 去重 \u002F 固定一个数",[36,662,663,664,667],{},"这题的标准思路是先排序，再固定第一个数 ",[60,665,666],{"code":666},"nums[i]","，把后面的区间当成一个两数之和问题来做。因为数组已经有序，所以剩下两个数可以用双指针从两端往中间逼近。",[36,669,670],{},"核心不只是“双指针”，而是“排序之后去重会变得很自然”：",[23,672,673,678],{},[26,674,675,677],{},[60,676,287],{"code":287}," 和前一个位置相同，就跳过，避免重复枚举第一位",[26,679,680,681,288,683,685],{},"找到一组答案后，",[60,682,218],{"code":218},[60,684,214],{"code":214}," 也都要跨过重复值，避免重复收集答案",[36,687,688],{},"这题真正要记住的是：排序把“去重”和“调指针”这两件事同时变简单了。",[18,690,692],{"title":691,"type":77},"为什么三数之和几乎都先排序",[36,693,694],{},"因为不排序的话，你很难一边移动指针一边判断该往哪边走，也很难优雅地去重。排序是这题的结构前提。",[36,696,697],{},[84,698,86],{},[23,700,701,705],{},[26,702,91,703],{},[60,704,443],{"code":443},[26,706,97,707,709],{},[60,708,361],{"code":361},"，不算答案",[101,711,715],{"className":712,"code":713,"filename":714,"language":107,"meta":108},[104],"class Solution {\npublic:\n    vector\u003Cvector\u003Cint>> threeSum(vector\u003Cint>& nums) {\n        int n = nums.size();\n        if (n \u003C 3) return {};\n        sort(nums.begin(), nums.end());\n        vector\u003Cvector\u003Cint>> ans;\n        for (int i = 0; i \u003C n - 2; i++) {\n            if (i > 0 && nums[i] == nums[i - 1]) continue;\n            for (int l = i + 1, r = n - 1; l \u003C r;) {\n                int sum = nums[i] + nums[l] + nums[r];\n                if (sum == 0) {\n                    ans.push_back({nums[i], nums[l], nums[r]});\n                    while (l \u003C r && nums[l] == nums[l + 1]) l++;\n                    while (l \u003C r && nums[r] == nums[r - 1]) r--;\n                    l++, r--;\n                } else if (sum > 0) {\n                    r--;\n                } else {\n                    l++;\n                }\n            }\n        }\n        return ans;\n    }\n};\n","three-sum.cpp",[60,716,713],{"__ignoreMap":108},[14,718,720],{"id":719},"_16-最接近的三数之和","16. 最接近的三数之和",[36,722,723,727,729],{},[46,724,51],{"href":725,"rel":726},"https:\u002F\u002Fleetcode.cn\u002Fproblems\u002F3sum-closest\u002Fdescription\u002F",[50],[53,728],{},"\n关键词：排序 \u002F 双指针 \u002F 维护最优答案",[36,731,732],{},"这题和三数之和的骨架几乎一样，也是先排序，再固定第一个数，然后用双指针去找后面两个数。不同点在于，这题不是找“刚好等于某个值”的所有组合，而是维护一个“当前最接近 target 的答案”。",[36,734,735,736,739,740,743,744,288,746,748],{},"所以每次算出 ",[60,737,738],{"code":738},"sum"," 之后，都要先检查它和 ",[60,741,742],{"code":742},"target"," 的距离是不是更小。如果更小，就更新答案。之后再根据 ",[60,745,738],{"code":738},[60,747,742],{"code":742}," 的大小关系去移动指针：",[23,750,751,757,763],{},[26,752,753,756],{},[60,754,755],{"code":755},"sum > target","，右指针左移",[26,758,759,762],{},[60,760,761],{"code":761},"sum \u003C target","，左指针右移",[26,764,765,768],{},[60,766,767],{"code":767},"sum == target","，可以直接返回，因为已经不可能比它更接近了",[36,770,771],{},"这题的重点不是去重，而是“每次移动指针前先更新当前最优值”。",[18,773,775],{"title":774,"type":77},"为什么这题也要排序",[36,776,777],{},"因为只有排序后，双指针移动才有方向感。你才能根据当前和偏大还是偏小，决定该动左边还是右边。",[36,779,780],{},[84,781,86],{},[23,783,784,788],{},[26,785,91,786],{},[60,787,443],{"code":443},[26,789,97,790],{},[60,791,361],{"code":361},[101,793,797],{"className":794,"code":795,"filename":796,"language":107,"meta":108},[104],"class Solution {\npublic:\n    int threeSumClosest(vector\u003Cint>& nums, int target) {\n        int n = nums.size();\n        sort(nums.begin(), nums.end());\n        int ans = nums[0] + nums[1] + nums[2];\n        for (int i = 0; i \u003C n - 2; i++) {\n            for (int l = i + 1, r = n - 1; l \u003C r;) {\n                int sum = nums[i] + nums[l] + nums[r];\n                if (abs(sum - target) \u003C abs(ans - target)) {\n                    ans = sum;\n                }\n                if (sum == target) {\n                    return ans;\n                } else if (sum > target) {\n                    r--;\n                } else {\n                    l++;\n                }\n            }\n        }\n        return ans;\n    }\n};\n","three-sum-closest.cpp",[60,798,795],{"__ignoreMap":108},[14,800,802],{"id":801},"_17-电话号码的字母组合","17. 电话号码的字母组合",[36,804,805,809,811],{},[46,806,51],{"href":807,"rel":808},"https:\u002F\u002Fleetcode.cn\u002Fproblems\u002Fletter-combinations-of-a-phone-number\u002Fdescription\u002F",[50],[53,810],{},"\n关键词：回溯 \u002F 递归 \u002F 路径构造",[36,813,814,815,818],{},"这题很适合拿来建立对回溯的第一印象。输入是一个数字串，比如 ",[60,816,817],{"code":817},"\"23\"","，其中：",[23,820,821,826],{},[26,822,823],{},[60,824,825],{"code":825},"2 -> abc",[26,827,828],{},[60,829,830],{"code":830},"3 -> def",[36,832,833],{},"也就是说，每一位数字都提供一组选项，而我们要枚举所有可能的拼接结果。这个结构天然就是一棵搜索树：当前位置选一个字符，递归处理下一位；回到上一层后撤销这次选择，再试另一个字符。",[36,835,836],{},"所以这题真正的模型是“路径枚举”：",[23,838,839,845,850],{},[26,840,841,844],{},[60,842,843],{"code":843},"path"," 记录当前已经选了什么",[26,846,847,849],{},[60,848,287],{"code":287}," 表示当前处理到第几位数字",[26,851,521,852,855],{},[60,853,854],{"code":854},"i == digits.size()"," 时，说明一条完整路径已经形成，可以收进答案",[36,857,858],{},"这是最典型的“进入分支 -> 做选择 -> 递归 -> 撤销选择”。",[18,860,862],{"title":861,"type":77},"为什么这题是回溯而不是动态规划",[36,863,864],{},"因为我们要的是全部组合，而不是某个最优值。每一位数字都会分出多条独立分支，这更像遍历搜索树，而不是复用子问题答案。",[36,866,867],{},[84,868,86],{},[23,870,871,887],{},[26,872,91,873,876,877,880,881,288,884],{},[60,874,875],{"code":875},"O(3^n * n)"," 到 ",[60,878,879],{"code":879},"O(4^n * n)","，取决于数字里有多少个 ",[60,882,883],{"code":883},"7",[60,885,886],{"code":886},"9",[26,888,97,889,709],{},[60,890,94],{"code":94},[101,892,896],{"className":893,"code":894,"filename":895,"language":107,"meta":108},[104],"class Solution {\npublic:\n    vector\u003Cstring> mp = {\n        \"\", \"\", \"abc\", \"def\", \"ghi\", \"jkl\",\n        \"mno\", \"pqrs\", \"tuv\", \"wxyz\"\n    };\n    vector\u003Cstring> ans;\n    string path;\n\n    void dfs(const string& digits, int i) {\n        if (i == digits.size()) {\n            ans.push_back(path);\n            return;\n        }\n        for (char c : mp[digits[i] - '0']) {\n            path.push_back(c);\n            dfs(digits, i + 1);\n            path.pop_back();\n        }\n    }\n\n    vector\u003Cstring> letterCombinations(string digits) {\n        if (digits.empty()) return {};\n        dfs(digits, 0);\n        return ans;\n    }\n};\n","letter-combinations-of-a-phone-number.cpp",[60,897,894],{"__ignoreMap":108},[14,899,901],{"id":900},"_28-找出字符串中第一个匹配项的下标","28. 找出字符串中第一个匹配项的下标",[36,903,904,908,910],{},[46,905,51],{"href":906,"rel":907},"https:\u002F\u002Fleetcode.cn\u002Fproblems\u002Ffind-the-index-of-the-first-occurrence-in-a-string\u002Fdescription\u002F",[50],[53,909],{},"\n关键词：KMP \u002F next 数组 \u002F 模式串匹配",[36,912,913],{},"这题当然可以用暴力匹配，但它更像是一个练 KMP 的入口题。KMP 的关键点不在于“记住代码”，而在于理解为什么失配之后不用把模式串整个退回去。",[36,915,916,917,920],{},"核心思想是：模式串自己身上也有重复结构。失配时，可以利用 ",[60,918,919],{"code":919},"next"," 数组，把模式串直接跳到“仍然可能匹配”的位置，而不是从头开始。",[36,922,923],{},"所以整套过程分成两步：",[23,925,926,936],{},[26,927,928,929,932,933,935],{},"先根据模式串 ",[60,930,931],{"code":931},"needle"," 构造 ",[60,934,919],{"code":919}," 数组",[26,937,938,939,941,942],{},"再用 ",[60,940,919],{"code":919}," 数组加速匹配主串 ",[60,943,944],{"code":944},"haystack",[36,946,947,948,950],{},"这题最值得记住的是：求 ",[60,949,919],{"code":919}," 和做匹配，本质上都是“当前位置失配后，看看还能退到哪里继续比较”。",[18,952,954],{"title":953,"type":77},"KMP 最难的地方通常在哪",[36,955,956,957,959,960,963],{},"通常不是主匹配过程，而是 ",[60,958,919],{"code":919}," 数组的定义和下标细节。只要把 ",[60,961,962],{"code":962},"next[j]"," 表示的含义想清楚，后面代码会顺很多。",[36,965,966],{},[84,967,86],{},[23,969,970,975],{},[26,971,91,972],{},[60,973,974],{"code":974},"O(m + n)",[26,976,97,977],{},[60,978,94],{"code":94},[101,980,984],{"className":981,"code":982,"filename":983,"language":107,"meta":108},[104],"class Solution {\npublic:\n    int strStr(string haystack, string needle) {\n        int m = haystack.size(), n = needle.size();\n        if (n == 0) return 0;\n        vector\u003Cint> next(n, 0);\n        next[0] = -1;\n        for (int j = 2, cn = 0; j \u003C n;) {\n            if (needle[j - 1] == needle[cn]) {\n                next[j++] = ++cn;\n            } else if (cn > 0) {\n                cn = next[cn];\n            } else {\n                j++;\n            }\n        }\n        int i = 0, j = 0;\n        while (i \u003C m && j \u003C n) {\n            if (haystack[i] == needle[j]) {\n                i++, j++;\n            } else if (j > 0) {\n                j = next[j];\n            } else {\n                i++;\n            }\n        }\n        return j == n ? i - j : -1;\n    }\n};\n","find-the-index-of-the-first-occurrence-in-a-string.cpp",[60,985,982],{"__ignoreMap":108},[14,987,989],{"id":988},"_31-下一个排列","31. 下一个排列",[36,991,992,996,998],{},[46,993,51],{"href":994,"rel":995},"https:\u002F\u002Fleetcode.cn\u002Fproblems\u002Fnext-permutation\u002Fdescription\u002F",[50],[53,997],{},"\n关键词：找规律 \u002F 从右往左 \u002F 交换 + 反转",[36,1000,1001],{},"这题最适合先拿一个例子看规律。比如：",[101,1003,1006],{"className":1004,"code":1005,"language":300,"meta":108},[298],"1 2 3 4\n1 2 4 3\n1 3 2 4\n1 3 4 2\n",[60,1007,1005],{"__ignoreMap":108},[36,1009,1010,1011,876,1014,1017],{},"从 ",[60,1012,1013],{"code":1013},"1 2 4 3",[60,1015,1016],{"code":1016},"1 3 2 4","，本质上发生了两件事：",[23,1019,1020,1023],{},[26,1021,1022],{},"从右往左找到第一个“还能变大一点”的位置",[26,1024,1025],{},"把这个位置换成右边刚好比它大的那个数，再把后缀重置成最小顺序",[36,1027,1028],{},"具体做法是：",[23,1030,1031,1040,1047,1055],{},[26,1032,1033,1034,1037,1038],{},"从右往左找第一个满足 ",[60,1035,1036],{"code":1036},"nums[i] \u003C nums[i + 1]"," 的位置 ",[60,1039,287],{"code":287},[26,1041,1042,1043,1037,1045],{},"再从右往左找第一个大于 ",[60,1044,666],{"code":666},[60,1046,62],{"code":62},[26,1048,1049,1050,288,1052],{},"交换 ",[60,1051,666],{"code":666},[60,1053,1054],{"code":1054},"nums[j]",[26,1056,1057,1058,1061],{},"最后反转 ",[60,1059,1060],{"code":1060},"i + 1"," 之后的区间",[36,1063,1064,1065,1067],{},"如果第一步根本找不到这样的 ",[60,1066,287],{"code":287},"，说明整个数组已经是降序，那么它的下一个排列就是最小排列，也就是整段反转。",[18,1069,1071],{"title":1070,"type":77},"为什么最后要反转后缀",[36,1072,1073],{},"因为从右往左找到断点时，右侧本来就是降序的。交换之后，只要把这段降序后缀反转成升序，它就是最小的合法后缀。",[36,1075,1076],{},[84,1077,86],{},[23,1079,1080,1084],{},[26,1081,91,1082],{},[60,1083,94],{"code":94},[26,1085,97,1086],{},[60,1087,361],{"code":361},[101,1089,1093],{"className":1090,"code":1091,"filename":1092,"language":107,"meta":108},[104],"class Solution {\npublic:\n    void nextPermutation(vector\u003Cint>& nums) {\n        int n = nums.size();\n        int i = n - 2;\n        while (i >= 0 && nums[i] >= nums[i + 1]) {\n            i--;\n        }\n        if (i >= 0) {\n            int j = n - 1;\n            while (j >= 0 && nums[j] \u003C= nums[i]) {\n                j--;\n            }\n            swap(nums[i], nums[j]);\n        }\n        reverse(nums.begin() + i + 1, nums.end());\n    }\n};\n","next-permutation.cpp",[60,1094,1091],{"__ignoreMap":108},[14,1096,1098],{"id":1097},"_32-最长有效括号","32. 最长有效括号",[36,1100,1101,1105,1107],{},[46,1102,51],{"href":1103,"rel":1104},"https:\u002F\u002Fleetcode.cn\u002Fproblems\u002Flongest-valid-parentheses\u002Fdescription\u002F",[50],[53,1106],{},"\n关键词：一维动态规划 \u002F 括号匹配 \u002F 向左追溯",[36,1109,1110,1111,1114,1115,1118],{},"这题很容易想到暴力枚举所有子串，但更顺的写法是一维动态规划。定义 ",[60,1112,1113],{"code":1113},"dp[i]"," 表示“以 ",[60,1116,1117],{"code":1117},"s[i]"," 结尾的最长有效括号长度”。",[36,1120,1121],{},"这个定义有两个好处：",[23,1123,1124,1134],{},[26,1125,1126,1127,1130,1131],{},"如果 ",[60,1128,1129],{"code":1129},"s[i] == '('","，那它不可能作为合法括号串结尾，",[60,1132,1133],{"code":1133},"dp[i] = 0",[26,1135,1126,1136,1139,1140,1143],{},[60,1137,1138],{"code":1138},"s[i] == ')'","，就去看它能不能和前面的某个 ",[60,1141,1142],{"code":1142},"'('"," 配对",[36,1145,1146],{},"关键位置是：",[101,1148,1151],{"className":1149,"code":1150,"language":300,"meta":108},[298],"p = i - dp[i - 1] - 1\n",[60,1152,1150],{"__ignoreMap":108},[36,1154,1155,1156,1159,1160,1162,1163,1166,1167,1170,1171,1174],{},"这里 ",[60,1157,1158],{"code":1158},"dp[i - 1]"," 代表前面已经配好的那一段长度，所以 ",[60,1161,36],{"code":36}," 就是“当前这个右括号理论上要去找的那个左括号”。如果 ",[60,1164,1165],{"code":1165},"p >= 0"," 且 ",[60,1168,1169],{"code":1169},"s[p] == '('","，那就说明当前位置又能新配出一对，并且还可能和 ",[60,1172,1173],{"code":1173},"p - 1"," 之前的有效括号串接起来。",[36,1176,1177],{},"这题真正漂亮的地方在于：它不是直接匹配括号，而是借助前一个状态，把需要回看的位置一步算出来。",[18,1179,1181],{"title":1180,"type":77},"为什么要定义成“以 i 结尾”",[36,1182,1183],{},"因为这样当前状态只依赖左边已经算好的结果，转移会非常自然。否则你会很难确定该从哪里往前接。",[36,1185,1186],{},[84,1187,86],{},[23,1189,1190,1194],{},[26,1191,91,1192],{},[60,1193,94],{"code":94},[26,1195,97,1196],{},[60,1197,94],{"code":94},[101,1199,1203],{"className":1200,"code":1201,"filename":1202,"language":107,"meta":108},[104],"class Solution {\npublic:\n    int longestValidParentheses(string s) {\n        int n = s.size();\n        if (n == 0) return 0;\n        vector\u003Cint> dp(n, 0);\n        int ans = 0;\n        for (int i = 1; i \u003C n; i++) {\n            if (s[i] == '(') continue;\n            int p = i - dp[i - 1] - 1;\n            if (p >= 0 && s[p] == '(') {\n                dp[i] = dp[i - 1] + 2 + (p > 0 ? dp[p - 1] : 0);\n                ans = max(ans, dp[i]);\n            }\n        }\n        return ans;\n    }\n};\n","longest-valid-parentheses.cpp",[60,1204,1201],{"__ignoreMap":108},[14,1206,1208],{"id":1207},"_43-接雨水","43. 接雨水",[36,1210,1211,1215,1217],{},[46,1212,51],{"href":1213,"rel":1214},"https:\u002F\u002Fleetcode.cn\u002Fproblems\u002Ftrapping-rain-water\u002Fdescription\u002F",[50],[53,1216],{},"\n关键词：双指针 \u002F 左右最大值 \u002F 按较短板结算",[36,1219,1220],{},"这题是双指针的经典题。",[36,1222,1223,288,1225,1228],{},[60,1224,587],{"code":587},[60,1226,1227],{"code":1227},"n - 1"," 位置本身接不了雨水，真正能结算的是中间位置。某个位置能接多少雨水，取决于它左边最高柱子和右边最高柱子里更矮的那个：",[101,1230,1233],{"className":1231,"code":1232,"language":300,"meta":108},[298],"min(leftMax, rightMax) - height[i]\n",[60,1234,1232],{"__ignoreMap":108},[36,1236,1237,1238,130,1240,1242,1243,130,1246,1249],{},"可以用前后缀数组做，也可以用单调栈做，但双指针是这里更顺手的写法。维护两个指针 ",[60,1239,218],{"code":218},[60,1241,214],{"code":214}," 和两边的最高值 ",[60,1244,1245],{"code":1245},"leftMax",[60,1247,1248],{"code":1248},"rightMax","：",[23,1251,1252,1261],{},[26,1253,1126,1254,1257,1258,1260],{},[60,1255,1256],{"code":1256},"leftMax \u003C= rightMax","，说明当前位置 ",[60,1259,218],{"code":218}," 的水量已经能确定，直接结算并右移",[26,1262,1263,1264],{},"反过来就结算 ",[60,1265,214],{"code":214},[36,1267,1268],{},"也就是说，这题真正要记住的是“谁矮先算谁”。",[10,1270,1272],{"icon":1271},"tabler:droplets",[36,1273,1274],{},"总之我的体感排序是：双指针 > 单调栈 \u002F 双数组前后缀 > 暴力。",[36,1276,1277],{},[84,1278,86],{},[23,1280,1281,1285],{},[26,1282,91,1283],{},[60,1284,94],{"code":94},[26,1286,97,1287],{},[60,1288,361],{"code":361},[101,1290,1294],{"className":1291,"code":1292,"filename":1293,"language":107,"meta":108},[104],"class Solution {\npublic:\n    int trap(vector\u003Cint>& height) {\n        int n = height.size();\n        int l = 1, r = n - 1;\n        int leftMax = height[0], rightMax = height[n - 1];\n        int ans = 0;\n        while (l \u003C= r) {\n            if (leftMax \u003C= rightMax) {\n                leftMax = max(leftMax, height[l]);\n                ans += leftMax - height[l];\n                l++;\n            } else {\n                rightMax = max(rightMax, height[r]);\n                ans += rightMax - height[r];\n                r--;\n            }\n        }\n        return ans;\n    }\n};\n","trapping-rain-water.cpp",[60,1295,1292],{"__ignoreMap":108},[14,1297,1299],{"id":1298},"_78-子集","78. 子集",[36,1301,1302,1306,1308],{},[46,1303,51],{"href":1304,"rel":1305},"https:\u002F\u002Fleetcode.cn\u002Fproblems\u002Fsubsets\u002Fdescription\u002F",[50],[53,1307],{},"\n关键词：回溯 \u002F 选或不选 \u002F 二叉决策树",[36,1310,1311],{},"子集问题是回溯里最基础的一类。对每个位置来说，本质上只有两个选择：",[23,1313,1314,1317],{},[26,1315,1316],{},"选当前元素",[26,1318,1319],{},"不选当前元素",[36,1321,1322,1323,1325],{},"所以整棵搜索树天然就是一棵二叉树。走到叶子节点时，当前路径 ",[60,1324,843],{"code":843}," 就代表一个完整子集，可以收进答案。",[36,1327,1328],{},"这题最适合拿来理解“回溯不一定是在排列组合里乱跳，它也可以只是很朴素地做二叉决策”。",[18,1330,1332],{"title":1331,"type":77},"为什么这题是在叶子节点收集答案",[36,1333,1334],{},"因为这套写法把“每个位置选还是不选”都当成必须做完的决策。只有当所有位置都处理完，当前路径才对应一个完整子集。",[36,1336,1337],{},[84,1338,86],{},[23,1340,1341,1346],{},[26,1342,91,1343],{},[60,1344,1345],{"code":1345},"O(2^n * n)",[26,1347,97,1348,709],{},[60,1349,94],{"code":94},[101,1351,1355],{"className":1352,"code":1353,"filename":1354,"language":107,"meta":108},[104],"class Solution {\npublic:\n    vector\u003Cvector\u003Cint>> ans;\n    vector\u003Cint> path;\n\n    void dfs(vector\u003Cint>& nums, int i) {\n        if (i == nums.size()) {\n            ans.push_back(path);\n            return;\n        }\n        path.push_back(nums[i]);\n        dfs(nums, i + 1);\n        path.pop_back();\n        dfs(nums, i + 1);\n    }\n\n    vector\u003Cvector\u003Cint>> subsets(vector\u003Cint>& nums) {\n        dfs(nums, 0);\n        return ans;\n    }\n};\n","subsets.cpp",[60,1356,1353],{"__ignoreMap":108},[14,1358,1360],{"id":1359},"_90-子集ii","90. 子集II",[36,1362,1363,1367,1369],{},[46,1364,51],{"href":1365,"rel":1366},"https:\u002F\u002Fleetcode.cn\u002Fproblems\u002Fsubsets-ii\u002Fdescription\u002F",[50],[53,1368],{},"\n关键词：回溯 \u002F 排序去重 \u002F 枚举下一个选谁",[36,1371,1372],{},"这题和 78. 子集 很像，但多了一个“数组里允许重复元素”的条件，所以重点从“怎么枚举”变成了“怎么避免重复答案”。",[36,1374,1375],{},"更顺手的写法不是“选或不选”，而是“当前层从哪个位置开始，枚举下一个选谁”。在这种视角下：",[23,1377,1378,1381,1388],{},[26,1379,1380],{},"搜索树上的每个节点都代表一个合法子集",[26,1382,1383,1384,1387],{},"所以进入 ",[60,1385,1386],{"code":1386},"dfs"," 的一开始就可以收集当前路径",[26,1389,1390],{},"排序之后，如果同一层里遇到相同元素，就跳过后面的重复值",[18,1392,1394],{"title":1393,"type":77},"为什么去重要写成 i > start",[36,1395,1396],{},"因为我们只想跳过“同一层的重复选择”，不能把不同层里本来合法的相同数字也一起跳掉。",[36,1398,1399],{},[84,1400,86],{},[23,1402,1403,1407],{},[26,1404,91,1405],{},[60,1406,1345],{"code":1345},[26,1408,97,1409,709],{},[60,1410,94],{"code":94},[101,1412,1416],{"className":1413,"code":1414,"filename":1415,"language":107,"meta":108},[104],"class Solution {\npublic:\n    vector\u003Cvector\u003Cint>> ans;\n    vector\u003Cint> path;\n\n    void dfs(vector\u003Cint>& nums, int start) {\n        ans.push_back(path);\n        for (int i = start; i \u003C nums.size(); i++) {\n            if (i > start && nums[i] == nums[i - 1]) continue;\n            path.push_back(nums[i]);\n            dfs(nums, i + 1);\n            path.pop_back();\n        }\n    }\n\n    vector\u003Cvector\u003Cint>> subsetsWithDup(vector\u003Cint>& nums) {\n        sort(nums.begin(), nums.end());\n        dfs(nums, 0);\n        return ans;\n    }\n};\n","subsets-ii.cpp",[60,1417,1414],{"__ignoreMap":108},[14,1419,1421],{"id":1420},"_93-复原ip地址","93. 复原IP地址",[36,1423,1424,1428,1430],{},[46,1425,51],{"href":1426,"rel":1427},"https:\u002F\u002Fleetcode.cn\u002Fproblems\u002Frestore-ip-addresses\u002Fdescription\u002F",[50],[53,1429],{},"\n关键词：回溯 \u002F 分段枚举 \u002F 合法性检查",[36,1432,1433],{},"这题本质上是在字符串里切三刀，把它分成四段。每一段都必须满足 IPv4 的要求：",[23,1435,1436,1442,1448],{},[26,1437,1438,1439],{},"长度在 ",[60,1440,1441],{"code":1441},"1 ~ 3",[26,1443,1444,1445],{},"数值在 ",[60,1446,1447],{"code":1447},"0 ~ 255",[26,1449,1450,1451,1454],{},"不能有前导零，比如 ",[60,1452,1453],{"code":1453},"\"01\""," 不合法",[36,1456,1457,1458,1460],{},"所以最自然的做法就是回溯：每次从当前位置切出 ",[60,1459,1441],{"code":1441}," 个字符，判断这一段是否合法；如果合法，就加入路径并继续处理下一段。等收集到 4 段且刚好把字符串用完时，就得到一个合法答案。",[36,1462,1463],{},"这题很适合练“回溯不是瞎搜，而是边搜边剪枝”。只要当前段不合法，就立刻停，不必继续往下递归。",[18,1465,1467],{"title":1466,"type":77},"这题最值得先剪掉什么",[36,1468,1469],{},"最值钱的剪枝通常是前导零和大于 255。因为这两类一旦出现，继续往下扩已经没有意义了。",[36,1471,1472],{},[84,1473,86],{},[23,1475,1476,1481],{},[26,1477,91,1478],{},[60,1479,1480],{"code":1480},"O(3^4)",[26,1482,97,1483,709],{},[60,1484,1485],{"code":1485},"O(4)",[101,1487,1491],{"className":1488,"code":1489,"filename":1490,"language":107,"meta":108},[104],"class Solution {\npublic:\n    vector\u003Cstring> ans;\n    vector\u003Cstring> path;\n    vector\u003Cstring> restoreIpAddresses(string s) {\n        if(s.size() \u003C 4 || s.size() > 12) return {};\n        dfs(s, 0);\n        return ans;\n    }\n    void dfs(const string& s,int i) {\n        if(path.size() == 4) {\n            if(s.size() == i) {\n                ans.push_back(path[0]+\".\"+path[1]+\".\"+path[2]+\".\"+path[3]);\n            }\n            return;\n        }\n        for(int len=1;len\u003C=3;len++) {\n            if(i+len > s.size()) break; \u002F\u002F 越界了\n            string seg = s.substr(i, len);\n            if(seg.size() > 1 && seg[0] == '0') continue; \u002F\u002F 前导0\n            int num = 0;\n            for(int j=0;j\u003Cseg.size();j++) {\n                num = num * 10 + (seg[j] - '0');\n            }\n            if (num > 255) continue; \u002F\u002F 超过范围了\n\n            path.push_back(seg);\n            dfs(s, i+len);\n            path.pop_back();\n        }\n    }\n};\n","restore_ip_address.cpp",[60,1492,1489],{"__ignoreMap":108},[14,1494,1495],{"id":1495},"暂时写到这里",[18,1497,1500],{"title":1498,"type":1499},"当前进度","warning",[36,1501,1502],{},"这一页现在还是边刷边记的状态，先把最常写的题和最顺手的解法沉淀下来。后面继续往下补时，我会尽量把每题都整理成同一套节奏。",[101,1504,1510],{"className":1505,"code":1507,"language":1508,"meta":1509},[1506],"language-md","谢谢 Leetcode。写题的时候像机器，整理的时候才发现自己到底会了没有。\n","md","wrap",[60,1511,1507],{"__ignoreMap":108},{"title":108,"searchDepth":1513,"depth":1513,"links":1514},4,[1515,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534],{"id":16,"depth":1516,"text":16},2,{"id":41,"depth":1516,"text":42},{"id":113,"depth":1516,"text":114},{"id":198,"depth":1516,"text":199},{"id":271,"depth":1516,"text":272},{"id":371,"depth":1516,"text":372},{"id":457,"depth":1516,"text":458},{"id":553,"depth":1516,"text":554},{"id":650,"depth":1516,"text":651},{"id":719,"depth":1516,"text":720},{"id":801,"depth":1516,"text":802},{"id":900,"depth":1516,"text":901},{"id":988,"depth":1516,"text":989},{"id":1097,"depth":1516,"text":1098},{"id":1207,"depth":1516,"text":1208},{"id":1298,"depth":1516,"text":1299},{"id":1359,"depth":1516,"text":1360},{"id":1420,"depth":1516,"text":1421},{"id":1495,"depth":1516,"text":1495},[1536],"技术","2026-04-22 22:44:05","目前在刷 Leetcode 1 - 100，这里按题号记录思路、模板和一点做题时的直觉。",false,"\u002Fimages\u002Fposts\u002F0001_leetcode_hot100.png",{"aside":1542,"slots":1545},[1543,1544],"toc","meta-aside-track",{"aside-track":1546},{"props":1547,"type":7,"value":1550},{":card":1548,"card":108,"title":1549},"true","刷题索引",[1551],[23,1552,1553,1554,1553,1557,1553,1560,1553],{},"\n",[26,1555,1556],{},"只写 C++",[26,1558,1559],{},"先记思路，再贴代码",[26,1561,1562],{},"先把 1 - 100 刷顺",true,"\u002F2026\u002Fleetcode-1-100",null,{"text":1567,"minutes":1568,"time":1569,"words":1570},"32 min read",31.36,1881600,6272,{"title":5,"description":1538},{"loc":1564},"posts\u002F2026\u002FLeetcode 1 - 100",[1575,1576,1577],"C++","LeetCode","算法","tech","2026-04-30 11:30:00","ZncTrJEJRsF0FKIY3a63QWiH2z1qJE5LyyDRLRCMCrE",[1565,1582],{"title":1583,"path":1584,"stem":1585,"date":1586,"type":1578,"children":-1},"high-concurrency-runtime:调度层设计","\u002F2026\u002Fhigh-concurrency-runtime","posts\u002F2026\u002Fhigh-concurrency-runtime\u002F调度层设计","2026-04-24 00:10:39",1777518299185]