通常程序总是根据运行之后才知道的某些条件去创建对象。在此之前并不知道所需对象的数量,甚至也不知道对象的类型。为了解决这个问题,则需要在任意时刻任意位置来创建这个所需要的对象。这样一来,数组是肯定行不通的。因为数组的尺寸是固定的不可改变的。所以在大多数编程语言里面都会提供某种方法来解决。比如C++有STL,而Java则有一套容器类。
C++ STL容器
在集合类的基本类型里,分别有List,Set,Map,Queue。容器提供了相当完善的方法来保存对象,我们可以用它们来解决很多的问题。
List可以将元素维护在特定的序列中,List接口在Collection的基础上添加了大量的方法,使得可以在List中间插入和移除元素。
本人过去也写过和List数据结构相关的博客。欢迎大家可以点进去看~嘿嘿
线性表的基本实现和概念
顺序表的操作
双链表的操作
栈和队列的基本概念
顺序栈和链栈
使用自定义的栈来优化二叉树的遍历
基本的顺序存储 : ArrayList。从名字就可以看出,这是一个数组的List,底层实现的则是动态数组,数据结构类似顺序表。底层原理类似前面说过的StringBuilder,但也不太一样。ArrayList的优点就是随机访问效率高。但是中间插入和删除则就有点慢了。在"顺序表的操作"那篇博客里说过具体原因。
链式数据存储:LinkedList。它的优点就是插入和删除的效率高,但是访问中间任何一个元素都需要遍历,所以随机访问效率不如ArrayList。在线性表的基本实现和概念里面有具体原因。
ArrayList底层是一个动态数组,在初次new它的时候,会构建一个大小为10的空列表。
public Arst() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
当ArrayList的数组容量不够用的时候,则会对数组的长度进行扩容。和StringBuilder不同的是,StringBuilder每次扩容的量是当前长度的一倍+2。而ArrayList则是扩容当前长度的0.5倍。所以,在资源上,ArrayList每次多出的容量会比StringBuilder小。ArrayList的构造方法还可以手动设置初始大小。如果用户在使用ArrayList之前就已经知道最少要存储多少数据,则可以将初始长度设置到最小值,这样可以减少扩容的次数,进一步微妙的提升效率。
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
ArrayList的add方法既可以直接在尾部添加元素,也可以根据索引来添加元素。其过程则是先判断容量是否够用,不够用则扩容,够用则直接在数组的索引位置后面的一个位置添加进去。ArrayList和StringBuilder一样,数组的长度并不等于容器的长度。当要根据索引来添加元素的时候,可能有人会想到循环,这样做也可以。但是为了效率则可以使用System.arraycopy在指定的位置腾出一个空间来存储需要存储的值。
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
remove方法既可以根据索引来删除元素,也可以根据对象来删除元素。当使用索引删除元素的时候则会将索引后面的元素向左移动覆盖掉它。而当根据对象删除元素的时候,它则会删除第一个和那个对象相同的元素。也就是说,假如我要删除元素'A'。即使这个容器里有多个'A',但最终也只会删除第一个,其工作原理则是利用循环找到索引,然后再采用索引删除的方案。因此效率上,对象删除要低于索引删除。
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null;
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的get方法就没什么好说的了,因为ArrayList底层是数组,只要索引合法,直接返回数组即可。
E elementData(int index) {
return (E) elementData[index];
}
这个方法主要就是使用循环进行遍历然后对比,根据对象找到第一个索引
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
LinkedList则是链式存储结果,其底层采用的是双向链表。链表的结点可以散落在内存中的任意位置,且不需要一次性划分所有结点所需的空间给链表,而是根据需求临时划分。因此,链表支持存储空间的动态分配。我在"线性表的基本概念"那篇博客有介绍。因此初次new一个LinkedList对象的时候,和ArrayList不同。初次new对象的时候LinkedList是个空的。
和ArrayList一样。add方法既可以直接在尾部添加元素,也可以根据索引来添加元素。根据索引添加元素的时候,不再像数组一样需要找到索引然后去腾出一个位置,而是直接改变指针的指向即可。可能对于学过C语言的人来说会更加容易理解一点。当索引添加的时候,索引的位置刚好是最后一个,则会调用尾部添加的方案。如果不是,则找到那个索引的前一个位置,加入到前一个位置的后一个位置即可。
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
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;
}
}
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
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;
size++;
modCount++;
}
在LinkedList中remove会有一个无参数的方案,那个方案则是直接删除头一个元素。由于链表删除的原理是将元素的前一个的指针指向元素后一个指针,(我在单链表的操作中提到过)然后夹在中间的则被释放掉实现,可当我要删除头元素的时候,无法做到这种操作所以Java中则会采取另一种解决方案。至于对象删除元素,则也是先循环找到索引,再采取索引删除元素的解决方案。
private E unlinkFirst(Node<E> f) {
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null;
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
E unlink(Node<E> x) {
// assert x != null;
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;
}
x.item = null;
size--;
modCount++;
return element;
}
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;
}
链表的优点就是在添加和删除的时候,不需要遍历只需要改变指针的指向就可以实现,但是缺点就是如果我要随机访问一个元素,就必须要循环遍历了。在这方面效率要低于ArrayList
Node<E> node(int 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;
}
}
Stack则是继承了Vector。而Vector也是List接口下的一个实现类,其原理和ArrayList差不多都是动态数组。区别就在于Vector的线程比较同步,而ArrayList则不同步。因此Vector属于线程安全但效率低,而ArrayList线程不安全但效率高。在使用上Vector几乎不用。
Stack也就是栈。栈是一种只能在一端进行插入或删除的线性表。其中,允许插入或删除的一端为栈顶(TOP)。栈顶由一个称为栈顶指针的位置指示器来指示。它是动态变化的。表的另一端为栈底,栈底固定不变。栈的插入和删除操作一般称为入栈和出栈。由栈的定义可以看出,栈的主要特点就是先进后出。
在过去数据结构的学习当中就曾多次使用过栈。当我需要二叉树遍历的时候,使用栈遍历二叉树会比递归效率好得多。当我需要实现链表反转的时候,栈的先进后出特性可以得到非常简便的效果。
事实上,栈其实就是受限制的线性表。以下三篇文章则是栈的基本概念和实现,这里就不重复了。
栈和队列的基本概念
顺序栈和链栈
使用自定义的栈来优化二叉树的遍历