【java_基础深入】JDK借助RandomAccess接口 ,定制ArrayList与LinkedList的二分查找策略

java常用集合类接口实现情况

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

RandomAccess 接口

ArrayListLinkedList 共同实现了空接口 Cloneable Serializable
ArrayList 独有的空接口 RandomAccess,这个空接口起的是一个标记的作用,具体用处看以下代码
查看Collections 中的代码binarySearch()

  public static <T> int binarySearch(List<? extends T> list, T key, Comparator<? super T> c){}

读注释

binarySearch 二分查找法是什么

	/**
     * Searches the specified list for the specified object using the binary
     * search algorithm.  The list must be sorted into ascending order
     * according to the specified comparator (as by the
     * {@link #sort(List, Comparator) sort(List, Comparator)}
     * method), prior to making this call.  If it is
     * not sorted, the results are undefined.  If the list contains multiple
     * elements equal to the specified object, there is no guarantee which one
     * will be found.
     */

二分查找法是对一个有序序列,进行查找,时间复杂度是O(logN)
以下是Collections.binarySearch (...) 的第一段注释,也说得很清楚,参与二分查找的序列必须是有序

binarySearch 使用 RandomAccess 的动机是什么

	/**
	 *This method runs in log(n) time for a "random access" list (which
     * provides near-constant-time positional access).  If the specified list
     * does not implement the {@link RandomAccess} interface and is large,
     * this method will do an iterator-based binary search that performs
     * O(n) link traversals and O(log n) element comparisons.
     */

该空接口有标识作用,第二段注释有点绕。意思是:实现了RandomAccess接口的List, 如ArrayList,能稳定得满足O(logn)的时间复杂度完成搜索。而LinkedList 需要有O(n)的时间复杂度用于遍历,加上 O(logn) 的时间复杂度用于比较。换言之,LinkedList 的二分查找效率是O(nlogn)。

内在原因:
ArrayList.get(index)时间复杂度:O(1)
LinkedList.get(index)时间复杂度:O(n) 。后文将说道如何优化这个O(n)的复杂度

基于ArrayList的下标,寻找value:16的下标

ArrayList是存在下标的, 存在以下有序序列

value15781115162839
index012345678
  1. 根据数组长度 mid 确认为 4,get(4) == 11 < 16
  2. 根据二分算法mid 确认为 6,get(6) == 16, 返回值就是下标:6

相关代码,省略get(index)代码:

private static <T> int indexedBinarySearch(List<? extends Comparable<? super T>> list, T key) {
        int low = 0;
        int high = list.size()-1;

        while (low <= high) {
            int mid = (low + high) >>> 1;
            Comparable<? super T> midVal = list.get(mid);
            int cmp = midVal.compareTo(key);

            if (cmp < 0)
                low = mid + 1;
            else if (cmp > 0)
                high = mid - 1;
            else
                return mid; // key found
        }
        return -(low + 1);  // key not found
    }

使用get(ListIterator<? extends T> i, int index) 优化遍历
value15781115162839
迭代器下标012345678

基于LinkedList,寻找value:16的下标,无下标。
尽管LinkedList可取得链表总长度,但是每次LinkedList.get(index)操作都会让双链表从某一端从头遍历
为了解决每次从头遍历的问题,JDK开发者使用ListIterator 来找index对应的value。
ListIterator 可以向前遍历,也可以向后遍历

    private static <T> T get(ListIterator<? extends T> i, int index) {
        T obj = null;  // 待返回的value
        int pos = i.nextIndex(); // 向后移动游标获取当前位置
        if (pos <= index) { // 当前位置小于index
            do {
                obj = i.next(); 
            } while (pos++ < index); // 一直向后找,直到到达index的位置,返回index对应的value
        } else {
            do {
                obj = i.previous(); // 反之向前找
            } while (--pos > index);
        }
        return obj;
    }

有了get(ListIterator<? extends T> i, int index) ,就可以完成下面的搜索过程了

基于LinkedList ,寻找value:16的下标
  1. 根据链表长度 mid 确认为 4,此时listIterator 的游标为0
    1.1 从0开始,往后一个一个找,get(listIterator,4) == 11 < 16; 返回值对应的value:11
  2. 11 < 16。mid确认为6。发现游标往后才有可能找到目标元素,如果用LinkedList.get(mid),还要从头遍历,好在有get(ListIterator<? extends T> i, int index),可以从5开始找
    2.1 == 从5开始==,往后一个一个
    2.2 get(listIterator, 6) == 16 ,返回值对应i的value::16
  3. 很幸运,16 为待寻找元素, 返回值为mid:5
private static <T>  int iteratorBinarySearch(List<? extends Comparable<? super T>> list, T key){

        int low = 0;
        int high = list.size()-1;
        ListIterator<? extends Comparable<? super T>> i = list.listIterator();

        while (low <= high) {
            int mid = (low + high) >>> 1;
            Comparable<? super T> midVal = get(i, mid); // 
            int cmp = midVal.compareTo(key);

            if (cmp < 0)
                low = mid + 1;
            else if (cmp > 0)
                high = mid - 1;
            else
                return mid; // key found
        }
        return -(low + 1);  // key not found
    }

结论

RandomAccess 接口只参与标记作用,目的是让ArrayList发挥其底层数据结构数组的O(1)查找能力
同时又没有放弃LinkedList,使用用ListIterator 避免LinkedList从头开始遍历。
以上也侧面说明了以下代码是个很低效的代码

		LinkedList linkedList = new LinkedList();
		for(int i = 0; i < linkedList.size(); i++) {
			System.out.println(linkedList.get(i);
		}

写成高效的,就是foreach循环的本质。

        LinkedList linkedList = new LinkedList();
        ListIterator listIterator = linkedList.listIterator(); // 只往后遍历可以写成iterator() 
        while (listIterator.hasNext()) { 
            listIterator.next();
        }
©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页