在编程的过程中,选择何种集合至关重要,下面由我来总结下选择集合的方法
选择集合所考虑的关键问题在于:效率代价与空间代价的平衡问题。
效率代价是指执行的效率,简单的说如果一个资源没有把索引记录下来,那么要找到他你就需要执行程序,那么你的代价在于系统花钱了时间。
空间代价是指存放的空间消耗内存的代价,如上边说到的如果把索引记录下来很方便就能找到要找的资源,也就是用空间代价换取运行时间的缩短。这就像电话本除了记号码还要记人名,总不能一个个打电话去问谁是张三李四的吧。
我想说的是,现在的储存器越来越便宜,空间可以说不是难题,空间代价不是优先考虑的问题。
下面我们来分析下各种常用的集合数据类型
1. ArrayList:数组实现的保持进入顺序的集合。
查询元素操作(提供索引位置):计算元素内存指针位置,一步便可跳到。
增加元素操作:查询到增加位置的元素,后面的元素挨个向后移一个位置,保存元素的 时间可以忽略不计
删除元素操作:查询到增加位置的元素,后面的元素挨个向前移一个位置,清空元素的时间可以忽略不计
修改元素操作:与查询元素操作效率相同
2. LinkedList:链表实现的保持进如顺序的集合。
查询元素操作(提供索引):从根节点遍历,不能跳越,直到索引要求的位置。
增加元素操作:查询到增加位置的元素,接上链的时间忽略不计
删除元素操作:查询到增加位置的元素,拆下链的时间忽略不计
修改元素操作:与查询元素操作效率相同
3. HashSet/HashMap:无序的Hash算法来索引的集合
查询元素操作:HashCode经过Hash算法直接指向内存地址,而后调用指向的数据的equals(),如果不等,继续Hash算法。
增加元素操作:查询到增加位置的元素,保存的时间忽略不计
删除元素操作:查询到增加位置的元素,删除的时间忽略不计
修改元素操作:与查询元素操作效率相同
4. TreeSet/TreeMap:根据指定值排序的集合,低层是链表
查询元素操作:红黑二分查找树算法,链表遍历跳过部分节点,效率大于LinkedList
增加元素操作:查询到增加位置的元素,接上链的时间忽略不计
删除元素操作:查询到增加位置的元素,拆下链的时间忽略不计
修改元素操作:与查询元素操作效率相同
性能分析表:
集合/操作 | A增 | B删 | C查 | D改 | 顺序性 |
ArrayList | C+④ | =A | ① | =C | 进入顺序 |
LinkedList | C+① | =A | ④ | =C | 进入顺序 |
HashSet/HashMap | C+② | =A | ② | =C | 无顺序 |
TreeSet/TreeMap | C+③ | =A | ③ | =C | 指定顺序 |
注释:=A表示效率上等于其A操作,也就是增操作,=C也同理;C+①表示查询效率要加入计算。
从上面中最核心的问题是ArrayList增删操作与LinkedList查找操作代价比较
案例1:ArrayList与LinkedList的add/remove操作的比较
分析1:低效率ArrayList.add/remaove操作的原因在于增删操作可能会引起元素的交换,而LinkedList的增删只需要接链头和链尾。
结果数据1:ArrayList的100万次的交换消耗了3mm
案例2:ArrayList与LinkedList的get操作的比较
分析2:LinkedList的查找操作效率低的原因是必须遍历过程中的每个节点,但是遍历一次仅相当于getter方法调用消耗非常小。
结果数据2:LinkedList的166万次的遍历都消耗都不到1mm
结论:通过以上案例测试得知,遍历元素与交换元素的效率差至少是十万级的。所以我们几乎可以忽略LinkedList查询的效率,而HashSet/HashMap,TreeSet/TreeMap效率比LinkedList还要高,也可以忽略其查询效率。
由此,应修改性能分析表如下:
性能分析表:
集合/操作 | A增 | B删 | C查 | D改 | 顺序性 |
ArrayList | ④ | =A | ① | =C | 进入顺序 |
LinkedList | ① | =A | ④ | =C | 进入顺序 |
HashSet/HashMap | ② | =A | ② | =C | 无顺序 |
TreeSet/TreeMap | ③ | =A | ③ | =C | 指定顺序 |
总结:
1:如果元素没有顺序要求,有优的选择是HashSet/HashMap,其增删查找的效率都很高。
2:如果元素有顺序要求,对于ArrayList、LinkedList和TreeSet/TreeMap都可以实现,只是ArrayList、LinkedList需要控制进入顺序,而TreeSet/TreeMap需要值记录顺序。如果记录顺序的值很方便提供,优先选择TreeSet/TreeMap。
3:如上述有顺序要求,而记录顺序的值不方便提供的情况,如Stack,如果提供入栈时间比较冗余。这个时候则考虑集合会不会出现元素顺序改动的增删操作,如果会则选择LinkedList。如案例1,2中的结论ArrayList的单位次交换耗时至少是LinkedList遍历一个节点的50万倍以上。
4:如上述情况有顺序要求,并且不希望提供值来排序,而且集合的顺序基本不会发生改变,选择ArrayList。
选择Set还是Map?
误点1:Map比Set数据多出一个维度,那么就可以利用多出的部分影响顺序或多出快捷收索功能。
正解1:原则是一个集合对象只能提供一个索引。
对于Map其有效的索引则是key(实际上是key的equals和HashCode),而value是不能主导一个条目的,如Map的remove就不能通过value来删除,Map也不支持Value找到key的方法(如果iterator遍历找到key那就不能称之为索引到)。
还是一个真理一个集合仅提供一个索引,Set和Map的区别只在于,索引是否唯一且数据本身就可以提供。只有满足这两个条件才可以使用Set。因为Set有赖于元素本身的equals/hashCode和comparTo方法,而这些方法只能由属性运算得出结果,所以属性要求有能力提供出索引。而Set的元素类型一旦被确定,也就说用两个Set为同一个数据加索引,其索引规则相同,这是因为元素类独有一份equals/hashCode和comparTo方法的实现。
如果索引需要多个或数据对象的属性没有能力给出此索引,那么我们会选择Map。Map的key可以是我们定制的对象类型,也就是提供不同的equals/hashCode和comparTo的方法实现,而不像Set只能调用到数据本身的一种实现。选择不同的key类型就会得到不同的方法实现,建立不同的索引,从而多个Map能实现多个索引。
如果希望同一个数据可能会被多种值检索到,那么就为他们各自建立一个Map,多个Map多个索引方式。Map总是能实现索引,而Set只能提供自身的一种索引,Set是Map的特例情况。