链表
相对于数组(动态)而言,链表是真正的动态数据结构。 数据存在节点中,各个节点通过指针连接。
与数组对比
链表不需要处理扩容缩容,各个节点的空间很分散,通过指针连接, 因此失去了随机访问(通过索引访问)的能力。
而数组,每次添加元素开辟的空间是相邻的,所以可以通过索引访问,但是不是真的动态。
虚拟头节点
在链表添加元素时,我们需要判断链表是否为空,如果为空,则直接创建一个新的节点作为头节点,如果不为空,就需要头节点添加next元素。其实我们一直判断的是否存在头节点,既然这样,我们可以通过创建链表的同时,创建一个虚拟的头节点,它只是为了统一头节点特殊的代码逻辑。有了虚拟头节点,不管是什么时候,我们都可以直接执行head.next 添加元素了。
删除元素
删除元素时,我们需要拿到当前的头节点,然后一直next,直到我们想要删除的元素的上一个元素,用这个元素的next 直接连接 这个元素的next的next。
删除2这个元素时,我们通过next 遍历到 “2”的上一个元素 “1”,让“1”的next 指向想要删除元素“2”的下一个元素“3”,就实现了删除“2”这个元素。
Node cur = dummyHead;
// 这里的 判断是伪代码
while(cur.next.e != delE){
cur = cur.next;
}
//现在 cur就是将要删除的前一个元素
Node delN = cur.next; // delN就是删除的元素
cur.next = delN.next // 将删除元素的前一个元素的next绕过删除的元素指向删除元素的next
delN.next = null; //处理下删除元素的next 我们返回的是这个元素的值
size--; // 链表的长度别忘了 -1
return delN.e; // 返回删除元素的值
时间复杂度
增加元素:
addFirst:O(1)
addLast:O(n) 添加到尾部,需要遍历到尾部
add(index):O(n/2)->O(n)
删除元素:
delFirst:O(1)
delLast:O(n) 删除尾部,需要遍历到尾部
del(index):O(n/2)->O(n)
修改元素:
setFirst:O(1)
setLast:O(n) 修改尾部,需要遍历到尾部
set(index):O(n/2)->O(n)
查找元素:同上
链表实现栈
public class LinkedListStack<E> implements Stack<E> {
private LinkedList<E> l;
LinkedListStack(){
this.l = new LinkedList<E>();
}
@Override
public int getSize() {
return l.getSize();
}
@Override
public boolean isEmpty() {
return l.isEmpty();
}
@Override
public void push(E e) {
l.addFirst(e);
}
@Override
public E pop() {
return l.removeFirst();
}
@Override
public E peek() {
return l.getFirst();
}
}
尾指针链表实现队列
栈是栈顶进栈顶出。
队列是队尾进队首出。
栈只涉及到头部的操作,所以是O(1)复杂度。
队列,头部可以添加可以删除,访问尾部势必要遍历到最后,所有是O(n)复杂度。
既然这样,和数组实现循环队列一样,我们需要给链表的尾部元素也添加一个指针,这样可以直接获取到尾部元素执行添加元素。
头部可以删除可以添加,尾部可以添加,这样就可以实现队列。
@Override
public void enqueue(E e) {
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("Queue is empty. dequeue failed!");
}
Node h = head;
head = head.next;
h.next = null;
if(head == null){
tail = null;
}
size--;
return h.e;
}