概述
就像C++的STL一样,java的集合类也提供了一些常用的算法,用于满足你的基本算法要求,比如排序,混排,二分查找等。
具体内容
1.排序
Collections类中的sort方法可以对实现了List接口的集合进行排序。
当集合中的对象重写了compareTo方法时,最直接的排序方法如下:
List<Employee> staff = new LinkedList<>();
. . .
Collections.sort(staff);
提醒:你还可以多加一个Comparator对象参数用于实现特定排序策略。
如果你将提供一个Comparator比较器实现特定排序策略,建议调用集合对象的sort方法。
list.sort(Comparator.comparingDouble(Employee::getSalary));
如果你想要逆规则排序,可以调用reverseOrder()方法和reversed()方法:
list.sort(Comparator.reverseOrder());
list.sort(Comparator.comparingDouble(Employee::getSalary).reversed());
附:根据文档说明,调用sort算法,需要列表必须是可修改的,但不一定可以改变大小。这也就意味着调用subList()方法获取的视图是可以对其调用sort方法的。
2.混排
打乱排序是一个非常常用的算法,它的使用非常简单:
Collections.shuffle(staff);
3. 二分查找
二分法查找元素是经典的算法之一,使用O(log n)的时间复杂度寻找有序列表中的元素,算法的逻辑实现细节在此就不再赘述了,调用Collections类的binarySearch()方法将会返回目标元素下标:
i = Collections.binarySearch(c, element);
i = Collections.binarySearch(c, element, comparator);
该方法如果没有找到目标元素,则会返回一个负值,其源码为: return -(low + 1);
因此,如果我们想要插入一个元素,并要求保证插入后的列表保持有序,则也要如此操作:
// 元素不存在,计算插入位置
index = Collections.binarySearch(c, element);
int insertPosition = -(index + 1); //或者是-index-1
附:二分查找由于要根据下标访问元素,所以列表必须要可以随机访问,如果对链表使用该方法,则会退化为线性查找。
4.其他常用算法
removeIf()方法
//删除所有长度小于3的元素
words.removeIf(w -> w.length() <= 3);
reverse()方法
//逆转列表顺序
Collections.reverse(words);
frequency()方法
//返回列表中元素o的个数
Collections.frequency(words, o);
disjoint()方法
//如果两个集合没有交集,就返回true
Collections.disjoint(words1, words2);
rotate()方法
//将列表中的元素后移d位,将末端的元素逐个补到列表头
Collections.rotate(list, d);
5.批操作
很多操作会成批的操作元素
replaceAll()方法
//用于将列表中的所有"C++"替换为"Java"
Collections.replaceAll(words, "C++", "Java");
//将所有的大写字母都转换为小写字母
words.replaceAll(String::toLowerCase);
retainAll()方法
//从coll1删除coll2中出现的所有元素
coll1.retainAll(coll2);
该方法常被用来取交集,先建立一个result集合,将其用coll1中的元素构造,然后再将default集合使用retainAll方法:
Set<Integer> default = new HashSet<>(coll1);
default.retainAll(coll2);
removeAll()方法
//删除coll1中与coll2重复的元素
coll1.removeAll(coll2);
对此我们可以进一步,假设我们现在想要解聘员工映射Map里的部分员工,我们先可以针对员工映射里的键建立一个解聘员工id的集,然后获取员工Map的keySet(),最后对这个keySet()调用removeAll方法:
Map<String, Employee> staffMap = . . .
Set<String> terminateIDs = . . .
terminateIDs.add(. . .);
. . .
staffMap.keySet().removeAll(terminateIDs);
6.集合与数组的转换
将数组转换为集合很简单,可以使用java 9的小集合(of方法),也可以使用旧版本中的asList方法:
String names = . . .
List<String> staff = List.of(names);
List<String> staff1 = List.asList(names);
将集合转换为数组则稍微复杂一点。
首先我们知道集合类是有toArray方法的,但这有一个缺陷,就是它返回的是Object[]数组,而且尽管你知道它就是String[]类型的数组,你也不能使用强制转换:
String[] names = (String[]) staff.toArray(); //Error
事实上,我们可以对toArray方法传入一个数组构造器表达式,这样就可以转换为目标类型了:
String[] names = staff.toArray(String[]::new);
但是,在java 11以前你不能用上面这个方法,你只能用下面这两种传统的方法传入参数:
String[] names = staff.toArray(new String[0]);
String[] names = staff.toArray(new String[staff.size()]);
“ 在低版本的 Java 中推荐使用初始化大小的数组,因为使用反射调用去创建一个合适大小的数组相对较慢。但是在 openJDK 6 之后的高版本中方法被优化了,传入空数组相比传入初始化大小的数组,效果是相同的甚至有时候是更优的。因为使用 concurrent 或 synchronized 集合时,如果集合进行了收缩,toArray()和size()方法可能会发生数据竞争,此时传入初始化大小的数组是危险的。 ”
7.编写自己的算法
如果编写自己的算法,应该尽可能的使用接口类,而不是具体的实现类。
例如你可能想要处理一个集合的元素,你可以接收一个ArrayList对象,但这会大大限制对这个方法的使用者,他可能想要处理的是LinkedList中的对象,用户就必须要自己进行转换。
这个时候就要问问自己,你写的这个算法关心集合的类型吗?你写的算法关心集合的顺序吗?如果你关心顺序,你应当接收List,反之,你更应该接收Collection:
public void processItems(ArrayList<Item> items) . . .
public void processItems(List<Item> items) . . .
public void processItems(Collection<Item> items) . . .
在这里,我们还可以做得更好,我们甚至可以调整为接收一个Iterable<Item>,增强for循环就是用的迭代器实现的,很多集合也都继承了Iterable接口:
public void processItems(Iterable<Item> items) . . .
反过来,我们对于算法的返回值也要考虑到,到底是返回什么样的接口,而不是具体的集合类型,在此就不多赘述了。