【问题标题】:Java LinkedList : remove from to toJava LinkedList:从到到删除
【发布时间】:2017-10-28 22:07:00
【问题描述】:

我有一个 java.util.LinkedList 包含逻辑上类似的数据

1 > 2 > 3 > 4 > 5 > null

我想从 2 到 4 中删除元素,并像这样制作 LinkedList

1 > 5 > null

实际上,考虑到您必须在 2 处断开链并在一次操作中将其连接到 5,我们应该能够以 O(n) 复杂度实现这一点。

在 Java LinkedList 中,我找不到任何可以在单个 O(n) 操作中使用 from 和 to 从链表中删除链的函数。

它只为我提供了一个单独删除元素的选项(使每个操作 O(n))。

有没有我可以通过一次操作来实现这一点(无需编写我自己的列表)?

这里提供的一种解决方案是使用单行代码解决问题,而不是单操作。 list.subList(1, 4).clear();

问题更多是关于算法和性能。当我检查性能时,这实际上比一个一个删除元素要慢。我猜这个解决方案实际上并没有删除 o(n) 中的整个子列表,而是对每个元素一个接一个地执行此操作(每次删除 O(n))。还添加了额外的计算来获取子列表。

平均 1000000 次计算(毫秒):

没有子列表 = 1414
使用提供的子列表解决方案:= 1846**

【问题讨论】:

  • 你有 java.util.LinkedList 或其他类型的 [LinkedList] 吗?
  • 问题针对 java.util.LinkedList。
  • 感谢您的澄清!

标签: java list linked-list


【解决方案1】:

一步到位的方法是

list.subList(1, 4).clear();

java.util.LinkedList#subList(int, int) 的 Javadoc 中所述。

检查了源代码后,我发现这最终会一次删除一个元素。 subList 继承自 AbstractList。此实现返回一个List,当您在其上调用clear 时,它仅在后备列表上调用removeRangeremoveRange也继承自AbstractList,实现是

protected void removeRange(int fromIndex, int toIndex) {
    ListIterator<E> it = listIterator(fromIndex);
    for (int i=0, n=toIndex-fromIndex; i<n; i++) {
        it.next();
        it.remove();
    }
}

如您所见,这会一次删除一个元素。 listIterator LinkedList 中被覆盖,它首先通过从列表的开头或结尾跟随链接来跟随链找到第一个节点(取决于fromIndex 是否在列表的前半部分或后半部分)。这意味着list.subList(i, j).clear() 具有时间复杂度

O(j - i + min(i, list.size() - i)).

除了最好从头开始并以相反的顺序删除元素的情况之外,我不相信有明显更快的解决方案。测试代码的性能并不容易,很容易得出错误的结论。

没有办法使用LinkedList类的公共API一次性删除中间的所有元素。这让我感到惊讶,因为 唯一 使用 LinkedList 而不是 ArrayList 的原因是您应该能够有效地从中间插入和删除元素,所以我认为这个值得优化的案例(特别是因为它很容易编写)。

如果您绝对需要 O(1) 性能,您应该能够从诸如

的调用中获得
list.subList(1, list.size() - 1)).clear();

您要么必须编写自己的实现,要么通过反射做一些脆弱和不明智的事情,如下所示:

public static void main(String[] args) {
    LinkedList<Integer> list = new LinkedList<>();
    for (int a = 0; a < 5; a++)
        list.add(a);
    removeRange_NEVER_DO_THIS(list, 2, 4);
    System.out.println(list);       // [0, 1, 4]
}

public static void removeRange_NEVER_DO_THIS(LinkedList<?> list, int from, int to) {
    try {
        Method node = LinkedList.class.getDeclaredMethod("node", int.class);
        node.setAccessible(true);
        Object low = node.invoke(list, from - 1);
        Object hi = node.invoke(list, to);
        Class<?> clazz = low.getClass();
        Field nextNode = clazz.getDeclaredField("next");
        Field prevNode = clazz.getDeclaredField("prev");
        nextNode.setAccessible(true);
        prevNode.setAccessible(true);
        nextNode.set(low, hi);
        prevNode.set(hi, low);
        Field size = LinkedList.class.getDeclaredField("size");
        size.setAccessible(true);
        size.set(list, list.size() - to + from);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

【讨论】:

  • 感谢您的解决方案。这一步解决了这个问题,但问题更多的是算法和性能。当我检查性能时,这实际上比一个一个删除元素要慢。我猜这个解决方案实际上并没有删除 o(n) 中的整个子列表,而是一个一个地删除(每个 O(n))。还添加了额外的计算来获取子列表。 ** 1000000 次计算的平均毫秒数:没有子列表 = 1414 使用提供的子列表解决方案:= 1846**
  • 请注意,如果您最终移除头部或最后一个,新的反思答案可能会中断。此外,它可能算作 LinkedList 在 removeRange 中效率低下的库错误(这是这个答案在下面使用的)。
  • 是的,我不知道为什么我使用反射来包含该代码。这是一个非常糟糕的主意。
  • 感谢您的更新。我向 Java 提交了功能请求。
  • 干得好。 removeRange 应该在 LinkedList 中被明确地覆盖。我想不出一个很好的理由。
【解决方案2】:

要在单个操作(方法调用)中删除中间元素,您可以将java.util.LinkedList 子类化,然后公开对List.removeRange(int, int) 的调用:

list.removeRange(1, 4);

(感谢发布此答案然后将其删除的人。:))但是,即使此方法也调用了 ListIterator.remove() n 次。

我不相信有一种方法可以在不执行 n 次操作的情况下从 java.util.LinkedList 中删除 n 个连续条目。

一般来说,从任何链表中删除 n 个连续项目似乎需要 O(n) 操作,因为必须一次从开始索引遍历到结束索引一个项目 - 本质上 - 为了找到下一个列表条目修改后的列表。

【讨论】:

  • 它被删除了,因为它是一个受保护的方法
猜你喜欢
  • 1970-01-01
  • 2012-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-03
  • 1970-01-01
相关资源
最近更新 更多