Hot100(开刷) 之 长度最小的数组--删除倒数第N个链表--层序遍历

27.长度最小的数组

解题方式
  • 这段代码利用滑动窗口 ,通过右指针不断扩展 窗口累加元素,并在和满足条件时移动左指针收缩 窗口,从而在 O(n)O (n ) 时间内找到满足和 ≥≥ 目标值的最小长度子数组
java 复制代码
    public int minSubArrayLen(int target, int[] nums) {
        int l = nums.length;
        int sum =0,left=0;
        int res = Integer.MAX_VALUE;
        for(int right =0 ;right<l ;right++){
            sum+=nums[right];
            while(sum>=target){
                res = Math.min(res,right-left+1);
                sum-=nums[left];
                left++;
            }
        }
        return  res == Integer.MAX_VALUE?0:res;
    }
Kotlin 复制代码
//数组长度    val len = nums.size
//最大值      var res = Int.MAX_VALUE
//最大值最小值   maxOf()/ minOf()

fun minSubArrayLen(target: Int, nums: IntArray): Int {
    
    val len = nums.size
    var sum = 0
    var left = 0
    var res = Int.MAX_VALUE

    for (right in 0 until len) {
        // 将右边界元素加入窗口
        sum += nums[right]

        // 当窗口和满足条件时,尝试收缩左边界
        while (sum >= target) {
            // 更新最小长度
            res = minOf(res, right - left + 1)
            
            // 移除左边界元素并移动左指针
            sum -= nums[left]
            left++
        }
    }

    // 如果 res 未被更新过,说明没有找到满足条件的子数组,返回 0
    return if (res == Int.MAX_VALUE) 0 else res
}
}
1.代码逻辑详解

这段代码利用滑动窗口的思想,在一次遍历中寻找满足条件的最小子数组长度。

  • 初始化阶段
    • sum = 0:用于记录当前滑动窗口内元素的总和。
    • left = 0:滑动窗口的左边界起始位置。
    • res = Integer.MAX_VALUE:用于记录满足条件的最小长度。初始化为最大值,是为了方便后续进行比较取最小值(如果最后这个值没变,说明没找到符合条件的子数组)。
  • 扩展窗口(移动右指针 right**)** :
    • 通过 for 循环遍历数组,right 指针充当窗口的右边界。
    • sum += nums[right]:将当前右边界指向的元素加入窗口,更新当前窗口的总和。
  • 收缩窗口(移动左指针 left**)** :
    • while (sum >= target):这是一个核心判断。只要当前窗口内的元素和 sum 大于或等于目标值 target,说明我们找到了一个合法的子数组。
    • 更新结果 :在 while 循环内部,首先计算当前窗口的长度 right - left + 1,并与历史最小值 res 比较,保留较小的那个。
    • 尝试更优解 :为了寻找可能更短的子数组,我们需要缩小窗口。
      • sum -= nums[left]:将左边界元素从总和中减去。
      • left++:左边界向右移动一位。
    • 这个过程会一直持续,直到窗口内的和小于 target 为止。
  • 返回结果
    • 最后判断 res 是否仍为 Integer.MAX_VALUE。如果是,说明从未进入过 while 循环(即没有任何子数组和满足条件),返回 0;否则返回记录的最小长度 res
2. 关键点
  • 窗口收缩的条件 :必须是 while (sum >= target)。这里不能写成 if,因为当找到一个满足条件的窗口后,可能需要连续移动多次 left 才能让 sum 再次小于 target。例如,如果数组元素很小而 target 很大,可能移动一次不够;或者当前窗口非常大,移动一次后可能仍然满足条件,我们需要一直移动到刚好不满足为止,以确保找到以当前 right 结尾的最短子数组。
  • 长度的计算 :数组下标是从 0 开始的,所以从 leftright 的长度是 right - left + 1
  • 哨兵值的使用 :使用 Integer.MAX_VALUE 作为初始值是一个常用的技巧,用来标记"尚未找到解"的状态。
3. 复杂度分析
  • 时间复杂度:O(N)
    • 其中 NN 是数组的长度。
    • 虽然代码中有两层循环(forwhile),但这并不是 O(N2)O (N 2) 。因为每个元素最多被访问两次:一次是被右指针 right 加入窗口,另一次是被左指针 left 移出窗口。因此,操作总次数是线性的,即 2N2N ,简化为 O(N)O (N) 。
  • 空间复杂度:O(1)
    • 只需要常数级别的额外空间来存储变量 sumleftrightres,不需要依赖输入数组大小的额外存储空间。
4. 优缺点
  • 优点
    • 高效 :相比于暴力解法(双重循环枚举所有子数组,时间复杂度 O(N2)O (N 2) ),滑动窗口将时间复杂度降低到了 O(N)O (N) ,在处理大规模数据时性能优势非常明显。
    • 逻辑清晰:代码结构简洁,通过维护两个指针即可动态调整窗口大小,易于理解和实现。
  • 缺点
    • 适用场景受限 :这种"快慢指针"式的滑动窗口主要适用于数组元素均为正整数的情况。
    • 局限性说明 :如果数组中包含负数,sum 随着 right 的增加不一定增加,随着 left 的增加不一定减少,此时 while (sum >= target) 的逻辑就会失效(因为缩小窗口可能会导致和变大,或者扩大窗口导致和变小),这种算法就不再适用了。

28.删除倒数第N个链表

  • 代码通过引入哑结点 简化边界处理,先遍历链表计算总长度 ,再根据长度定位到待删除节点的前驱,最后修改指针指向完成删除。
java 复制代码
public ListNode removeNthFromEnd(ListNode head, int n) {
    //建立含头节点的节点,防止删除头节点无法操作
    ListNode dummy = new ListNode(0, head);
    int length = getLength(head);
    ListNode cur = dummy;
    for (int i = 1; i < length - n + 1; ++i) {
        cur = cur.next;
    }
    cur.next = cur.next.next;
    ListNode ans = dummy.next;
    return ans;
}

public int getLength(ListNode head) {
    int length = 0;
    while (head != null) {
    ++length;
    head = head.next;
    }
    return length;
    }
}
1.代码逻辑详解

这段代码的核心思路非常直观:要删除倒数第 NN 个节点,首先需要知道链表的总长度,从而算出该节点在正数第几个位置。

  • 引入哑结点
    • 创建一个哑结点 dummy,其 next 指向 head
    • 目的 :处理边界情况。如果链表只有一个节点且需要删除它,或者需要删除头结点,直接操作 head 会非常麻烦。使用 dummy 可以让我们统一处理所有节点的删除逻辑,最后返回 dummy.next 即可。
  • 第一次遍历(计算长度)
    • 调用 getLength(head) 方法。
    • 该方法通过 while 循环遍历整个链表,统计节点总数 length
  • 定位待删除节点的前驱
    • 我们需要删除的是倒数第 NN 个节点。
    • 假设链表长度为 LL ,那么倒数第 NN 个节点就是正数第 L−N+1LN+1 个节点。
    • 为了执行删除操作(cur.next = cur.next.next),我们需要找到待删除节点的前一个节点 (即第 L−NLN 个节点)。
    • 代码中 cur 初始化为 dummy(相当于第 0 个节点)。
    • for 循环从 i = 1 执行到 i < length - n + 1。这意味着循环体执行了 L−NLN 次,cur 指针最终停留在第 L−NLN 个节点上。
  • 执行删除
    • cur.next = cur.next.next:跳过待删除的节点,将其前驱直接连接到其后继。
  • 返回结果
    • 返回 dummy.next,即处理后的新链表头结点。
2. 关键点
  • 哑结点(Dummy Node) :这是链表题目中的经典技巧。它避免了在删除头结点时需要单独判断 head == null 或返回 head.next 的复杂逻辑,使代码更加健壮。
  • 索引计算
    • 链表长度: LL
    • 目标节点位置(从1开始): L−N+1LN+1
    • 目标前驱节点位置(从0开始,dummy为0): L−NLN
    • 循环条件 i < length - n + 1 实际上就是让 cur 走 L−NLN 步,正好停在待删除节点的前一位。
  • 边界情况 :例如链表长度为 5,删除倒数第 1 个(即正数第 5 个)。cur 需要走到第 4 个节点。公式 5−1=45−1=4 ,逻辑成立。
3. 复杂度分析
  • 时间复杂度:O(L)
    • 其中 LL 是链表的长度。
    • getLength 方法遍历了一次链表,耗时 O(L)O (L) 。
    • 主函数中的 for 循环遍历了部分链表(从头到待删除节点前),耗时 O(L)O (L) 。
    • 总时间复杂度为 O(L)+O(L)=O(L)O (L )+O (L )=O (L) 。
  • 空间复杂度:O(1)
    • 只需要常数级别的额外空间来存储 dummycurlength 等变量,不依赖链表长度。
4. 优缺点
  • 优点
    • 逻辑简单:符合直觉,先算总数再找位置,容易理解和编写,不易出错。
    • 代码清晰:将"计算长度"封装成独立函数,主逻辑非常干净。
  • 缺点
    • 遍历次数:需要遍历链表两次(一次算长度,一次找位置)。虽然时间复杂度量级相同,但在实际运行中比"一次遍历"的方法(如双指针/快慢指针法)稍慢。
    • 改进空间 :可以使用快慢指针 技巧。让快指针先走 NN 步,然后快慢指针同时移动,当快指针到达末尾时,慢指针正好指向待删除节点的前驱。这样只需要一次遍历。

29.层序遍历

解题方式
  • 利用队列 实现广度优先搜索 ,通过记录每一层的节点数量来分层处理,依次将节点出队取值并将其子节点入队,从而完成二叉树的层序遍历。
java 复制代码
//offeer() 加到末尾
//poll() 删除并返回第一个元素

public List<List<Integer>> levelOrder(TreeNode root) {
    // 结果列表,每个元素是一个 List<Integer>,代表一层的节点值
    List<List<Integer>> res = new ArrayList<>();
    
    // 如果根节点为空,直接返回空结果
    if (root == null) return res;
    
    // 使用 LinkedList 作为队列,实现广度优先搜索(BFS)
    // offer() 方法在尾部添加元素,poll() 方法从头部取出并删除元素
    LinkedList<TreeNode> queue = new LinkedList<>();
    queue.offer(root);  // 先将根节点入队
    
    // 当队列不为空时,说明还有节点未处理
    while (!queue.isEmpty()) {
        // 临时列表,用于存储当前层的所有节点值
        List<Integer> levelValues = new ArrayList<>();
        
        // 当前队列的大小就是当前层的节点个数(因为上一层节点已经全部出队)
        int size = queue.size();
        
        // 循环处理当前层的每一个节点
        while (size != 0) {
            // 弹出队首节点(当前层的节点)
            TreeNode currentNode = queue.poll();
            // 将当前节点的值加入当前层的列表
            levelValues.add(currentNode.val);
            
            // 如果左子节点不为空,则将其加入队列(下一层)
            if (currentNode.left != null) queue.offer(currentNode.left);
            // 如果右子节点不为空,则将其加入队列(下一层)
            if (currentNode.right != null) queue.offer(currentNode.right);
            
            // 当前层剩余待处理节点数减1
            size--;
        }
        
        // 当前层所有节点处理完毕,将这一层的结果加入最终结果列表
        res.add(levelValues);
    }
    
    // 返回完整的层序遍历结果
    return res;
}
Kotlin 复制代码
fun levelOrder(root: TreeNode?): List<List<Int>> {
    val res = ArrayList<List<Int>>()

    // 如果根节点为空,直接返回空结果
    if (root == null) return res

    // Kotlin 中通常使用 ArrayDeque 来实现队列,性能优于 LinkedList
    val queue: Queue<TreeNode> = ArrayDeque()
    queue.offer(root)

    // 当队列不为空时,说明还有节点未处理
    while (queue.isNotEmpty()) {
        val levelValues = ArrayList<Int>()

        // 记录当前层的节点数量
        val size = queue.size

        // 循环处理当前层的每一个节点
        for (i in 0 until size) {
            // 弹出队首节点
            val currentNode = queue.poll()

            // 将当前节点的值加入当前层的列表
            levelValues.add(currentNode.`val`)

            // 如果左子节点不为空,加入队列
            if (currentNode.left != null) queue.offer(currentNode.left)

            // 如果右子节点不为空,加入队列
            if (currentNode.right != null) queue.offer(currentNode.right)
        }

        // 当前层处理完毕,加入结果列表
        res.add(levelValues)
    }

    return res
}
1. 代码逻辑详解

这段代码使用了广度优先搜索 的策略,借助一个队列来实现二叉树的层序遍历。

  • 初始化与边界处理
    • 创建一个结果列表 res,用于存储最终的层序遍历结果。
    • 首先判断根节点 root 是否为空。如果为空,直接返回空列表,避免空指针异常。
  • 队列初始化
    • 使用 LinkedList 创建一个队列 queue,并将根节点 root 加入队列(offer)。
    • 队列的作用是暂存当前层以及下一层需要访问的节点,遵循"先进先出"的原则。
  • 外层循环(控制层数)
    • while (!queue.isEmpty()):只要队列不为空,说明树中还有节点未被访问。
    • 每次进入外层循环,意味着开始处理新的一层。
  • 内层循环(处理当前层)
    • 创建一个临时列表 levelValues,用于存储当前层所有节点的值。
    • 关键步骤int size = queue.size()。在开始处理当前层之前,先记录队列的大小。这个 size 正好是当前层节点的总数(因为上一层的节点已经全部出队,而当前层的节点已经全部入队)。
    • while (size != 0):循环 size 次,确保只处理当前层的节点,而不混入下一层新加入的节点。
      • queue.poll():取出队首节点(即当前层的一个节点)。
      • levelValues.add(...):将该节点的值存入临时列表。
      • 子节点入队 :如果当前节点有左子节点或右子节点,将它们依次加入队列。注意,这些新加入的节点属于下一层,它们会在下一次外层循环中被处理。
      • size--:当前层待处理节点数减 1。
  • 收集结果
    • 当内层循环结束时,说明当前层的所有节点都已处理完毕。
    • levelValues(当前层的节点值列表)加入到最终结果 res 中。
2. 关键点
  • 队列的作用:队列是 BFS 的核心数据结构。它保证了节点是按照"从上到下、从左到右"的顺序被访问的。
  • "快照"思想( size****变量) :这是层序遍历最关键的技巧。因为在内层循环中,我们会不断向队列中添加下一层的节点,导致队列长度动态变化。如果不先记录 size,就无法区分哪些节点属于当前层,哪些属于下一层。通过固定 size,我们将每一层的处理过程隔离开来。
  • 空树处理 :代码开头对 root == null 的判断是必不可少的鲁棒性检查。
3. 复杂度分析
  • 时间复杂度:O(N)
    • 其中 NN 是二叉树的节点总数。
    • 每个节点都会被入队一次、出队一次,且仅被访问一次。因此总的时间复杂度是线性的。
  • 空间复杂度:O(W)
    • 其中 WW 是二叉树的最大宽度(即某一层拥有的最大节点数)。
    • 队列中最多同时存储一层的所有节点。在最坏情况下(例如完全二叉树的最后一层),空间复杂度为 O(N)O (N ) ;在最好情况下(链状树),空间复杂度为 O(1)O(1) 。平均来看,空间复杂度取决于树的最大宽度。
4. 优缺点
  • 优点
    • 逻辑清晰:利用队列模拟层级扩散的过程,非常符合人类对"层"的直观理解。
    • 通用性强:这是解决二叉树层级相关问题(如求平均层值、锯齿形遍历、寻找最底层最左边节点等)的标准模板。
  • 缺点
    • 空间开销:相比于深度优先搜索,BFS 需要维护一个队列来存储节点。对于宽度非常大的树(例如完全二叉树),队列可能会占用较多内存。
    • 递归替代:虽然也可以用深度优先搜索配合记录"当前深度"来通过递归实现层序遍历,但那种方法需要额外的逻辑来管理层级索引,不如 BFS 直观
相关推荐
luoganttcc2 小时前
dim3 grid_size(2, 3, 4); dim3 block_size(4, 8, 4)算例
算法
2601_950703942 小时前
PyCharm性能优化终极指南
java
WBluuue2 小时前
Codeforces 1088 Div1+2(ABC1C2DEF)
c++·算法
yzp-3 小时前
Spring 三级缓存 ---- 简单明了豆包版
java·mysql·spring
像素猎人3 小时前
map<数据类型,数据类型> mp和unordered_map<数据类型,数据类型> ump的讲解,蓝桥杯OJ4567最大数目
c++·算法·蓝桥杯·stl·map
Narrastory3 小时前
Note:强化学习(一)
人工智能·算法·强化学习
隐退山林3 小时前
JavaEE进阶:导读&SpringBoot快速上手
java·spring boot·java-ee
送秋三十五3 小时前
Spring 源码---------Spring Core
java·数据库·spring
悟空码字3 小时前
SpringBoot + 微信支付实现“扫码开门,取货自动扣款”售货柜
java·spring boot·后端