移动开发最新面试被问到线段树,已经这么卷了吗?(1),2024Android常见面试题

Android进阶资料

以下的资料是近年来,我和一些朋友面试收集整理了很多大厂的面试真题和资料,还有来自如阿里、小米、爱奇艺等一线大厂的大牛整理的架构进阶资料。希望可以帮助到大家。

Android进阶核心笔记

百万年薪必刷面试题

最全Android进阶学习视频

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

}

/**

  • 建树

  • @param treeIndex 当前线段树索引

  • @param treeLeft 节点区间左端点

  • @param right treeRight 节点区间右端点

*/

private fun buildSegmentTree(treeIndex: Int, treeLeft: Int, treeRight: Int) {

if (treeLeft == treeRight) {

// 叶子节点

tree[treeIndex] = merge(data[treeLeft], null)

return

}

val mid = (treeLeft + treeRight) ushr 1

val leftChild = leftChildIndex(treeIndex)

val rightChild = rightChildIndex(treeIndex)

// 构建左子树

buildSegmentTree(leftChild, treeLeft, mid)

// 构建右子树

buildSegmentTree(rightChild, mid + 1, treeRight)

tree[treeIndex] = merge(tree[leftChild], tree[rightChild])

}

复制代码

建树复杂度分析:

  • 时间复杂度:O(n)O(n)O(n)

  • 空间复杂度:O(4∗n)O(4 * n)O(4∗n) = O(n)O(n)O(n)

3.2 区间查询

区间查询是查询一段期望区间的结果,基本思路是递归查询子区间的结果,再通过合并子区间的结果来得到期望区间的结果。逻辑如下:

  • 0、从根节点开始查找(根节点是整个区间),递归执行以下步骤:

  • 1、如果查找范围正好等于节点区间范围,直接返回节点聚合数据;

  • 2、如果查找范围正好落在左子树区间范围,那么递归地在左子树查找;

  • 3、如果查找范围正好落在右子树区间范围,那么递归地在右子树查找;

  • 4、如果查找范围横跨两棵子树,那么拆分为两次递归查找,查找完成后 合并 结果。

/**

  • 区间查询

  • @param left 区间左端点

  • @param right 区间右端点

*/

fun query(left: Int, right: Int): E {

if (left < 0 || left >= data.size || right < 0 || right >= data.size || left > right) {

throw IllegalArgumentException(“Index is illegal.”);

}

return query(0, 0, data.size - 1, left, right) // 注意:取数据长度

}

/**

  • 区间查询

  • @param treeIndex 当前节点索引

  • @param dataLeft 当前节点左区间

  • @param dataRight 当前节点右区间

  • @param left 区间左端点

  • @param right 区间右端点

*/

private fun query(treeIndex: Int, dataLeft: Int, dataRight: Int, left: Int, right: Int): E {

if (dataLeft == left && dataRight == right) {

// 查询范围正好是线段树节点区间范围

return tree[treeIndex]!!

}

val mid = (dataLeft + dataRight) ushr 1

val leftChild = leftChildIndex(treeIndex)

val rightChild = rightChildIndex(treeIndex)

// 查询区间都在左子树

if (right <= mid) {

return query(leftChild, dataLeft, mid, left, right)

}

// 查询区间都在右子树

if (left >= mid + 1) {

return query(rightChild, mid + 1, dataRight, left, right)

}

// 查询区间横跨两棵子树

val leftResult = query(leftChild, dataLeft, mid, left, mid)

val rightResult = query(rightChild, mid + 1, dataRight, mid + 1, right)

return merge(leftResult, rightResult)

}

复制代码

查询复杂度分析:

  • 时间复杂度:取决于树的高度,为 O(lgn)O(lgn)O(lgn)

  • 空间复杂度:O(1)O(1)O(1)

3.3 单点更新

单点更新就是在数据变化之后适当调整线段树的结构,基本思路是递归地修改子区间的结果,再通过合并子区间的结果来更新期望当前节点的结果。逻辑如下:

  • 0、更新原数据(data 数组),然后从根节点开始更新值(根节点是整个区间),递归执行以下步骤:

  • 1、如果是叶子节点(left = right),直接更新;

  • 2、如果更新节点正好落在左子树区间范围,那么递归地在左子树更新;

  • 3、如果更新节点正好落在右子树区间范围,那么递归地在右子树更新;

  • 4、更新左右子树之后,再通过合并子树信息来更新当前节点。

/**

  • 单点更新

  • @param index 数据索引

  • @param value 新值

*/

fun set(index: Int, value: E) {

if (index < 0 || index >= data.size) {

throw IllegalArgumentException(“Index is illegal.”);

}

data[index] = value

set(0, 0, data.size - 1, index, value) // 注意:取数据长度

}

private fun set(treeIndex: Int, dataLeft: Int, dataRight: Int, index: Int, value: E) {

if (dataLeft == dataRight) {

// 叶子节点

tree[treeIndex] = value

return

}

// 先更新左右子树,再更新当前节点

val mid = (dataLeft + dataRight) ushr 1

val leftChild = leftChildIndex(treeIndex)

val rightChild = rightChildIndex(treeIndex)

if (index <= mid) {

set(leftChild, dataLeft, mid, index, value)

} else if (index >= mid + 1) {

set(rightChild, mid + 1, dataRight, index, value)

}

tree[treeIndex] = merge(tree[leftChild], tree[rightChild])

}

复制代码

更新复杂度分析:

  • 时间复杂度:取决于树的高度,为 O(lgn)O(lgn)O(lgn)

  • 空间复杂度:O(1)O(1)O(1)

到这里,我们的线段树数据结构就实现完成了,完整代码如下:SegmentTree


4. 典型例题 · 区域和检索 - 数组可变

=======================

307. 区域和检索 - 数组可变 【题解】

给你一个数组 nums ,请你完成两类查询,其中一类查询要求更新数组下标对应的值,另一类查询要求返回数组中某个范围内元素的总和。

class NumArray(nums: IntArray) {

fun update(index: Int, val: Int) {

}

fun sumRange(left: Int, right: Int): Int {

}

}

这道题与 【题 303】 是差不多的,区别在于数组是否可变,属于 动态数据 的场景。上一节,我们已经实现了一个通用的线段树数据结构,我们直接使用就好啦。

参考代码:

class NumArray(nums: IntArray) {

private val segmentTree = SegmentTree(nums.toTypedArray()) { e1: Int?, e2: Int? ->

if (null == e1)

e2!!

else if (null == e2)

e1

else

e1 + e2

}

fun update(index: Int, val: Int) {

segmentTree.set(index, val)

}

fun sumRange(left: Int, right: Int): Int {

return segmentTree.query(left, right)

}

}

有点东西~~没几行代码就搞定了,运行结果也比采用前缀树的方法优秀更多。但是单纯从做题的角度,如果每做一道题都要编写这么一大串 SegmentTree 代码,似乎就太蛋疼了。有没有别的变通方法呢?


5. 线段树的解题框架

============

定义 SegmentTree 数据结构太花力气,这一节,我们来讨论一种不需要定义 SegmentTree 的通用解题框架。这个解法还是很巧妙的,它虽然不严格满足线段树的定义(不是二叉搜索树,但依然是平衡二叉树),但是实现更简单。

参考代码:

class NumArray(nums: IntArray) {

private val n = nums.size

private val tree = IntArray(2 * n) { 0 } // 注意:线段树大小为 2 * n

init {

// 构建叶子节点

for (index in n until 2 * n) {

tree[index] = nums[index - n]

}

// 依次构建父节点

for (index in n - 1 downTo 0) {

tree[index] = tree[index * 2] + tree[index * 2 + 1]

}

}

fun update(index: Int, val: Int) {

// 1、先直接更新对应的叶子节点

var treeIndex = index + n

tree[treeIndex] = val

while (treeIndex > 0) {

// 2、循环更新父节点,根据当前节点是偶数还是奇数,判断选择哪两个节点来合并为父节点

val left = if (0 == treeIndex % 2) treeIndex else treeIndex - 1

val right = if (0 == treeIndex % 2) treeIndex + 1 else treeIndex

tree[treeIndex / 2] = tree[left] + tree[right]

treeIndex /= 2

}

}

fun sumRange(i: Int, j: Int): Int {

var sum = 0

var left = i + n

var right = j + n

while (left <= right) {

if (1 == left % 2) {

sum += tree[left]

left++

}

if (0 == right % 2) {

sum += tree[right]

right–

}

left /= 2

right /= 2

}

return sum

}

}

这种实现的优点是只需要 2 * n 空间,而不需要 4 * n 空间下面解释下代码。代码主要由三个部分组成:

5.1 建树

构建线段树需要初始化一个 2∗n2*n2∗n 空间的数组,采用 自底向上 的方式来构建整棵线段树。首先,构建叶子节点,叶子节点的位于数组区间 [n,2n−1][n,2n -1][n,2n−1],随后再根据子节点的结果来构建父节点(下标为indexindexindex的节点,左子节点下标:2∗index2*index2∗index,右子节点下标:2∗index+12*index+12∗index+1)。参考以下示意图:

5.2 区间查询

区间查询是查询一段期望区间的结果,相对于常规方法构造的线段树,这种线段树的区间查询过程相对较难理解。基本思路是递归地寻找能够代表该区间的节点。逻辑如下:

  • 1、一开始的区间查询等同于线段树数组 [n,2n−1][n,2n-1][n,2n−1] 之间的若干个叶子节点 [left,right][left,right][left,right] 的合并,我们需要向上一层寻找能够代表这些节点的父节点;

  • 2、对于节点 indexindexindex,它的左子节点下标:2∗index2*index2∗index,右子节点下标:2∗index+12*index+12∗index+1,这意味着所有左子节点下标是偶数,所有右子节点下标是奇数;

  • 3、left/=2left /= 2left/=2和right/=2right /= 2right/=2则是寻找父节点,如果 leftleftleft 指针是奇数,那么 leftleftleft 指针节点一定是一个右节点,此时 left/2left/2left/2 节点就无法直接代表 leftleftleft 指针节点,于是只能单独加上这个 “落单” 的节点。同理,如果 rightrightright 指针是偶数,那么 righttrighttrightt 指针节点一定是一个左节点,,此时 right/2right /2right/2 节点就无法直接代表 rightright right 指针节点,于是只能单独加上这个 “落单” 的节点;

  • 4、最后循环退出前leftrightleft == rightleftright,说明当前节点的区间(去除 “落单” 的节点)正好是所求的区间,直接加上。并且下一躺循环leftleftleft一定大于rightrightright,跳出循环。

5.3 单点更新

单点更新就是在数据变化之后适当调整线段树的结构,基本思路是:先更新目标位置对应的节点,递归地更新父节点。需要注意根据当前节点的索引是偶数或奇数,来确定选择哪两个节点来合并为父节点。

例如,更新的节点是 “a” 节点,它在线段树数组索引 index 是偶数(下标为 6),那么它的父节点是 “ab” 节点需要通过合并 tree[index] + tree[index+1] 来获得的。


6. 总结

======

  • 前缀和数组与线段树都适用与区间查询问题,前者在数据更新频繁时整体性能会下降,后者平衡了更新与查询两者的时间复杂度,复杂度都是O(lgn)O(lgn)O(lgn);

  • 从解题的角度,常规的构建线段树的方法太复杂,可以采用反常规的线段树构建方式,代码会更加简洁,空间复杂度也更优秀;

  • 除了线段树,你还知道什么类似的数据结构擅长于区间查询和单点更新吗?

7. 最后

======

最后

如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。

欢迎大家一起交流讨论啊~

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!


6. 总结

======

  • 前缀和数组与线段树都适用与区间查询问题,前者在数据更新频繁时整体性能会下降,后者平衡了更新与查询两者的时间复杂度,复杂度都是O(lgn)O(lgn)O(lgn);

  • 从解题的角度,常规的构建线段树的方法太复杂,可以采用反常规的线段树构建方式,代码会更加简洁,空间复杂度也更优秀;

  • 除了线段树,你还知道什么类似的数据结构擅长于区间查询和单点更新吗?

7. 最后

======

最后

如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。

[外链图片转存中…(img-HpkgSZkd-1715467308779)]

欢迎大家一起交流讨论啊~

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 19
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值