一文搞懂双向链表

1,839 阅读6分钟

一. 链表的概念

链表和数组一样也是数据结构之一,只不过因为链表中的数据“链接”在一起所以叫链表。如图:

image.png

二. 单向链表

单向链表:只有一个方向的链表,比如前面的元素指向后面的元素(一般使用这种),即只能前一个数据指向后一个数据。想要查询某个数据时,必须知道它前面的数据,也就是只能顺序查找。

三. 双向链表

双向链表:有两个方向的链表,即前一个元素指向后一个元素,后一个元素也指向前一个元素。就像这样

image.png 双向链表的每个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 数组如何操作删除

上面我们提到,链表的优势在于对数据的插入和删除,这是为什么呢?大家先回想一下对于一个数组,如果要删除一个数据,很容易想到可以利用后面的元素依次向前移动一位,达到删除数据的效果。如图

image.png

比如我们要删除数据10,那么11,5,2就要依次向前移动一位。这是极其不方便的,因为我只想删除10,但是10后面所有的数据都移动了,如果数据量大呢,那么牵一发而动全身,这样效率是极其低下的。

6.2 双向链表如何操作删除

双向链表中每个结点都维护了两个指针,分别指向下一个结点和上一个结点,数据之间也是通过这两个指针来找到彼此的,往这个方向考虑删除只需要这样操作:把与10有关的指针删掉,8的next指向11,11的prev指向8。就算数据量再大也只需要改变10相邻的数据指针,优雅的处理删除。再把10的nextprev置为null,帮助GC回收。

image.png

6.3 数组如何插入数据

插入前首先要判断容量是否充足,不充足则无法插入;当然也可以像ArrayList那样写个扩容算法,那就比较麻烦了。如果充足,被插入的元素就要向后移动一位,后续的元素也要依次后移。就像排队打饭,这个时候有个人插队,那么后面的人都需要后移一位。

image.png 如上图,数据666要插入到8和10之间,那么从10开始往后的数据都必须往后挪给666让位。

6.4 双向链表如何插入数据

双向链表可以直接插入数据,不需要判断容量是否充足。链表不像数组需要在内存中开辟一块连续的空间,它的每个数据可以在内存的任意一个地方,只要通过nextprev告诉该数据它的后一个数据,前一个数据在哪就行,理论上链表长度可以无限长,只要内存够。

还是插入666,只需要将8的next指向666,666的prev指向8,8和666就链接起来了。同理把666的next指向10,10的prev指向666,666和10就链接起来了。这样就成功插入了一个数据。

image.png

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. 数组和链表该如何选择

两种数据结构各有各的优势,数组在查询修改方面表现突出,链表在插入删除方面表现优秀,使用哪种数据结构还是要看具体的业务逻辑。比如有个需求要帮助老年人用药,那么就需要建立药品信息,这个情境下自然是采用数组好,因为查询药品信息远比插入删除药品信息的需求大。而做一个购物车的业务时,当然是链表好,因为购物车的物品流动性强,对插入和删除的需求更大。