Why do we need LinkedList?
Java的Collection当中有ArrayList作为可变(表面上可变)长的数组,然而其底层也是利用数组实现的。所以,存在两个明显的短板。
1)当增加元素填满了底层数组的时候,底层数组需要resize,这时候需要创建一个1.5x长度的数组,然后复制旧数组的内容到新的数组,耗费许多时间;
2)当删除某个index的元素时,由于底层数组不能有hole,导致每次删除时都需要元素移位。同时,由于这个原因,ArrayList一旦remove某个index的元素后,之后的所有元素的坐标都会往前移。
那么LinkedList有什么好处呢?
1)空间效率:由于ArrayList的底层数组是一开始就创建好的,所以当没填满时,多余的空间是浪费的;而链表只需要在需要的时候重新创建多一个Node即可。
2)添加效率:没有resize的消耗;
3)删除效率:删除元素不需要移位。
LinkedList的坏处:
1)随机读取某个index的时候,必须要顺序读取其前面所有的节点。
2)每个存储单元(节点)需要额外的空间存储指向下一个节点的指针。
LinkedList的基础实现代码:
package datastruct.firehotest.net.csdn.blog;
import java.util.Iterator;
import java.util.NoSuchElementException;
public class LinkedListImp<AnyType> implements Iterable<AnyType> {
/*only need to maintain a head*/
private Node<AnyType> head;
/*Inner class node*/
private static class Node<AnyType> {
public AnyType value;
public Node<AnyType> next;
public Node(AnyType input,Node<AnyType> n) {
value = input;
next = n;
}
}
public LinkedListImp (){
head = null;
}
/**
* implemented Iterable because it is a data structure
*/
@Override
public LListIterator iterator() {
return new LListIterator();
}
private class LListIterator implements Iterator<AnyType> {
private Node<AnyType> nextNode;
public LListIterator () {
nextNode = head;
}
@Override
public boolean hasNext() {
return (nextNode != null);
}
@Override
public AnyType next() {
if (nextNode == null) {
throw new NoSuchElementException();
}
AnyType returnVal = nextNode.value;
nextNode = nextNode.next;
return returnVal;
}
}
/**
* Need to implement addFirst addLast insertAfter insertBefore remove
*
*/
public void addFirst(AnyType ele) {
head = new Node<AnyType>(ele,head);
}
public void addLast(AnyType ele) {
if (head == null) {
addFirst(ele);
}
Node<AnyType> tmp = this.head;
while(tmp.next != null) {
tmp = tmp.next;
}
tmp.next = new Node<AnyType>(ele,null);
}
public boolean insertAfter(AnyType key,AnyType ele) {
if (head == null) {
return false;
}
Node<AnyType> tmp = this.head;
while(tmp != null && !(tmp.value.equals(key))) {
tmp = tmp.next;
}
if (tmp == null) {
return false;
}
tmp.next = new Node<AnyType>(ele,tmp.next);
return true;
}
public boolean insertBefore(AnyType key,AnyType ele) {
if (head == null) {
return false;
}
//这里漏了考虑,凡是涉及到前后的双指针的题目都需要考虑只有一个元素的极端情况
if (head.value.equals(ele)) {
addFirst(ele);
}
Node<AnyType> prev = null;
Node<AnyType> cur = head;
while(cur != null && !(cur.value.equals(key))){
prev = cur;
cur = cur.next;
}
if (cur == null) {
return false;
}
prev.next = new Node<AnyType>(ele,prev.next);
return true;
}
public boolean remove(AnyType ele) {
if (head == null) {
return false;
}
if (head.value.equals(ele)) {
head = head.next;
return true;
}
Node<AnyType> prev = null;
Node<AnyType> cur = head;
while(cur != null && !(cur.value.equals(ele))){
prev = cur;
cur = cur.next;
}
if (cur == null) {
return false;
}
prev.next = cur.next;
return true;
}
public static void main(String[] args) {
}
}
实现的过程中有以下问题:
1)记得涉及的泛型的自定义类且非实现具备泛型的接口的自定义类,需要在声明的时候声明泛型。内部定义构造函数的时候不需要写上泛型的尖括号。但是,在类外用到这个类型的时候,包括通过new调用构造函数,时刻记得加上尖括号。
2)当这个自定义类的实现具有泛型的接口的时候,把尖括号放在继承的接口那里,而自定义类的类名在定义的时候或者外部调用的时候都不需要加尖括号。
3)当用到了prev 和 cur这种两个指针的题目,需要考虑链表只有一个元素的时候,prev是Null的情况。此时,需要处理这一extreme cases.