第二章 数组、链表、栈和队列(基础)
表、栈、队列都属于线性的数据结构
这一章是基于数组及对数组的优化来讲。
一、普通数组
int[] arr = new int[10];
这是一个普通数组的基本结构,也是java语言中最基础的数据结构。数组也分为一维数组和多维数组。我们这次主要来讲一维数组。
1、解决数据存储的问题。它是带有一组操作对象的集合。(由于java是强类型的,所以数组中只能存同一类型的元素值)
2、它能通过索引进行快速定位。(这是最大的优点。可以把查找的时间复杂度O(1)常数级别)
3、容量必须在初始化时定义好,超过容量则数组越界。
4、除非在数组尾添加和删除元素,否则对数组添加、删除元素的成本过高。因为要进行元素移动。
例子:
对于数组不再多说,工作中常用到。下面来结合java标准库说下对于普通数组的优化。
二、动态数组ArrayList
由于普通数组初始化时必须指定容量大小,如果我们事先无法准确的预估数组元素的大小,这样程序执行过程中,可能会导致数组容量不够用的问题。这时我们需要使用动态数据。也就是说当数组元素的个数等于数组容量的时候进行扩容。
1、扩容(添加元素)
最后的结果:
当capacity已经满了时候,就调用扩容,这里扩容2倍。
java的arrayList默认是1.5倍。
2、缩容(删除元素)
其实这样写太过于简单粗暴,因为二分之一是一个临界值,如果你缩容完之后,马上又添加一个元素,这时候还需要进行扩容。最好的办法就是让这个临界值比起到比较的作用。
if (size == data.length /4){
resize(data.length /2)
}
3、复杂度分析
查询元素:O(1)
修改元素:O(1)
添加元素:尾节点O(1),其它位置O(n)
删除元素:尾节点O(1),其它位置O(n)
三、链表
链表基于Node节点的数据结构,node节点是自定义的。
也就是说链表和数组是一个级别的。都是最底层的结构
数组:静态的数据结构
链表:动态的数据结构(指在运行时刻才能确定所需内存空间大小的数据结构)
1、链表解决的是数组添加和删除元素效率的问题。
2、但是链表牺牲了数组可以索引查找的高效
对于java标准库来说,基于双向链表且有虚拟头和尾节点实现的。
1、单链表和双链表的区别
单链表结构中仅包含下一个节点。
双链表结构中包含上一个节点和下一个节点。
优缺点:
提高了添加和删除元素的效率。
但增加了前一个节点地址的维护成本。
2、虚拟头节点和虚拟尾节点
虚拟尾节点对于链表来说是有很大意义的,由于链表不能通过索引快速定位最后一个元素,假如我们需要在链表尾进行查询、添加、修改、删除元素,这时虚拟尾节点可以帮助我们快速定位。
虚拟头节点的作用更大程度是是为了简单程序代码的实现。方便对头节点的添加和删除而已。
上面是一个添加节点的代码,可以看出,对于添加节点分两种不同的情况,在头节点添加还是在中间节点添加。如果有了虚拟头节点,那么就变成了一种情况。所有的添加都是在中间节点操作。
3、代码实现
1)定义Node节点
public class Node<T> {
public T value;
public Node<T> prev;
public Node<T> next;
public Node(T value, Node<T> prev, Node<T> next) {
this.value = value;
this.prev = prev;
this.next = next;
}
}
2)初始化链表
private Node<T> beginMarker;
private Node<T> endMarker;
private int size;
public MyLinkedList() {
beginMarker = new Node<T>(null, null, null);
endMarker = new Node<T>(null, beginMarker, null);
beginMarker.next = endMarker;
this.size = 0;
}
3)查询节点
public Node<T> getNode(int idx) {
Node<T> currentNode = beginMarker.next;
for (int i = 0; i < idx; i++) {
currentNode = currentNode.next;
}
return currentNode;
}
4)添加节点
public void add(int idx, T value) {
if (idx < 0 || idx > size) {
throw new IllegalArgumentException("索引越界");
}
Node currentNode = getNode(idx);
Node<T> newNode = new Node<T>(value, currentNode.prev, currentNode);
newNode.prev.next = newNode;
currentNode.prev = newNode;
size++;
}
5、删除节点
public void remove(int idx) {
if (idx < 0 || idx >= size) {
throw new IllegalArgumentException("索引越界");
}
Node currentNode = getNode(idx);
currentNode.prev.next = currentNode.next;
currentNode.next.prev = currentNode.prev;
size--;
}
4、复杂度分析:
查询元素:头尾节点O(1)、中间节点O(n)
修改元素:头尾节点O(1)、中间节点O(n)
添加元素:头尾节点O(1)、中间节点O(n)
删除元素:头尾节点O(1)、中间节点O(n)
三、栈(O(1))
根据栈的结构我们可以看出,它主要解决的对数据结构的一端进行查询、添加、删除元素的操作。解决了快速定位的问题。所以说我们实现的ArrayList、LinkedList都可以做为栈的实现。
- arrayList不用说,这能通过索引快速定位,但必须把数组尾作为栈顶进行操作,才能使得时间复杂度都在O(1)(限制条件)。
- linkedList,我们实现的是带虚拟头尾节点的双向链表,所以把链表头或链表尾当作栈顶都可以。时间复杂度一样。
- 在java标准库中 public class Stack<E> extends Vector<E> 可以看出是基于Vector 实现的,而 Vector底层是数组
1、 栈的基本实现
2、代码实现
这是基于动态数组。实现起来还是比较简单的。
四、队列(O(1))
1、它是一种后进先出的数据结构
2、只能是一端入队在另一端出队。
3、不能对队中元素进行任何修改(但后期会说到优先队列)
我们现在实现的ArrayList和 LinkedList都可以做为队列的实现但是实现方式也不同:
1、ArrayList实现,由于Queue要求从一端入队,从另一端出队。对于数组实现是在数组尾入队,在数据头出队。但是出队的时候就会有问题,删除数组头元素是不是后面的元素都要进行上移。最好的方式就是作循环列队,不进行删除,重复使用
2、使用LinkedList实现,这种是最理想的实现,我们的LinkedList是带有虚拟头尾节点的双向链表,在链表头尾添加和删除的时间复杂度都是O(1)。
3、java标准库中Queue的实现是LinkedList。
代码实现:
public synchronized T push(T value) {
super.add(0, value);
return value;
}
public synchronized T pop() {
T value = peek();
super.remove(0);
return value;
}
public synchronized T peek() {
int size = super.size();
if (size == 0) {
throw new EmptyStackException();
}
return super.get(0);
}
五、总结
1、 ArrayList动态扩容是对普通数组扩容的优化。(查询快、添加和删除慢)
2、 LinkedList是对数组添加和删除元素效率的提升,但失去了索引查找的特性(添加和删除快、查询慢)
3、Stack是基于数组实现的,解决的是后进先出的问题。它的操作局限在栈顶元素。(操作单一)
4、Queue是基于LinkedList实现的。解决的是先进先出排队的问题。只能在队尾添加,队首删除。(操作单一)