java数据结构arraylist和linkedlist
一,首先看看内存结构图:
二,详细描述:
ArrayList: 可以看作是能够自动增长容量的数组
ArrayList 的toArray方法返回一个数组
ArrayList 的asList方法返回一个列表
ArrayList 底层的实现是Array, 数组扩容实现
ArrayList 线程不安全
ArrayList 必须是连续的
ArrayList 只能在数组末尾添加数据
ArrayList 查询快,增删慢
ArrayList 在初始化的时候指定长度肯定是要比不指定长度的性能好很多,这样不用重复的申请空间, 复制数组, 销毁老的分配空间了
LinkedList可以看做为一个双向链表(内部每个元素都双向指向下一个元素)
LinkedList实现了Deque接口和List接口
LinkedList也是线程不安全
LinkedList可以不连续
LinkedList查询慢,增删快
LinkList可以很方便在链表头或者链表尾插入数据,或者在指定结点前后插入数据,add()方法默认在链表尾部插入数据
三,查询快慢的原因:
ArrayList从原理上就是数据结构中的数组,也就是内存中一片连续的空间,这意味着,当我get(index)的时候,我可以根据数组的(首地址+偏移量),直接计算出我想访问的第index个元素在内存中的位置。
LinkedList可以简单理解为数据结构中的链表(说简单理解,因为其实是双向循环链表),在内存中开辟的不是一段连续的空间,而是每个元素有一个[元素|和下一元素地址]这样的内存结构。当get(index)时,只能从首尾元素开始,依次获得下一个元素的地址。
用时间复杂度表示的话,ArrayList的get(n)是o(1),而LinkedList是o(n)。
首先看看ArrayList查询的原理
先看一下ArrayList的get方法源代码:
public E get(int index) {
rangeCheck(index);
return 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中。如果内存地址不连续的话,就无法确定内存存放元素的顺序和index的顺序一致
如果内存地址不连续的话,就无法确定内存存放元素的顺序和index的顺序一致
如果内存地址不连续的话,就无法确定内存存放元素的顺序和index的顺序一致
这时就很简单了,取ints[1]的时候,计算机就会算出ints[1]的数据是存放在以0x04开头,占据4个字节空间的内存中,因此,计算机会从0x04~0x07这块地址空间中读取数据出来。
整个过程,和数组有多大,并没有关系,计算机做的只是算出起始地址-->去该地址中取数据而已,因此我们看到使用普通for循环遍历ArrayList的速度很快,也很稳定。
再看看linkedList查询的原理
因为内存地址不连续,就无法确定内存存放元素的顺序和index的顺序一致
因为内存地址不连续,就无法确定内存存放元素的顺序和index的顺序一致
因为内存地址不连续,就无法确定内存存放元素的顺序和index的顺序一致
如下图的linkedList元素内存分布,根本无法用get(index)快速存到元素,只能从头到尾一个一地访问
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
原因就在第6~第7行,第11~第12行的两个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个数据,那么LinikedList将要查询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大一点,程序就会执行得比较慢。
后记
切记一定不要使用普通for循环去遍历LinkedList。使用迭代器或者foreach循环(foreach循环的原理就是迭代器)去遍历LinkedList即可,这种方式是直接按照地址去找数据的,将会大大提升遍历LinkedList的效率。
四,增删元素快慢的原因:
arraylist 顺序表: 需要申请连续的内存空间保存元素,可以通过内存中的物理位置直接找到元素的逻辑位置。在顺序表中间插入or删除元素需要把该元素之后的所有元素向前or向后移动。如果继续添加元素,但是数组已满,没有空闲位置的时候,ArrayList
先创建一个更大的新数组,然后把旧数组的所有元素复制到新数组,紧接着用新数组取代旧数组。这个过程非常复杂耗时!
linkedlist 双向链表: 不需要申请连续的内存空间保存元素,需要通过元素的头尾指针找到前继与后继元素(查找元素的时候需要从头or尾开始遍历整个链表,直到找到目标元素)。在双向链表中插入or删除元素不需要移动元素,只需要改变相关元素的头尾指针即可。但是如果在linkedlist中间位置增删元素效率就会慢很多,因为linkedlist需要查询定位到中间位置(这个过程很慢)
五,实测数据对比:
ArrayList尾部插入100000个元素耗时:26ms
LinkedList尾部插入100000个元素耗时:28ms
ArrayList头部插入100000个元素耗时:859ms
LinkedList头部插入100000个元素耗时:15ms
ArrayList中间插入100000个元素耗时:1848ms
LinkedList中间插入100000个元素耗时:15981ms
ArrayList头部读取100000个元素耗时:7ms
LinkedList头部读取100000个元素耗时:11ms
ArrayList尾部读取100000个元素耗时:12ms
LinkedList尾部读取100000个元素耗时:9ms
ArrayList中间读取100000个元素耗时:13ms
LinkedList中间读取100000个元素耗时:11387ms
ArrayList的查找性能绝对是一流的,无论查询的是哪个位置的元素。
ArrayList除了尾部插入的性能较好外(位置越靠后性能越好),其他位置性能就不如人意了。
LinkedList在头尾查找、插入性能都是很棒的,但是在中间位置进行操作的话,性能就差很远了,而且跟ArrayList完全不是一个量级的。
下面是源码分析:
ArrayList尾部插入
add(E e)
方法
public boolean add(E e) {
// 检查是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 直接在尾部添加元素
elementData[size++] = e;
return true;
}
可以看出,对ArrayList的尾部插入,直接插入即可,无须额外的操作。
LinkedList尾部插入
LinkedList中定义了头尾节点
/**
* Pointer to first node.
*/
transient Node first;/**
* Pointer to last node.
*/transient Node last;
add(E e)
方法,该方法中调用了linkLast(E e)
方法
public boolean add(E e) {
linkLast(e);
return true;
}
linkLast(E e)方法,可以看出,在尾部插入的时候,并不需要从头开始遍历整个链表,因为已经事先保存了尾结点,所以可以直接在尾结点后面插入元素
/**
* Links e as last element.
*/
void linkLast(E e) {
// 先把原来的尾结点保存下来
final Node l = last;// 创建一个新的结点,其头结点指向lastfinal Node newNode = new Node<>(l, e, null);// 尾结点置为newNode
last = newNode;if (l == null)
first = newNode;else// 修改原先的尾结点的尾结点,使其指向新的尾结点
l.next = newNode;
size++;
modCount++;
}
总结
对于尾部插入而言,ArrayList与LinkedList的性能几乎是一致的
ArrayList头部插入
add(int index, E element)
方法,可以看到通过调用系统的数组复制方法来实现了元素的移动。所以,插入的位置越靠前,需要移动的元素就会越多
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
// 把原来数组中的index位置开始的元素全部复制到index+1开始的位置(其实就是index后面的元素向后移动一位)
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 插入元素
elementData[index] = element;
size++;
}
LinkedList头部插入
add(int index, E element)
方法,该方法先判断是否是在尾部插入,如果是调用linkLast()
方法,否则调用linkBefore()
,那么是否真的就是需要重头开始遍历呢?我们一起来看看
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
在头尾以外的位置插入元素当然得找出这个位置在哪里,这里面的node()方法就是关键所在,这个函数的作用就是根据索引查找元素,但是它会先判断index的位置,如果index比size的一半(size >> 1,右移运算,相当于除以2)要小,就从头开始遍历。
否则,从尾部开始遍历。从而可以知道,对于LinkedList来说,操作的元素的位置越往中间靠拢,效率就越低
Node node(int index) {
// assert isElementIndex(index);
if (index > 1)) {
Node x = first;for (int i = 0; i x = x.next;return x;
} else {
Node x = last;for (int i = size - 1; i > index; i--)
x = x.prev;return x;
}
}
这个函数的工作就只是负责把元素插入到相应的位置而已,关键的工作在node()方法中已经完成了
void linkBefore(E e, Node succ) {
// assert succ != null;
final Node pred = succ.prev;final Node newNode = new Node<>(pred, e, succ);
succ.prev = newNode;if (pred == null)
first = newNode;else
pred.next = newNode;
size++;
modCount++;
}
总结
- 对于LinkedList来说,头部插入和尾部插入时间复杂度都是O(1),中间插入删除都很慢
- 但是对于ArrayList来说,头部的每一次插入都需要移动size-1个元素,效率很慢
- 但是如果都是在最中间的位置插入的话,ArrayList速度比LinkedList的速度快近10倍
ArrayList、LinkedList查找
- 这就没啥好说的了,对于ArrayList,无论什么位置,都是直接通过索引定位到元素,时间复杂度O(1)
- 而对于LinkedList查找,其核心方法就是上面所说的node()方法,所以头尾查找速度极快,越往中间靠拢效率越低
原文地址
https://blog.csdn.net/weixin_39932330/article/details/111252225