Collections源码分析(二)之二分搜索实现

对一个集合的二分搜索的前提是:这个集合必须是升序排列的,也就是前面讲的排序方法的结果。

public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
    if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
        return Collections.indexedBinarySearch(list, key);
    else
        return Collections.iteratorBinarySearch(list, key);
}复制代码

上面是二分搜索方法。在这里分了2类情况来进行二分搜索:

1. 这个集合是可以随机访问的,并且其集合的大小小雨二分搜索的协调常量值

2. 这个集合不可以随机访问。

我们先分析第一种情况的代码。

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
}复制代码

上面是可随机访问的集合的搜索方法。它的思路:先获取集合的开始位置的下标和结束位置的下标, 这是为了获取这个有序集合的中间元素的下标。然后用这个下标来获取中间元素的值,用来和要查找的key来进行大小的比较。 如果这个中间值小于这个key, 则表示key是位于中间值右边的某个元素,或者是不存在;这个时候对low进行重新赋值,low=mid+1。而对于中间值大于这个key的分析也是和上面类似的。


这里我们有一次看到源码用到了>>>的优化方法。

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
}复制代码

上面是另一种情况。可以看出2种实现的区别就在于获取中间值的方法上。


private static <T> T get(ListIterator<? extends T> i, int index) {
    T obj = null;
    int pos = i.nextIndex();
    if (pos <= index) {
        do {
            obj = i.next();
        } while (pos++ < index);
    } else {
        do {
            obj = i.previous();
        } while (--pos > index);
    }
    return obj;
}复制代码

上面是第二种方法的get方法的实现。它是通过迭代器来遍历来获取索引位index的元素,并且先用null来初始化用来接收这个索引对应的元素。而对于像ArrayList这样的可以随机访问的集合来讲,是不需要遍历整个集合元素来获取指定索引的元素的。可以看出在二分搜索方面,可随机访问的集合的性能往往是高于非随机访问的集合如LinkedList。


---------------------华丽的分割线-------------------

下面再来分析几个容易理解的方法。


public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) {
    Iterator<? extends T> i = coll.iterator();
    T candidate = i.next();

    while (i.hasNext()) {
        T next = i.next();
        if (next.compareTo(candidate) > 0)
            candidate = next;
    }
    return candidate;
}复制代码

上面是求集合中最大的元素的方法。

Object & Comparable是什么意思呢?我们知道&要返回true的话,需要2边的结果都要是true。这里其实就是用到了集合的迭代器来循环整个集合来获得集合的最大值。它这里没有用for循环,如果是我们来写的话,可能首先想到的是用for循环来遍历集合。因为这里的参数类型是Collection,而且这里不区分是否是可随机访问的集合。如果是可随机访问的集合,源码会用for循环,如果是非可随机访问集合,源码会用迭代器。在下面的例子中会体现出这点。


public static <T> boolean replaceAll(List<T> list, T oldVal, T newVal) {
    boolean result = false;
    int size = list.size();
    if (size < REPLACEALL_THRESHOLD || list instanceof RandomAccess) {
        if (oldVal==null) {
            for (int i=0; i<size; i++) {
                if (list.get(i)==null) {
                    list.set(i, newVal);
                    result = true;
                }
            }
        } else {
            for (int i=0; i<size; i++) {
                if (oldVal.equals(list.get(i))) {
                    list.set(i, newVal);
                    result = true;
                }
            }
        }
    } else {
        ListIterator<T> itr=list.listIterator();
        if (oldVal==null) {
            for (int i=0; i<size; i++) {
                if (itr.next()==null) {
                    itr.set(newVal);
                    result = true;
                }
            }
        } else {
            for (int i=0; i<size; i++) {
                if (oldVal.equals(itr.next())) {
                    itr.set(newVal);
                    result = true;
                }
            }
        }
    }
    return result;
}
复制代码

上面是用一个值来替换集合中的某个值,并且如果这个集合中的值出现了多次,那么他们都会被替换为新的值。

这个方法首先准备了一个返回值result。但是我们注意到在处理非随机访问集合的时候,先是用到了for循环然后才是用的迭代器,为什么不在一开始也用迭代器呢? 因为这里已经知道要遍历的次数,所以先用for循环,而用迭代器的时候是不知道要遍历多少次的,这也是优化的一个点吧。

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);
        }
    }
}
复制代码

上面是反转集合元素的方法。

public static void swap(List<?> list, int i, int j) {
    // 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
    final List l = list;
    l.set(i, l.set(j, l.get(i)));
}复制代码

先看是可随机访问的情况。上面是交换2个元素值的方法。也是这种情况的核心方法。

它首先把传进来的list转为final不可变的list。list的set方法是在ArrayList中实现的,它会返回旧的值,也是因为这一点,所以l.set(j, l.get(i))就会返回原来在j索引上的元素,最后把j索引上的值赋给索引i上的元素,实现交换。


当不是可随机访问的元素的时候,源码的实现可以说是很巧妙的了。先是准备了2个迭代器,一个是向后迭代,一个是向前迭代的,这样就可以分别获取下一个元素和前一个元素,进行交换。


public static void shuffle(List<?> list, Random rnd) {
    int size = list.size();
    if (size < SHUFFLE_THRESHOLD || list instanceof RandomAccess) {
        for (int i=size; i>1; i--)
            swap(list, i-1, rnd.nextInt(i));
    } else {
        Object arr[] = list.toArray();

        // Shuffle array
        for (int i=size; i>1; i--)
            swap(arr, i-1, rnd.nextInt(i));

        // Dump array back into list
        // 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 it = list.listIterator();
        for (int i=0; i<arr.length; i++) {
            it.next();
            it.set(arr[i]);
        }
    }
}
复制代码

上面是一个打乱集合里面元素的相对位置的方法。

这里和上面一样也分了2类情况。引入一个随机数,并把它作为swap方法的一个参数来使用,达到集合里面 的第i-1个元素和集合里面相遇等于i索引前面的元素进行交换。


如果不是可随机访问集合,就把list转为数组,最后利用list的迭代器,将得到的数组的每个元素设置到list中去。


总结:

1.  当你的参数是一个List类型的时候,需要判断他是否是可随机访问的集合,然后分情况来处理,这也是一个优化的点

2. 当你的参数是一个List类型的时候,你想要有双向迭代的能力,就可用list和list的size来创建向后迭代器和向前迭代器

3. 在使用迭代器的时候,需要考虑最后一个元素的情况


转载于:https://juejin.im/post/5cd79b0b518825682348452a

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值