初步学习线段树

182 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情

线段树是什么?

线段树是一个平衡的二叉树,它将每个长度不为1的区间划分成左右两个区间递归求解。令整个区间的长度为N,则其有N个叶节点,每个叶节点代表一个单位区间,每个内部结点代表的区间为其两个儿子代表区间的联集。这种数据结构可以方便的进行大部分的区间操作。(维基百科
线段树常用于算法竞赛中。以序列{10,11,12,13,14}为例。

image.png

图源oi-wiki.org/ds/seg/

该线段树主要表达区间和的信息,也可以构造表达最大值或最小值的区间信息。线段树可以让有关区间的操作复杂度在o(logn)

线段树的基本操作

以洛谷该道题为例。www.luogu.com.cn/problem/P33…
该题分两个操作,节点x加y/输出区间[x,y]的和。

线段树的建树过程

已知序列,对序列进行线段树的构造。1<=n<=5x10^5,因此需要开辟一个4n的数组。

为什么线段树需要开辟4n的大小?

首先,对于高为h层的满二叉树,共有2^h-1个节点,最后一层有2^(h-1)个节点。也就是说除了最后一层的前面所有节点数=总节点数-最后一层节点数=(2^h-1)-2^(h-1)=2^(h-1)-1,前面所有层节点之和等于最后一层节点数

  • 如果n恰好是2的k次幂m,由于线段树的叶子结点存储的是数组元素本身,节点数为n,前面所有层节点数之和是n-1,总节点数是2n-1。
  • 如果n不是2的k次幂,最坏情况是n = 2^k+1,有一个元素需要开辟新的一层存储,需要n-2+2(n-1)+n-2=4n-5的大小。

对于该题,建树过程如下

// 建树过程,序号为i的节点表示区间[l,r],完善tree
func buildTree(i, l, r int) {
   if l == r {
      tree[i] = nums[l]
      return
   }
   mid := (l + r) >> 1
   buildTree(i+i, l, mid)
   buildTree(i+i+1, mid+1, r)
   tree[i] = tree[i+i] + tree[i+i+1]
}

使用buildTree(1,1,n)表示序号1开始,建立从1-n的线段树。建树的基本思路就是递归。
而对于两个操作。

线段树的其他操作

  1. 操作1:节点x+y,那么线段树中从根结点到x的路径都需要+y
// 对于符号k,对应区间[l,r],区间中第x个数+y
func add(k, l, r, x, y int) {
   // 区间[l,r]包含x,对于x的路径都要加y
   tree[k] += y
   // 叶子结点
   if l == r {
      return
   }
   mid := (l + r) >> 1
   if x <= mid {
      add(k+k, l, mid, x, y)
   } else {
      add(k+k+1, mid+1, r, x, y)
   }
}
  1. 操作2: 返回[x,y]的区间和。其实就是在线段树中寻找区间[x,y],返回对应节点的值。
// 符号为i的节点,对应区间[l,r],将[x,y]区间的和输出
func calc(i, l, r, x, y int) int {
   if l == x && r == y {
      return tree[i]
   }
   mid := (l + r) >> 1
   if y <= mid {
      // 所求区间在左子树
      return calc(i+i, l, mid, x, y)
   } else if x > mid {
      //     所求区间在右子树
      return calc(i+i+1, mid+1, r, x, y)
   } else {
      // 所求区间在左子树和右子树中间
      return calc(i+i, l, mid, x, mid) + calc(i+i+1, mid+1, r, mid+1, y)
   }
}

其他

我是用go语言完成该题,输入输出使用fmt.Scan/fmt.Println,提交后超时,最高70分。查看题解,获知了go语言竞赛的常用模版。

  • 标准库的fmt包没有缓冲区,需要使用bufio包进行封装。
  • fscan不是从标准输入中读取数据而是从io.Reader中读取数据。
var (
   in  = bufio.NewReader(os.Stdin)
   out = bufio.NewWriter(os.Stdout)
)
func main() {
   defer out.Flush()
   var n, m int
   fmt.Fscan(in, &n, &m)
   nums = make([]int, n+1)
   for i := 1; i <= n; i++ {
      fmt.Fscan(in, &nums[i])
   }
   buildTree(1, 1, n)
   for i := 0; i < m; i++ {

      fmt.Fscan(in, &flag, &x, &y)
      if flag == 1 {
         add(1, 1, n, x, y)
      } else if flag == 2 {
         fmt.Fprintln(out, calc(1, 1, n, x, y))
      }
   }
}

其他应用

每天有10亿人的登录,用户登出时,会输出登入时间和登出时间,平均在线时长为1小时,如何快速的求某一秒钟内的在线人数?

思路:可以用线段树来处理。在知道(loginTime,logoutTime)时,将区间(loginTime, logoutTime-1)加1。

参考

juejin.cn/post/685861…
www.bilibili.com/video/BV1qY…
pkg.go.dev/bufio#Write…
blog.csdn.net/xiexingshis…