• 切勿用普通的for循环遍历LinkedList


     前言

      对于大多数的程序员来说,平时使用和见到的最多的List应该是ArrayList,对于LinkedList使用和看见的地方不多,本篇文章不阐述和说明这两个List的区别或特点,只说说对他们的循环遍历。

    ArrayList和LinkedList的普通for循环遍历

    直接上代码:

     1 public class ListTraverse {
     2 
     3     private final static int LIST_SIZE = 100000;
     4 
     5     public static void main(String[] args){
     6         List<Integer> arrayList = new ArrayList<Integer>();
     7         List<Integer> linkedList = new LinkedList<Integer>();
     8 
     9         for (int i = 0; i < LIST_SIZE; i++){
    10             arrayList.add(i);
    11             linkedList.add(i);
    12         }
    13 
    14         long startTime = System.currentTimeMillis();
    15         for (int i = 0; i < arrayList.size(); i++){
    16             arrayList.get(i);
    17         }
    18         System.out.println("ArrayList遍历速度:" + (System.currentTimeMillis() - startTime) + "ms");
    19 
    20         startTime = System.currentTimeMillis();
    21         for (int i = 0; i < linkedList.size(); i++){
    22             linkedList.get(i);
    23         }
    24         System.out.println("LinkedList遍历速度:" + (System.currentTimeMillis() - startTime) + "ms");
    25     }
    26 }

     不断的增大上述代码的循环次数,也就是LIST_SIZE。用表格展示一下循环所需的时间:

      1000 5000 10000 50000 100000
    ArrayList 0ms 0ms 1ms 2ms 2ms
    LinkedList 1ms 14ms 45ms 1075ms 4250ms

    从运行结果我们看到,按倍数增大List容量,ArrayList的遍历显得比较稳定,而LinkedList的遍历几乎是爆发式的增长,再测试下去已经没有必要了。

    下面解释一下产生此现象的原因。

    ArrayList使用普通for循环遍历的原因

      来看一下ArrayList的get()方法的源码:

    public E get(int index) {
    RangeCheck(index);
    
    return (E) elementData[index];
    }

    可以看出ArrayList的get()方法只是从数组里面拿一个位置上的元素而已。我们知道,ArrayList的get()方法的时间复杂度是O(1),O(1)的意思就是说时间复杂度是一个常数,和数组的大小并没有关系,只要给定数组的位置,直接就能定位到数据。

    再解释一下为什么对数组使用get()就快?

      在计算机的底层,数据都是有地址的,就跟人有住址一样的,假设下面的一句代码:

    int[3] ints = {1, 2, 3};

      在Java中一个Int型的数据是占4个字节的,为了存放这个数组,那么计算机内部做的事情是:在内存空间中找到一块连续的、足以存放12字节(3个4字节)的数组内存空间,并返回该内存空间的首地址。比如说该内存空间的首地址是0x00,那么1就放在地址为0x00~0x03中、2放在0x04~0x07中,3放在0x08~0x0B中。这样的话就很简单了,取ints[1]的时候,计算机就会算出ints[1]的数据是以0x04开头、占据4个字节空间的内存中,因此,计算机会将0x04~0x07这块地址空间中的数据读取出来。

      以上读取数据的整个过程,和数组有多大,并没有关系,计算机做的只是算出起始地址-->去该地址中取数据而已,因此我们看到使用普通的for循环遍历ArrayList的速度很快,也很稳定。

    LinkedList使用普通for循环遍历慢的原因

      先来看一下LinkedList的get()方法的源码:

    public E get(int index) {
        return entry(index).element;
    }
     1 private Entry<E> entry(int index) {
     2     if (index < 0 || index >= size)
     3         throw new IndexOutOfBoundsException("Index: "+index+
     4                                             ", Size: "+size);
     5     Entry<E> e = header;
     6     if (index < (size >> 1)) {
     7         for (int i = 0; i <= index; i++)
     8             e = e.next;
     9     } else {
    10         for (int i = size; i > index; i--)
    11             e = e.previous;
    12     }
    13     return e;
    14 }

      由于LinkedList是双向链表,因此上述代码在第二个方法entry()中第6行的意思是:算出 i 是在一半前还是一半后,一半前正序遍历、一半后倒序遍历,这就是二分排序。

      再看代码第7~8行、10~11行的两个for循环,以第7~8行为例: 

        1、get(0),直接拿到0位的Node0的地址,拿到Node0里面的数据

        2、get(1),直接拿到0位的Node0的地址,从0位的Node0中找到下一个1位的Node1的地址,找到Node1,拿到Node1里面的数据

        3、get(2),直接拿到0位的Node0的地址,从0位的Node0中找到下一个1位的Node1的地址,找到Node1,从1位的Node1中找到下一个2位的Node2的地址,找到Node2,拿到Node2里面的数据。

      后面的就以此类推。

      也就是说,LinkedList在get任何一个位置的数据的时候,都会把前面的数据走一遍。假如我有10个数据,那么将要查询1+2+3+4+5+5+4+3+2+1=30次数据,相比ArrayList,却只需要查询10次数据就行了,随着LinkedList的容量越大,差距会越拉越大。其实使用LinkedList到底要查询多少次数据,大家应该已经很明白了,来算一下:按照前一半算应该是(1 + 0.5N) * 0.5N / 2,后一半算上即乘以2,应该是(1 + 0.5N) * 0.5N = 0.25N2 + 0.5N,忽略低阶项和首项系数,得出结论,LinikedList遍历的时间复杂度为O(N2)N为LinkedList的容量

      时间复杂度有以下经验规则:

        O(1) < O(log2N) < O(n) < O(N * log2N) < O(N2) < O(N3) < 2N < 3N < N!

      前四个比较好、中间两个一般、后3个很烂。也就是说O(N2)是相对糟糕的一种时间复杂度了,N大一点,程序就会执行得比较慢。

    ArrayList和LinkedList使用迭代器循环遍历

    直接上代码:

     1 public class ListTraverse {
     2 
     3     private final static int LIST_SIZE = 1000;
     4 
     5     public static void main(String[] args){
     6         List<Integer> arrayList = new ArrayList<Integer>();
     7         List<Integer> linkedList = new LinkedList<Integer>();
     8 
     9         for (int i = 0; i < LIST_SIZE; i++){
    10             arrayList.add(i);
    11             linkedList.add(i);
    12         }
    13 
    14         long startTime = System.currentTimeMillis();
    15         Iterator iterator = arrayList.iterator();
    16 
    17         while (iterator.hasNext()){
    18             iterator.next();
    19         }
    20         System.out.println("ArrayList遍历速度:" + (System.currentTimeMillis() - startTime) + "ms");
    21 
    22         startTime = System.currentTimeMillis();
    23         Iterator iterator1 = linkedList.iterator();
    24         while (iterator1.hasNext()){
    25             iterator1.next();
    26         }
    27         System.out.println("LinkedList遍历速度:" + (System.currentTimeMillis() - startTime) + "ms");
    28     }
    29 }

    跟上述一样,不断增大LIST_SIZE,还是用表格展示所需时间:

      1000 5000 10000 50000 100000
    ArrayList 1ms 1ms 2ms 2ms 2ms
    LinkedList 0ms 0ms 3ms 3ms 3ms

     可以看出,随着循环数目的增大,对循环的时间几乎没有影响。

    迭代器是通过next()和Pre()来定位的,采用的是顺序访问的方法来访问数据。基于顺序存储集合的Iterator可以直接按位置访问数据。而基于链式存储集合的Iterator,正常的实现,都是需要保存当前遍历的位置。然后根据当前位置来向前或者向后移动指针。

     ArrayList和LinkedList使用forEach循环遍历

      屏蔽了显式声明的Iterator和计数器,foreach内部也是采用了Iterator的方式实现。

      优点:代码简洁,不易出错。

            缺点:只能做简单的遍历,不能在遍历过程中操作(删除、替换)数据集合。

    由于forEach循环内部也是采用Iterator循环,所以循环的效率几乎是一样的,这里就不再做实验了;

    但是,由于forEach循环在简洁性和预防Bug方面有着传统的for循环和Iterator无法比拟的优势,并且没有性能损失,应该尽可能的使用forEach循环。

    有以下三种情况无法使用forEach循环:

      (1)过滤:如果需要遍历集合,并删除选定元素,就需要使用显示的Iterator迭代器,以便可以调用显示迭代器的remove()方法;

      (2)转换:如果需要遍历列表或者数组,并取代它部分或者全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值;

      (3)平行迭代:如果需要并行的遍历多个集合,就需要显示地控制迭代器或者索引变量,以便所有的迭代器或者索引变量都可以得到同步前移;

    结论

      根据以上的分析,各位Java程序员朋友们,切记一定不要使用普通for循环去遍历LinkedList。使用迭代器或者foreach循环(foreach循环的原理就是迭代器)去遍历LinkedList即可,这种方式是直接按照地址去找数据的,将会大大提升遍历LinkedList的效率。 

    参考:https://www.cnblogs.com/xrq730/p/5189565.html

    作者:Joe
    努力了的才叫梦想,不努力的就是空想,努力并且坚持下去,毕竟这是我相信的力量
  • 相关阅读:
    pandas 修改列顺序
    read_csv,to_csv 前n行
    郭盛华:未来黑客攻击的将远不止网络
    微软的 Linux 存储库停机 18 多个小时
    警惕黑客利用 Google Docs进行网络钓鱼
    苹果发布紧急补丁!修复被黑客利用的2个零日漏洞
    谷歌发布新框架以防止软件供应链攻击
    郭盛华:以知识见识锤炼真本领,年轻人要有理想
    通过 GDPR 加强密码政策,是企业网络的第一道防线
    肉类供应商遭黑客攻击,并支付了 1100 万美元的赎金
  • 原文地址:https://www.cnblogs.com/Joe-Go/p/10008555.html
Copyright © 2020-2023  润新知