滑动窗口下的极限挑战:我在实时数据流中挖掘最大价值分(1695. 删除子数组的最大得分)

滑动窗口下的极限挑战:我在实时数据流中挖掘最大价值分 😎

嘿,各位在代码世界里遨游的伙伴们!我是你们的老朋友,一个坚信"好代码能说话"的开发者。今天,我要和大家分享一次我在项目中遇到的真实挑战,以及我是如何从一道非常有趣的算法题------LeetCode 1695. 删除子数组的最大得分------中找到解题的钥匙,并最终打造出高效解决方案的。这绝对是一次充满"啊哈!"时刻的旅程!

我遇到了什么问题?

我最近在负责一个实时用户行为分析系统 。系统会接收一连串的用户事件流,每个事件(比如点击、浏览、购买)都有一个对应的"价值分"。产品经理提出了一个需求:我们需要识别出用户在短时间内不重复的、高价值的行为序列

具体来说,就是在一个连续的事件序列中,如果用户的行为都是各不相同 的(比如,先浏览商品、再加入购物车、然后去支付),那么这个序列就是高质量的。我们需要找到这样的"高质量序列",并计算出其总价值分,目标是找到所有可能序列中的最大价值分

这个需求被我迅速抽象成了这道算法题的核心:

在一个数字数组(事件价值分)中,找到一个元素唯一的连续子数组 ,并使其元素和最大

例如,对于价值流 [4,2,4,5,6],最优的序列是 [2,4,5,6],因为其中的元素 2,4,5,6 互不相同,并且它们的和 17 是所有可能组合中最大的。

先看看"通关提示"(题目提示解读)

开工前,瞄一眼提示是我们的好习惯:

  • 1 <= nums.length <= 10^5:数组长度最长可达十万!这个量级直接给 O(N^2) 的暴力解法判了死刑。任何检查所有子数组(两层循环)的想法都可以直接pass了。我们的目标必须是 O(N) 或 O(N log N) 的高效算法。
  • 1 <= nums[i] <= 10^4:元素值是正整数,且范围不大。这暗示我们可以用一个数组来代替哈希表做频率统计(桶排序的思想),但对于这个问题,哈希表可能更直观。

我是如何用"滑动窗口"丝滑地解决它的

看到"连续子数组"这种关键词,我的"算法雷达"立刻就锁定了滑动窗口这个强大的技术。这就像在一条长长的纸带上,用一个可伸缩的框框来寻找符合条件的片段。

解法一:标准滑动窗口 ------ 稳扎稳打,步步为营

这是最经典、最直观的滑动窗口实现。我们需要维护一个"窗口",并保证这个窗口内的元素始终是唯一的。

我的思路是这样的:

  • 定义窗口 :用 leftright 两个指针代表窗口的左右边界。
  • 检查唯一性 :我需要一个能快速告诉我"这个元素在窗口里吗?"的数据结构。HashSet 简直是为这个场景量身定做的,它的 contains, add, remove 操作平均时间复杂度都是 O(1)。
  • 窗口的"伸"与"缩"
    1. right 指针不断向右移动,试图"吃"进新元素,这是扩张
    2. 在吃进新元素 nums[right] 之前,我们检查 HashSet。如果 HashSet里已经有了 nums[right],说明窗口内出现了重复,我们必须收缩窗口。
    3. 收缩时,left 指针向右移动,并从 HashSet 和当前的总和 currentSum 中移除 nums[left]。这个过程一直持续,直到 nums[right] 不再是重复元素为止。

踩坑与顿悟 😉:我最初的担忧是,那个 while 收缩循环会不会让整体复杂度退化成 O(N^2)?后来我恍然大悟:并不会!因为 leftright 指针从头到尾都只向右移动,每个元素最多被 right 指针访问一次,被 left 指针访问一次。所以均摊下来,总时间复杂度依然是 O(N),这就是滑动窗口的魅力!

java 复制代码
/*
 * 思路:标准滑动窗口。使用left/right指针定义窗口,HashSet维护窗口内元素的唯一性。
 * right指针扩张窗口,当遇到重复元素时,内层while循环收缩left指针,直到窗口恢复唯一。
 * 实时维护窗口和,并更新最大得分。
 * 时间复杂度:O(N),每个元素最多进出窗口一次。
 * 空间复杂度:O(M),M为数组中不同元素的数量。
 */
import java.util.HashSet;
import java.util.Set;

class Solution {
    public int maximumUniqueSubarray(int[] nums) {
        int left = 0;
        int maxScore = 0;
        int currentSum = 0;

        // 为什么用HashSet?因为它提供了平均O(1)的add, remove, contains操作,
        // 是维护窗口唯一性的不二之选,保证了算法的整体性能。
        Set<Integer> windowElements = new HashSet<>();

        for (int right = 0; right < nums.length; right++) {
            // 当新元素已存在于窗口中时,开始收缩
            while (windowElements.contains(nums[right])) {
                // 将最左侧的元素踢出窗口
                windowElements.remove(nums[left]);
                currentSum -= nums[left];
                left++; // 左边界向右移动
            }
          
            // 此刻窗口对新元素是安全的,将其纳入
            windowElements.add(nums[right]);
            currentSum += nums[right];
          
            // 每次窗口状态合法时,都尝试更新最大得分
            maxScore = Math.max(maxScore, currentSum);
        }
        return maxScore;
    }
}

解法二:优化滑动窗口 ------ 从"挪动"到"跳跃"

虽然解法一已经很棒了,但作为一个追求极致的开发者,我在想:那个 while 循环能不能也省掉?left 指针能不能不一步步挪,而是一次性"跳"到正确的位置?

答案是肯定的!为此,我们需要升级我们的工具:

  • HashSetHashMapHashSet 只能告诉我们元素在不在 ,而 HashMap<Integer, Integer> 可以告诉我们元素上一次出现在哪个位置 (value -> index)
  • currentSum前缀和 :因为 left 指针要跳跃,实时维护 currentSum 变得复杂。我们可以预先计算一个 prefixSum 数组,这样任何子数组 [l, r] 的和都能用 prefixSum[r+1] - prefixSum[l] 在 O(1) 时间内算出来。

新策略 :当 right 指针遇到 nums[right],我们查 HashMap 找到它上一次出现的位置 lastIndex。如果 lastIndex 在当前窗口内(>= left),我们就让 left 指针直接跳到 lastIndex + 1 的位置,一个崭新的、无重复的窗口就瞬间形成了!

java 复制代码
/*
 * 思路:优化的滑动窗口 + 前缀和。left指针直接跳跃到新位置,避免了内层循环。
 * 使用 HashMap 记录元素值及其最新索引。使用前缀和数组在O(1)内计算子数组和。
 * 当right指针遇到重复时,根据HashMap记录的位置,更新left指针,保证其不后退且跳到正确位置。
 * 时间复杂度:O(N)。
 * 空间复杂度:O(N),主要用于前缀和数组和HashMap。
 */
import java.util.HashMap;
import java.util.Map;

class Solution {
    public int maximumUniqueSubarray(int[] nums) {
        int n = nums.length;
        // 1. 预处理前缀和数组,为O(1)计算子数组和做准备
        int[] prefixSum = new int[n + 1];
        for (int i = 0; i < n; i++) {
            prefixSum[i + 1] = prefixSum[i] + nums[i];
        }

        // 2. 滑动窗口
        // HashMap: value -> last_seen_index
        Map<Integer, Integer> lastSeenIndex = new HashMap<>();
        int left = 0;
        int maxScore = 0;

        for (int right = 0; right < n; right++) {
            int currentNum = nums[right];
          
            if (lastSeenIndex.containsKey(currentNum)) {
                // 这是关键!left指针只能前进不能后退。
                // 如果找到的重复元素位置比left还靠前,那它早已不在窗口内,无需理会。
                left = Math.max(left, lastSeenIndex.get(currentNum) + 1);
            }
          
            // 更新当前元素的最新位置
            lastSeenIndex.put(currentNum, right);
          
            // 计算当前有效窗口 [left, right] 的得分,并更新最大值
            maxScore = Math.max(maxScore, prefixSum[right + 1] - prefixSum[left]);
        }
        return maxScore;
    }
}

举一反三,滑动窗口的应用远不止此

这个滑动窗口的模式,简直是解决各种"子串/子数组"问题的瑞士军刀:

  1. 最长无重复字符的子串(LeetCode 3:今天问题的变体,不求和,求最大长度。
  2. 串联所有单词的子串(LeetCode 30:更复杂的滑动窗口,窗口大小固定,但需要验证窗口内的单词构成是否满足要求。
  3. 最小覆盖子串(LeetCode 76 :在一个字符串中找到包含另一个字符串所有字符的最小子串。
  4. 服务端日志分析:在一段时间的日志流中,找到包含"error"、"exception"、"timeout" 这几个关键词的最短时间窗口,以快速定位问题。

最终,我用优化后的滑动窗口方案实现了用户行为分析功能,系统运行得又快又稳。从一个实际的业务需求出发,回归到经典的算法模型,再用代码将其实现,这个过程本身就是一种享受,不是吗?😉


两种解法对比表格

对比维度 解法1: 标准滑动窗口 解法2: 优化滑动窗口 + 前缀和
核心思想 HashSet + 内循环收缩窗口 HashMap + 前缀和 + 左指针跳跃
窗口调整方式 左指针逐步挪动 左指针一次性跳跃
求和方式 实时变量 currentSum 预计算 prefixSum 数组
优点 代码更直观,空间消耗可能更低(当唯一元素少时)。 逻辑更显技巧,避免了内循环,常数时间上可能更优。
缺点 存在内层循环(虽然均摊复杂度仍是线性的)。 代码稍复杂,且prefixSum数组需要固定的O(N)空间。
适用场景 通用首选,易于理解和实现。 对性能有极致追求,或在面试中展示对滑动窗口模式的深入理解。
相关推荐
CoovallyAIHub4 分钟前
避开算力坑!无人机桥梁检测场景下YOLO模型选型指南
深度学习·算法·计算机视觉
YouQian7728 分钟前
问题 C: 字符串匹配
c语言·数据结构·算法
yanxing.D13 分钟前
408——数据结构(第二章 线性表)
数据结构·算法
艾莉丝努力练剑43 分钟前
【LeetCode&数据结构】二叉树的应用(二)——二叉树的前序遍历问题、二叉树的中序遍历问题、二叉树的后序遍历问题详解
c语言·开发语言·数据结构·学习·算法·leetcode·链表
YuTaoShao1 小时前
【LeetCode 热题 100】51. N 皇后——回溯
java·算法·leetcode·职场和发展
1 小时前
3D碰撞检测系统 基于SAT算法+Burst优化(Unity)
算法·3d·unity·c#·游戏引擎·sat
Tony沈哲1 小时前
OpenCV 图像调色优化实录:基于图像金字塔的 RAW / HEIC 文件加载与调色实践
opencv·算法
我就是全世界2 小时前
Faiss中L2欧式距离与余弦相似度:究竟该如何选择?
算法·faiss
boyedu2 小时前
比特币运行机制全解析:区块链、共识算法与数字黄金的未来挑战
算法·区块链·共识算法·数字货币·加密货币
KarrySmile2 小时前
Day04–链表–24. 两两交换链表中的节点,19. 删除链表的倒数第 N 个结点,面试题 02.07. 链表相交,142. 环形链表 II
算法·链表·面试·双指针法·虚拟头结点·环形链表