Skip to content

算法刷题总结(一)

链表总结

  1. 利用 dummy节点统一删除头节点和中间节点的逻辑
  2. 查找倒数第n个节点
    • 快慢指针法,快指针先走 n 步,慢指针再走,等快指针走到底时,慢指针即为删除的前置节点
    • 利用栈先进后出,先全部进栈,再依次弹出 n 个节点,栈顶即为删除的前置节点
  3. 删除节点时需要考虑操作节点指针之间的顺序

哈希表

  1. 数值比较小且连贯时,可以用数组下标来作为哈希表
  2. 使用 set,map 来做哈希表每次 set 值的时候需要对 key 进行 hash 处理,此时对比数组直接用下标会有额外的性能开销

字符串

  1. 大部分字符串都依靠双指针解决。
  2. 少部分可以利用栈和队列的数据结构以及 KMP 算法来处理。

单调队列

底层存储

  1. 单调队列也叫优先级队列,底层其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系

单调队列应用场景

  1. 滑动窗口极值问题​​:当需要在一个​​滑动窗口​​中快速获取最大值或最小值时,单调队列可以在 O(1) 时间返回极值。例如:
    • 窗口滑动时,移除窗口外的旧元素,保持队列头部始终是当前窗口的极值。
    • 每次新元素入队时,删除队列中比当前元素小(或大)的元素,保持单调性。
  2. ​子数组问题优化​​:当问题涉及寻找满足特定条件的子数组(如最短长度、最长长度、区间和限制等),单调队列可以结合​​前缀和​​等技巧,快速判断子数组的合法性。
  3. ​​动态规划优化​​:在动态规划问题中,如果状态转移方程需要从某个区间内获取极值,单调队列可以将时间复杂度从 O(n) 优化到 O(1)。

优先队列要点

  1. 及时从队首弹出元素,保证队首为最值(最大或者最小)
  2. 维护队列内部单调性
  3. 队列存储下标更加通用

LeetCode 中常见的单调队列题型​​

​1. ​滑动窗口极值​​

  • 239.滑动窗口最大值(模板题,必刷)
  • 剑指 Offer 59 - II. 队列的最大值

​2. ​子数组和的最短/最长问题​​

  • 862.和至少为 K 的最短子数组(结合前缀和 + 单调队列)
  • 209.长度最小的子数组(暴力超时时可用单调队列优化)

​3. ​其他区间极值问题​​

  • 1438.绝对差不超过限制的最长连续子数组(维护两个单调队列,分别存最大值和最小值)

​单调队列的实现要点​​

​1. ​队列中存储索引​​(而非直接存储数值),便于判断元素是否在窗口内。 ​2. ​维护单调性​​:插入新元素前,从队列尾部删除破坏单调性的元素。 ​3. ​删除过期元素​​:窗口滑动时,检查队首元素是否超出窗口范围,若超出则移除。

单调栈

单调栈是栈的变种,通过维护栈内元素的单调性(单调递增或单调递减),其依赖输入顺序,主要用来处理“邻近”关系。 其与优先队列的区别在于:单调栈解决​​顺序敏感​​的局部单调问题,优先队列解决​​动态全局优先级​​问题

单调栈应用场景

  1. 邻近问题:下/上一个更大/小元素
  2. 区间边界问题(在栈口形成凹形或者凸形结构):接雨水、矩形面积

单调栈核心要点

  1. 及时弹出无用元素
  2. 维护栈内单调性
  3. 栈内存储下标值更通用
  4. 对于单调递增栈,经常会在左右两边预填充 0([84] 柱状图中最大的矩形),防止边界问题

二叉树

种类

  1. 满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上。
  2. 完全二叉树:除了最底层节点可能没填满外,其余每层节点数都达到最大值,同时最底层节点从左到右依次排列。
  3. 二叉搜索树:二叉搜索树是一个有序树。
    • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
    • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
    • 它的左、右子树也分别为二叉排序树。
  4. 平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
    • C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn;
    • unordered_map、unordered_set,unordered_map、unordered_set底层实现是哈希表。

存储方式

  1. 链式存储:通过用链表指针的形式把分布在各个地址的节点串联一起。
  2. 顺序存储:通过用数组的形式顺序存储,其元素在内存是连续分布的。父节点的数组下标是 i,那么它的左孩子就是 i *2 + 1,右孩子就是 i* 2 + 2

遍历方式

  1. 深度优先搜索(前中后,其实指的就是中间节点的遍历顺序)
    • 前序遍历(递归法[底层是 函数调用栈],迭代法[利用栈模拟递归])
    • 中序遍历(递归法,迭代法)
    • 后序遍历(递归法,迭代法)
    • DFS遍历方式
  2. 广度优先搜索
    • 层次遍历(迭代法[利用队列])

递归三步曲

  1. 确定递归终止条件
  2. 确定递归的参数和返回值
  3. 确定单层递归的逻辑

回溯算法

回溯算法是一种暴力搜索的思想,通常回溯过程发生在递归函数的下一步。

回溯算法应用场景

  1. 数组组合问题
  2. 数组排列问题(排列强调顺序,组合不强调顺序)
  3. 字符串切割问题
  4. 数组求子集问题
    • 每个元素都可以 选|不选,背包问题也是一种子集型回溯
  5. 棋盘问题(N 皇后问题、解数独问题)

回溯算法模版

  1. 回溯算法一般是在集合中递归搜索,并且解决的问题都可以抽象为一颗树形结构,其中集合的大小构成了树的宽度,递归的深度构成的树的深度。 backtracking
text
function backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

关于去重

  1. 去重前需要判断是否应该对集合排序
  2. 树层上在 for 循环前可以用 set 来去重,每次递归重新声明。
  3. 树枝去重(排列问题)需要用 visited 数组传入递归函数中去重。
text
function backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    // 声明 set 来做树层去重
    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
         if (set.has(选择)) {
               continue; // 树层去重
         }

         // 基于数组排序前提下,判断当前节点和上一个节点值相同
         if (当前节点和上一个节点值相同 && visited[上一节点] == false) {
               continue; // 树枝去重
         }

        处理节点;
        visited[选择] = true; // 处理节点
        backtracking(路径,visited, 选择列表); // 递归
        回溯,撤销处理结果
      visited[选择] = false; // 回溯
    }
}

贪心算法

  1. 贪心算法是从局部最优解出发,推导出全局最优解的一种算法设计思想。
  2. 在做贪心算法类题目时可以通过思考每个阶段的局部最优解是什么?局部最优能否推出全局最优。至于如何思考局部最优,更多是通过分析题目以及反证法的思路来进行推导。

难点题目

135.分发糖果 406.根据身高重建队列 763.划分字母区间 738.单调递增的数字 968.监控二叉树