Java TreeSet – 如何高效地插入然后轮流获取相邻元素?

huangapple go评论70阅读模式
英文:

Java TreeSet - How to efficiently insert then poll immediate neighbors?

问题

寻找一个适用于纯粹的Java 8,不使用任何库的解决方案。

我正在尝试实现标准的线段相交算法。这涉及到对当前“活动”线段的按y坐标排序的集合进行跟踪。

算法的一部分是,假设有一个TreeSet actives 和一个新的线段 s

  • s 插入到 actives
  • 查找 activess 的相邻线段并进行一些数学计算

我已经将这个实现为:

// s 的左邻居,就好像已经添加了
try {
    Segment sa = actives.tailSet(s).first();
	if (math(sa, s) {
        ...
    }
} catch(NoSuchElementException ignore) {}
// s 的右邻居,就好像已经添加了
try {
    Segment sb = actives.headSet(s).last();
	if (math(sb, s) {
        ...
    }
} catch(NoSuchElementException ignore) {}	
				
// 将 s 添加到 actives
actives.add(s);

除了让人烦恼的 try/catch 来处理 first()last() 失败的情况(这是为了在首先将子集存储在一个变量中,然后检查它是否为空,然后在继续执行之间进行权衡),我所理解的真正问题是这是低效的,即:

tailSet()headSet()add() 在我的 TreeSet actives 上都需要 O(log(N)) 的工作,而算法假设我只需要 O(log(N)) 插入一次,然后在 O(1) 时间内查找相邻线段。

我该如何做类似的事情,但只产生一次 O(log(N)) 的二进制查找?我做的工作是必要工作的3倍。

英文:

Looking for a solution that would work on stock Java 8, no libraries.

I'm trying to implement the standard Line Segment intersection algorithm. This involves keeping track of a sorted collection (on y-coordinate) of Line Segments currently "active".

Part of the algorithm is, assuming a TreeSet actives and a new line Segment s:

  • insert s into actives
  • lookup the immediate neighbors of s in actives and do some math

I've implemented this as:

// left neighbor of s, as though already added
try {
    Segment sa = actives.tailSet(s).first();
	if (math(sa, s) {
        ...
    }
} catch(NoSuchElementException ignore) {}
// right neighbor versus s, as though already added
try {
    Segment sb = actives.headSet(s).last();
	if (math(sb, s) {
        ...
    }
} catch(NoSuchElementException ignore) {}	
				
// add s => actives
actives.add(s);

Aside from the aggravating try/catch in case first() or last() fails (which is a trade off against first getting the subset in a variable, THEN checking if it's empty, THEN proceeding if not), the real problem, as I understand it, is this is inefficient, namely:

tailSet(), headSet(), and add() all incur O(log(N)) work on my TreeSet actives, while the algorithm assumes I can insert once in O(log(N)), and then look up neighbors in O(1).

How can I do something similar, but incur the O(log(N)) binary lookup only once? I'm doing 3x as much work as necessary.

答案1

得分: 1

也许首先将元素s添加到actives,然后调用actives.lower(s)actives.higher(s)TreeSet文档未说明这些方法的复杂度,但在源代码中它们似乎是O(1)。

英文:

Perhaps try adding the element s to actives first, then calling actives.lower(s) and actives.higher(s). The TreeSet docs don't state the complexity of those methods but they look O(1) to me in the source code.

答案2

得分: 1

我稍微修改了你的代码:

// s 左边的邻居,就好像已经被添加了一样
SortedSet<Segment> trailSet = actives.tailSet(s);
if (!trailSet.isEmpty()) {
    Segment sa = trailSet.first();
    if (math(sa, s) {
        ...
    }
}

// s 右边的邻居,就好像已经被添加了一样
SortedSet<Segment> headSet = actives.headSet(s);
if (!headSet.isEmpty()) {
    Segment sb = headSet.last();
    if (math(sb, s) {
        ...
    }
}

// 将 s 添加到 actives
actives.add(s);

不需要捕获任何异常(因为你可以在获取第一个(或最后一个)元素之前检查是否为空)。这样做更高效,因为它不会生成堆栈跟踪。

但你甚至可以使用更好的方法。根据 https://docs.oracle.com/javase/7/docs/api/java/util/TreeSet.html ,你可以调用一个方法,该方法会:

返回严格小于给定元素的此集合中的最大元素;如果没有这样的元素,则返回 null。

或者返回最小的、严格大于给定元素的元素。这些方法被称为 lowerhigher

Segment sa = actives.higher(s);
if (sa != null && math(sa, s) {
    ...
}

// s 右边的邻居,就好像已经被添加了一样
Segment sb = actives.lower(s);
if (sb != null && math(sb, s) {
    ...
}

// 将 s 添加到 actives
actives.add(s);
英文:

I rewrote your code a bit:

// left neighbor of s, as though already added
SortedSet&lt;Segment&gt; trailSet = actives.tailSet(s);
if (!trailSet.isEmpty()) {
    Segment sa = trailSet.first();
    if (math(sa, s) {
        ...
    }
}

// right neighbor versus s, as though already added
SortedSet&lt;Segment&gt; headSet = actives.headSet(s);
if (!headSet.isEmpty()) {
    Segment sb = headSet.last();
    if (math(sb, s) {
        ...
    }
}

// add s =&gt; actives
actives.add(s);

There is no need to catch anything (as you can check emptieness ahead of retrieving the first (or last) element). This is more performant, as it does not cause stack trace generation.

But you can even go with something better. According to https://docs.oracle.com/javase/7/docs/api/java/util/TreeSet.html , you can call a method that

> Returns the greatest element in this set strictly less than the given element, or null if there is no such element.

or the smallest, strictly greater. The methods are called lower and higher.

Segment sa = actives.higher(s);
if (sa != null &amp;&amp; math(sa, s) {
    ...
}

// right neighbor versus s, as though already added
Segment sb = actives.lower(s);
if (sb != null &amp;&amp; math(sb, s) {
    ...
}

// add s =&gt; actives
actives.add(s);

huangapple
  • 本文由 发表于 2020年5月4日 13:10:07
  • 转载请务必保留本文链接:https://go.coder-hub.com/61585539.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定