(重学算法系列)---链表(LinkedList)

162 阅读5分钟

策略

常见的策略有三种:先进先出策略

  • FIFO ( First In , First Out )、
  • 最少使用策略 LFU ( Least Frequently Used )、
  • 最近最少使用策略 LRU ( Least Recently Used

链表

一、什么是链表?

  1. 和数组一样,链表也是一种线性表。
  2. 从内存结构来看,链表的内存结构是不连续的内存空间,是将一组零散的内存块串联起来,从而进行数据存储的数据结构。
  3. 链表中的每一个内存块被称为节点Node。节点除了存储数据外,还需记录链上下一个节点的地址,即后继指针 next

链表分类

  • 单链表
  • 循环链表
  • 双向链表
  • 双向循环链表

其中,我们把内存块称为链表的“结点”。为了将所有的结点串起来,每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址。如图所示,我们把这个记录下个结点地址的指针叫作后继指针next。

其中有两个结点是比较特殊的,它们分别是第一个结点和最后一个结点。我们习惯性地把第一个结点叫作头结点,把最后一个结点叫作尾结点。其中,头结点用来记录链表的基地址。有了它,我们就可以遍历得到整条链表。而尾结点特殊的地方是:指针不是指向下一个结点,而是指向一个空地址NULL,表示这是链表上最后一个结点。

空间换时间

对于执行较慢的程序,可以通过消耗更多的内存(空间换时间)来进行优化;而消耗过多内存的程序,可以通过消耗更多的时间(时间换空 间)来降低内存的消耗。

时间换空间

相反,如果内存比较紧缺,比如代码跑在手机或者单片机上,这个时候,就要反过来用 时间换空间的设计思路。

选择数组还是链表?

  1. 插入、删除和随机访问的时间复杂度 数组:插入、删除的时间复杂度是 O(n) ,随机访问的时间复杂度是 O(1) 。 链表:插入、删除的时间复杂度是 O(1) ,随机访问的时间复杂端是 O(n) 。
  2. 数组缺点 1 )若申请内存空间很大,比如 100M ,但若内存空间没有 100M 的连续空间时,则会申请失败,尽管内存可用空间超过 100M 。 2 )大小固定,若存储空间不足,需进行扩容,一旦扩容就要进行数据复制,而这时非常费时的。
  3. 链表缺点 1)内存空间消耗更大,因为需要额外的空间存储指针信息。 2 )对链表进行频繁的插入和删除操作,会导致频繁的内存申请和释放,容易造成内存碎片,如果是 Java 语言,还可能会造成频繁的 GC (自动垃圾回收器) 操作

操作链表的一些技巧

  • 理解指针或引用的含义
    • 不管是 “ 指针 ” 还是 “ 引用,都是存储所指对象的内存地址
  • 警惕指针丢失和内存泄漏
    • 丢失:注意添加时的先后顺序,避免自己指向自己,造成丢失。
  • 利用哨兵简化实现难度
    • 种有哨兵结点的链表叫带头链表。相反,没有哨兵结点的链表就叫作不带头链表
  • 边界条件处理(检查和思考)
    • 如果链表为空时,代码是否能正常工作?
    • 如果链表只包含一个结点时,代码是否能正常工作?
    • 如果链表只包含两个结点时,代码是否能正常工作?
    • 代码逻辑在处理头结点和尾结点的时候,是否能正常工作?
  • 画图,辅助思考

练习:

  1. 单链表反转
  2. 链表中环的检测
  3. 两个有序的链表合并
  4. 删除链表倒数第n个结点
  5. 求链表的中间结点
  • (LeetCode 对应编号: 206 , 141 , 21 , 19 , 876)

LeetCode解答小结:

反转一个单链表。 示例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

解1:

可以理解成两个栈,我们把head当成栈顶,不断从栈顶取出放入另一个栈底,使栈顶的next指针指向当前栈,直到原栈没有数据(null)

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode curr = head;
        while(curr!=null){
            ListNode tempNext = curr.next;
            // 分离出一个栈顶Node节点
            curr.next = pre;
             // 相当于放到另一个栈的栈顶 
            pre = curr;
            // 重新移动head指针
            curr = tempNext;
        }
        // 返回新栈
        return pre;
    }
}

解2:

递归实现,

class Solution {
    public ListNode reverseList(ListNode head) {
        // 解2:递归实现
        if(head == null || head.next == null){
            return head;
        }
        ListNode node = reverseList(head.next);
        // 关键代码,head是node的前节点,这里为什么不能用node?
        // 因为这里每次返回值是node,相当于重新修改了next值,最终只剩两个[5,1]
        // 而head.next是未修改时,也相当于head的后继节点
        head.next.next = head;
        head.next = null;
        return node;
    }
}

LinkedList

特点

  1. 允许null
  2. 非synchronized,非线程安全
  3. fail-fast特性
  4. Deque(double ended queue)

源码

add

public boolean add(E e) {
    linkLast(e);
    return true;
}

void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

remove

public boolean remove(Object o) {
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}