每日算法-250326
83. 删除排序链表中的重复元素
题目描述

思路
使用快慢指针遍历排序链表。
slow指针指向当前不重复序列的最后一个节点,fast指针用于向前遍历探索。当fast找到一个与slow指向的节点值不同的新节点时,就将slow的next指向fast,然后slow前进。
解题过程
- 处理边界情况:如果链表为空 (
head == null),直接返回null。 - 初始化指针:定义 
slow和fast指针,都初始化为head。 - 遍历链表:使用 
while循环,条件是fast != null(slow不会是null因为它总是在fast或其之前)。- 在循环中,移动 
fast指针向前探索。 - 判断重复:如果 
fast.val != slow.val,说明fast指向的节点是一个新的不重复元素。 - 更新链表:此时,将 
slow的next指针指向fast(slow.next = fast),然后将slow指针也向前移动一步 (slow = slow.next)。 - 无论是否找到不重复元素,
fast指针都需要在每次迭代中向前移动 (fast = fast.next)。 
 - 在循环中,移动 
 - 断开尾部链接:循环结束后,
slow指向的是新链表的最后一个节点。为了确保链表正确终止,需要将slow的next设置为null(slow.next = null)。这会断开与后面可能存在的重复元素的连接。 - 返回结果:返回原始的 
head,因为头节点可能没变,或者即使变了,head变量仍然指向修改后链表的起始位置。 
复杂度
- 时间复杂度:  
      
       
        
        
          O 
         
        
          ( 
         
        
          n 
         
        
          ) 
         
        
       
         O(n) 
        
       
     O(n),其中  
      
       
        
        
          n 
         
        
       
         n 
        
       
     n 是链表的节点数。因为 
fast指针遍历整个链表一次。 - 空间复杂度: O ( 1 ) O(1) O(1),只使用了常数级别的额外空间(两个指针)。
 
Code (Java)
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        if (head == null) {
            return head; // 或者 return null,效果一样
        }
        ListNode slow = head, fast = head;
        // fast 指针用于遍历
        while (fast != null) {
            // 当 fast 遇到与 slow 不同的值时
            if (fast.val != slow.val) {
                // slow 的下一个节点指向 fast 这个不重复的节点
                slow.next = fast;
                // slow 移动到新的不重复节点位置
                slow = slow.next;
            }
            // fast 继续向后遍历
            fast = fast.next;
        }
        // 循环结束后,slow 是最后一个不重复节点,断开其后的链接
        slow.next = null;
        return head;
    }
}
 
904. 水果成篮
题目描述

思路
这是一道典型的滑动窗口问题。目标是找到一个最长的子数组,其中最多包含两种不同的元素。
我们可以使用一个哈希表(或者利用题目
0 <= fruits[i] < fruits.length的条件,使用一个数组hash)来记录窗口内每种水果出现的次数。同时,用一个变量count记录窗口内不同水果的种类数量。
解题过程
- 初始化: 
  
ret = 0: 用于存储最长子数组的长度(即最多能收集的水果数)。count = 0: 记录当前窗口内不同水果的种类数。n = fruits.length: 数组长度。hash = new int[n]: 使用数组作为哈希表,hash[i]存储水果i在当前窗口内的数量。left = 0,right = 0: 滑动窗口的左右边界。
 - 扩展窗口(右移 
right):right指针向右移动,考察fruits[right]这个水果,令in = fruits[right]。- 更新计数:如果 
hash[in] == 0,说明这是窗口内第一次遇到这种水果,因此不同水果种类数count增加 1。 - 将 
in水果的计数加 1:hash[in]++。 
 - 收缩窗口(右移 
left):- 判断条件:当 
count > 2时,表示窗口内的水果种类超过了 2 种,此时窗口不满足条件,需要收缩。 - 处理出窗口元素:令 
out = fruits[left]。 - 将 
out水果的计数减 1:hash[out]--。 - 更新计数:如果 
hash[out] == 0,说明移除这个水果后,窗口内不再有这种类型的水果了,因此不同水果种类数count减少 1。 - 左边界 
left向右移动:left++。 - 这个收缩过程(
while (count > 2))会持续进行,直到窗口重新满足count <= 2的条件。 
 - 判断条件:当 
 - 更新结果:在每次移动 
right之后(并且窗口调整为合法状态后),当前窗口[left, right]是合法的(最多包含两种水果)。计算当前窗口的长度right - left + 1,并更新ret = Math.max(ret, right - left + 1)。 - 循环结束:当 
right到达数组末尾时,循环结束。 - 返回结果:返回 
ret。 
复杂度
- 时间复杂度:  
      
       
        
        
          O 
         
        
          ( 
         
        
          n 
         
        
          ) 
         
        
       
         O(n) 
        
       
     O(n),其中  
      
       
        
        
          n 
         
        
       
         n 
        
       
     n 是数组 
fruits的长度。左右指针left和right都最多遍历数组一次。 - 空间复杂度:  
      
       
        
        
          O 
         
        
          ( 
         
        
          n 
         
        
          ) 
         
        
       
         O(n) 
        
       
     O(n),在最坏情况下(例如所有水果种类都不同,但这里水果种类数受限于数组长度 
n),用于存储水果计数的hash数组需要 O ( n ) O(n) O(n) 的空间。如果水果种类数远小于n,可以认为是 O ( C ) O(C) O(C),其中 C C C 是水果种类的数量上限。 
Code (Java)
class Solution {
    public int totalFruit(int[] fruits) {
        int ret = 0, count = 0;
        int n = fruits.length;
        // 利用题目条件,可以使用数组代替 HashMap
        int[] hash = new int[n];
        for (int left = 0, right = 0; right < n; right++) {
            // 元素进入窗口
            int in = fruits[right];
            // 如果是新水果种类,count增加
            if (hash[in] == 0) {
                count++;
            }
            // 该水果数量增加
            hash[in]++;
            // 如果水果种类超过2种,需要收缩窗口
            while (count > 2) {
                // 元素离开窗口
                int out = fruits[left];
                // 该水果数量减少
                hash[out]--;
                // 如果移除后该水果数量为0,说明少了一种水果
                if (hash[out] == 0) {
                    count--;
                }
                // 左指针右移
                left++;
            }
            // 窗口调整完毕后,更新最大长度
            ret = Math.max(ret, right - left + 1);
        }
        return ret;
    }
}
 
1695. 删除子数组的最大得分
题目描述

思路
这个问题要求找到一个元素唯一的子数组,使其元素和最大。这同样可以用滑动窗口解决。
我们需要维护一个窗口,确保窗口内的所有元素都是唯一的。可以使用一个哈希表 (HashMap) 来记录窗口内每个数字出现的次数。同时,维护一个变量
sum记录当前窗口内元素的和。
解题过程
- 初始化: 
  
ret = 0: 用于存储最大得分(即元素唯一的子数组的最大和)。sum = 0: 当前窗口内元素的和。hash = new HashMap<>(): 记录窗口内数字及其出现次数。left = 0,right = 0: 滑动窗口的左右边界。
 - 扩展窗口(右移 
right):right指针向右移动,考察nums[right],令in = nums[right]。- 更新窗口和:
sum += in。 - 更新哈希表:将 
in的计数加 1。hash.put(in, hash.getOrDefault(in, 0) + 1)。 
 - 收缩窗口(右移 
left):- 判断条件:当 
hash.get(in) > 1时,表示新加入的元素in在窗口内出现了重复,窗口不再满足“元素唯一”的条件,需要收缩。 - 处理出窗口元素:令 
out = nums[left]。 - 更新窗口和:
sum -= out。 - 更新哈希表:将 
out的计数减 1。hash.put(out, hash.get(out) - 1)。 - 左边界 
left向右移动:left++。 - 这个收缩过程 (
while (hash.get(in) > 1)) 会持续进行,直到窗口内in的计数变回 1(即窗口内不再有重复的in)。 
 - 判断条件:当 
 - 更新结果:在每次移动 
right之后,窗口[left, right]必然是元素唯一的(因为收缩步骤保证了这一点)。此时,当前窗口的和sum是一个有效的得分。更新ret = Math.max(ret, sum)。 - 循环结束:当 
right到达数组末尾时,循环结束。 - 返回结果:返回 
ret。 
复杂度
- 时间复杂度:  
      
       
        
        
          O 
         
        
          ( 
         
        
          n 
         
        
          ) 
         
        
       
         O(n) 
        
       
     O(n),其中  
      
       
        
        
          n 
         
        
       
         n 
        
       
     n 是数组 
nums的长度。左右指针left和right都最多遍历数组一次。哈希表操作平均时间复杂度为 O ( 1 ) O(1) O(1)。 - 空间复杂度: O ( n ) O(n) O(n),在最坏情况下(例如数组中所有元素都不同),哈希表需要存储 O ( n ) O(n) O(n) 个元素。如果数组中不同元素的数量上限为 U U U,则空间复杂度为 O ( min  ( n , U ) ) O(\min(n, U)) O(min(n,U))。
 
Code (Java)
import java.util.HashMap;
import java.util.Map;
class Solution {
    public int maximumUniqueSubarray(int[] nums) {
        int ret = 0, sum = 0;
        Map<Integer, Integer> hash = new HashMap<>();
        for (int left = 0, right = 0; right < nums.length; right++) {
            // 元素进入窗口
            int in = nums[right];
            // 更新窗口和
            sum += in;
            // 更新元素计数
            hash.put(in, hash.getOrDefault(in, 0) + 1);
            // 如果窗口内出现重复元素 (刚加入的 in 导致重复)
            while (hash.get(in) > 1) {
                // 元素离开窗口
                int out = nums[left];
                // 更新窗口和
                sum -= out;
                // 更新元素计数
                hash.put(out, hash.get(out) - 1);
                // 左指针右移
                left++;
            }
            // 此时窗口内元素唯一,更新最大得分
            ret = Math.max(ret, sum);
        }
        return ret;
    }
}
 
1423. 可获得的最大点数(复习)
题目描述

复习思路
这道题要求从数组两端取走总共
k张牌,使得分数总和最大。这个问题可以转化为:找到数组中间连续
n - k个元素,使其和最小。因为数组总和是固定的,要让两端k个元素的和最大,等价于让中间n - k个元素的和最小。因此,我们可以使用滑动窗口来找到长度为
m = n - k的子数组的最小和。
解题过程(滑动窗口找最小和)
- 计算窗口大小:计算中间部分的长度 
m = n - k。 - 处理特殊情况:如果 
m == 0(即n == k),说明需要取走所有卡牌,直接计算并返回整个数组的总和。 - 初始化: 
  
totalSum = 0: 整个数组的总和。windowSum = 0: 当前滑动窗口(长度为m)的和。minWindowSum = Integer.MAX_VALUE: 用于记录所有长度为m的窗口中,和的最小值。left = 0: 窗口左边界。
 - 遍历数组与滑动窗口: 
  
- 使用 
right指针从 0 遍历到n-1。 - 累加总和:
totalSum += cardPoints[right]。 - 累加窗口和:
windowSum += cardPoints[right]。 - 维护窗口大小:当窗口达到大小 
m时(即right - left + 1 == m或right >= m - 1),开始执行以下操作:- 更新最小窗口和:
minWindowSum = Math.min(minWindowSum, windowSum)。 - 收缩窗口:从 
windowSum中减去最左边的元素cardPoints[left]。 - 移动左边界:
left++。 
 - 更新最小窗口和:
 
 - 使用 
 - 计算结果:遍历结束后,
minWindowSum存储了长度为n - k的子数组的最小和。最终结果为totalSum - minWindowSum。 
复杂度
- 时间复杂度:  
      
       
        
        
          O 
         
        
          ( 
         
        
          n 
         
        
          ) 
         
        
       
         O(n) 
        
       
     O(n),其中  
      
       
        
        
          n 
         
        
       
         n 
        
       
     n 是数组 
cardPoints的长度。只需要遍历数组一次。 - 空间复杂度: O ( 1 ) O(1) O(1),只使用了常数级别的额外空间。
 
Code (Java)
class Solution {
    public int maxScore(int[] cardPoints, int k) {
        int n = cardPoints.length;
        int m = n - k; // 中间要保留的元素个数 (窗口大小)
        int totalSum = 0; // 数组总和
        int windowSum = 0; // 当前窗口的和
        // ret 在这里用来存储长度为 m 的窗口的最小和
        int minWindowSum = Integer.MAX_VALUE;
        // 特殊情况:k == n, m == 0
        if (m == 0) {
            for (int point : cardPoints) {
                totalSum += point;
            }
            return totalSum;
        }
        for (int left = 0, right = 0; right < n; right++) {
            // 累加当前元素到窗口和
            windowSum += cardPoints[right];
            // 同时累加到总和 (只需计算一次)
            totalSum += cardPoints[right];
            // 当窗口大小达到 m 时
            if (right - left + 1 >= m) {
                // 更新最小窗口和
                minWindowSum = Math.min(minWindowSum, windowSum);
                // 从窗口和中移除最左边的元素
                windowSum -= cardPoints[left];
                // 左指针右移,保持窗口大小
                left++;
            }
        }
        // 最大得分 = 总和 - 中间 m 个元素的最小和
        // 注意: 如果 m > n (k < 0) 或 m < 0 (k > n) 是无效输入, 但题目保证 1 <= k <= cardPoints.length
        // 如果 m=n (k=0), minWindowSum 应该等于 totalSum,结果是 0 (逻辑上正确,但未覆盖 m=0 的代码路径)
        // minWindowSum 如果没被更新过(例如 m > n), 结果会出错, 但题目约束避免了此情况。
        // 在 m > 0 时,minWindowSum 一定会被更新。
        return totalSum - minWindowSum;
    }
}
                