线段树(四):代码解析·续


在介绍完父类和相关工具类,最后我们来重点解析一下SegmentTree类。

1. 整体代码

现在让我们来看看今天的主角SegmentTree类:

/**
 * Data structure of SegmentTree with generics.
 *
 * Support two kinds of RMQ,
 * range minimum query and range maximum query
 *
 * Application: Windowing Query in computational geometry
 * @see <a href=https://www.edx.org/course/computational-geometry>Windowing Query</a>
 *
 * Related POJ problem:
 * @see <a href=http://poj.org/problem?id=2991>Crane</a>
 *
 * Reference resource:
 * Programming Contest Challenge Book, The Second edition
 * Authors: Takuya Akiba, Yoichi Iwata, Masatoshi Kitagawa
 *
 * @author       Xiaoyu Tongyang, or call me sora for short
 */

public class SegmentTree<E> extends PerfectBinaryTree<SegmentNode<E>> {
    private final int originSize;
    // power of 2 for the size of elements
    private final int n;
    // comparator to compare element, E
    protected final Comparator<E> comparator;

    /**
     * constructs to create an instance of SegmentTree
     * */

    public SegmentTree( List<E> elements ) {
        this( elements, null );
    }

    public SegmentTree( List<E> elements, Comparator<E> comparator ) {}

    /**
     * make the size of heap tree the power of 2
     *
     * e.g.
     * 5, 3, 7 -> 5, 3, 7, null
     * */

    private static
    int grow( int capacity ) {}

    /**
     * initialize this Segment Tree
     * */

    private void initialize( List<E> elements, int index, int left, int right ) {}

    /**
     * update while handling null elements
     * */

    private void update( SegmentNode<E> node,
                         SegmentNode<E> left, SegmentNode<E> right ) {}

    /**
     * set the element at index to the new one
     * */

    public void update( int index, E element ) {}

    private E query( int a, int b, int k,
                     int l, int r, boolean isMin ) {}

    /**
     * range minimum query and range maximum query
     *
     * @param  isMin     true, minimum query; false maximum query
     * */

    public E query( int leftTarget, int rightTarget, boolean isMin ) {}

    private static
    void testOne() {}

    private static
    void testTwo() {}

    private static
    void testFour() {}

    private static
    void testFive() {}

    public static
    void main( String[] args ) {}
}

2. 初始化

2.1 构造函数

首先我们先来看看SG的初始化,首先是构造函数和grow()方法:

public SegmentTree( List<E> elements, Comparator<E> comparator ) {
    // total number of elements in this heap tree
    super( grow( elements.size() ) * 2 - 1 );

    if ( elements.isEmpty() )
        throw new RuntimeException( "Empty initialization in SegmentTree" );

    originSize = elements.size();
    n = grow( elements.size() );

    // initializing capacity, mainly for segment tree
    int capacity = n * 2 - 1;
    for ( int i = 0; i < capacity; i++ )
        tree.add( null );

    this.comparator = comparator;
    initialize( elements, 0, 0, n );
}

    /**
     * make the size of heap tree the power of 2
     *
     * e.g.
     * 5, 3, 7 -> 5, 3, 7, null
     * */

    private static
    int grow( int capacity ) {
        int n = 1;
        while ( n < capacity ) n <<= 1;

        return n;
    }

首先在构造函数里面,我们不允许空树的初始化,因为现阶段SG只能更新修改元素,但不能增加元素:

if ( elements.isEmpty() )
    throw new RuntimeException( "Empty initialization in SegmentTree" );

其次,我们看看grow()方法,这个方法主要是将给定数组长度处理成2的幂,比如传进来3,最后会生成长度为4的树数组,如果传进来的是6,则会生成长度为8的树数组。为什么需要这样处理呢?如果不进行这样处理,我们不能保证生成的树是一颗完美二叉树,即使将奇数处理成偶数也不行,必须是2的幂。

我们以3和6为例,用下图给大家展示不处理成2的幂的情况:

在这里插入图片描述
至于初始化的树数组的长度,大家应该还记得我们证明过一颗完美二叉树的节点个数:
n + n 2 + n 4 + . . . + 4 + 2 + 1 = n ∗ 1 − ( 1 2 ) l o g n 1 − 1 2 = 2 n ∗ ( 1 − ( 1 2 ) l o g n ) < = 2 n n + \frac{n}{2} + \frac{n}{4} + ... + 4 + 2 + 1 = n * \frac{1-(\frac{1}{2})^{logn}}{1-\frac{1}{2}} = 2n * ( 1 - (\frac{1}{2})^{logn}) <= 2n n+2n+4n+...+4+2+1=n1211(21)logn=2n(1(21)logn)<=2n
所以实际的树数组长度只需要 2 * n(2的幂处理后的长度)- 1,因为顶层只有Root一个节点,没有偶数个,所以会有这里的代码:

// total number of elements in this heap tree
super( grow( elements.size() ) * 2 - 1 );

2.2 递归生成SG

接下来是递归生成SG的方法:

/**
 * initialize this Segment Tree
 *
 * @param left   inclusive
 * @param right  exclusive
 * */

private void initialize( List<E> elements, int index, int left, int right ) {
    // base cases
    if ( left >= elements.size() ) {
        tree.set( index, new SegmentNode<E>( null ) );
        return;
    }
    // one element left
    if ( left + 1 >= right ) {
        assert left + 1 == right;
        tree.set( index, new SegmentNode<E>( elements.get( left ) ) );
        return;
    }

    // set this interval node
    List<E> minMax = CompareElement.minMax( comparator, elements,
    left, Math.min( right, elements.size() ) );
    tree.set( index, new SegmentNode<>( minMax.get( 0 ), minMax.get( 1 ) ) );

    // set its left and right children
    int mid = MyArrays.mid( left, right );
    initialize( elements, getChildrenIndex( index, true ), left, mid );
    initialize( elements, getChildrenIndex( index, false ), mid, right );
}

对于刚才扩容的方法,我们在生成SG的时候,就必须保证原数组中已经填充了null元素,然后进行递归生成。教材里面用的就是这种思路,但是教材里面的元素是int,然后用一个用不到的数值表示null,比如INT_MAX。而我们这里实现了一个更难的思路,传进来的元素是不确定的,为通配符,所以我们需要用到null来表示某个元素不存在。

但我在这里没有直接填充null到原数组里面,而是用下标越界来判断某个区间是不是我们填充的null,非原数组原本就有的元素。我们通过观察可以发现:

如果当前左下标left >= 原数组长度,则left下标(含)后面的元素都是填充的null元素,我们直接返回一个包含null的节点;

如果当前左下标 < 数组长度,且右下标right >= 原数组长度,则right下标(含)后面的元素都是填充的null节点,我们只需对有效区间进行查找最大值和最小值,并生成相应的一个节点即可;

还是不是很明白的童鞋,可以针对上面两个例子,结合代码自己画一画递归流程图就能理解啦哒,或者直接填充原数组,简单粗暴。

3. 查询(Query)

/**
 * range minimum query and range maximum query
 *
 * @param  r     exclusive
 * */
     
private E query( int a, int b, int k,
                int l, int r, boolean isMin ) {
    // base cases
    // 1. not overlapping intervals, so no result
    if ( r <= a || b <= l )
        return null;
    // 2. target interval fully covers the current interval,
    // return the result
    if ( a <= l && r <= b )
        return isMin ? tree.get( k ).min : tree.get( k ).max;

    // query left and right children
    int mid = MyArrays.mid( l, r );
    E left = query( a, b, getChildrenIndex( k, true ),
                   l, mid, isMin );
    E right = query( a, b, getChildrenIndex( k, false ),
                    mid, r, isMin );

    // return the right result
    if ( left == null ) return right;
    else if ( right == null ) return left;

    return isMin ? CompareElement.min( comparator, left, right ) :
    CompareElement.max( comparator, left, right );
}

/**
 * range minimum query and range maximum query
 *
 * @param  isMin     true, minimum query; false maximum query
 * */

public E query( int leftTarget, int rightTarget, boolean isMin ) {
    return MyArrays.isOutOfIndex( leftTarget, rightTarget - 1, originSize ) ? null :
    query( leftTarget, rightTarget, 0, 0, n, isMin );
}

查询的代码基本就是之前我们讲解思路的直接实现,这里只需要注意一下这里:

MyArrays.isOutOfIndex( leftTarget, rightTarget - 1, originSize )

这里是检查给定查找区间是否越界,如果越界则直接返回null。

4. 更新(Update)

/**
 * update while handling null elements
 * */

private void update( SegmentNode<E> node,
                    SegmentNode<E> left, SegmentNode<E> right ) {
    assert left.max != null || right.max != null;
    if ( left.max == null ) {
        node.min = right.min;
        node.max = right.max;
    }
    else if ( right.max == null ) {
        node.min = left.min;
        node.max = left.max;
    }
    else {
        node.min = CompareElement.min( comparator,
                                      left.min, right.min );
        node.max = CompareElement.max( comparator,
                                      left.max, right.max );
    }
}

/**
 * set the element at index to the new one
 * */

public void update( int index, E element ) {
    if ( MyArrays.isOutOfIndex( index, originSize ) )
        return;

    // get the index of leaf node in the heap tree
    index += n - 1;
    // update the leaf node
    SegmentNode<E> node = tree.get( index );
    node.min = element;
    node.max = element;

    // bottom-up updating
    while ( index > 0 ) {
        index = getParentIndex( index );
        node = tree.get( index );
        SegmentNode<E> left = tree.get( getChildrenIndex( index, true ) );
        SegmentNode<E> right = tree.get( getChildrenIndex( index, false ) );

        update( node, left, right );
    }
}

更新的代码也基本就是之前我们讲解思路的直接实现,只是因为我们引入了null,所以更新最大值和最小值的时候,比较麻烦,需要检查null,但是代码思路不难,也就是第一个update()方法;这里需要重点讲解一下我们如何得到原数组中某个下标在树数组中的下标:

// get the index of leaf node in the heap tree
index += n - 1;

之前我们构造完美二叉树时,把原数组的元素都放在最下一层,且填充的元素一定在最底层的右边,所以我们需要用0(根结点的下标) + 上面层级的节点数,就能得到第一个元素在树数组中的下标。那么,上层一共有多少个节点呢?之前我们知道整个二叉完美数的节点数为2 * n - 1,除去最后一层的n个节点,所以上层一共有n - 1个节点,这也是我们用原数组下标 += n - 1的意义。

到此,我们将把线段树给大家讲解完毕了,整个项目的代码可以在系列文章汇总里面找到哦,能看到这里真是辛苦大家啦哒~

上一节:线段树(三):代码解析

系列汇总:超详细!线段树讲解文章汇总(含代码)

5. 拓展阅读

  1. 计算几何课堂:几何寻路之旅
  2. 超详细!红黑树详解文章汇总(含代码)
  3. 挑战程序设计竞赛:反转法
  4. 挑战程序设计竞赛:尺取法

6. 参考资料

  1. 《挑战程序设计竞赛(第2版)》,(日)秋叶拓哉 等著,人民邮电出版社;

7. 免责声明

※ 本文之中如有错误和不准确的地方,欢迎大家指正哒~
※ 此项目仅用于学习交流,请不要用于任何形式的商用用途,谢谢呢;


在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值