一、什么是链表
先从数据结构上看。数组需要一块连续的内存存储,但是,如果存储100M的数据,内存中没有连续的足够打的存储空间时,数组就不适合了。
链表正好相反,他不需要连续的内存空间,通过指针将零散的内存块串起来使用。
链表结构五花八门,我们只介绍最常用的三种。单链表、双向链表、循环连表。
二、单链表
刚刚说道,连表是通过指针将一组零散的内存串联在一起。我们把内存块称为链表的节点(node)。为了将节点串联起来,每个节点除了存储数据外,还要存储一个下一个节点的地址。我们把记录下个节点地址的指针叫做后继指针(next)。
图上有2个节点是比较特殊的,第一个和最后一个。第一个叫做头节点,最后一个叫做尾节点。头结点用来记录链表的基地址也就是起始地址。尾节点的next指向的是null。通过这个可以判断是否是链表的最后一个节点。
1.插入删除
和数组一样,链表也支持数据的插入,删除、查找。
数组中,为了保持数据的连续性,插入删除一个数据,需要做大量的数据迁移,时间复杂度是O(n)。但是链表不同,他本身就不是连续的,插入删除是非常快的。
2. 链表的查询
有利就有弊,要想随机访问第k个元素,就没有数组那么容易了。因为他不是连续内存存储,无法向数组那样计算出第k个数据的内存地址。连表需要从头遍历,直到找到相应的数据。
3. 单链表代码实现
/**
* 链表
*/
public class LinkedList {
//头
private Node head;
//尾
private Node tail;
private int size;
public LinkedList(){
this.size = 0;
}
public void addFirst(int data){
Node node = new Node(data);
node.next = this.head;
this.head = node;
this.size ++;
}
public void addLast(int data){
add(data, this.size-1);
}
public void add(int data, int index){
if(index < 0 || index >= size){
throw new IllegalArgumentException("index error");
}
if(index == 0){
addFirst(data);
return;
}
Node pre = this.head;
for(int i =0;i<index;++i){
pre = pre.next;
}
Node node = new Node(data);
node.next = pre.next;
pre.next = node;
this.size++;
}
public int get(int index){
if(index >= size){
return -1;
}
int i =0;
Node find = head;
while(i < index){
++i;
find = find.next;
}
return find.data;
}
private static class Node{
int data;
Node next;
Node(int data){
this.data = data;
}
}
}
三、循环列表
循环列表是一种特殊的单列表。和单列表唯一的区别就是,循环列表的尾节点指向头节点,而单列表的尾节点是指向null的。
和单链表相比,循环链表的优点是从链表尾到链表头比较方便。需要处理的问题具有环形结构特点时,就适合使用循环链表。比如著名的约瑟夫问题。
四、双向链表
双向链表,顾名思义,它有2个方向,每个节点不仅有一个后继指针next指向下一个节点,还有一个前驱指针prev指向上一个节点。
从图中可以看到,双向链表需要额外的2个空间来存储prev和next指针。虽然占用内存多,单支持双向遍历。
1.优点
删除操作
- 删除链表中等于给定值的节点
- 删除给定指针指向的节点
第一种情况, 单链表和双向链表查找节点都需要遍历,复杂度是O(n), 删除节点复杂度都是O(1)。
对于第二种情况,我们拿到了要删除的节点,但是要删除的话得知道他的前驱节点。因此对于双向链表来说,时间复杂度还是O(1)。但是对于单链表来说,需要从头遍历,指导p->next = q,证明p是q的前驱节点,时间复杂度是O(n)。
其他操作
同理,我们需要在指定节点前面插入一个节点。
或者对于一个有序的链表(比如从小到大排),要查找某个具体的值,我们可以记录上次查询的位置,然后与上次的值比较来决定向前还是向后查找,也会比单链表快。
2. 总结
在实际开发中,尽管双向链表占用内存更多,但比单链表应用更加广泛,比如Java中的LinkedHashMap就用到了双向链表。
这还有一个重要的知识点,空间换时间,当内存充足,追求速度的时候,就可以使用空间复杂度高时间复杂度低的方式。