计算机程序的思维逻辑 (53) - 剖析Collections - 算法
本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http://item.jd.com/12299018.html
之前几节介绍了各种具体容器类和抽象容器类,上节我们提到,Java中有一个类Collections,提供了很多针对容器接口的通用功能,这些功能都是以静态方法的方式提供的。
都有哪些功能呢?大概可以分为两类:
- 对容器接口对象进行操作
- 返回一个容器接口对象
对于第一类,操作大概可以分为三组:
- 查找和替换
- 排序和调整顺序
- 添加和修改
对于第二类,大概可以分为两组:
- 适配器:将其他类型的数据转换为容器接口对象
- 装饰器:修饰一个给定容器接口对象,增加某种性质
它们都是围绕容器接口对象的,第一类是针对容器接口的通用操作,这是我们之前在接口的本质一节介绍的面向接口编程的一种体现,是接口的典型用法,第二类是为了使更多类型的数据更为方便和安全的参与到容器类协作体系中。
由于内容比较多,我们分为两节,本节讨论第一类,下节我们讨论第二类。下面我们分组来看下第一类中的算法。
查找和替换
查找和替换包含多组方法,我们分别来看下。
二分查找
我们在剖析Arrays类的时候介绍过二分查找,Arrays类有针对数组对象的二分查找方法,Collections提供了针对List接口的二分查找,如下所示:
- public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key)
- public static <T> int binarySearch(List<? extends T> list, T key, Comparator<? super T> c)
从方法参数,容易理解,一个要求List的每个元素实现Comparable接口,另一个不需要,但要求提供Comparator。
二分查找假定List中的元素是从小到大排序的。如果是从大到小排序的,也容易,传递一个逆序Comparator对象,Collections提供了返回逆序Comparator的方法,之前我们也用过:
- public static <T> Comparator<T> reverseOrder()
- public static <T> Comparator<T> reverseOrder(Comparator<T> cmp)
比如,可以这么用:
- List<Integer> list = new ArrayList<>(Arrays.asList(new Integer[]{
- 35, 24, 13, 12, 8, 7, 1
- }));
- System.out.println(Collections.binarySearch(list, 7, Collections.reverseOrder()));
输出为:
- 5
List的二分查找的基本思路与Arrays中的是一样的,但,数组可以根据索引直接定位任意元素,实现效率很高,但List就不一定了,我们来看它的实现代码:
- 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);
- }
分为两种情况,如果List可以随机访问(如数组),即实现了RandomAccess接口,或者元素个数比较少,则实现思路与Arrays一样,调用indexedBinarySearch根据索引直接访问中间元素进行查找,否则调用iteratorBinarySearch使用迭代器的方式访问中间元素进行查找。
indexedBinarySearch的代码为:
- 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
- }
调用list.get(mid)访问中间元素。
iteratorBinarySearch的代码为:
- 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
- }
调用get(i, mid)寻找中间元素,get方法的代码为:
- 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;
- }
通过迭代器方法逐个移动到期望的位置。
我们来分析下效率,如果List支持随机访问,效率为O(log2(N)),如果通过迭代器,比较的次数为O(log2(N)),但遍历移动的次数为O(N),N为列表长度。
查找最大值/最小值
Collections提供了如下查找最大最小值的方法:
- public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)
- public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp)
- public static <T extends Object & Comparable<? super T>> T min(Collection<? extends T> coll)
- public static <T> T min(Collection<? extends T> coll, Comparator<? super T> comp)
含义和用法都很直接,实现思路也很简单,就是通过迭代器进行比较,比如,其中一个方法的代码为:
- 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;
- }
其他方法就不赘述了。
查找元素出现次数
方法为:
- public static int frequency(Collection<?> c, Object o)
返回元素o在容器c中出现的次数,o可以为null。含义很简单,实现思路也是,就是通过迭代器进行比较计数。
查找子List
在剖析String类一节,我们介绍过,String类有查找子字符串的方法:
- public int indexOf(String str)
- public int lastIndexOf(String str)
对List接口对象,Collections提供了类似方法,在source List中查找target List的位置:
- public static int indexOfSubList(List<?> source, List<?> target)
- public static int lastIndexOfSubList(List<?> source, List<?> target)
indexOfSubList从开头找,lastIndexOfSubList从结尾找,没找到返回-1,找到返回第一个匹配元素的索引位置,比如:
- List<Integer> source = Arrays.asList(new Integer[]{
- 35, 24, 13, 12, 8, 24, 13, 7, 1
- });
- System.out.println(Collections.indexOfSubList(source, Arrays.asList(new Integer[]{24, 13})));
- System.out.println(Collections.lastIndexOfSubList(source, Arrays.asList(new Integer[]{24, 13})));
输出为:
- 1
- 5
这两个方法的实现都是属于"暴力破解"型的,将target列表与source从第一个元素开始的列表逐个元素进行比较,如果不匹配,则与source从第二个元素开始的列表比较,再不匹配,与source从第三个元素开始的列表比较,依次类推。
查看两个集合是否有交集
方法为:
- public static boolean disjoint(Collection<?> c1, Collection<?> c2)
如果c1和c2有交集,返回值为false,没有交集,返回值为true。
实现原理也很简单,遍历其中一个容器,对每个元素,在另一个容器里通过contains方法检查是否包含该元素,如果包含,返回false,如果最后不包含任何元素返回true。这个方法的代码会根据容器是否为Set以及集合大小进行性能优化,即选择哪个容器进行遍历,哪个容器进行检查,以减少总的比较次数,具体我们就不介绍了。
替换
替换方法为:
- public static <T> boolean replaceAll(List<T> list, T oldVal, T newVal)
将List中的所有oldVal替换为newVal,如果发生了替换,返回值为true,否则为false。用法和实现都比较简单,就不赘述了。
排序和调整顺序
针对List接口对象,Collections除了提供基础的排序,还提供了若干调整顺序的方法,包括交换元素位置、翻转列表顺序、随机化重排、循环移位等,我们逐个来看下。
排序
Arrays类有针对数组对象的排序方法,Collections提供了针对List接口的排序方法,如下所示:
- public static <T extends Comparable<? super T>> void sort(List<T> list)
- public static <T> void sort(List<T> list, Comparator<? super T> c)
使用很简单,就不举例了,内部它是通过Arrays.sort实现的,先将List元素拷贝到一个数组中,然后使用Arrays.sort,排序后,再拷贝回List。代码如下所示:
- public static <T extends Comparable<? super T>> void sort(List<T> list) {
- Object[] a = list.toArray();
- Arrays.sort(a);
- ListIterator<T> i = list.listIterator();
- for (int j=0; j<a.length; j++) {
- i.next();
- i.set((T)a[j]);
- }
- }
交换元素位置
方法为:
- public static void swap(List<?> list, int i, int j)
交换list中第i个和第j个元素的内容。实现代码为:
- public static void swap(List<?> list, int i, int j) {
- final List l = list;
- l.set(i, l.set(j, l.get(i)));
- }
翻转列表顺序
方法为:
- public static void reverse(List<?> list)
将list中的元素顺序翻转过来。实现思路就是将第一个和最后一个交换,第二个和倒数第二个交换,依次类推直到中间两个元素交换完毕。
如果list实现了RandomAccess接口或列表比较小,根据索引位置,使用上面的swap方法进行交换,否则,由于直接根据索引位置定位元素效率比较低,使用一前一后两个listIterator定位待交换的元素。具体代码为:
- 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 {
- 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);
- }
- }
- }
随机化重排
我们在随机一节介绍过洗牌算法,Collections直接提供了对List元素洗牌的方法:
- public static void shuffle(List<?> list)
- public static void shuffle(List<?> list, Random rnd)
实现思路与随机一节介绍的是一样的,从后往前遍历列表,逐个给每个位置重新赋值,值从前面的未重新赋值的元素中随机挑选。如果列表实现了RandomAccess接口,或者列表比较小,直接使用前面swap方法进行交换,否则,先将列表内容拷贝到一个数组中,洗牌,再拷贝回列表。代码如下:
- 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
- ListIterator it = list.listIterator();
- for (int i=0; i<arr.length; i++) {
- it.next();
- it.set(arr[i]);
- }
- }
- }
循环移位
我们解释下循环移位的概念,比如列表为:
- [8, 5, 3, 6, 2]
循环右移2位,会变为:
- [6, 2, 8, 5, 3]
如果是循环左移2位,会变为:
- [3, 6, 2, 8, 5]
因为列表长度为5,循环左移3位和循环右移2位的效果是一样的。
循环移位的方法是:
- public static void rotate(List<?> list, int distance)
distance表示循环移位个数,一般正数表示向右移,负数表示向左移,比如:
- List<Integer> list1 = Arrays.asList(new Integer[]{
- 8, 5, 3, 6, 2
- });
- Collections.rotate(list1, 2);
- System.out.println(list1);
- List<Integer> list2 = Arrays.asList(new Integer[]{
- 8, 5, 3, 6, 2
- });
- Collections.rotate(list2, -2);
- System.out.println(list2);
输出为:
- [6, 2, 8, 5, 3]
- [3, 6, 2, 8, 5]
这个方法很有用的一点是,它也可以用于子列表,可以调整子列表内的顺序而不改变其他元素的位置。比如,将第j个元素向前移动到k (k>j),可以这么写:
- Collections.rotate(list.subList(j, k+1), -1);
再举个例子:
- List<Integer> list = Arrays.asList(new Integer[]{
- 8, 5, 3, 6, 2, 19, 21
- });
- Collections.rotate(list.subList(1, 5), 2);
- System.out.println(list);
输出为:
- [8, 6, 2, 5, 3, 19, 21]
这个类似于列表内的"剪切"和"粘贴",将子列表[5, 3]"剪切","粘贴"到2后面。如果需要实现类似"剪切"和"粘贴"的功能,可以使用rotate方法。
循环移位的内部实现比较巧妙,根据列表大小和是否实现了RandomAccess接口,有两个算法,都比较巧妙,两个算法在《编程珠玑》这本书的2.3节有描述。
篇幅有限,我们只解释下其中的第二个算法,它将循环移位看做是列表的两个子列表进行顺序交换。再来看上面的例子,循环左移2位:
- [8, 5, 3, 6, 2] -> [3, 6, 2, 8, 5]
就是将[8, 5]和[3, 6, 2]两个子列表的顺序进行交换。
循环右移两位:
- [8, 5, 3, 6, 2] -> [6, 2, 8, 5, 3]
就是将[8, 5, 3]和[6, 2]两个子列表的顺序进行交换。
根据列表长度size和移位个数distance,可以计算出两个子列表的分隔点,有了两个子列表后,两个子列表的顺序交换可以通过三次翻转实现,比如有A和B两个子列表,A有m个元素,B有n个元素:
要变为:
可经过三次翻转实现:
1. 翻转子列表A
2. 翻转子列表B
3. 翻转整个列表
这个算法的整体实现代码为:
- 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);
- }
mid为两个子列表的分割点,调用了三次reverse以实现子列表顺序交换。
添加和修改
Collections也提供了几个批量添加和修改的方法,逻辑都比较简单,我们看下。
批量添加
方法为:
- public static <T> boolean addAll(Collection<? super T> c, T... elements)
elements为可变参数,将所有元素添加到容器c中。这个方法很方便,比如,可以这样:
- List<String> list = new ArrayList<String>();
- String[] arr = new String[]{"深入", "浅出"};
- Collections.addAll(list, "hello", "world", "老马", "编程");
- Collections.addAll(list, arr);
- System.out.println(list);
输出为:
- [hello, world, 老马, 编程, 深入, 浅出]
批量填充固定值
方法为:
- public static <T> void fill(List<? super T> list, T obj)
这个方法与Arrays类中的fill方法是类似的,给每个元素设置相同的值。
批量拷贝
方法为:
- public static <T> void copy(List<? super T> dest, List<? extends T> src)
将列表src中的每个元素拷贝到列表dest的对应位置处,覆盖dest中原来的值,dest的列表长度不能小于src,dest中超过src长度部分的元素不受影响。
小结
本节介绍了类Collections中的一些通用算法,包括查找、替换、排序、调整顺序、添加、修改等,这些算法操作的都是容器接口对象,这是面向接口编程的一种体现,只要对象实现了这些接口,就可以使用这些算法。
在与容器类和Collections中的算法进行协作时,经常需要将其他类型的数据转换为容器接口对象,为此,Collections同样提供了很多方法。都有哪些方法?有什么用?体现了怎样的设计模式和思维?让我们在下一节继续探索。
----------------
未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心原创,保留所有版权。
计算机程序的思维逻辑 (53) - 剖析Collections - 算法的更多相关文章
- Java编程的逻辑 (53) - 剖析Collections - 算法
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...
- 计算机程序的思维逻辑 (54) - 剖析Collections - 设计模式
上节我们提到,类Collections中大概有两类功能,第一类是对容器接口对象进行操作,第二类是返回一个容器接口对象,上节我们介绍了第一类,本节我们介绍第二类. 第二类方法大概可以分为两组: 接受其他 ...
- 计算机程序的思维逻辑 (29) - 剖析String
上节介绍了单个字符的封装类Character,本节介绍字符串类.字符串操作大概是计算机程序中最常见的操作了,Java中表示字符串的类是String,本节就来详细介绍String. 字符串的基本使用是比 ...
- 计算机程序的思维逻辑 (31) - 剖析Arrays
数组是存储多个同类型元素的基本数据结构,数组中的元素在内存连续存放,可以通过数组下标直接定位任意元素,相比我们在后续章节介绍的其他容器,效率非常高. 数组操作是计算机程序中的常见基本操作,Java中有 ...
- 计算机程序的思维逻辑 (38) - 剖析ArrayList
从本节开始,我们探讨Java中的容器类,所谓容器,顾名思义就是容纳其他数据的,计算机课程中有一门课叫数据结构,可以粗略对应于Java中的容器类,我们不会介绍所有数据结构的内容,但会介绍Java中的主要 ...
- 计算机程序的思维逻辑 (48) - 剖析ArrayDeque
前面我们介绍了队列Queue的两个实现类LinkedList和PriorityQueue,LinkedList还实现了双端队列接口Deque,Java容器类中还有一个双端队列的实现类ArrayDequ ...
- 计算机程序的思维逻辑 (51) - 剖析EnumSet
上节介绍了EnumMap,本节介绍同样针对枚举类型的Set接口的实现类EnumSet.与EnumMap类似,之所以会有一个专门的针对枚举类型的实现类,主要是因为它可以非常高效的实现Set接口. 之前介 ...
- 计算机程序的思维逻辑 (30) - 剖析StringBuilder
上节介绍了String,提到如果字符串修改操作比较频繁,应该采用StringBuilder和StringBuffer类,这两个类的方法基本是完全一样的,它们的实现代码也几乎一样,唯一的不同就在于,St ...
- 计算机程序的思维逻辑 (43) - 剖析TreeMap
40节介绍了HashMap,我们提到,HashMap有一个重要局限,键值对之间没有特定的顺序,我们还提到,Map接口有另一个重要的实现类TreeMap,在TreeMap中,键值对之间按键有序,Tree ...
随机推荐
- 在离线环境中发布.NET Core至Windows Server 2008
在离线环境中发布.NET Core至Windows Server 2008 0x00 写在开始 之前一篇博客中写了在离线环境中使用.NET Core,之后一边学习一边写了一些页面作为测试,现在打算发布 ...
- salesforce 零基础学习(六十二)获取sObject中类型为Picklist的field values(含record type)
本篇引用以下三个链接: http://www.tgerm.com/2012/01/recordtype-specific-picklist-values.html?m=1 https://github ...
- PowerShell实现批量重命名文件
[string]$FileName="E:\test11" #-------------------------------------- Clear-Host foreach($ ...
- MySQL中interactive_timeout和wait_timeout的区别
在用mysql客户端对数据库进行操作时,打开终端窗口,如果一段时间没有操作,再次操作时,常常会报如下错误: ERROR (HY000): Lost connection to MySQL server ...
- [干货来袭]C#6.0新特性
微软昨天发布了新的VS 2015 ..随之而来的还有很多很多东西... .NET新版本 ASP.NET新版本...等等..太多..实在没消化.. 分享一下也是昨天发布的新的C#6.0的部分新特性吧.. ...
- Take into Action!
很久没有认真地写文字了. 刚毕业一两年断断续续在csdn上写过一些当时的工作记录,然后没有坚持下去.有时候是觉得自己不牛,记录的东西旁人看起来也许不值一提:有时候觉得结婚生娃了,然后时间不够用(确实是 ...
- (转载) RESTful API 设计指南
作者: 阮一峰 日期: 2014年5月22日 网络应用程序,分为前端和后端两个部分.当前的发展趋势,就是前端设备层出不穷(手机.平板.桌面电脑.其他专用设备......). 因此,必须有一种统一的机制 ...
- MyBatis3.2从入门到精通第一章
第一章一.引言mybatis是一个持久层框架,是apache下的顶级项目.mybatis托管到goolecode下,再后来托管到github下.(百度百科有解释)二.概述mybatis让程序将主要精力 ...
- SpringMVC入门
Spring Web MVC是一种基于Java的实现了Web MVC设计模式的请求驱动类型的轻量级Web框架,即使用了MVC架构模式的思想,将web层进行职责解耦,基于请求驱动指的就是使用请求-响应模 ...
- ASP.NET Core 性能对比评测(ASP.NET,Python,Java,NodeJS)
前言 性能是我们日常生活中经常接触到的一个词语,更好的性能意味着能给我们带来更好的用户体检.比如我们在购买手机.显卡.CPU等的时候,可能会更加的关注于这样指标,所以本篇就来做一个性能评测. 性能也一 ...