关注公众号:EZ大数据(ID:EZ_DATA)。每天进步一点点,感觉很爽!
承接上文,今天我们来用链表实现栈和队列。
先看栈,我们使用链表的头部作为栈顶,链表的尾部作为栈底。实现比较简单,直接上代码:
@Override
public void push(E e){
list.addFirst(e);
}
@Override
public E pop(){
return list.removeFirst();
}
@Override
public E peek(){
return list.getFirst();
}
代码基于LinkedList来实现,那么getSize(),isEmpty()等的操作就不提了。
接下来,我们来测试下基于数组实现的栈和基于链表实现的栈的区别:
public class CompareStack {
// 测试使用stack运行opCount个push和pop操作所需的时间,单位:秒
private static double testStack(Stack<Integer> stack, int opCount){
long startTime = System.nanoTime();
Random random = new Random();
for (int i = 0; i< opCount; i++){
stack.push(random.nextInt(Integer.MAX_VALUE));
}
for (int i = 0; i<opCount; i++){
stack.pop();
}
long endTime = System.nanoTime();
return (endTime - startTime) / 1000000000.0;
}
public static void main(String[] args) {
int opCount = 10000000;
ArrayStack<Integer> arrayStack = new ArrayStack<>();
double time1 = testStack(arrayStack, opCount);
System.out.println("arrayStack, time: " + time1 + "s");
LinkedListStack<Integer> linkedListStack = new LinkedListStack<>();
double time2 = testStack(linkedListStack, opCount);
System.out.println("linkedListStack, time: " + time2 + "s");
}
}
我们随机在栈中插入1000000个数字,并取出元素,那么测试结果如下:
arrayStack, time: 0.4852778s2
linkedListStack, time: 2.5215067s
其实这个时间比较很复杂,因为LinkedListStack中包含更多的new操作(node),需要不停的在内存中寻找地方开辟空间,当数量越多,耗时越长。实际上不管是arrayStack还是linkedListStack在我们的测试中,他们的时间复杂度都是一样的。
现在我们来看用链表来实现队列。
对于链表来说,头部节点head的操作是O(1),如果想实现队列,那么我们需要定义个尾部节点tail,记录最后一个元素的位置。同时,上篇文章我们说过链表的头部节点添加元素还是删除元素都是O(1),那么我们如何定义队首队尾的位置呢?
如上图所示,我们使用tail来记录链表尾部元素位置,此时我们在tail端添加元素就变为O(1),但是当我们删除元素时,因为必须知道前一个元素的节点,所以需要遍历整个链表,此时就变的比较复杂。而我们在head端,无论是添加元素还是删除元素,时间复杂度都是O(1),那么我们就可以把head端作为队首出队,把tail端作为队尾入队,也就是从head端删除元素,从tail端插入元素。
同时呢,由于没有dummyhead,那么需要注意链表为空时的情况,此时head和tail都指向同一位置。
下面我们来用链表实现队列。
private Node head, tail;
@Override
public void enqueue(E e){
// 当tail为空时,也就意味着head也为空
if(tail == null){
tail = new Node(e);
head = tail;
}
else {
tail.next = new Node(e);
tail = tail.next;
}
size++;
}
@Override
public E dequeue(){
if (isEmpty()){
throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
}
Node retNode = head;
head = head.next;
retNode.next = null;
// 特殊情况:链表中只有一个元素,若删除,则为空
if (head == null){
tail = null;
}
size--;
return retNode.e;
}
@Override
public E getFront(){
if (isEmpty()){
throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
}
return head.e;
}
队列实现完毕,我们来测试一波arrayQueue,loopQueue和linkedListQueue的运行效率。测试代码如下:
// 测试使用q运行opCount个enqueue和dequeue操作所需的时间,单位:秒
private static double testQueue(Queue<Integer> q, int opCount){
long startTime = System.nanoTime();
Random random = new Random();
for (int i = 0; i< opCount; i++){
q.enqueue(random.nextInt(Integer.MAX_VALUE));
}
for (int i = 0; i<opCount; i++){
q.dequeue();
}
long endTime = System.nanoTime();
return (endTime - startTime) / 1000000000.0;
}
我们在队列中插入100000个数字,并取出。测试时间如下:
ArrayQueue, time: 2.8499096s
LoopQueue, time: 0.0099262s
linkedListQueue, time: 0.0128844s
由于arrayQueue出队操作时间复杂度是O(n),而loopQueue和linkedListQueue时间复杂度都是O(1),所以arrayQueue的时间消耗最多,然后linkedListQueue是基于链表来实现的,在操作的过程中,需要new出很多节点,加之各种原因(系统因素,元素数量等)的影响,所以时间会略有不同。
好了,今天我们用链表来实现了栈和队列,也算是巩固了之前学的知识。经过这几天的总结,相信大家对于数组、栈、队列、链表的认识更加深刻。后续呢,我们会尽可能每天刷点题,提升自己的代码能力。
OK,拜了个拜~