一. 链表的概念
链表和数组一样也是数据结构之一,只不过因为链表中的数据“链接”在一起所以叫链表。如图:
二. 单向链表
单向链表:只有一个方向的链表,比如前面的元素指向后面的元素(一般使用这种),即只能前一个数据指向后一个数据。想要查询某个数据时,必须知道它前面的数据,也就是只能顺序查找。
三. 双向链表
双向链表:有两个方向的链表,即前一个元素指向后一个元素,后一个元素也指向前一个元素。就像这样
双向链表的每个
Node结点由next,item,prev三部分组成,其中next指向下一个结点,prev指向前一个结点,数据保存在item中,多个Node结点相互链接在一起就构成了一条双向链表,其中first头结点一般不存储数据,用于指向第一个存储数据的结点,last称为尾结点,也不存储数据,用于指向最后一个结点。
四. 链表的优势
链表的优势在于增加数据,和删除数据。而双向链表比起单向链表的优势在于在某些情况下查询更快。比如查询最后一个结点时单向链表需要依次遍历前面n个结点才能访问到最后一个结点;而双向链表可以通过last结点直接访问到最后一个结点。当然这种效率的优势是通过消耗更多的空间来换取的,因为双向链表的每个结点都比单向链表多维护了一个prev对象指向前一个结点。
五. 代码实现
public class DoublyLinkedList {
public static void main(String[] args) {
//定义头尾结点
Node first = new Node();
Node last = new Node();
//定义3个数据结点,这里它们还没串起来
Node node01 = new Node(1);
Node node02 = new Node(2);
Node node03 = new Node(3);
//先从前往后串起来,可以结合上面的双链表图,理解此处代码
first.next = node01;
node01.next = node02;
node02.next = node03;
node3.next = last;
//再从后往前串起来
last.prev = node03;
node03.prev = node02;
node02.prev = node01;
node01.prev = first
//顺序遍历
while (first.next.item != null) {
first = first.next;
System.out.println(first.item);
}
//倒叙遍历
while(last.prev.item != null){
last = last.prev;
System.out.println(last.item);
}
}
}
//定义结点
class Node{
Object item;
Node prev;
Node next;
public Node(){}
public Node(Object item){
this.item = item;
}
}
六. 向链表插入、删除数据
6.1 数组如何操作删除
上面我们提到,链表的优势在于对数据的插入和删除,这是为什么呢?大家先回想一下对于一个数组,如果要删除一个数据,很容易想到可以利用后面的元素依次向前移动一位,达到删除数据的效果。如图
比如我们要删除数据10,那么11,5,2就要依次向前移动一位。这是极其不方便的,因为我只想删除10,但是10后面所有的数据都移动了,如果数据量大呢,那么牵一发而动全身,这样效率是极其低下的。
6.2 双向链表如何操作删除
双向链表中每个结点都维护了两个指针,分别指向下一个结点和上一个结点,数据之间也是通过这两个指针来找到彼此的,往这个方向考虑删除只需要这样操作:把与10有关的指针删掉,8的next指向11,11的prev指向8。就算数据量再大也只需要改变10相邻的数据指针,优雅的处理删除。再把10的next和prev置为null,帮助GC回收。
6.3 数组如何插入数据
插入前首先要判断容量是否充足,不充足则无法插入;当然也可以像ArrayList那样写个扩容算法,那就比较麻烦了。如果充足,被插入的元素就要向后移动一位,后续的元素也要依次后移。就像排队打饭,这个时候有个人插队,那么后面的人都需要后移一位。
如上图,数据666要插入到8和10之间,那么从10开始往后的数据都必须往后挪给666让位。
6.4 双向链表如何插入数据
双向链表可以直接插入数据,不需要判断容量是否充足。链表不像数组需要在内存中开辟一块连续的空间,它的每个数据可以在内存的任意一个地方,只要通过next和prev告诉该数据它的后一个数据,前一个数据在哪就行,理论上链表长度可以无限长,只要内存够。
还是插入666,只需要将8的next指向666,666的prev指向8,8和666就链接起来了。同理把666的next指向10,10的prev指向666,666和10就链接起来了。这样就成功插入了一个数据。
7. 链表的缺点
7.1 如何遍历链表
既然是存储数据的数据结构,那么我们需要用数据的时候要如何取呢?链表不像数组那样可以直接通过下标获取数据,前面说了链表不需要开辟一块连续的内存空间,因此不存在一组连续的下标对链表数据进行访问。所以获取链表就需要遍历了。
//顺序遍历
while (first.next.item != null) {
first = first.next;
System.out.println(first.item);
}
//倒叙遍历
while(last.prev.item != null){
last = last.prev;
System.out.println(last.item);
}
7.2 查询和修改效率低
前面我们知道了链表的优势是插入和删除,人无完人,链表也一样。它的缺点就是查询和修改,修改很好理解,因为要修改必须先查询要修改的数据。那为什么查询的效率低呢?因为链表不能随机访问,要访问某个元素必须先访问它的前一个元素。
8. 数组和链表该如何选择
两种数据结构各有各的优势,数组在查询修改方面表现突出,链表在插入删除方面表现优秀,使用哪种数据结构还是要看具体的业务逻辑。比如有个需求要帮助老年人用药,那么就需要建立药品信息,这个情境下自然是采用数组好,因为查询药品信息远比插入删除药品信息的需求大。而做一个购物车的业务时,当然是链表好,因为购物车的物品流动性强,对插入和删除的需求更大。