关于做者java
郭孝星,程序员,吉他手,主要从事Android平台基础架构方面的工做,欢迎交流技术方面的问题,能够去个人Github提issue或者发邮件至guoxiaoxingse@163.com与我交流。node
文章目录git
更多文章:github.com/guoxiaoxing…程序员
咱们将形如A1,A2,A3,A4 ... AN称之为一个表,大小为0的表咱们称之为空表。github
经常使用的表以下:数组
数组是最为简单的一种表,它在查找操做是线性时间的,可是插入与删除则潜藏额外的开销。安全
总结起来,插入/删除的时间时间复杂度为O(N)。bash
链表由一系列节点组成,这些节点没必要在内存中相连,每一个节点均含有表元素和到包含该元素后继元的节点的链,称为next链。这样的链表
称为单链表,另外,若是节点还包含指向它在链表中的前驱节点的链,则成该链表为双链表。数据结构
链表是为了不插入/删除带来的额外开销,咱们又引入了链表,其中,链表又能够分为单链表与双链表。架构
栈是限制插入和删除只能在表的末端进行的表,也就是出栈与入栈操做。
栈是为了在指定的位置就行插入和删除。
应用场景
平衡符号
在咱们编写代码的时候,常常会遇写错了一个符号而报错,例如[()]是正确的,而[(])就是错误的。复制代码
方法调用
咱们知道在Java中有个叫方法调用栈的东西,它会为每一个执行的方法建立一个栈帧,用来存储局部变量表,操做数栈,动态连接和方法出口等信息。这种场景与上面提到
的平衡符号十分类似,由于方法的调用和放回和符号的开闭很是类似。
每次调用方法都会往栈里插入一个栈帧,方法返回是再将其出栈,若是咱们将方法设计成递归调用,错误的结束条件或者过于庞大的数据量可能会引发栈溢出。
队列也是一种表,使用队列时在一端进行插入而在另外一端进行删除。
如同栈同样,对于队列而言任何表的实现都是合法的。
说完了关于表的基本概念,咱们来聊一聊Java中对表的基本实现。Java中用接口List来定义表的基本功能,包括增、删、改、查等基本操做。
List接口定义以下:
注:链表、栈、队列都是表,所以对于实现了表方法的ArrayList/LinkedList都支持者三种数据结构。
public interface List<E> extends Collection<E> {
// Query Operations
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
// Modification Operations
boolean add(E e);
boolean remove(Object o);
// Bulk Modification Operations
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean addAll(int index, Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean retainAll(Collection<?> c);
void clear();
// Comparison and hashing
boolean equals(Object o);
int hashCode();
// Positional Access Operations
E get(int index);
E set(int index, E element);
void add(int index, E element);
E remove(int index);
// Search Operations
int indexOf(Object o);
int lastIndexOf(Object o);
// List Iterators
ListIterator<E> listIterator();
ListIterator<E> listIterator(int index);
// View
List<E> subList(int fromIndex, int toIndex);
@Override
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.ORDERED);
}
default void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final ListIterator<E> li = this.listIterator();
while (li.hasNext()) {
li.set(operator.apply(li.next()));
}
}
default void sort(Comparator<? super E> c) {
Collections.sort(this, c);
}
}复制代码
关于List接口,咱们有两个经常使用的实现类。
ArrayList提供了一种List ADT的可增加的实现,优势在于get、set花费常量时间,缺点在于插入、删除代价昂贵。
ArrayList是以数组为基础实现的线性数据结构,具体说来,它有如下特色:
ArrayList类图以下所示:
实现了如下接口:
ArrayList采用数组存取元素。
//保存ArrayList数据的数组,它采用transient关键字标记,说明序列化ArrayList忽略掉该字段
transient Object[] elementData;
//数据数量
private int size;复制代码
咱们接下来看看ArrayList关于增、删、改、查的实现。
时间复杂度:O(N)
实现原理:ArrayList能够在指定位置增长元素,增长元素后,当前元素后面的元素都要向后移动一位。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
ensureCapacityInternal(size + 1); // Increments modCount!!
//将数组elementData从index位置开始的全部元素,拷贝到elementData从index + 1位置开始的位置
//也就是index位置后面的元素所有向后移动一位
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
}复制代码
ArrayList会调用ensureCapacityInternal()方法来保证数组容量的自动增加,咱们先来看看它的实现。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//ArrayList内部数组的最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
}复制代码
能够看到ArrayList调用grow(int minCapacity)方法来增长容量,这里有个最小容量minCapacity,它等于当前数组大小size+1。该方法的
计算流程以下:
这里咱们还要提到一个数组拷贝的方法,它是一个native方法。
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);复制代码
时间复杂度:O(N)
实现原理:删除指定位置的元素,删除元素后,把该元素后面的全部元素向前移动一位
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
public E remove(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
modCount++;
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0)
//当前删除index后面的元素所有向前移动一位
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
}复制代码
ArrayList提供了两种移除元素的方法,按索引移除与按对象移除,按对象移除会先去遍历该对象的索引,而后再按索引移除。
时间复杂度:O(1)
实现原理:替换内部数组相应位置上的元素
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
public E set(int index, E element) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}
}复制代码
时间复杂度:O(1)
实现原理:根据指定索引获取当前元素,无需额外的计算。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
public E get(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
return (E) elementData[index];
}
}复制代码
LinkedList提供了一种List ADT的双链表的实现。优势在于插入、删除操做开销较小,缺点在于不用于作索引,get操做代价昂贵。
LinkedList基于双向链表实现的,它具备以特色:
ArrayList类图以下所示:
实现了如下接口:
LinkedList里用节点来描述里面的数据,以下所示:
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}复制代码
能够看到,每一个节点包含了当前的元素以及它的前驱节点和后继节点。
LinkedList包含了三个成员变量:
//节点个数
transient int size = 0;
//第一个节点
transient Node<E> first;
//最后一个节点
transient Node<E> last;复制代码
咱们接下来看看ArrayList关于增、删、改、查的实现。
时间复杂度:add(E e) - O(1),add(int index, E element) - O(N)
实现原理:增长和删除的过程都是一个针对指定节点进行前驱和后继关系修改的过程,若是是在起始节点和末尾节点插入、整个过程无需额外的遍历计算。
若是实在中间位置插入,则须要查找当前索引的节点。
原理图以下:
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
public boolean add(E e) {
linkLast(e);
return true;
}
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
//查找指定索引的节点
Node<E> node(int index) {
//根据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;
}
}
private void linkFirst(E e) {
final Node<E> f = first;
//建立一个以起始节点last为后继的节点
final Node<E> newNode = new Node<>(null, e, f);
//将新建立的节点从新设置为起始节点
first = newNode;
//若是起始节点为空,则将新节点设置为末尾,若是不为空,则将起始节点的前驱指向新建立的节点
if (f == null)
last = newNode;
else
f.prev = newNode;
//大小加自增1,修改字数自增1
size++;
modCount++;
}
//末尾插入元素
void linkLast(E e) {
final Node<E> l = last;
//建立一个以末尾节点last为前驱的节点
final Node<E> newNode = new Node<>(l, e, null);
//将新建立的节点从新设置为末尾节点
last = newNode;
//若是末尾节点为空,则将新节点设置为起始节点,若是不为空,则将末尾节点的后继指向新建立的节点
if (l == null)
first = newNode;
else
l.next = newNode;
//大小加自增1,修改字数自增1
size++;
modCount++;
}
void linkBefore(E e, Node<E> succ) {
final Node<E> pred = succ.prev;
//以原来节点的前驱做为本身的前驱,以原来节点做为本身的后继,建立新的节点
final Node<E> newNode = new Node<>(pred, e, succ);
//将新的节点做为原来节点的前驱
succ.prev = newNode;
//若是原来节点的前驱为空,即它原来是起始节点,则直接将新建立的节点做为新的起始节点,若是不为空,则将
//原来节点的前驱的后继设置为当前建立的新节点
if (pred == null)
first = newNode;
else
pred.next = newNode;
//大小加自增1,修改字数自增1
size++;
modCount++;
}
}复制代码
咱们能够在指定位置插入元素,若是没有指定位置,默认在链表末尾插入元素。能够看到add方法的实现是依赖于link方法来创建前驱与后继的联系。
具体说来:
linkFirst
linkBefore
时间复杂度:O(1)
实现原理:增长和删除的过程都是一个针对指定节点进行前驱和后继关系修改的过程,整个过程无需额外的遍历计算。
原理图以下:
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
public E remove() {
return removeFirst();
}
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null; // help GC
last = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
E unlink(Node<E> x) {
//找出要删除节点的前驱与后继
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
//若是前驱为空,则将要删除节点的后继设置为起始节点,不然将删除节点的前驱的后继指向删除节点的后继
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
//若是后继为空,则直接将删除节点的前驱做为末尾节点,不然将删除节点的后继的前驱指向删除节点的前驱
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
//删除元素置空,大小加自增1,修改字数自增1
x.item = null;
size--;
modCount++;
return element;
}
}复制代码
能够看出,删除的过程就是一个接触前驱和后继的过程。主要的有unlink方法来实现,具体说来:
unlink
这么描述有点绕,就是删除原来的前驱后继关系,从新创建链接。
时间复杂度:O(N)
实现原理:查找指定index对应节点,替换掉节点里的元素,set操做也有个查找过程。
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
//查找指定索引的节点
Node<E> node(int index) {
//根据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;
}
}
}复制代码
时间复杂度:O(N)
实现原理:根据index的位置决定是从起始节点开始查找,仍是从末尾节点开始查找。
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
//查找指定索引的节点
Node<E> node(int index) {
//根据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;
}
}
}复制代码
关于表、栈与队列的内容咱们就讲到这里,后续有新的内容还会在这篇文章更新。