ArrayList与LinkedList的普通for循环遍历
对于大部分Java程序员朋友们来说,可能平时使用得最多的List就是ArrayList,对于ArrayList的遍历,一般用如下写法:
public static void main(String[] args) { List<Integer> arrayList = new ArrayList<Integer>(); for (int i = 0; i < 100; i++) arrayList.add(i); for (int i = 0; i < 100; i++) System.out.println(arrayList.get(i)); }
如果以后要用到LinkedList了,可能有些朋友就会用一样的方式去遍历LinkedList了:
public static void main(String[] args) { List<Integer> linkedList = new LinkedList<Integer>(); for (int i = 0; i < 100; i++) linkedList.add(i); for (int i = 0; i < 100; i++) System.out.println(linkedList.get(i)); }
请记住:这是一种非常糟糕的做法。这其实已经不是Java的问题,而是数据结构的问题了,我相信语言从Java换成其他的也都一样。
下面对ArrayList和LinkedList的普通for循环效率进行测试以及分析原因。
ArrayList和LinkedList使用普通for循环遍历速度对比
先给出测试代码:
public class ListIteratorTest { private final static int LIST_SIZE = 1000; public static void main(String[] args) { List<Integer> arrayList = new ArrayList<Integer>(); List<Integer> linkedList = new LinkedList<Integer>(); for (int i = 0; i < LIST_SIZE; i++) { arrayList.add(i); linkedList.add(i); } long startTime = System.currentTimeMillis(); for (int i = 0; i < arrayList.size(); i++) arrayList.get(i); System.out.println("ArrayList遍历速度:" + (System.currentTimeMillis() - startTime) + "ms"); startTime = System.currentTimeMillis(); for (int i = 0; i < linkedList.size(); i++) linkedList.get(i); System.out.println("LinkedList遍历速度:" + (System.currentTimeMillis() - startTime) + "ms"); } }
不断增大LIST_SIZE,我用表格表示一下运行结果:
1000 | 5000 | 10000 | 50000 | 100000 | |
ArrayList | 0ms | 1ms | 2ms | 3ms | 3ms |
LinkedList | 3ms | 16ms | 88ms | 2446ms | 18848ms |
从运行结果我们看到,按倍数增大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)的意思也就是说时间复杂度是一个常数,和数组的大小并没有关系,只要给定数组的位置,直接就能定位到数据。
其实熟悉C、C++或者对指针理解的朋友一定很好理解为什么,我解释一下为什么对数组使用get就快。
在计算机底层,数据都是有地址的,就像人有住址一样。假设我写了这么一句代码:
int[3] ints = {1, 3, 5};
在Java中一个int型数据是4个字节,此时计算机内部做的事情是,在内存空间中找到一块连续的、足以存放3个4字节也就是12字节的数组的内存空间,并返回该内存空间的首地址。比方说该内存空间的首地址是0x00吧,那么那么1就放在0x00~0x03中、3就放在0x04~0x07中、5就放在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是双向链表,因此第6行的意思是算出i在一半前还是一半后,一半前正序遍历、一半后倒序遍历,这样会快很多,当然,先不管这个,分析一下为什么使用普通for循环遍历LinkedList会这么慢。
原因就在第7~第8行,第10~第11行的两个for循里面,以前者为例:
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大一点,程序就会执行得比较慢。
后记
根据以上的分析,各位Java程序员朋友们,切记一定不要使用普通for循环去遍历LinkedList。使用迭代器或者foreach循环(foreach循环的原理就是迭代器)去遍历LinkedList即可,这种方式是直接按照地址去找数据的,将会大大提升遍历LinkedList的效率。
我不能保证写的每个地方都是对的,但是至少能保证不复制、不黏贴,保证每一句话、每一行代码都经过了认真的推敲、仔细的斟酌。每一篇文章的背后,希望都能看到自己对于技术、对于生活的态度。
我相信乔布斯说的,只有那些疯狂到认为自己可以改变世界的人才能真正地改变世界。面对压力,我可以挑灯夜战、不眠不休;面对困难,我愿意迎难而上、永不退缩。
其实我想说的是,我只是一个程序员,这就是我现在纯粹人生的全部。
==================================================================================
相关推荐
然而,对于LinkedList来说,使用普通for循环进行遍历可能会导致性能问题,这是由于LinkedList的内部实现机制所决定的。 LinkedList是一种基于链表结构的数据结构,每个元素(节点)包含数据以及指向下一个节点的...
Java程序员必读:基础篇是一本专为初学者设计的指南,旨在深入浅出地介绍Java编程语言的核心概念和基础知识。这本书涵盖了从安装Java开发环境到编写、编译和运行第一个Java程序的所有步骤,是入门Java编程的理想选择...
【Java程序员转正知识点】 1. **自我介绍**: 在转正答辩中,自我介绍是展示个人背景和技能的关键环节。作为一名Java程序员,应当包括自己的教育背景、编程经验、项目经历以及对Java语言的理解。 2. **技术成长**...
- **控制流**:包括条件语句(if-else)、循环(for、while、do-while)和跳转语句(break、continue)。 - **方法**:方法是Java中的函数,用于封装代码逻辑。 3. **面向对象编程**: - **类与对象**:Java是纯...
Java集合框架是面试中常考的部分,包括ArrayList、LinkedList、HashSet、HashMap等容器的使用、性能比较以及并发安全问题。面试宝典会重点讲解List、Set、Map接口的实现类,以及它们在不同场景下的选择策略。 三、...
首先,Java初学者通常会从学习基础语法开始,包括变量、数据类型、控制结构(如if语句和循环)、类与对象的概念。理解这些基础知识是构建扎实编程技能的第一步。同时,深入学习Java集合框架,如ArrayList、...
本篇文章将深入探讨如何在Java中实现多叉树以及其遍历方法。 首先,我们需要定义一个多叉树节点类。这个类通常包含一个数据字段来存储节点值,以及一个ArrayList或LinkedList等动态数组来存储子节点。以下是一个...
1. Java基础:教程首先会介绍Java的基础知识,包括语法、变量、数据类型、运算符、控制结构(如if语句、for循环、while循环)以及方法的定义和调用。这些是编写任何Java程序的基础,理解和掌握它们至关重要。 2. 类...
本笔记将深入探讨如何使用不同类型的循环来遍历Java集合,包括数组列表(ArrayList)、链表(LinkedList)、集合(Set)和映射(Map)。 首先,让我们从最基础的`for`循环开始。传统的`for`循环,也称为索引循环,...
【JAVA程序员培训定制课程】是一门全面而深入的教程,旨在帮助学员掌握Java编程的核心技能,从而成为合格的Java程序员。课程分为五个主要部分,涵盖了从基础到高级的Java编程知识,同时也为Java认证考试提供了充分的...
Java编程语言是软件开发领域广泛使用的工具,尤其对于初学者来说,掌握Java基础知识至关重要。"JAVA程序员必读:基础篇.zip"这个压缩包文件显然包含了针对新手的Java学习资料,帮助他们踏上编程之旅。HTML文件可能...
1. **Java基础**:包括Java语法、数据类型、控制流程(如if语句、for循环、while循环)、类与对象、封装、继承、多态等面向对象特性。 2. **异常处理**:Java中的异常分类,如何抛出和捕获异常,finally块的使用,...