1. 背景介绍
从这章开始,我们将会深入讲解一下Voronoi图(Voronoi Diagrams)相关的内容,但是在进入到Voronoi图讲解之前,我们需要先来讲解一下特殊的数据结构——双向链接的红黑树(Doubly-linked red black tree),它的实现和理解对于我们高效实现Voronoi图算法至关重要,所以整个系列文章的结构如下:
- 双向链接的二叉查找树(Doubly-linked binary search tree);
- 双向链接的红黑树(Doubly-linked red black tree);
- 点定位(Point Location);
- Voronoi图(Voronoi Diagrams);
为了清晰地理解前两个数据结构,你需要理解原版的二叉查找树和红黑树,不太清楚的童鞋可以参考我写的系列文章:超详细 | 红黑树详解文章汇总(含代码)
2. 基本概念
2.1 为什们需要双向链接?
之所以要在BST(二叉查找树)和BBST(平衡二叉查找树,这里特指红黑树)中引入双向链接的概念,是因为在某些特殊的算法(如Voronoi图)中,我们在插入或删除某个节点时,我们不能使用key的比较来进行查找删除操作,必须依赖树本身的空间结构进行插入和删除,这是什么意思呢?比如下面这个场景:
再上图的场景中,我们是无法使用之前的插入方式,即自顶向下,通过比较key的大小来确定节点插入到某个节点的左还是右子树之中。但是通过观察,我们也发现了我们可以通过BST的空间结构进行插入,即明确知道插入位置的时候,我们不必依赖key的比较,直接进行插入即可。对于删除操作,也会有同样的问题,大家可以想象一下,某一时刻BST中出现了数个key相同,但是我们明确知道删除的是哪个节点,所以也无需依赖key的比较,直接在树进行删除即可。
相似地,BBST中也会有同样的场景应用,只是会更加复杂,因为BBST中会涉及旋转自平衡,所以接下来我们先来讲解如何在BST中实现上述的需求,在理解BST的基础上,在深入讨论BBST的实现。
除了插入和删除,我们在查找直接前继(Predecessor)或直接后继(Successor)时,也无法通过key的比较来查找,但是通过BST的空间结构关系,我们可以快速知道某个节点的直接前继或后继,比如M的前继就是L,后继是N,我们可以通过某一机制来实现这个快速查找过程,无需依赖key的比较。总之,双向链接的主要目的为:
使BST和BBST无需依赖key的比较,也能快速进行查找,删除等操作。
或许大家也能注意到,出现这些问题的关键在于,我们的BST或BBST的key不再是静态的,而是随时某种状态的改变而改变,即key为动态的,这种状态或是时间,也或是算法的状态。这种BST或BBST,笔者一般称之为动态BST或BBST( Dynamic BST or BBST ),这样的数据结构在计算几何中非常的常见,因为树中存储在Value中的空间几何结构会随着算法的变化,而其对应的key也会产生相应的改变,并非静态一成不变的。
2.2 结构和定义
那双向链接到底是什么意思呢?首先,双向是指:
每个父节点有指针指向其子节点,每个子节点有指针指向其父结点。
其次,链接是指:
BST或BBST都会维护一个有序双向链表用来表示树中的数据。
即2.1中的例子,我们把树的节点投影到数值上,而我们用双向链表(Doubly-linked list)来存储这样的数轴。这样,我们就能在O(1)时间内查找某个节点的直接前继或后继,下图将展示一个完整的双向链接BST:
经过这样的处理,大家也需要注意到,这样的BST已经不是一棵树了,严格意义上来说,它是一个连接图(Connected Graph)。
3. 直接前后继
在理解双向链接的概念之后,我们就来看看如何实现上面的需求。首先,我们先来看看直接前后继的实现。实现这个操作,我们需要维护一个双向链表。双向链表是一个非常基础的数据结构,不是很懂的童鞋可以网上查查其相关的概念和基础操作,这里不再赘述。
与普通的双向链表不同,我们这里需要它有直接插入和删除节点的能力,因为它的结构会根据BST的改变而改变,所以Java内置的LinkedList就不能满足我们的需求,因为内置的链表不会暴露节点给外部,所以插入和删除操作都是O(n),如果用它去实现双向链接BST,会使BST的性能大幅退化,所以需要一个能允许外部直接插入和删除节点的双向链表。
以删除为例:
/**
* remove the given node in this linked list and return its value.
* return null if this list is empty.
* Do nothing if the node is not in this list.
* */
public E remove( DoublyLinkedNode<E> node ) {
if ( node == null ) return null;
assert contains( node );
if ( node.prev != null ) node.prev.next = node.next;
if ( node.next != null ) node.next.prev = node.prev;
if ( front == node ) front = node.next;
if ( end == node ) end = node.prev;
node.prev = null;
node.next = null;
size--;
assert check();
return node.data;
}
传入的参数就是一个节点,而不是下标或者节点里面的值,具体的删除操作和普通的链表基本一致,大家可以结合代码自己画图理解一下,应该不难。在实现这样的链表之后,我们就能快速查找某个树节点的直接前后继了,方法非常简单,我们只需要找到某个树节点在链表中的节点,再查找其上一个(下一个)节点即可。所以除了链表节点需要存储树节点,这个树节点也需要存储它所在的链表节点(树节点 <=> 链表节点),之后我们可以查看链表节点的直接前继或后继。以查找前继为例:
/**
* find node's predecessor. Assume that the node is already in this BST.
* Note that this method doesn't compare keys to find the predecessor,
* so is different from lower();
*
* Ordered linked list is the key to implementing this method.
* */
default MapTreeNode<K, V> predecessor( MapTreeNode<K, V> node ) {
if ( node == null ) return null;
return node.node.getPrev() == null ? null : node.node.getPrev().getData();
}
我们查找给定树节点中的链表节点,看看它是否有前继节点。另外,如果想实现BST或BBST能保留插入顺序,可以使用这里相似的思路:维护另一个双向链表,用来存储插入顺序下的数据,而非树本身的空间结构。
接下来,我们将讲解一下实现双向链接功能的插入和删除操作,之后我们会进入到双向链接BBST的讲解之中。
上一节:终章:二叉查找树和红黑树的性能比较
下一节:双向链接的二叉查找树(二):直接插入和删除
系列汇总:塞尔达和计算几何 | Voronoi图详解文章汇总(含代码)
4. 免责声明
※ 本文之中如有错误和不准确的地方,欢迎大家指正哒~
※ 此项目仅用于学习交流,请不要用于任何形式的商用用途,谢谢呢;