[hot100 NO.19~24]

19🎈. 螺旋矩阵

螺旋矩阵遍历算法解析

这个算法的核心思路是通过维护四个边界(上、下、左、右),按 "右→下→左→上" 的顺序逐层遍历矩阵,每遍历完一条边就收缩对应边界,直到边界交叉(遍历完成)。

一、核心变量定义

cpp 复制代码
int u = 0;          // 上边界(初始为第0行)
int d = matrix.size() - 1;  // 下边界(初始为最后一行)
int l = 0;          // 左边界(初始为第0列)
int r = matrix[0].size() - 1;  // 右边界(初始为最后一列)
vector<int> ans;    // 存储遍历结果

二、遍历逻辑(循环体)

循环中按右→下→左→上的顺序遍历,每完成一条边就收缩边界,并检查是否已遍历完成(边界交叉)。

1. 第一步:从左到右遍历 "上边界" 行
cpp 复制代码
for(int i = l; i <= r; ++i) ans.push_back(matrix[u][i]); // 向右遍历上边界行
if(++ u > d) break; // 上边界向下收缩,若上边界超过下边界,说明遍历完成
  • 遍历范围:左边界 l → 右边界 r,行固定为 u(上边界)。
  • 收缩边界:上边界 u 加 1(后续不再遍历这一行)。
  • 终止检查:如果新的上边界 u 大于下边界 d,说明所有行已遍历,直接退出循环。
2. 第二步:从上到下遍历 "右边界" 列
cpp 复制代码
for(int i = u; i <= d; ++i) ans.push_back(matrix[i][r]); // 向下遍历右边界列
if(-- r < l) break; // 右边界向左收缩,若右边界小于左边界,遍历完成
  • 遍历范围:上边界 u → 下边界 d,列固定为 r(右边界)。
  • 收缩边界:右边界 r 减 1(后续不再遍历这一列)。
  • 终止检查:如果新的右边界 r 小于左边界 l,说明所有列已遍历,退出循环。
3. 第三步:从右到左遍历 "下边界" 行
cpp 复制代码
for(int i = r; i >= l; --i) ans.push_back(matrix[d][i]); // 向左遍历下边界行
if(-- d < u) break; // 下边界向上收缩,若下边界小于上边界,遍历完成
  • 遍历范围:右边界 r → 左边界 l,行固定为 d(下边界)。
  • 收缩边界:下边界 d 减 1(后续不再遍历这一行)。
  • 终止检查:如果新的下边界 d 小于上边界 u,退出循环。
4. 第四步:从下到上遍历 "左边界" 列
cpp 复制代码
for(int i = d; i >= u; --i) ans.push_back(matrix[i][l]); // 向上遍历左边界列
if(++ l > r) break; // 左边界向右收缩,若左边界大于右边界,遍历完成
  • 遍历范围:下边界 d → 上边界 u,列固定为 l(左边界)。
  • 收缩边界:左边界 l 加 1(后续不再遍历这一列)。
  • 终止检查:如果新的左边界 l 大于右边界 r,退出循环。

三、示例演示

以矩阵 [[1,2,3],[4,5,6],[7,8,9]] 为例:

步骤 遍历范围 结果 ans 边界变化
1 上边界行(0 行):1→2→3 [1,2,3] u=1(上边界下移)
2 右边界列(2 列):4→5→6?不,u=1→d=2 → 6→9 [1,2,3,6,9] r=1(右边界左移)
3 下边界行(2 行):8→7 [1,2,3,6,9,8,7] d=1(下边界上移)
4 左边界列(0 列):4 [1,2,3,6,9,8,7,4] l=1(左边界右移)
循环再次执行 上边界行(1 行):5 [1,2,3,6,9,8,7,4,5] u=2 > d=1 → 退出

最终结果:[1,2,3,6,9,8,7,4,5],符合螺旋遍历顺序。

四、关键注意点

  1. 空矩阵处理 :开头判断 matrix.empty(),避免越界。
  2. 边界检查时机:每遍历完一条边就检查边界是否交叉,而非最后统一检查,避免重复遍历或越界。
  3. 遍历方向 :向左 / 向上遍历时,循环变量需从大到小(i--/i>=)。

五、复杂度分析

  • 时间复杂度:O (m×n),每个元素仅被遍历一次(m 行 n 列)。
  • 空间复杂度:O (1)(除结果数组外,仅使用常数级变量)。

这个算法的优势是逻辑清晰、无冗余遍历,是螺旋矩阵遍历的最优解法之一。


20🎈. 旋转图像

矩阵旋转(顺时针 90 度)算法解析

这个算法的核心思路是分块原地旋转:将矩阵按 "层" 划分,每个元素与它旋转后对应的另外 3 个元素形成一组,通过临时变量完成 4 个元素的循环交换,最终实现整体顺时针旋转 90 度。

一、核心前提:旋转规律

对于 n×n 的正方形矩阵,顺时针旋转 90 度后,原位置 (i, j) 的元素会移动到 (j, n-1-i),且 4 个位置形成一个循环:

cpp 复制代码
原位置 A(i, j) → 位置 B(j, n-1-i) → 位置 C(n-1-i, n-1-j) → 位置 D(n-1-j, i) → 回到 A(i, j)

举例(n=3):原位置 (0,0)(0,2)(2,2)(2,0) → 回到 (0,0)

二、变量与循环范围

cpp 复制代码
int n = matrix.size(); // 矩阵边长(n×n)
// 外层循环:遍历"层",只需处理前 n/2 层(内层循环处理每层的元素)
for (int i = 0; i < n / 2; i++) {
    // 内层循环:遍历每层需要交换的元素,范围是 (n+1)/2
    for (int j = 0; j < (n + 1) / 2; j++) {
        // ... 交换逻辑
    }
}
循环范围的关键解释:
  • 外层循环 i < n/2:矩阵按 "环" 分层,比如 n=3 时只有 1 层(i=0),n=4 时有 2 层(i=0、1),只需处理前半层即可覆盖所有需要交换的元素。
  • 内层循环 j < (n+1)/2 :避免重复交换,比如:
    • n=3(奇数):(3+1)/2=2,j=0、1(覆盖每层的 2 个元素,即可完成所有 4 元组交换);
    • n=4(偶数):(4+1)/2=2(整数除法),j=0、1(覆盖每层的 2 个元素)。

三、核心交换逻辑

cpp 复制代码
// 保存原位置 A(i,j) 的值到临时变量
int tmp = matrix[i][j];
// D → A:把位置 D 的值放到 A
matrix[i][j] = matrix[n - 1 - j][i];
// C → D:把位置 C 的值放到 D
matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j];
// B → C:把位置 B 的值放到 C
matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i];
// 原A(tmp)→ B:把临时变量的值放到 B
matrix[j][n - 1 - i] = tmp;

交换步骤拆解(以 4 元组 A→B→C→D→A 为例):

  1. 先保存 A 的值(避免被覆盖);
  2. D 放到 A 的位置;
  3. C 放到 D 的位置;
  4. B 放到 C 的位置;
  5. 把最初保存的 A 放到 B 的位置;最终完成 4 个元素的顺时针旋转。

四、示例演示(n=3,矩阵 [[1,2,3],[4,5,6],[7,8,9]]

第一步:i=0(第一层),j=0
  • 4 元组位置:A (0,0)=1、B (0,2)=3、C (2,2)=9、D (2,0)=7
  • 交换过程:
    • tmp = 1;
    • matrix[0][0] = matrix[2][0] → (0,0)=7;
    • matrix[2][0] = matrix[2][2] → (2,0)=9;
    • matrix[2][2] = matrix[0][2] → (2,2)=3;
    • matrix[0][2] = tmp → (0,2)=1;
  • 矩阵变为:[[7,2,1],[4,5,6],[9,8,3]]
第二步:i=0,j=1
  • 4 元组位置:A (0,1)=2、B (1,2)=6、C (2,1)=8、D (1,0)=4
  • 交换过程:
    • tmp = 2;
    • matrix[0][1] = matrix[1][0] → (0,1)=4;
    • matrix[1][0] = matrix[2][1] → (1,0)=8;
    • matrix[2][1] = matrix[1][2] → (2,1)=6;
    • matrix[1][2] = tmp → (1,2)=2;
  • 矩阵最终变为:[[7,4,1],[8,5,2],[9,6,3]](符合顺时针旋转 90 度的结果)。

五、复杂度分析

  • 时间复杂度:O (n²),每个元素仅被交换一次(总共 n² 个元素)。
  • 空间复杂度 :O (1),仅使用临时变量 tmp,原地旋转无额外空间开销。

六、关键注意点

  1. 仅适用于正方形矩阵 :代码中默认输入是 n×n 矩阵,若为非正方形需额外处理(但题目通常要求正方形旋转);
  2. 循环范围的准确性n/2(n+1)/2 是避免重复交换的核心,若范围错误会导致元素被重复旋转或遗漏;
  3. 4 元组位置的对应关系 :需牢记 (i,j) ↔ (n-1-j,i) ↔ (n-1-i,n-1-j) ↔ (j,n-1-i) 的映射规律,这是交换的核心。

21🎈. 搜索二维矩阵 II

解题思路:

若使用暴力法遍历矩阵 matrix ,则时间复杂度为 O(NM) 。暴力法未利用矩阵 "从上到下递增、从左到右递增" 的特点,显然不是最优解法。

该算法利用矩阵的行递增、列递增特性 ,选择矩阵左下角 作为起始点,通过 "比较 - 移动" 的方式逐步缩小搜索范围,时间复杂度为O(m+n)(m 为行数,n 为列数),空间复杂度为**(O(1)**,是最优解法之一。

如下图所示,我们将矩阵逆时针旋转 45° ,并将其转化为图形式,发现其类似于 二叉搜索树 ,即对于每个元素,其左分支元素更小、右分支元素更大。因此,通过从 "根节点" 开始搜索,遇到比 target 大的元素就向左,反之向右,即可找到目标值 target

以左下角为起点的合理性:

情况 移动方向 逻辑依据
当前值 > target 向上 列递增 → 当前列下方的数都比当前值大,不可能是 target,只能向上找更小的数
当前值 < target 向右 行递增 → 当前行左侧的数都比当前值小,不可能是 target,只能向右找更大的数
当前值 == target 返回 true 找到目标

边界情况说明

  1. 矩阵只有一行:起始点为该行第一个元素,若target更大则向右移动,直到找到或越界;
  2. 矩阵只有一列:起始点为最后一行该列,若target更小则向上移动,直到找到或越界;
  3. target 小于矩阵最小值:向上移动直到 i=-1,返回 false
  4. target 大于矩阵最大值:向右移动直到j = 列数 ,返回false

复杂度分析:

时间复杂度O(M+N) :其中,N 和 M 分别为矩阵行数和列数,此算法最多循环 M+N 次。

空间复杂度 O(1) : i, j指针使用常数大小额外空间。


🔥链表

22🎈. 相交链表

一、核心思路

该算法利用双指针 "对齐长度差" 的思想,解决两个单链表的相交节点查找问题。核心逻辑是:让两个指针分别遍历两个链表,遍历完自身链表后切换到另一个链表的头节点继续遍历,最终两个指针会在相交节点相遇(或同时到达 nullptr,表示无相交)。时间复杂度 \(O(m+n)\)(\(m、n\) 为两链表长度),空间复杂度 \(O(1)\),是最优解法。

二、问题前提与关键特性

  1. 相交链表定义 :两个链表相交,指的是节点地址相同(而非值相同),相交后两个链表共享后续所有节点(形状像 "Y",而非 "X");
  2. 若两链表相交,设:
    • 链表 A:a 个节点(不包含相交部分) + c 个公共节点;
    • 链表 B:b 个节点(不包含相交部分) + c 个公共节点;
    • 则指针 A 遍历路径:a + c + b,指针 B 遍历路径:b + c + a → 路径长度相等,最终会在相交节点相遇。

四、核心逻辑推导

场景 1:两链表相交

假设链表 A:1 → 2 → 3 → 4(长度 4),链表 B:6 → 7 → 3 → 4(长度 4),相交节点为3

  • 指针 pA 遍历路径:1 → 2 → 3 → 4 → 6 → 7 → 3
  • 指针 pB 遍历路径:6 → 7 → 3 → 4 → 1 → 2 → 3
  • 最终 pA 和 pB 在节点3相遇,循环结束,返回3
场景 2:两链表无相交

假设链表 A:1 → 2 → 3,链表 B:4 → 5

  • 指针 pA 遍历路径:1 → 2 → 3 → nullptr → 4 → 5 → nullptr
  • 指针 pB 遍历路径:4 → 5 → nullptr → 1 → 2 → 3 → nullptr
  • 最终 pA 和 pB 同时到达nullptr,循环结束,返回nullptr
场景 3:两链表长度差异大

假设链表 A:1 → 2 → 3 → 4 → 5(长度 5),链表 B:6 → 4 → 5(长度 3),相交节点为4

  • 指针 pA 遍历路径:1 → 2 → 3 → 4 → 5 → 6 → 4(总长度 5+3=8)
  • 指针 pB 遍历路径:6 → 4 → 5 → 1 → 2 → 3 → 4(总长度 3+5=8)
  • 最终在节点4相遇,返回4

五、关键细节说明

  1. 为什么切换链表能对齐长度差? 假设链表 A 长度为m,链表 B 长度为n

    • m > n:pB 先遍历完 B(n步),切换到 A 后,还需走m-n步才能和 pA "同步";
    • m < n:pA 先遍历完 A(m步),切换到 B 后,还需走n-m步才能和 pB "同步";
    • 最终两指针遍历的总步数均为m+n,必然在相交节点(或nullptr)相遇。
  2. 为什么循环条件是pA != pB

    • 若有相交:两指针最终会指向同一个相交节点,循环终止;
    • 若无相交:两指针最终会同时指向nullptr,循环终止(因为nullptr == nullptr)。
  3. 边界情况覆盖

    • 任一链表为空:直接返回nullptr
    • 两链表完全重合(头节点就是相交节点):第一次循环pA==pB,直接返回头节点;
    • 相交节点为最后一个节点:指针遍历完自身链表后切换,最终在最后一个节点相遇。

六、对比其他解法的优势

解法 时间复杂度 空间复杂度 缺点
哈希表存储节点 O(m+n) O(m)或O(n) 需要额外空间存储节点地址
先计算长度差再遍历 O(m+n) O(1) 需两次遍历(计算长度 + 找交点)
本解法(双指针切换) O(m+n) O(1) 一次遍历完成,逻辑简洁

该解法的核心是用 "切换链表" 的方式隐式补偿长度差,无需额外计算长度,代码简洁且效率最优。


23🎈. 反转链表

方法一:迭代(双指针)

考虑遍历链表,并在访问各节点时修改 next 引用指向

复杂度分析:

  • 时间复杂度 O(N) : 遍历链表使用线性大小时间。
  • 空间复杂度 O(1) : 变量 precur 使用常数大小额外空间。

方法二:递归

考虑使用递归法遍历链表,当越过尾节点后终止递归,在回溯时修改各节点的 next 引用指向。

一、核心思路

该算法采用递归 方式实现单链表反转,核心思想是:通过递归逐层深入到链表末尾(找到反转后的头节点),再从后往前逐层修改节点的 next 指针指向(让当前节点指向其前驱节点),最终完成整个链表的反转。时间复杂度 \(O(n)\)(n 为链表长度,每个节点递归一次),空间复杂度 \(O(n)\)(递归调用栈的深度)。

二、递归的核心逻辑

递归反转的关键是 **"归的过程" 修改指针 **:

  1. 递:不断递归到链表最后一个节点(该节点会成为反转后的头节点);
  2. 归:从最后一个节点开始,逐层将当前节点的 next 指向其前驱节点,直到回到原链表头节点。

三、递归过程推演(示例链表:1→2→3→nullptr)

以链表 1→2→3→nullptr 为例,逐步拆解递归调用和指针修改过程:

步骤 1:初始调用

reverseList(head=1) → 调用 recur(cur=1, pre=nullptr)

步骤 2:递的过程(逐层深入)
递归调用 执行逻辑 状态说明
recur(1, nullptr) cur≠null → 调用 recur(2, 1) 准备处理节点 2
recur(2, 1) cur≠null → 调用 recur(3, 2) 准备处理节点 3
recur(3, 2) cur≠null → 调用 recur(null, 3) 准备处理节点 null
recur(null, 3) cur=null → 返回 pre=3(反转后的头节点) 递归终止,开始 "归" 的过程
步骤 3:归的过程(逐层修改指针)
递归返回 执行逻辑 链表状态变化
返回到 recur(3, 2) res=3 → 执行 3->next = 2 链表变为:1→2←3,3→null
返回到 recur(2, 1) res=3 → 执行 2->next = 1 链表变为:1←2←3,3→null
返回到 recur(1, nullptr) res=3 → 执行 1->next = nullptr 链表变为:null←1←2←3(完成反转)
步骤 4:最终返回

recur(1, nullptr) 返回 res=3 → reverseList 返回 3(反转后的头节点)。

四、关键细节说明

  1. 递归参数设计

    • cur:当前需要处理的节点;
    • pre:当前节点的前驱节点(反转后当前节点要指向的节点);
    • 初始时 cur=headpre=nullptr(原头节点反转后是尾节点,指向 null)。
  2. 终止条件

    • cur == nullptr 时,说明已经遍历到链表末尾,此时 pre 就是反转后的头节点(原链表最后一个节点),直接返回。
  3. res 变量的作用

    • 保存递归返回的 "反转链表头节点"(始终是原链表最后一个节点),在 "归" 的过程中逐层传递,最终作为整个递归的返回值。
  4. 指针修改的顺序

    • 先递归处理后继节点,再修改当前节点的 next 指向 → 保证从后往前修改,避免提前断开链表(若先改指针,会丢失后继节点的引用)。

五、边界情况覆盖

  1. 空链表head=nullptr → 调用 recur(null, nullptr) → 直接返回 nullptr
  2. 单节点链表head=1 → 调用 recur(1, nullptr) → 递归 recur(null, 1) 返回 1 → 执行 1->next=null → 返回 1(反转后还是自身);
  3. 多节点链表:如上述示例,正常完成从后往前的指针反转。

六、递归与迭代的对比

解法 时间复杂度 空间复杂度 优势 缺点
递归 \(O(n)\) \(O(n)\) 代码简洁,逻辑直观 递归栈可能溢出(链表过长时)
迭代 \(O(n)\) \(O(1)\) 空间效率更高,无栈溢出风险 代码稍繁琐,需手动维护指针

该递归解法的核心是利用递归栈天然的 "后进先出" 特性,实现从链表末尾到头部的指针反转,是链表反转的经典递归实现方式。


24🎈. 回文链表

【代码功能解析】

这段代码实现了判断单链表是否为回文链表 的功能。回文链表的定义是:链表节点的值从前往后和从后往前读取结果一致(如 1->2->2->1 是回文,1->2->3 不是)。

核心思路是:

  1. 找到链表的中间节点;
  2. 反转中间节点后的后半段链表;
  3. 对比前半段和反转后的后半段链表的节点值,全部相等则为回文。

【逐函数拆解】

cpp 复制代码
ListNode* middleList(ListNode* head) {
    ListNode *slow = head, *fast = head;
    while (fast && fast->next) {
        slow = slow->next;       // 慢指针:每次走1步
        fast = fast->next->next; // 快指针:每次走2步
    }
    return slow;
}
  • 原理 :快慢指针法(龟兔赛跑)
    • 快指针速度是慢指针的 2 倍,当快指针走到链表末尾时,慢指针恰好走到中间位置。
    • 链表长度为偶数 (如 1->2->3->4):慢指针停在第 n/2 + 1 个节点(示例中是 3);
    • 链表长度为奇数 (如 1->2->3->4->5):慢指针停在正中间节点(示例中是 3)。
  • 作用:分割链表为前半段和后半段,后续只需反转后半段即可对比。
2. 辅助函数:reserveList(反转链表)
cpp 复制代码
ListNode* reserveList(ListNode* head) {
    ListNode* cur = head;       // 当前遍历节点
    ListNode* newNode = nullptr;// 反转后的新链表头
    while (cur) {
        ListNode* next = cur->next; // 保存当前节点的下一个节点(防止断链)
        cur->next = newNode;        // 当前节点指向新链表(头插法)
        newNode = cur;              // 更新新链表头为当前节点
        cur = next;                 // 遍历下一个节点
    }
    return newNode;
}
  • 原理 :迭代法反转链表(头插法)
    • 逐个遍历原链表节点,将每个节点 "摘下来" 插到新链表的头部,最终得到反转后的链表。
  • 示例 :原链表 3->4 → 反转后 4->3
3. 主函数:isPalindrome(核心逻辑)
cpp 复制代码
bool isPalindrome(ListNode* head) {
    ListNode* mid = middleList(head);   // 找中间节点
    ListNode* rmid = reserveList(mid);  // 反转后半段链表

    while (head && rmid) {              // 对比前半段和反转后的后半段
        if (head->val != rmid->val) {
            return false;              // 有值不等,不是回文
        }
        head = head->next;
        rmid = rmid->next;
    }
    return true;                       // 全部相等,是回文
}

【示例推演】

示例 1:回文链表 1->2->2->1
  1. 找中间节点:慢指针走 2 步到 第二个2mid = 2->1);
  2. 反转后半段:rmid = 1->2
  3. 对比:
    • head=1 vs rmid=1 → 相等;
    • head=2 vs rmid=2 → 相等;
    • 循环结束,返回 true
示例 2:非回文链表 1->2->3
  1. 找中间节点:慢指针走 2 步到 3mid = 3);
  2. 反转后半段:rmid = 3
  3. 对比:
    • head=1 vs rmid=3 → 不相等,返回 false

【复杂度分析】

  • 时间复杂度 :O(n)
    • 找中间节点:O (n/2);
    • 反转后半段:O (n/2);
    • 对比:O (n/2);
    • 总复杂度为 O (n)。
  • 空间复杂度 :O(1)
    • 仅使用了常数个指针变量,没有额外开辟空间。

【注意事项】

  1. 边界情况:
    • 空链表:直接返回 true
    • 单节点链表:直接返回 true
    • 双节点链表(如 1->1):返回 true1->2 返回 false
  2. 链表结构:代码中 reserveList 会修改原链表的指针指向,但题目仅要求 "判断是否回文",未要求保留原链表结构,因此无需恢复。
  3. 拼写错误:代码中 reserveList 应为 reverseList(笔误,不影响逻辑,但实际开发中需修正)。
相关推荐
电子硬件笔记3 小时前
Python语言编程导论第七章 数据结构
开发语言·数据结构·python
Tony_yitao3 小时前
15.华为OD机考 - 执行任务赚积分
数据结构·算法·华为od·algorithm
C雨后彩虹4 小时前
任务总执行时长
java·数据结构·算法·华为·面试
风筝在晴天搁浅4 小时前
代码随想录 463.岛屿的周长
算法
柒.梧.4 小时前
数据结构:二叉排序树构建与遍历的解析与代码实现
java·开发语言·数据结构
一个不知名程序员www4 小时前
算法学习入门---priority_queue(C++)
c++·算法
zhuzewennamoamtf5 小时前
Linux内核platform抽象、数据结构、内核匹配机制
linux·运维·数据结构
TL滕5 小时前
从0开始学算法——第十八天(分治算法)
笔记·学习·算法