算法导论-第14章-数据结构的扩张

文章介绍了通过扩展红黑树实现的两种数据结构:顺序统计树(OS树)和区间树。OS树支持快速找到集合中的第i小元素或元素的秩,插入和删除操作保持O(logn)时间复杂度。区间树则是在红黑树基础上增加了区间信息,支持区间查询操作,同样保持了O(logn)的效率。文章详细阐述了这两种数据结构的设计、维护和操作实现。

本章讨论通过扩展红黑树构造出的两种数据结构。14.1节介绍一种支持一般动态集合上顺序统计操作的数据结构。通过这种数据结构,我们可以快速地找到一个集合中的第 iii 小的数,或给出一个指定元素在集合的全序中的位置。14.2节抽象出数据结构的扩张过程,并给出一个简化红黑树扩张的定理。14.3节使用这个定理来设计一种用于维护由区间(如时间区间)构成的动态集合的数据结构。给定一个要查询的区间,我们能快速地找到集合中一个能与其重叠的区间。

14.1 动态顺序统计

对于一个无序的集合,我们能够在 O(n)\Omicron(n)O(n) 的时间内确定任何的顺序统计量(order statistic)。本节将介绍如何修改红黑树,使得可以在 O(log⁡n)\Omicron(\log n)O(logn) 时间内确定。我们还将看到如何在 O(log⁡n)\Omicron(\log n)O(logn) 时间内计算一个元素的,即它在集合线性序中的位置。

在一个由 nnn 个元素组成的集合中,第 iii 个顺序统计量(order statistic)是该集合中第 iii 小的元素。

OS树的定义

OS树,又称顺序统计树(Order-Statistic tree),是一棵红黑树在每个结点上扩充一个 sizesizesize 属性而得到的,如下图所示。在红黑树的结点 xxx 中,除了 x.key、x.color、x.left、x.right、x.px.key、x.color、x.left、x.right、x.px.keyx.colorx.leftx.rightx.p 之外,还包括 x.sizex.sizex.size 属性,这个属性包含了以 xxx 为根的子树(包括 xxx 本身)的结点数,即这棵子树的大小。如果定义 T.nil=0T.nil=0T.nil=0,即哨兵的大小为0,则有
x.size=x.left.size+x.right.size+1 x.size=x.left.size+x.right.size+1 x.size=x.left.size+x.right.size+1

在一棵顺序统计树中,并不要求关键字各不相同。例如,上图中的树就包含了两个值为14的关键字和两个值为21的关键字。在有相等关键字的情况下,前面秩的定义就不再合适。为此,我们通过定义一个元素的秩为在中序遍历时输出的位置来消除原先定义的不确定性。如上图所示,存储在黑色结点的关键字14的秩为5,存储在红色结点的关键字14的秩为6。

选择问题及算法

选择问题:在以 xxx 为根的子树中,查找第 iii 个最小元素。

OS-SELECT(x, i)
    r = x.left.size + 1
    if i == r 
        return x // 若i=r,则返回x
    elseif i < r
        return OS-SELECT(x.left, i) // 若i<r,则递归地在x的左子树中继续寻找第i个元素
    else return OS-SELECT(x.right, i-r) // 若i>r,则递归地在x的右子树中继续寻找第i-r个元素

OS-SELECT的运行时间为 O(log⁡n)\Omicron(\log n)O(logn)

求秩问题及算法

求秩问题:在OS树中,查找给定结点 xxx 的rank。

OS-RANK(T, x)
    r = x.left.size + 1
    y = x
    while y != T.root
        if y == y.p.right
            r = r + y.p.left.size + 1
        y = y.p
    return r

OS-RANK的运行时间为 O(log⁡n)\Omicron(\log n)O(logn)

OS树的维护:插入

OS树的插入和红黑树相似,分两个阶段。第一阶段,从根开始沿树下降,将新结点插入到树的末梢;第二阶段,沿树上升,做一些变色和旋转操作维持红黑树性质。

  • 第一阶段,为了维护子树的规模,对由根至叶子的路径上遍历的每一个结点 xxxx.size=x.size+1x.size = x.size + 1x.size=x.size+1。新增结点的的 sizesizesize 置1。由于一条遍历的路径上共有 O(log⁡n)\Omicron(\log n)O(logn) 个结点,故维护 sizesizesize 属性的额外代价为 O(log⁡n)\Omicron(\log n)O(logn)

  • 第二阶段,变色不改变 sizesizesize,旋转可能改变 sizesizesize。由于旋转是局部操作,只有轴上的两个结点的 sizesizesize 违反定义,只需在旋转操作后,对违反性质的结点 sizesizesize 进行修改。参考13.2节的左旋代码,在下面增加两行

    y.size = x.size
    x.size = x.left.size + x.right.size + 1
    

因为在红黑树的插入过程中最多进行两次旋转,所以在第二阶段更新 sizesizesize 属性只需要 O(1)\Omicron(1)O(1) 的额外时间。因此,对一棵有 nnn 个结点的OS树插入元素所需要的总时间为 O(log⁡n)\Omicron(\log n)O(logn)

OS树的维护:删除

  • Phase1:物理上删除 yyy,在删除 yyy 时从 yyy 上溯至根,将所经历的节点的 sizesizesize 均减1;

    • 附加成本:O(log⁡n)\Omicron(\log n)O(logn)
  • Phase2:采用变色和旋转方法,从叶子向上调整一变色不改变 sizesizesize;一旋转可能改变 sizesizesize,至多有3个旋转;

    • 附加成本:O(log⁡n)\Omicron(\log n)O(logn)

14.2 如何扩充数据结构

扩充一种数据结构可以分为4个步骤:

  1. 选择一种基础数据结构。
  2. 确定基础数据结构中要维护的附加信息。
  3. 检验基础数据结构上的基本修改操作能否维护附加信息。
  4. 设计一些新操作。

定理:红黑树的扩张
fffnnn 个结点的红黑树 TTT 扩张的属性,且假设对任一结点 xxxfff 的值仅依赖于结点 x、x.leftx、x.leftxx.leftx.rightx.rightx.right 的信息,还可能包括 x.left.fx.left.fx.left.fx.right.fx.right.fx.right.f。那么,我们可以在插入和删除操作期间对 TTT 的所有结点的 fff 值进行维护,并且不影响这两个操作的O(log⁡n)\Omicron(\log n)O(logn) 渐近时间性能。

14.3 区间树

基本概念

  • 区间:一个事件占用的时间。
  • 闭区间:实数的有序对 [t1,t2][t_1, t_2][t1,t2],其中 t1≤t2t_1 \le t_2t1t2
  • 区间的对象表示[t1,t2][t_1, t_2][t1,t2]可以用对象 iii 表示,有两个属性:
    • low[i]=t1low[i] = t_1low[i]=t1 // 起点或低点
    • high[i]=t2high[i] = t_2high[i]=t2 // 终点或高点
  • 区间的重叠i⋂i′≠∅⇔(low[i]≤high[i′])and(low[i′]≤high[i])i \bigcap i' \ne \varnothing \Leftrightarrow (low[i] \le high[i']) and (low[i'] \le high[i])ii=(low[i]high[i])and(low[i]high[i])

区间重叠的三分律

任何两个区间 iiii′i'i 满足区间三分律(interval trichotomy),即下面三条性质之一成立:

  • iiii′i'i 重叠。
  • iiii′i'i 的左边(也就是 i.high<i’.lowi.high \lt i’.lowi.high<i’.low)。
  • iiii′i'i 的右边(也就是 i′.high<i.lowi'.high \lt i.lowi.high<i.low)。

红黑树的扩充:区间树

区间树(interval tree)是一种对动态集合进行维护的红黑树,其中每个元素 xxx 都包含一个区间 x.intx.intx.int。区间树支持下列操作:

  • INTERVAL-INSERT(T, x):将包含区间属性 intintint 的元素 xxx 插入到区间树 TTT 中。
  • INTERVAL-DELETE(T, x):从区间树 TTT 中删除元素 xxx
  • INTERVAL-SEARCH(T, i):返回一个指向区间树 TTT 中元素 xxx 的指针,使 x.intx.intx.intiii 重叠;若此元素不存在,则返回 T.nilT.nilT.nil

在上图的区间树中,每个结点 xxx 包含一个区间,显示在结点中上方;一个以 xxx 为根的子树中所包含的区间端点的最大值显示在结点中下方。这棵树的中序遍历得到按左端点顺序排列的各个结点。

下面按照14.2节中介绍的四个步骤设计区间树:

第一步:基础数据结构(Step1: Underlying data structure)

选择红黑树作为区间树的基础数据结构,每个结点 xxx 包含区间属性 x.intx.intx.int,且 xxx 的关键字为 x.int.lowx.int.lowx.int.low。因此,该数据结构按中序遍历出的就是按低端点的次序排列的各区间。

第二步:附加信息(Step2: Additional information)

每个结点 xxx 中除了自身区间信息之外,还需要增加一个属性 x.maxx.maxx.max,它是以 xxx 为根的子树中所有区间端点的最大值。

第三步:维护信息(Step3: Maintaining information)

我们必须验证有 nnn 个结点区间树上的插入和删除操作能否在 O(log⁡n)\Omicron(\log n)O(logn) 时间内完成。通过给定区间 x.intx.intx.int 和结点 xxx 的子结点的 maxmaxmax值,可以确定 x.maxx.maxx.max值:
x.max=max{x.int.high,x.left.max,x.right.max} x.max=max\{x.int.high, x.left.max, x.right.max\} x.max=max{x.int.high,x.left.max,x.right.max}
这样,根据红黑树的扩充定理可得,插入和删除操作的运行时间为 O(log⁡n)\Omicron(\log n)O(logn)

第四步:设计新操作(Step 4: Developing new operations)

我们只需要增加唯一的新操作INTERVAL-SEARCH(T, i),它是用来找出树 TTT 中与区间 iii 重叠的那个结点。若树中与 iii 重叠的结点不存在,则下面过程返回指向哨兵 T.nilT.nilT.nil 的指针。伪代码如下:

INTERVAL-SEARCH(T. i)
    x = T.root
    while x ≠ T.nil and i does not overlap x.int
        if x.left ≠ T.nil and x.left.max ≥ i.low
            x = x.left    // overlap in left subtree or no overlap in right subtree
        else x = x.right    // no overlap in left subtree
    return x

查找与 iii 重叠的区间 xxx 的过程从 T.rootT.rootT.root 开始,逐步向下搜索。当找到一个重叠区间或者 xxx 指向 T.nilT.nilT.nil 时过程结束。由于基本循环每次迭代耗费 O(1)\Omicron(1)O(1) 时间,又因为 nnn 个结点的红黑树高度为 O(log⁡n)\Omicron(\log n)O(logn),所以INTERVAL-SEARCH的运行时间为 O(log⁡n)\Omicron(\log n)O(logn)

区间树上重叠区间的查找算法

对红黑树进行修改,使其成为一颗区间树,并实现区间树上的重叠区间查找算法。

输入:生成区间树,文件名: insert.txt(文件如下)。文件格式:第一行为待插入数据的个数,第二行之后每一行表示一个区间。初始时树应为空,按顺序插入。

30
18 23
45 46
30 34
2 17
13 18
9 18
32 43
43 45
21 24
6 15
49 50
14 22
11 22
38 43
15 16
19 24
36 42
8 13
7 14
42 50
48 49
0 1
4 15
24 25
34 43
10 17
31 40
17 18
16 25
33 37

输出:控制台直接打印查找结果。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

/**
 * 区间类定义
 */
class Interval {
    int left; // 左端点
    int right; // 右端点

    public Interval(int left, int right) {
        this.left = left;
        this.right = right;
    }
}

/**
 * 红黑树颜色枚举类定义
 */
enum Color {
    RED,
    BLACK
}

/**
 * 区间树结点类
 */
class IntervalNode {
    Interval interval; // 区间
    int max; // 区间端点的最大值
    Color color; // 结点颜色
    IntervalNode left; // 左孩子
    IntervalNode right; // 右孩子
    IntervalNode p; // 父结点

    public IntervalNode(Interval interval) {
        this.interval = interval;
        this.max = interval.right;
        this.color = Color.RED;
        this.left = null;
        this.right = null;
        this.p = null;
    }
}

/**
 * 区间树
 */
public class IntervalTree {
    private IntervalNode root;
    private IntervalNode nil; // 哨兵

    public IntervalTree() {
        this.nil = new IntervalNode(new Interval(-1, -1));
        nil.color = Color.BLACK;
        root = nil;
    }


    public void insert(Interval interval) {
        IntervalNode intervalNode = new IntervalNode(interval);

        IntervalNode y = nil;
        IntervalNode x = root;

        while (x != nil) {
            y = x;
            if (intervalNode.interval.right > x.max) {
                x.max = intervalNode.interval.right;
            }
            if (intervalNode.interval.left < x.interval.left) {
                x = x.left;
            } else {
                x = x.right;
            }
        }
        intervalNode.p = y;
        if (y == nil) {
            root = intervalNode;
        } else if (intervalNode.interval.left < y.interval.left) {
            y.left = intervalNode;
        } else {
            y.right = intervalNode;
        }
        intervalNode.left = nil;
        intervalNode.right = nil;
        intervalNode.color = Color.RED;
        insertFix(intervalNode);
    }


    public void insertFix(IntervalNode z) {
        IntervalNode y;
        while (z.p.color == Color.RED) {
            if (z.p == z.p.p.left) {                   // z的父节点是一个左孩子?
                y = z.p.p.right;                       // y是z的叔结点
                if (y.color == Color.RED) {            // z的父结点和叔结点都是红色?
                    z.p.color = Color.BLACK;           // case 1
                    y.color = Color.BLACK;             // case 1
                    z.p.p.color = Color.RED;           // case 1
                    z = z.p.p;                         // case 1
                    //System.out.println("case 1: z的父结点是左孩子,z的父结点和叔结点都是红色");
                } else {
                    if (z == z.p.right) {             // z的叔结点y是黑色且z是一个右孩子
                        z = z.p;                      // case 2
                        leftRotate(z);                // case 2
                        //System.out.println("case 2: z的父结点是左孩子,z的父结点是红色,z的叔结点y是黑色,z是一个右孩子");
                    }
                    // z的叔结点y是黑色且z是一个左孩子
                    z.p.color = Color.BLACK;          // case 3
                    z.p.p.color = Color.RED;          // case 3
                    rightRotate(z.p.p);               // case 3
                    //System.out.println("case 3: z的父结点是左孩子,z的父结点是红色,z的叔结点y是黑色,z是一个左孩子");
                }
            } else {                                  // z的父节点是一个右孩子?
                y = z.p.p.left;                       // y是z的叔结点
                if (y.color == Color.RED) {           // z的父结点和叔结点都是红色?
                    z.p.color = Color.BLACK;          // case 4
                    y.color = Color.BLACK;            // case 4
                    z.p.p.color = Color.RED;          // case 4
                    z = z.p.p;                        // case 4
                    //System.out.println("case 4: z的父结点是右孩子,z的父结点和叔结点都是红色");
                } else {
                    if (z == z.p.left) {              // z的叔结点y是黑色且z是一个左孩子
                        z = z.p;                      // case 5
                        rightRotate(z);               // case 5
                        //System.out.println("case 5: z的父结点是右孩子,z的父结点是红色,z的叔结点y是黑色,z是一个左孩子");
                    }
                    // z的叔结点y是黑色且z是一个左孩子
                    z.p.color = Color.BLACK;          // case 6
                    z.p.p.color = Color.RED;          // case 6
                    leftRotate(z.p.p);                // case 6
                    //System.out.println("case 6: z的父结点是右孩子,z的父结点是红色,z的叔结点y是黑色,z是一个右孩子");
                }
            }
        }
        root.color = Color.BLACK;
    }

    /**
     * 左旋
     *
     * @param x 左旋
     */
    public void leftRotate(IntervalNode x) {
        IntervalNode y = x.right;
        x.right = y.left;
        if (y.left != nil) {
            y.left.p = x;
        }
        y.p = x.p;
        if (x.p == nil) {
            root = y;
        } else if (x == x.p.left) {
            x.p.left = y;
        } else {
            x.p.right = y;
        }
        y.left = x;
        x.p = y;
        // 计算结点以x和y为根的子树中所有区间端点的最大值
        y.max = x.max;
        x.max = Math.max(x.interval.right, Math.max(x.left.max, x.right.max));
    }

    /**
     * 右旋
     *
     * @param x 右旋
     */
    public void rightRotate(IntervalNode x) {
        IntervalNode y = x.left;
        x.left = y.right;
        if (y.right != nil) {
            y.right.p = x;
        }
        y.p = x.p;
        if (x.p == nil) {
            root = y;
        } else if (x == x.p.right) {
            x.p.right = y;
        } else {
            x.p.left = y;
        }
        y.right = x;
        x.p = y;
        // 计算结点以x和y为根的子树中所有区间端点的最大值
        y.max = x.max;
        x.max = Math.max(x.interval.right, Math.max(x.left.max, x.right.max));
    }

    /**
     * 区间是否重叠
     *
     * @param i1 区间1
     * @param i2 区间2
     * @return boolean
     */
    public static boolean isOverLap(Interval i1, Interval i2) {
        return (i1.left <= i2.right) && (i1.right >= i2.left);
    }

    /**
     * 区间搜索
     *
     * @param intervalTree 区间树
     * @param interval     待搜索的区间
     * @return IntervalNode
     */
    public IntervalNode intervalSearch(IntervalTree intervalTree, Interval interval) {
        IntervalNode x = intervalTree.root;
        while (x != nil && !isOverLap(interval, x.interval)) {
            if (x.left != nil && x.left.max >= interval.left) {
                x = x.left;
            } else {
                x = x.right;
            }
        }
        return x;
    }

    /**
     * 中序遍历
     *
     * @param node   区间树结点
     * @param inList 返回集合
     */
    public void inOrder(IntervalNode node, List<IntervalNode> inList) {
        if (node == nil) return;
        inOrder(node.left, inList);
        inList.add(node);
        inOrder(node.right, inList);
    }


    public static void main(String[] args) throws IOException {
        // 读取insert.txt文件中的数据(第一行为数组长度, 第二行为数组(int类型)的内容)
        String path = "C:\\Projects\\IDEAProjects\\algorithms\\src\\main\\java\\ch14\\insert.txt";
        List<String> readAllLines = Files.readAllLines(Paths.get(path));
        int count = Integer.parseInt(readAllLines.get(0));

        IntervalTree intervalTree = new IntervalTree();

        for (int i = 1; i < readAllLines.size(); i++) {
            String[] split = readAllLines.get(i).split("\\s+");
            int left = Integer.parseInt(split[0]);
            int right = Integer.parseInt(split[1]);
            Interval interval = new Interval(left, right);
            intervalTree.insert(interval);
        }

        List<IntervalNode> list = new ArrayList<>();
        intervalTree.inOrder(intervalTree.root, list);
        for (IntervalNode intervalNode : list) {
            System.out.println("interval-left: " + intervalNode.interval.left + "\t"
                    + " interval-right: " + intervalNode.interval.right + "\t\t"
                    + " max: " + intervalNode.max);
        }

        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            int left = scanner.nextInt();
            int right = scanner.nextInt();
            IntervalNode res = intervalTree.intervalSearch(intervalTree, new Interval(left, right));
            if (res != intervalTree.nil) {
                System.out.println("查询结果:"
                    + res.interval.left + "\t"
                    + res.interval.right + "\t"
                    + res.color);
            }else {
                System.out.println("查询结果:查不到重叠区间");
            }

        }
    }
}

运行代码,可以看到中序遍历的结果正确,查询[16, 17]这个区间。首先在区间树中查询根结点[30, 34]区间,不符合,然后查询左子树[13, 18]区间,是重叠区间,查询返回结果正确!

interval-left: 0	 interval-right: 1		 max: 1
interval-left: 2	 interval-right: 17		 max: 17
interval-left: 4	 interval-right: 15		 max: 15
interval-left: 6	 interval-right: 15		 max: 22
interval-left: 7	 interval-right: 14		 max: 14
interval-left: 8	 interval-right: 13		 max: 14
interval-left: 9	 interval-right: 18		 max: 22
interval-left: 10	 interval-right: 17		 max: 17
interval-left: 11	 interval-right: 22		 max: 22
interval-left: 13	 interval-right: 18		 max: 25
interval-left: 14	 interval-right: 22		 max: 22
interval-left: 15	 interval-right: 16		 max: 25
interval-left: 16	 interval-right: 25		 max: 25
interval-left: 17	 interval-right: 18		 max: 25
interval-left: 18	 interval-right: 23		 max: 25
interval-left: 19	 interval-right: 24		 max: 24
interval-left: 21	 interval-right: 24		 max: 25
interval-left: 24	 interval-right: 25		 max: 25
interval-left: 30	 interval-right: 34		 max: 50
interval-left: 31	 interval-right: 40		 max: 40
interval-left: 32	 interval-right: 43		 max: 43
interval-left: 33	 interval-right: 37		 max: 37
interval-left: 34	 interval-right: 43		 max: 43
interval-left: 36	 interval-right: 42		 max: 50
interval-left: 38	 interval-right: 43		 max: 50
interval-left: 42	 interval-right: 50		 max: 50
interval-left: 43	 interval-right: 45		 max: 50
interval-left: 45	 interval-right: 46		 max: 46
interval-left: 48	 interval-right: 49		 max: 50
interval-left: 49	 interval-right: 50		 max: 50

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值