0
点赞
收藏
分享

微信扫一扫

Java Collections.rotate 方法浅析

yongxinz 2023-03-19 阅读 63


一、概述

前面一篇文讲述了 Java 中移动 ​​ArrayList​​​元素的方法。其中涉及到了​​java.util.Collections#rotate​​ 方法,该方法可以实现 list 元素的旋转,即统一向前或向后移动多少个位置。

Java Collections.rotate 方法浅析_jvm

本文简单对 ​​java.util.Collections#rotate​​ 方法进行分析和学习。

二、研究

2.1 rotate 源码解析

先上 ​​java.util.Collections#rotate​​ 方法的源码:

/**
* Rotates the elements in the specified list by the specified distance.
* After calling this method, the element at index {@code i} will be
* the element previously at index {@code (i - distance)} mod
* {@code list.size()}, for all values of {@code i} between {@code 0}
* and {@code list.size()-1}, inclusive. (This method has no effect on
* the size of the list.)
*
* <p>For example, suppose {@code list} comprises{@code [t, a, n, k, s]}.
* After invoking {@code Collections.rotate(list, 1)} (or
* {@code Collections.rotate(list, -4)}), {@code list} will comprise
* {@code [s, t, a, n, k]}.
*
* <p>Note that this method can usefully be applied to sublists to
* move one or more elements within a list while preserving the
* order of the remaining elements. For example, the following idiom
* moves the element at index {@code j} forward to position
* {@code k} (which must be greater than or equal to {@code j}):
* <pre>
* Collections.rotate(list.subList(j, k+1), -1);
* </pre>
* To make this concrete, suppose {@code list} comprises
* {@code [a, b, c, d, e]}. To move the element at index {@code 1}
* ({@code b}) forward two positions, perform the following invocation:
* <pre>
* Collections.rotate(l.subList(1, 4), -1);
* </pre>
* The resulting list is {@code [a, c, d, b, e]}.
*
* <p>To move more than one element forward, increase the absolute value
* of the rotation distance. To move elements backward, use a positive
* shift distance.
*
* <p>If the specified list is small or implements the {@link
* RandomAccess} interface, this implementation exchanges the first
* element into the location it should go, and then repeatedly exchanges
* the displaced element into the location it should go until a displaced
* element is swapped into the first element. If necessary, the process
* is repeated on the second and successive elements, until the rotation
* is complete. If the specified list is large and doesn't implement the
* {@code RandomAccess} interface, this implementation breaks the
* list into two sublist views around index {@code -distance mod size}.
* Then the {@link #reverse(List)} method is invoked on each sublist view,
* and finally it is invoked on the entire list. For a more complete
* description of both algorithms, see Section 2.3 of Jon Bentley's
* <i>Programming Pearls</i> (Addison-Wesley, 1986).
*
* @param list the list to be rotated.
* @param distance the distance to rotate the list. There are no
* constraints on this value; it may be zero, negative, or
* greater than {@code list.size()}.
* @throws UnsupportedOperationException if the specified list or
* its list-iterator does not support the {@code set} operation.
* @since 1.4
*/
public static void rotate(List<?> list, int distance) {
if (list instanceof RandomAccess || list.size() < ROTATE_THRESHOLD)
rotate1(list, distance);
else
rotate2(list, distance);
}

通过源码注释和源码本身,我们可以看到:该方法分别对 ​​RandomAccess​​​(支持随机访问)类型的 ​​List​​​ 或者 size 小于阈值(100) 的​​List​​ 和不支持随机访问以及 size 较大的集合,分别采用两种不同的算法。

注释中也提到了 Jon Bentley《Programming Pearls》(中文名:编程珠玑,Addison-Wesley, 1986) 有详细的描述,翻阅该图书,我们发现对于集合的旋转,提到了三种算法: A Juggling Algorithm 、The Block-Swap Algorithm 和 The Reversal Algorithm。

Java Collections.rotate 方法浅析_List_02


通过书中给出的性能对比可以发现,随着 ​​distance​​​ 的增大, A Juggling Algorithm 陡然上升,然后上下浮动,最后趋于下降;而 The Reversal Algorithm 相对平稳,随着 ​​distance​​的增加,Reversal 算法比 Juggling 算法更优。

Java Collections.rotate 方法浅析_Java_03

​java.util.Collections#rotate1​​ 和文中提到的 A Juggling Algorithm 思想一致:

Java Collections.rotate 方法浅析_List_04

具体代码如下:

private static <T> void rotate1(List<T> list, int distance) {
int size = list.size();
if (size == 0)
return;
distance = distance % size;
if (distance < 0)
distance += size;
if (distance == 0)
return;

for (int cycleStart = 0, nMoved = 0; nMoved != size; cycleStart++) {
T displaced = list.get(cycleStart);
int i = cycleStart;
do {
i += distance;
if (i >= size)
i -= size;
displaced = list.set(i, displaced);
nMoved ++;
} while (i != cycleStart);
}
}

  • ​rotate1​​方法的逻辑是:
  • 首先获取列表的大小,如果列表为空,那么就直接返回。
  • 然后对距离进行取模运算,使得距离在 0 到列表大小之间,如果距离为负数,那么就加上列表大小,如果距离为 0 ,那么也直接返回。
  • 接着用一个循环来遍历列表中的元素,每次从一个起始位置开始,用一个变量​​displaced​​​来保存被移动的元素,然后用一个内部循环来计算被移动元素的新位置,每次加上距离,如果超过了列表大小,那么就减去列表大小,然后用列表的 set 方法来替换新位置的元素,并把被替换的元素赋值给 ​​displaced​​,同时记录移动的元素的个数,直到移动的元素的个数等于列表的大小为止。

​java.util.Collections#rotate2​​ 和 The Reversal Algorithm 思想一致:

Java Collections.rotate 方法浅析_jvm_05

对应源码如下:

private static void rotate2(List<?> list, int distance) {
int size = list.size();
if (size == 0)
return;
int mid = -distance % size;
if (mid < 0)
mid += size;
if (mid == 0)
return;

reverse(list.subList(0, mid));
reverse(list.subList(mid, size));
reverse(list);
}

  • ​rotate2​​ 方法的逻辑是:
  • 首先获取列表的大小,如果列表为空,那么就直接返回。
  • 然后计算一个中间位置,用负的距离对列表大小取模,如果结果为负数,那么就加上列表大小,如果结果为0,那么也直接返回。
  • 接着用之前定义的 ​​reverse​​方法来反转列表的三个子列表,分别是从 0`到中间位置,从中间位置到列表大小,和整个列表,这样就实现了旋转的效果。

该方法依赖 ​​reverse​​ 方法实现 list 元素的翻转,这里趁机了解下该方法:

/**
* Reverses the order of the elements in the specified list.<p>
*
* This method runs in linear time.
*
* @param list the list whose elements are to be reversed.
* @throws UnsupportedOperationException if the specified list or
* its list-iterator does not support the {@code set} operation.
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public static void reverse(List<?> list) {
int size = list.size();
if (size < REVERSE_THRESHOLD || list instanceof RandomAccess) {
for (int i=0, mid=size>>1, j=size-1; i<mid; i++, j--)
swap(list, i, j);
} else {
// instead of using a raw type here, it's possible to capture
// the wildcard but it will require a call to a supplementary
// private method
ListIterator fwd = list.listIterator();
ListIterator rev = list.listIterator(size);
for (int i=0, mid=list.size()>>1; i<mid; i++) {
Object tmp = fwd.next();
fwd.set(rev.previous());
rev.set(tmp);
}
}
}

这段代码的主要逻辑是:

  • 首先获取列表的大小,如果列表的大小小于一个常量 REVERSE_THRESHOLD (值为18),或者列表是一个随机访问的列表,那么就用一个简单的循环来交换列表中的元素,从两端向中间遍历,每次交换两个对称位置的元素,直到遍历到中间位置为止。
  • 否则,如果列表不是一个随机访问的列表,那么就用两个列表迭代器来交换列表中的元素,一个从前往后遍历,一个从后往前遍历,每次交换两个迭代器所指向的元素,直到遍历到中间位置为止。

可以看出 List 的翻转也采用了类似的思想,对于元素较少或支持随机访问时,采用交换;否则,通过迭代器来实现交换。

2.3 另外一种 rotate 算法

书中还介绍了另外一种算法: The Block-Swap Algorithm。

Java Collections.rotate 方法浅析_java_06


The Block-Swap Algorithm 是一种用于数组旋转的算法,它可以在 O(n) 的时间复杂度内交换两个相邻但不等长的数组区域。它的基本思想是将数组分成两部分 A 和 B,然后根据A和B的大小关系,不断地交换A和B的子区域,直到A和B的大小相等,然后再交换A和B。

Java 的 ​​Collections​​​ 的 ​​rotate​​ 算法中没有采用这种算法估计有以下几个原因:

  • The Block-Swap Algorithm 是针对数组设计的,而 Java Collections 的 ​​rotate​​方法是针对列表设计的,列表可能不是随机访问的,也可能不是连续存储的,所以这种算法可能不适用于列表。
  • The Block-Swap Algorithm 需要不断地划分和交换子区域,这可能会增加代码的复杂度和可读性,而Java ​​Collections​​​ 的 ​​rotate​​ 方法使用了两种相对简单的方法,一种是直接交换元素,另一种是利用反转操作,这可能会更容易理解和维护。
  • The Block-Swap Algorithm 的性能可能并不比 Java ​​Collections​​​ 的 ​​rotate​​​ 方法的性能好,因为它需要进行多次的取模运算和数组访问,这可能会增加运行时间和空间开销,而 Java ​​Collections​​​ 的 ​​rotate​​ 方法的性能可能取决于列表的实现和大小,有时可能更快,有时可能更慢。

三、启发

3.1 知其然,知其所以然

不管是在学习还是在工作,使用某些习以为常,尤其是 JDK 和经典的三方类库的 API 时,建议可以简单去源码中看一眼。
JDK 很多方法都是非常值得学习的典范,细细体会,能够有很多意外收获,也有助于夯实自己的基础。

3.2 多种方法相结合

​Collections​​​ 的 ​​rotate​​​ 方法的设计对我们日常设计技术方案也很有启发。
每种算法都有自己最适合的场景,通常我们需要根据具体的情况选择最适合算法。
我们日常做方案时,如果有多种方法可以选择,也可以考虑分情况采用不同的方法,以达到最佳的效果。


举报

相关推荐

0 条评论