五、你真的懂链表吗?

162 阅读4分钟

关注公众号:EZ大数据(ID:EZ_DATA)。每天进步一点点,感觉很爽!

今天心情莫名的低调,可能是因为太帅了吧。

为了调整一波颓废的心态,我觉得还是学习是最好的良方。那就从链表开始……

那么今日目标有哪些呢?认识链表、链表的增删查改、链表来实现栈和队列。好,那就开始上正餐。

链表是真正的动态数据结构,我们前几天总结的动态数组、栈、队列底层依托于静态数组来实现,靠resize解决固定容量问题。

对于链表来说,是通过节点来装载元素,并通过节点与节点间连接起来的数据结构。如果我们想访问其中所有的元素,我们需要记录链表的头节点head。此外,链表中数据存储在“节点”Node中,其优点:真正的动态,不需要处理固定容量的问题,需要存储多少数据,就可以开辟响应的空间。同时缺点是:丧失了随机访问的能力。

那么数组和链表相比,二者有什么不同呢?对于数组来说,最好用于索引有语意的情况,优点:支持快速查询。相反,链表不适合用于索引有语意的情况,但链表的优点是:动态。

说了这么多理论,是不是有点小困,接下来我们来coding,增删查改操作一波。

现在我们来看链表中添加元素。首先在链表头部添加元素,原理是这样的:新添加的元素节点node.next=head当前的位置,然后再改变head的指向,head=node,这样的话,head就指向新添加的元素。具体示意图如下:

代码如下:

    // 在链表头部添加新的元素e
    public void addFirst(E e) {
        Node node = new Node(e);
        node.next = head;
        head = node;
        head = new Node(e, head);
        size++;
        add(0, e);
    }

接下来呢,我们尝试链表中间添加元素,这个操作的关键是找到待添加的节点的前一个节点,我们用prev来表示。具体的原理就是,新添加的元素节点node.next=prev.next,然后再改变prev节点的指向,prev.next=node,这样就实现了中间插入元素。具体示意图如下:

此时,就有一个疑问,如果我们插入头部元素,那么头部节点head之前的节点就没有。那么,有什么方法可以通用呢?

答案就是我们可以虚拟个链表的头结点dummyhead。我们设置虚拟节点dummyhead,该节点不存储任何元素。示意图如下:

对于用户来说,dummyhead不存在,dummyhead.next=head节点。没有任何实际意义,只是为了我们编写逻辑方便。那么此时,我们就不需要对头结点添加元素进行特殊处理,只需要使用dummydead就可以。那么此时链表添加元素的代码,就可以写成:

    // 在链表的index(0-based)位置添加新的元素e
    // 在链表中不是一个常用的操作
    public void add(int index, E e) {
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Add failed. Illegal index.");
        }
        Node prev = dummyhead;
        for (int i = 0; i < index; i++) {
            prev = prev.next;
        }
        prev.next = new Node(e, prev.next);
        size++;
    }

说完添加元素的操作,我们接下来看看删除元素的操作。在删除元素时,需要找到待删除元素delNode之前的元素节点prev,然后让prev.next=delNode.next,同时呢,需要资源释放,把delNode.next=null,示意图如下:

具体实现代码如下:

    // 从链表中删除index(0-based)位置的元素,返回删除的元素
    public E remove(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Remove failed.Index is illegal.");
        }

        Node prev = dummyhead;
        for (int i = 0; i < index; i++) {
            prev = prev.next;
        }
        Node retNode = prev.next;
        prev.next = retNode.next;
        retNode.next = null;
        size--;
        return retNode.e;
    }

现在我们来看看链表中查找元素的操作,首先获取链表的第index位置的元素,话不多说直接上code:

    // 获取链表的第index个位置的元素
    public E get(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Get failed. Illegal index.");
        }
        Node cur = dummyhead.next;
        for (int i = 0; i < index; i++) {
            cur = cur.next;
        }
        return cur.e;
    }

对于判断链表中某个元素是否存在,比较EZ,就不贴代码,辱智商了……

最后,我们来看看链表修改元素的骚操作。

    // 修改链表的第index个位置的元素e
    public void set(int index, E e) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Get failed. Illegal index.");
        }

        Node cur = dummyhead.next;
        for (int i = 0; i < index; i++) {
            cur = cur.next;
        }
        cur.e = e;
    }

额……今天的代码简洁,高效。嗯,确实不难啊

代码搞完后,我们来简单分析一波时间复杂度。

对于添加元素来说,整体的时间复杂度是O(n)。

1.addFirst(e) O(1)

2.addLast(e) O(n)

3.add(index, e) O(n)

对于删除元素来说,整体的时间时间复杂度也是O(n)。

1.removeLast(e) O(n)

2.removeFirst(e) O(1)

3.remove(index, e) O(n)

由此可以看出,对于添加和删除元素来说,如果只对链表的头部进行元素操作时,时间复杂度是O(1),但从整体来看时间复杂度是O(n)。

那么对于查找元素的操作来说,整体的时间时间复杂度也是O(n)。

1.contains(e) O(n)

2.get(index, e) O(n)

当然,如果只查链表头部的话,时间复杂度为O(1)。

最后,对于修改操作来说,set(index, e) 其时间复杂度为O(n)。

今日汇总:

1.区别于数组,链表是动态的线性结构

2.对于链表而言,我们需要继续头部节点head的位置,所以对头部元素的操作,其时间复杂度为O(1),而从整体上来看,操作链表的时间复杂度为O(n)。

3.通常我们在操作链表时,由于头部节点的特殊性,一般情况下,我们设置虚拟节点dummyhead

4.对于链表的添加、删除操作方面,关于节点位置的改变,相对来说比较绕,需要多琢磨。

好了,今天的总结到此结束,额,由于今天事儿多,LeetCode就先不发文章了,当然我肯定刷题了,不容置疑……

明天我们来看看如何用链表来实现栈和队列,并比较下数组队列、循环队列和基于链表实现的队列,三者有何区别。

OK,拜了个拜~