线段树

开一贴,把之前学过的线段树重新总结一下

 

 

引例

来看一个例子

对于一个长度为1000000的数组,现在有两种操作:

  • 查询数组中某个区间的最值
  • 更新数组中某个区间的值

这样的操作要是有1000000次,如果我们用朴素的算法,即每次以O(n)的效率去处理操作,无疑是非常耗时的。

线段树对于每个查询更新 的操作,都是O(logn)的时间复杂度,因此大大提高了效率。


线段树基本概念

 如上图所示,是一颗完整的线段树,接下来有关的操作都通过这张图来说明,我们可以从图中得出一些结论:

  • 线段树是一颗二叉搜索树
  • 每个结点存储着区间和数据信息

首先看第一个结论。线段树是一颗二叉搜索树说明每个结点最多有两个子结点,由于线段树将区间不断二分的特点,因此结点如果有子结点则必定是两个子结点,否则是叶子节点。设左边界为left, 右边界为right, 如果left != right,则左子树区间为[left, (left+right)/2], 右子树是[(left+right)/2+1, right],否则 left = right, 是叶子节点,不必继续划分。在上图的例子中,区间[1,8]可以划分,(1 + 8) / 2 = 4, 则左子树的区间为[1,4], 右子树的区间为[5,8];继续划分左子树,可以发现区间[1, 1]已经不能划分,因此停止递归。

有了这样的数据结构,那么我们就可以在其中存储相关的信息。

例如上图中的线段树就是存储每个区间中的最大值。显然我们只需要在每个叶子结点中存储相对应的数据,之后的整棵树经过构造之后,就会记录所有区间的最大值信息。图中的[1,2]区间的最大值显然是通过比较左右子树的最大值得到的,左子树的值为1,右子树的值为2,因此区间[1,2]上的最大值为2,同理得出其他所有子树的最大值,最后可以算出根节点的最大值为8。

接下来我们讨论线段树的存储问题,线段树可以用指针的方式来存储,但在实际的做题过程中,采用结构体数组的方式比较方便。下图中的 l 表示左边界,r 表示右边界,Max用来存储区间的最大值。

#define maxn 200010
struct node{
    int l;
    int r;
    int Max;
}T[maxn*4];

使用上图的数组存储中,我们可以很方便的访问一个非叶子结点的左右子树,假设一个非叶子结点的下标为i,则左子树为2*i,右子树为2*i+1

此外,一般在实际应用中,数组的大小为所要求的结点的4倍即可。


线段树基本操作

有了线段树的基本概念后,我们来讨论线段树的四种操作:构造(build), 查询(query),更新(update), 查找(search)。

构造

int val[maxn]; // 原始数组
void build(int left, int right, int num)
{
    T[num].l = left;
    T[num].r = right;
    if(left == right)
    {
        T[num].Max = val[left];
        return;
    }
    int mid = (left+right)/2;
    build(left, mid, 2*num);
    build(mid+1, right, 2*num+1);
    T[num].Max = max(T[2*num].Max, T[2*num+1].Max);
}

上图是求最大值的线段树构造过程,其他有关的线段树算法都大同小异,基本的框架是一样的,只有细节有变化。

构造是从根节点开始,不断深度优先搜索的过程,如果left == right,就递归到头,返回;否则继续搜索该结点的左右子树,在递归的过程中,自底向上更新最大值。

查询

void query(int x, int y, int num)
{
    if(T[num].l == x && T[num].r == y)
    {
        ans = max(ans, T[num].Max);
        return ;
    }
    int mid = (T[num].l + T[num].r)/2;
    if(x > mid) query(x, y, 2*num+1);
    else if(y <= mid) query(x, y, 2*num);
    else 
    {
        query(x, mid, 2*num);
        query(mid+1, y, 2*num+1);
    }
}

上图是线段树的查询过程,其中[x,y]为待查询的区间,每次查询总是和区间中点的值进行比较,分为以下三种情况:

  • x > mid,说明查询的区间在线段树右边,直接递归线段树的右子树
  • y <= mid, 说明查询的区间在线段树左边,直接递归线段树的左子树
  • x <= mid < y, 说明查询的区间同时包含线段树的左右的一部分,所以两边都需要递归

这里必须注意:因为 mid = (T[num].l + T[num].r)/2, int 类型总是向下取整的,因此每次算出来的mid实际上是在左子树上,因此只有整个区间 > mid(而不是>=)才能递归右子树。

以上面的例子来说明,整个区间为[1,8],mid = 4

  1. 如果查询[1,3] ,x = 1,y = 3, y <= mid,直接递归线段树的左子树
  2. 如果查询[5,7] ,x = 5,y = 7, x > mid,直接递归线段树的右子树
  3. 如果查询[3,6] ,x = 3,y = 6,x <= mid < y,两边的子树都需要递归

这里的查询操作是用了全局变量,因此不返回值,如果用局部变量的话,则需要最后的返回。

更新

void update(int x, int y, int num)
{
    if(T[num].l == T[num].r && T[num].l == x)
    {
        T[num].Max = y;
        return ;
    }
    int mid = (T[num].l+T[num].r)/2;
    if(x > mid) update(x, y, 2*num+1);
    else if(x <= mid) update(x, y, 2*num);
    T[num].Max = max(T[2*num].Max, T[2*num+1].Max);
}

 上图是单点更新的线段树操作,x为要更新的数组下标,y为要更新的值,只要递归到此区间更新值即可,同时注意最后需要更新区间的最大值。

查找

void Search(int num)
{
    printf("l=%d r=%d Max=%d\n",T[num].l,T[num].r,T[num].Max);
    if(T[num].l == T[num].r) return ;
    Search(2*num);
    Search(2*num+1);
}

查找是用来debug,可以输出整棵线段树的信息。 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值