Java Collections.rotate 方法浅析

Posted 明明如月学长

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java Collections.rotate 方法浅析相关的知识,希望对你有一定的参考价值。

一、概述

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

本文简单对 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。

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

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

具体代码如下:

    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 思想一致:

对应源码如下:

    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。

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

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

三、启发

3.1 知其然,知其所以然

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

3.2 多种方法相结合

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

以上是关于Java Collections.rotate 方法浅析的主要内容,如果未能解决你的问题,请参考以下文章

Google Web Toolkit 和第 3 方 Java 库

Java并发概念-1

华为机试真题 Java 实现最长连续方波信号

java中获取文件或文件夹的路径方

JAVA-回调实现小例子

Java 加密解密的方法有哪些