树状数组之查找中位数详解(1057 Stack (30 分))

本文介绍了如何使用树状数组解决在栈中查找中位数的问题。首先阐述了树状数组的原理,包括其存储特点和动态前缀和的应用。接着详细讲解了如何利用树状数组查询和修改元素,以及如何找到二进制表示下的第一个1。最后,通过实例展示了如何在序列中找到中位数,并给出了柳神的代码实现。
摘要由CSDN通过智能技术生成

intuition

最近在刷pta甲级的题目, 解题过程中遇到一个之前没有用过的知识点——树状数组, 原题链接在这里1057 Stack (30 分), 题目的大致意思是在传统的栈的基础上,要添加一个查询当前栈中元素的中位数的功能, 首先想到可以用通过维护一个BST来做, 在一个二叉搜索树中插入和删除元素都是数据结构的基本内容, 查找在所有节点中比当前节点小的节点数也可以在log(n)的复杂度实现, 实现的细节在最后补充中进行了说明. 但实现一个BST数据结构做一道OJ题来说有些傻和重, 所以搜索了一下题解, 发现可以通过树状数组来得到中位数.

在介绍树状数组的查找中位数应用之前,有两个知识点需要介绍:

  1. 中位数的性质:很重要 对于一个大小为N的序列中的中位数, 序列中存在(N+1) / 2 - 1个数比它小. 要找中位数也就是要统计比在序列中比当前值小的元素个数是否是(N+1)/2-1个.
  2. 树状数组的概念和特点.

树状数组

网上有不少讲树状数组的中文资料, 但很多都讲得不够直观. 我好不容易搞懂了之后在这里做个记录.
首先这张图反映了树状数组的存储内容特点.

树状数组

树状数组的元素并不都是存储自己的值, 对于有的位置的元素, 它存储的是一个区间的元素的和, 比如2位置存储的是1和2的值之和, 12存储的是9, 10, 11, 12的元素的值之和, 而8位置存储的是1到8所有元素之和, 每个位置存储哪个区间的值之和的规律是什么呢?
这要讲到二进制相关的规律.
我们看下这几个位置的索引的二进制有什么特点

index 二进制数值 存储的元素之和包含哪些 存储的元素之和包含的元素个数
2 0010 1, 2 2
12 1100 9,10, 11,12 4
8 1000 1,2,…,8 8

回过来, 看这几个索引的二进制包含的零的数量k和其存储的元素之和包含的元素个数m 存在 m = 2 k m = 2^k m=2k的关系.
也即该索引的二进制表示中, 第一个1表示的数与其包含的求和区域有关, 比如12的第一个1出现在100即是4,8的第一个1出现在1000也即8. 索引和该索引位置存储的求和范围的关系我们搞清楚了, 接下来要回答的是, 这种关系能做什么.

树状数组的常见应用之前缀和

这种关系的一个典型应用是动态前缀和. 所谓动态前缀和就是对于一个序列 a1, a2, ... , an有查询和修改操作, 查询的内容是对于给定的am,要查询a1+a2+...+am的值,;修改是这个序列会动态变化, 增加元素, 删除元素, 修改元素. 对于暴力算法, 一次修改的复杂度是O(1), 一次查询的复杂度是O(n), 我们想要通过某种方法降低查询的复杂度, 这就是树状数组.

树状数组如何查询

由上面的那张图可以看出, 例如要查找13位置的前缀和, 就只需要访问位置13, 12, 8, 对他们的值累加, 就得到了13位置的元素的前缀和.
我们来看二进制视角下, 这一次查询访问的位置有什么特点:

13: 1101
12: 1100
8:   1000
1101 = 1000 + 0100 + 0001

可以看出来, 访问的位置数量就是该索引中1出现的数量, 访问的位置和1的位置有关.

访问13
13: 第一个1为1
13 - 1 -> 12
访问12
12: 第一个1为100
12-4(100) -> 8
访问8
8: 第一个1为1000
8-8 -> 0
结束

树状数组如何修改

修改某个索引的值 am时, 要修改所有包含了am的元素, 对于上图, 修改13时, 要修改14, 16, 修改9时, 要修改10, 12, 16
我们在二进制视角下看看访问的这些位置有什么特点

修改a9
9: 1001 第一个1为1
9+1->10
修改10
10: 1010 第一个1为10
10+2(10)->12
修改12
12: 1100 第一个1为100
12+4(100)->16
修改16, 到达数组的最大尺寸,结束

讲完了动态前缀和, 应该大家就很容易想到, 找中位数的核心是找序列中比它小的数的个数, 也可以用树状数组来做. 此时当am这个值在序列中时, 我们就令am=1, 否则am=0, 统计a_q前缀和就是统计比a_q小的数有多少个.

举个例子, 有一个序列[1,3,4,5,7], 现在用一个长度8起点索引为1的hashTable来表示, 就是[1, 0, 1, 1, 1, 0, 1, 0], 将这个hashTable转换成树状数组的表示就是[1, 1, 1, 3, 1, 1, 1, 5]. 见下表, 可以对照上面的示意图一起看.

1 2 3 4 5 6 7 8
hash表示 1 0 1 1 1 0 1 0
树状数组表示 1 1 1 3 1 1 1 5

在讲找中位数之前, 还要讲的一个问题是如何找到一个数的二进制表示下的第一个1的位置
回顾一个问题, 一个正数的补码如何表示, 例如,对于6(0110),它的补码是其反码加1, 也就是(1001+1= 1010), 求一个二进制数的补码, 就是找到这个数中的第一个1, 其右边保持不变, 左边取反. 于是我们可以用一个很巧妙的方法, 取到第一个1的位置

int lowbit(int i) {
   
    return i & (-1);
}

通过该二进制数和其补码做与运算,保留了第一个1以及其右边的部分, 得到的就是这个1

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值