数据结构(优化篇)

数据结构优化篇

List

ArrayList

初始化数组容量对比

ArrayList指定长度的情况下

List array1 = new ArrayList(10000000);
long start1 = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
array1.add(i);
}
long end1 = System.currentTimeMillis();
System.out.println(end1-start1);

List array2 = new ArrayList();
long start2 = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
array2.add(i);
}
long end2 = System.currentTimeMillis();
System.out.println(end2-start2);

#输出结果
    239
	  492

结论:指定长度比不指定长度时速度要快

原因分析:指定长度时,省去了容器扩容花费的时间,但是同时,如果随意设置,则浪费计算机内存资源,所以尽量的按需设置!

存储数据类型对比

这里主要以字符型和长整型作为示例

List list1 = new ArrayList(10000000);
long start1 = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
    list1.add("1111111111");
}
long end1 = System.currentTimeMillis();
System.out.println(end1-start1);

List list2 = new ArrayList(10000000);
long start2 = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
    list2.add(1111111111);
}
long end2 = System.currentTimeMillis();
System.out.println(end2-start2);

#输出结果
    100
	464

结论:字符型比长整型要快

原因分析:真不知道!有哪位大佬知道请留言,不甚感谢

插入数据时指定角标对比
List list1 = new ArrayList(10000000);
long start1 = System.currentTimeMillis();
//不能再多了,多了得等到天昏地暗
for (int i = 0; i < 100000; i++) {
    list1.add(0,i);
}
long end1 = System.currentTimeMillis();
System.out.println("头部插入耗时:" + (end1-start1));

List list2 = new ArrayList(10000000);
long start2 = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
    list2.add(list2.size(),i); //等于list2.add(i);
}
long end2 = System.currentTimeMillis();
System.out.println("尾部插入耗时:" + (end2-start2));

List list3 = new ArrayList(10000000);
long start3 = System.currentTimeMillis();
//不能再多了,多了得等到天昏地暗
for (int i = 0; i < 100000; i++) {
    list3.add(list3.size()/2,i);
}
long end3 = System.currentTimeMillis();
System.out.println("中间插入耗时:" + (end3-start3));

#输出结果
    头部插入耗时:1133
    尾部插入耗时:4
    中间插入耗时:520

结论:尾部插入>头部插 ~中间插入

原因分析:

​ 中间插入需要对角标之后所有元素重新排序并且需要执行复制行为,性能非常差,所以如果不是特殊要求,基本不考虑

​ 头部插入需要对所有元素的重新排序并且需要执行复制行为,性能奇差,基本不考虑,其实你可以把头部插入看成是中间插入的极端

​ 尾部插入在没有发生扩容下,不会有元素排序的过程,性能最高,也是默认使用的方式!并且只有在空间不足的情况下,才会有复制行为

删除位置对比
List list1 = new ArrayList(100000);
List list2 = new ArrayList(100000);
for (int i = 0; i < 100000; i++) {
    list1.add(i);
}
for (int i = 0; i < 100000; i++) {
    list2.add(i);
}
long start1 = System.currentTimeMillis();
for (int i = 0; i < 25000; i++) {
    list1.remove(i);
}
long end1 = System.currentTimeMillis();
System.out.println("头部删除耗时:" + (end1-start1));

long start2 = System.currentTimeMillis();
for (int i = 50000; i < 75000 ; i++) {
    list2.remove(i);
}
long end2 = System.currentTimeMillis();
System.out.println("尾部删除耗时:" + (end2-start2));

#输出结果
    头部删除耗时:395
	尾部删除耗时:128

结论:Arraylist每一次删除,都会进行数组的重组,且删除越靠前的元素,数组的重组开销就越大.

原因分析:越靠近头部的元素被删除后,需要重组的元素排序越多,时间复杂度为O(size-n),size为数组的长度,n为删除元素的角标

LinkedList

插入位置对比
List list1 = new LinkedList();
List list2 = new LinkedList();
List list3 = new LinkedList();

long start1 = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
    list1.add(0,i);//等于list1.add(i)
}
long end1 = System.currentTimeMillis();
System.out.println("头部插入耗时:" + (end1-start1));

long start2 = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
    list2.add(list2.size(),i);
}
long end2 = System.currentTimeMillis();
System.out.println("尾部插入耗时:" + (end2-start2));

long start3 = System.currentTimeMillis();
//少一点,少等一下,但是你会发现比头尾插入不会快
for (int i = 0; i < 100000; i++) {
    list3.add(list3.size()/2,i);
}
long end3 = System.currentTimeMillis();
System.out.println("中部插入耗时:" + (end3-start3));

#输出结果
    头部插入耗时:156
    尾部插入耗时:161
    中部插入耗时:8182

结论:因为LinkedList是双链表实现,所以头尾插入性能差不多,性能都比较快(当然,你知道默认用哪个吧?),而中部插入是最慢的!

原因分析:原因也是因为LinkedList的数据结构是双链表,它遍历获取插入位置是从两头往中间搜,所以插入位置越往中间,LinkedList效率越低

遍历数据方式对比
List<Integer> list = new LinkedList();
for (int i = 0; i < 100000; i++) {
    list.add(i);
}
long start1 = System.currentTimeMillis();
for (Integer i : list) {
    Integer ii = i;
}
long end1 = System.currentTimeMillis();
System.out.println("for-each耗时:" + (end1-start1));

Iterator<Integer> iterator = list.iterator();
long start2 = System.currentTimeMillis();
while (iterator.hasNext()){
    Integer ii = iterator.next();
}
long end2 = System.currentTimeMillis();
System.out.println("iterator耗时:" + (end2-start2));

long start3 = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++) {
    Integer ii = list.get(i);
}
long end3 = System.currentTimeMillis();
System.out.println("for-i耗时:" + (end3-start3));

#输出结果
    for-each耗时:4
    iterator耗时:2
    for-i耗时:4395

结论:for-each和iterator耗时性能处于同一个水平,for-i循环相对奇慢无比

原因分析:这个原因很玄妙,光说理论会说不清楚,我们先来看看以上代码的编译后的class类

List<Integer> list = new LinkedList();

for(int i = 0; i < 100000; ++i) {
    list.add(i);
}

long start1 = System.currentTimeMillis();

Integer i;
for(Iterator var4 = list.iterator(); var4.hasNext(); i = (Integer)var4.next()) {
}

long end1 = System.currentTimeMillis();
System.out.println("for-each耗时:" + (end1 - start1));
Iterator<Integer> iterator = list.iterator();

long start2;
Integer var9;
for(start2 = System.currentTimeMillis(); iterator.hasNext(); var9 = (Integer)iterator.next()) {
}

long end2 = System.currentTimeMillis();
System.out.println("iterator耗时:" + (end2 - start2));
long start3 = System.currentTimeMillis();

for(int i = 0; i < list.size(); ++i) {
    Integer var14 = (Integer)list.get(i);
}

long end3 = System.currentTimeMillis();
System.out.println("for-i耗时:" + (end3 - start3));

看出来了没?首先可以解释for-each和iterator是不是同一个妈妈生的?同时看iterator耗时源码:

//没有循环,只有判断下一个角标是否越界
public boolean hasNext() {
    return nextIndex < size;
}
//没有循环,只有判断,且记录下一个角标nextIndex++
public E next() {
    checkForComodification();
    if (!hasNext())
        throw new NoSuchElementException();

    lastReturned = next;
    next = next.next;
    nextIndex++;
    return lastReturned.item;
}

但是为什么for-i会那么慢呢?看关键源码:

if (index < (size >> 1)) {//判断角标在前半部分还是后半部分
    Node<E> x = first; //如果是前半部分,拿到头
    for (int i = 0; i < index; i++) //通过遍历头到角标,通过赋值指向操作拿到角标所在的node元素
        x = x.next;
    return x;
} else {
    Node<E> x = last;//如果是后半部分,拿到尾
    for (int i = size - 1; i > index; i--) //遍历尾到角标,通过赋值指向操作拿到角标所在的node元素
        x = x.prev;
    return x;
}
//问题是每个node元素都要这么循环来一遍!

所以,当需要遍历LinkedList时,优先使用哪一个就不用多说了吧?

Map

HashMap

结构解释

非常有必要解释一下HashMap的结构(JDK8以后):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C5GbfJxW-1647831872017)(C:\Users\paratera\AppData\Roaming\Typora\typora-user-images\image-20220318134428555.png)]

归纳一下就是:数组+链表+红黑树

  • 元素key的hash值不同,放在数组里。(横向)
  • 元素key的hash值相同,会产生hash冲突,需要把他放在数组同一位置的链表里,有些地方也理解为桶bucket。(纵向)
  • 横向数组长度大于等于64,纵向链表长度大于8,链表就转换为红黑树

HashMap在JDK1.8后已经非常完善了,各API方法基本上都是最优化了,所以使用的灵活性不会很大,这里主要考虑一些使用需要注意的地方!

根据加载因子设置初始容量
Map<Integer,Object> map1 = new HashMap();
long start1 = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
    map1.put(i,i);
}
long end1 = System.currentTimeMillis();
System.out.println("未初始化容量:" + (end1-start1));

Map<Integer,Object> map2 = new HashMap(1333334);
long start2 = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
    map2.put(i,i);
}
long end2 = System.currentTimeMillis();
System.out.println("设置合理初始容量:" + (end2-start2));

Map<Integer,Object> map3 = new HashMap(99999999);
long start3 = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
    map3.put(i,i);
}
long end3 = System.currentTimeMillis();
System.out.println("过量设置初始容量:" + (end3-start3));

#输出结果
    未初始化容量:142
    设置合理初始容量:74
    过量设置初始容量:412

结论:设置合理初始化容量性能最高

原因分析:

​ HashMap扩容机制触发公式:(int) ((float) expectedSize / 0.75F + 1.0F),扩容后HashMap size: expectedSize * 2

遍历删除安全问题
Map<Integer, Object> map1 = new HashMap(1333334);
Map<Integer, Object> map2 = new ConcurrentHashMap<>(1333334);
Map<Integer, Object> map3 = new HashMap<>(1333334);
for (int i = 0; i < 1000000; i++) {
    map1.put(i, i);
    map2.put(i, i);
    map3.put(i, i);
}

Set<Map.Entry<Integer, Object>> entries1 = map1.entrySet();
Iterator<Map.Entry<Integer, Object>> iterator = entries1.iterator();
long start1 = System.currentTimeMillis();
while (iterator.hasNext()) {
    iterator.next();//必须要进行指针下移
    iterator.remove();
}
long end1 = System.currentTimeMillis();
System.out.println("iterator删除耗时:" + (end1 - start1) + ",删除后size:" + map1.size());

Set<Map.Entry<Integer, Object>> entries2 = map2.entrySet();
long start2 = System.currentTimeMillis();
for (Map.Entry<Integer, Object> entry : entries2) {
    entries2.remove(entry);
}
long end2 = System.currentTimeMillis();
System.out.println("ConcurrentHashMap遍历删除耗时:" + (end2 - start2) + ",删除后size:" + map2.size());

Set<Map.Entry<Integer, Object>> entries3 = map3.entrySet();
long start3 = System.currentTimeMillis();
for (Map.Entry<Integer, Object> entry : entries3) {
    entries3.remove(entry);
}
long end3 = System.currentTimeMillis();
System.out.println("HashMap遍历删除耗时:" + (end3 - start3) + ",删除后size:" + map3.size());

#输出结果
    iterator删除耗时:65,删除后size:0
    ConcurrentHashMap遍历删除耗时:86,删除后size:0
    Exception in thread "main" java.util.ConcurrentModificationException
        at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:1597)
        at java.base/java.util.HashMap$EntryIterator.next(HashMap.java:1630)
        at java.base/java.util.HashMap$EntryIterator.next(HashMap.java:1628)
        at com.paratera.console.dict.utils.Test2.main(Test2.java:40)

结论:iterator和ConcurrentHashMap方式遍历删除都能成功,ConcurrentHashMap因为同步锁问题稍慢,但是区别不大;HashMap遍历删除报异常ConcurrentModificationException

原因分析:

​ 1、HashMap里维护了一个modCount变量,HashIterator迭代器里维护了一个expectedModCount变量,一开始两者是一样的

​ 2、进行HashMap.removeNode操作的时候就会++modCount1,此时迭代器里的expectedModCount还是之前的值

​ 3、在下一次对迭代器进行next()调用时,判断是否HashMap.this.modCount != this.expectedModCount,如果是则抛出异常

我们可以使用异常捕获打印一下,异常发生后map3 size大小,实际上第一次remove是成功的,只是指向下一个entry时被HashMap认为是不安全的删除而报错,所以使用iterator迭代遍历删除时作者增加了一个操作:remove了一个元素之后会对expectedModCount重新赋值

Set<Map.Entry<Integer, Object>> entries3 = map3.entrySet();
long start3 = System.currentTimeMillis();
try {
    for (Map.Entry<Integer, Object> entry : entries3) {
        entries3.remove(entry);
    }
}catch (Exception e){
    
}
long end3 = System.currentTimeMillis();
System.out.println("HashMap遍历删除耗时:" + (end3 - start3) + ",删除后size:" + map3.size());

#输出结果
    HashMap遍历删除耗时:0,删除后size:999999
加载因子设置

前提:没法预估Map容量的情况下

当Map比较稳定,并且长期存在(比如作为服务器缓存)时,可以适当减少初始化大小,减小加载因子;—稳定时减少资源占用

当Map比较活跃,并且是动态生成时,可以适当增加初始化大小,增大加载因子;—活跃时减少扩容频率

TreeMap

顺序遍历

适用于按自然顺序或自定义顺序遍历键(key),在需要排序的Map时候才建议使用TreeMap,否则使用HashMap,提高性能

for (Map.Entry<String, String> entry : map1.entrySet()) {
    System.out.println(entry.getKey());
}
System.out.println("---------");
Map<String, String> map2 = new TreeMap<>();
map2.put("aaa", "aaa");
map2.put("bbb", "bbb");
map2.put("ccc", "ccc");

for (Map.Entry<String, String> entry : map2.entrySet()) {
    System.out.println(entry.getKey());
}

#输出结果
    aaa
    ccc
    bbb
    ---------
    aaa
    bbb
    ccc
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for (int i = 0; i < 1000000; i++) {
    map.put(i, i);
}
Iterator<Integer> iterator1 = map.keySet().iterator();
long start1 = System.currentTimeMillis();
while (iterator1.hasNext()) {
    map.get(iterator1.next());
}
long end1 = System.currentTimeMillis();
System.out.println("HashMap遍历耗时:" + (end1 - start1));

Map<Integer, Integer> tmp = new TreeMap<Integer, Integer>();
for (int i = 0; i < 1000000; i++) {
    tmp.put(i, i);
}
Iterator<Integer> iterator_2 = tmp.keySet().iterator();
long start2 = System.currentTimeMillis();
while (iterator_2.hasNext()) {
    tmp.get(iterator_2.next());
}
long end2 = System.currentTimeMillis();
System.out.println("TreeMap遍历耗时:" + (end2 - start2));

#输出结果
    HashMap遍历耗时:41
	TreeMap遍历耗时:92

结论:HashMap性能高,无序;TreeMap性能低,有序

原因分析:

​ HashMap是数组+链表+有条件红黑树,无法保证内部存储的键值对的有序性;但是数组的查询效果比其他任何方式都要高,而JDK8以后,限制了HashMap的长度,衍生红黑树,从而有效控制了链表的查询效果,所以综合起来,性能不错!

​ TreeMap是纯红黑树,能够保证有序性;但是查询性能比数组差

Set

HashSet和TreeSet

二者类似于HashMap和TreeMap,一个偏向性能,一个偏向有序,只是HashMap和TreeMap存储的是key/value;HashSet和TreeSet存储的是集合元素,所以这里不再赘述!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值