Android Kotlin使用前缀和数组解决“区间和查询“问题

Posted 上马定江山

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Kotlin使用前缀和数组解决“区间和查询“问题相关的知识,希望对你有一定的参考价值。

前言

大家好,我是小彭。

今天分享到一种非常有趣的数据结构 —— 前缀和数组。前缀和的思想本身很容易理解,同时也是理解更高难度的线段树、字典树等数据结构的基础。

那么,什么是前缀和,我们可以使用前缀和解决什么问题呢?今天我们就围绕这两个问题展开。


学习路线图:


1. 什么是前缀和

前缀和数组是一种用来高效地解决 “静态数据的频繁区间和查询” 问题的数据结构。

这就需要使用前缀和 + 差分技巧:

前缀和示意图


2. 典型例题 · 区间和检索

理解以上概念后,就已经具备解决区间和问题的基本知识了。我们来看一道 LeetCode 上的前缀和典型例题:LeetCode 303. 区域和检索 - 数组不可变

LeetCode 例题

题解

class NumArray(nums: IntArray) 

    // 前缀和数组
    // 数组长度加一后不用考虑数组越界,代码更简洁
    private val preSum = IntArray(nums.size + 1)  0 

    init 
        for (index in nums.indices) 
            preSum[index + 1] = preSum[index] + nums[index]
        
    

    fun sumRange(i: Int, j: Int): Int 
        return preSum[j + 1] - preSum[i]
    

代码很简单,其中前缀和数组 preSum 的长度要额外加 1 是为了简化数组越界判断。我们来分析它的复杂度:

另外,前缀和还适用于二维区间和检索,思路都是类似的,你可以试试看: LeetCode · 304. 二维区域和检索 - 矩阵不可变


3. 典型例题 · 前缀和 + 哈希表

继续看另一道前缀和与哈希表结合的例题:LeetCode 560. 和为 K 的子数组

LeetCode 例题

这道题就是在考前缀和思想,我们可以轻松地写出第一种解法:

题解

class Solution 
    fun subarraySum(nums: IntArray, k: Int): Int 
        // 1、预处理:构造前缀和数组
        var preSum = IntArray(nums.size + 1)  0 
        for (index in nums.indices) 
            preSum[index + 1] = preSum[index] + nums[index]
        

        // 2、枚举所有子数组,使用「前缀和 + 差分」技巧计算区间和
        var result = 0
        for (i in nums.indices) 
            for (j in i until nums.size) 
                val sum_i_j = preSum[j + 1] - preSum[i]
                if (k == sum_i_j) 
                    result++
                
            
        
        return result
    

在这个题解中,我们枚举每个子数组,使用「前缀和 + 差分」技巧计算区间和为 K 的个数,我们来分析下它的复杂度:

前缀和示意图

题解

class Solution 
    fun subarraySum(nums: IntArray, k: Int): Int 
        var preSum = 0
        var result = 0

        // 维护哈希表<前缀和,出现次数>
        val map = HashMap<Int, Int>()
        map[0] = 1

        for (index in nums.indices) 
            preSum += nums[index]
            // 获得前缀和为 preSum - k 的出现次数
            val offset = preSum - k
            if (map.contains(offset)) 
                result += map[offset]!!
            
            map[preSum] = map.getOrDefault(preSum, 0) + 1
        
        return result
    

我们来分析下它的复杂度:


4. 典型例题 · 前缀和 + 单调队列

继续看一道前缀和与单调队列结合的例题,你可以先做一下第 53 题:

LeetCode 例题

在第 53 题中,我们只需要维护一个当前观察到的最小前缀和变量,将其与当前的前缀和做差值,就可以得到以当前节点为右端点的最大的区间和。这一题就是考前缀和思想,相对简单。

第 53 题题解

class Solution 
    fun maxSubArray(nums: IntArray): Int 
        // 前缀和 + 单调:维护最小的前缀和
        var minPreSum = 0
        var result = Integer.MIN_VALUE
        var preSum = 0

        for (element in nums) 
            preSum += element
            result = Math.max(result, preSum - minPreSum)
            minPreSum = Math.min(minPreSum, preSum)
        
        return result
    

在第 918 题中,数组变为环形数组,环形数组的问题一般都会用 2 倍的 “假数据长度” 做模拟,求前缀和数组这一步大同小异。

关键在于: “子数组最多只能包含固定缓冲区 num 中的每个元素一次”,这意味随着观察的区间右节点逐渐向右移动,所允许的左区间会限制在一个滑动窗口范围内,以避免元素重复出现。因此,一个变量不再能够满足题目需求。

示意图

所以我们的问题就是要求这个 “滑动窗口中的最小前缀和”。Wait a minute! 滑动窗口的最小值?这不就是 使用单调队列解决 “滑动窗口最大值” 问题 这篇文章讲的内容吗,秒懂,单调队列安排一下。

第 918 题题解

class Solution 
    fun maxSubarraySumCircular(nums: IntArray): Int 
        val preSum = IntArray(nums.size * 2 + 1).apply 
            for (index in 0 until nums.size * 2) 
                this[index + 1] = this[index] + nums[index % nums.size]
            
        

        // 单调队列(从队头到队尾递增)
        val queue = LinkedList<Int>()
        var result = Integer.MIN_VALUE

        for (index in 1 until preSum.size) 
            // if:移除队头超出滑动窗口范围的元素
            // 前缀和窗口 k 为 length + 1,比原数组上的逻辑窗口大 1 位,因为区间的差值要找前一个节点的前缀和
            if (!queue.isEmpty() && queue.peekFirst() < index - nums.size /* index - k + 1 */) 
                queue.pollFirst()
            

            // 从队头取目标元素
            result = Math.max(result, preSum[index] - (preSum[queue.peekFirst() ?: 0]))

            // while:队尾元素大于当前元素,说明队尾元素不再可能是最小值,后续不再考虑它
            while (!queue.isEmpty() && preSum[queue.peekLast()] >= preSum[index]) 
                // 抛弃队尾元素
                queue.pollLast()
            
            queue.offerLast(index)
        
        return result
    

我们来分析它的时间复杂度:


5. 总结

到这里,前缀和的内容就讲完了。文章开头也提到了, 前缀和数组是一种高效地解决静态数据的频繁区间和查询问题的数据结构,只需要构造一次前缀和数组,就能使用 O(1) 时间完成查询操作。

那么,在动态数据的场景中(即动态增加或删除元素),使用前缀和数组进行区间和查询是否还保持高效呢?如果不够高效,有其他的数据结构可以解决吗?你可以思考以下 2 道题:

更多同类型题目:

前缀和难度题解
303. 区间和检索 - 数组不可变Easy【题解】
724. 寻找数组的中心下标Easy【题解】
304. 二维区域和检索 - 矩阵不可变Medium【题解】
560. 和为 K 的子数组Medium【题解】
974. 和可被 K 整除的子数组Medium【题解】
1314. 矩阵区域和Medium【题解】
918. 环形子数组的最大和Medium【题解】
525. 连续数组Medium【题解】
1248. 统计「优美子数组」Medium【题解】

参考资料

作者:彭旭锐
链接:https://juejin.cn/post/7147962276534812685

高效掌握 Kotlin 的方法

Kotlin 是门典型的易学难精的语言:语法简洁,极容易入门,但又拥有许多的新特性,不容易掌;即使掌握了 Kotlin 的语法,想要写出优雅的代码,也不容易,更别提 Kotlin 特性的应用场景、底层实现原理了。

在这里给大家分享由谷歌工程师整理的总结的《Kotlin从入门到精通》及《Kotlin高级强化实战》资料,帮助大家用最少的精力入门并掌握Kotlin编程语言。教程内容由浅入深,通俗易懂,实例丰富,既有基础知识,也有进阶技能,需要的伙伴们可以点击文末卡片免费领取!!

一、《Kotlin从入门到精通》

目录

该内容会使用Kotlin作为主要的语言来开发一个android应用。方式是通过开发一个应用来学习这门语言,而不是根据传统的结构来学习。

我会在感兴趣的点停下来通过与Java1.7对比的方式讲讲Kotlin的一些概念和特性。用这种方法你就能知道它们的不同之处,并且知道哪部分语言特性可以让你提高你的工作效率。


现在你知道使用Kotlin实现的小例子了,我确信你会希望尽可能快地把它用在你的实践当中去。不要担心,在第一章中会帮助你去搭建你的开发环境,这样你才能立即编写代码。

从Kotlin语法概念的理解项目的创建再到整个项目的完成,每篇都是是采用“一对一”的讲解方式, 侧重总结工作上的实践经验,并和你分享一些疑难问题的解决思路,让你在以后的工作中,能够有方法论的指导。


二、《Kotlin高级强化实战》

第一章 Kotlin入门教程

  • Kotlin 概述
  • Kotlin 与 Java 比较
  • 巧用 Android Studio
  • 认识 Kotlin 基本类型
  • 走进 Kotlin 的数组
  • 走进 Kotlin 的集合
  • 集合问题
  • 完整代码
  • 基础语法

第二章 Kotlin 实战避坑指南

  • 方法入参是常量,不可修改
  • 不要 Companion 、INSTANCE ?
  • Java 重载,在 Kotlin 中怎么巧妙过渡一下?
  • Kotlin 中的判空姿势
  • Kotlin 复写 Java 父类中的方法
  • Kotlin “狠”起来,连TODO 都不放过!
  • is、as` 中的坑
  • Kotlin 中的 Property 的理解
  • also 关键字
  • takeIf 关键字
  • takeIf 关键字
  • 单例模式的写法

“工欲善其事,必先利其器”,学习以上知识点,可以让你在接下来的实战学习中,事半功倍。

第三章 项目实战《Kotlin Jetpack 实战》

  • 从一个膜拜大神的 Demo 开始
  • Kotlin 写 Gradle 脚本是一种什么体验?
  • Kotlin 编程的三重境界
  • Kotlin 高阶函数
  • Kotlin 泛型
  • Kotlin 扩展
  • Kotlin 委托
  • 协程“不为人知”的调试技巧
  • 图解协程:suspend

由于文章篇幅有限,资料内容过多,只展示目录和部分截图,需要该资料完整版的小伙伴可点击文末卡片免费领取!!

以上是关于Android Kotlin使用前缀和数组解决“区间和查询“问题的主要内容,如果未能解决你的问题,请参考以下文章

Android Kotlin + Klaxon - 解析 JSON 根数组

用 Kotlin 写 Android 3—数组,集合与字符串

kotlin-android-extensions插件被废弃--解决办法

Kotlin-android:未解决的参考数据绑定

每天学一点 Kotlin -- 集合:Array 数组

[M前缀和] lc930. 和相同的二元子数组(滑动窗口+双指针+哈希优化)