3. 数据结构与算法 —— 链表

198 阅读9分钟

内存分布

链表通过「指针」将一组「零散的内存块」串联起来使用。所以理论上链表是可以无限大的。

链表的结构五花八门,最常用的有三种:单链表、双向链表和循环链表。

在学习链表之前先要搞清楚几个概念:结点、后继指针 next、头结点、尾结点

  • 结点:链表的内存块。
  • 后继结点 next:存储下一个结点的地址的指针。
  • 头结点:第一个结点。
  • 尾结点:最后一个结点,它的后继结点 next 不是指向下一个结点,而是 null。(只有单链表有尾结点)

单链表

单链表

与数组一样,链表也支持数据的查找、插入和删除操作。在链表中,插入和删除一个数据并不需要为了内存的连续性而搬移结点。所以,在链表中插入和删除一个数据的非常快,时间复杂度为 O(1)。但是在链表中想要随机访问第 k 个元素,就需要从头结点一个一个的依次遍历查找,时间复杂度为 O(n)。

public class SinglyLinkedList<K> {

    private Node<K> mHead = null;

    /**
     * 表头插入
     * 这种操作将输入的顺序相反,逆序
     *
     * @param value 要插入元素的值
     */
    public void insertToHead(K value) {
        Node<K> newNode = new Node<>(value, null);
        insertToHead(newNode);
    }

    public void insertToHead(Node<K> node) {
        if (mHead == null) {
            mHead = node;
        } else {
            node.next = mHead;
            mHead = node;
        }

    }


    /**
     * 顺序插入到链表尾部
     *
     * @param value 要插入元素的值
     */
    public void insertTail(K value) {
        Node<K> newNode = new Node<>(value, null);

        if (mHead == null) {
            mHead = newNode;
        } else {
            Node<K> p = mHead;
            while (p.next != null) {
                p = p.next;
            }
            p.next = newNode;
        }
    }

    /**
     * 插入到结点后面
     *
     * @param p     被插入的目标结点
     * @param value 要插入元素的值
     */
    public void insertAfter(Node<K> p, K value) {
        Node<K> newNode = new Node<>(value, null);

        insertAfter(p, newNode);
    }

    public void insertAfter(Node<K> p, Node<K> newNode) {
        if (p == null) return;

        // 防止 p 后面的链表丢失
        newNode.next = p.next;
        p.next = newNode;
    }

    /**
     * 插入到目标结点前
     *
     * @param p     被插入的目标结点
     * @param value 要插入元素的值
     */
    public void insertBefore(Node<K> p, K value) {
        Node<K> newNode = new Node<>(value, null);

        insertBefore(p, newNode);
    }

    public void insertBefore(Node<K> p, Node<K> newNode) {
        if (p == null) return;

        if (mHead == p) {
            insertToHead(newNode);
            return;
        }

        Node<K> q = mHead;
        // 找到目标结点的前置结点
        while (q != null && q.next != p) {
            q = q.next;
        }

        // 整个链表都找完了,还是没有找到 p 结点
        if (q == null) return;

        newNode.next = q.next;
        q.next = newNode;
    }

    // 删除
    public void deleteByNode(Node<K> node) {

        if (mHead == null || node == null) return;

        if (mHead == node) {
            mHead = mHead.next;
            return;
        }

        Node<K> p = mHead;
        while (p != null && p.next != node) {
            p = p.next;
        }

        if (p == null) return;

        p.next = p.next.next;
    }

    public void deleteByValue(K value) {

        if (mHead == null) return;

        Node<K> p = mHead;  // p 指向目标结点
        Node<K> q = null;   // q 用来保存目标结点的前置结点
        while (p != null && p.data.equals(value)) {
            q = p;
            p = p.next;
        }

        if (p == null) return;

        if (q == null) {
            mHead = mHead.next;
        } else {
            q.next = q.next.next;
        }

    }

    // 查找
    public Node<K> findByValue(K value) {
        Node<K> p = mHead;

        while (p != null && p.data.equals(value)) {
            p = p.next;
        }
        return p;
    }

    public Node<K> findByIndex(int index) {
        Node<K> p = mHead;
        int pos = 0;
        while (p != null && pos != index) {
            p = p.next;
            pos++;
        }

        return p;
    }

    @Override
    public String toString() {
        Node<K> p = mHead;
        StringBuilder sb = new StringBuilder();
        while (p != null) {
            sb.append(p.data);
            sb.append(" -> ");
            p = p.next;
        }
        sb.append("null");
        return sb.toString();
    }
    
    public static class Node<T> {
        private T data;
        private Node<T> next;

        public Node(T data, Node<T> node) {
            this.data = data;
            next = node;
        }

        public T getData() {
            return data;
        }
    }
}

循环链表

循环链表

「循环链表」是一种特殊的单链表。它跟单链表唯一的区别就是,尾结点不是指向 null 而是指向头结点。

双向链表

双向链表

「双向链表」是比单项链表和循环链表更加常用的数据结构。它支持两个方向,每个结点不止有后继结点 next 指向后面的结点,还有前驱结点 prev 指向前面的结点。

public class DoublyLinkedList {

    private Node mHead = null;

    private Node preFindNode = null;

    // 查找

    /**
     * 查找链表中第一次出现目标值结点
     * 时间复杂度 O(n)
     * 空间复杂度 O(1)
     *
     * @param value
     * @return
     */
    public Node findByValue(int value) {
        if (mHead == null) return null;

        Node p = mHead;
        while (p != null && p.data != value) {
            p = p.next;
        }

        return p;
    }

    /**
     * 根据下标查找结点
     * 时间复杂度 O(n)
     * 空间复杂度 O(1)
     *
     * @param index 需要查找的下标
     * @return 返回找到的结点
     */
    public Node findByIndex(int index) {
        if (mHead == null) return null;

        Node p = mHead;
        int pos = 0;
        while (p != null && pos != index) {
            p = p.next;
            pos++;
        }

        return p;
    }

    // 插入

    /**
     * 插入元素到头结点
     * 一般用来逆序存储数据
     * 时间复杂度 O(1)
     * 空间复杂度 O(1)
     *
     * @param value 待插入的值
     */
    public void insertToHead(int value) {
        Node newNode = new Node(value, null, null);

        insertToHead(newNode);
    }

    public void insertToHead(Node node) {
        if (mHead == null) {
            mHead = node;
        } else {
            node.next = mHead;
            mHead.prev = node;
            mHead = node;
        }
    }

    /**
     * 插入元素到尾结点
     * 时间复杂度 O(n)
     * 空间复杂度 O(1)
     *
     * @param value 待插入的值
     */
    public void insertTail(int value) {
        Node newNode = new Node(value, null, null);

        insertTail(newNode);
    }

    public void insertTail(Node node) {
        if (mHead == null) {
            mHead = node;
        } else {
            Node p = mHead;
            while (p.next != null) {
                p = p.next;
            }
            node.prev = p;
            p.next = node;
        }
    }

    /**
     * 插入元素到目标结点的后面
     * 时间复杂度 O(1)
     * 空间复杂度 O(1)
     *
     * @param p     目标结点
     * @param value 待插入的值
     */
    public void insertAfter(Node p, int value) {
        if (p == null || mHead == null) return;

        Node newNode = new Node(value, null, null);

        newNode.next = p.next;
        p.next.prev = newNode;

        p.next = newNode;
        newNode.prev = p;
    }

    /**
     * 插入元素到目标结点的前面
     * 时间复杂度 O(1)
     * 空间复杂度 O(1)
     *
     * @param p     目标结点
     * @param value 待插入的值
     */
    public void insertBefore(Node p, int value) {
        if (p == null || mHead == null) return;

        Node newNode = new Node(value, null, null);

        Node q = p.prev;    // 保存前置结点指针

        newNode.next = p;
        p.prev = newNode;

        q.next = newNode;
        newNode.prev = q;
    }

    // 删除

    /**
     * 删除第一个等于 value 的结点
     * 时间复杂度 O(n),其实就是查找这个结点消耗的时间
     * 空间复杂度 O(1)
     *
     * @param value 待删除的值
     * @return 返回被删除的结点
     */
    public Node deleteFirstValue(int value) {
        Node p = findByValue(value);

        // 没有找到这个值
        if (p == null) return null;

        p.prev.next = p.next;
        p.next.prev = p.prev;

        return p;
    }

    /**
     * 删除所有等于 value 的结点
     * 时间复杂度 O(n),要找到最后一个等于 value 的结点
     * 空间复杂度 O(1)
     *
     * @param value 待删除的值
     */
    public void deleteAllValue(int value) {
        if (mHead == null) return;

        Node p = mHead;
        while (p != null) {
            if (p.data == value) {
                p.prev.next = p.next;
                p.next.prev = p.prev;
            }
            p = p.next;
        }
    }

    /**
     * 删除目标结点
     * 时间复杂度 O(1)
     * 空间复杂度 O(1)
     *
     * @param node 待删除的结点
     */
    public void deleteByNode(Node node) {
        if (node == null) return;

        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
    
    public static class Node {
        private int data;
        private Node next;  // 后继结点
        private Node prev;  // 前驱结点

        public Node(int data, Node next, Node prev) {
            this.data = data;
            this.next = next;
            this.prev = prev;
        }

        public int getData() {
            return data;
        }
    }
}

课后习题

如何基于链表实现 LRU 缓存淘汰算法

基于链表实现的 LRU 缓存淘汰算法
/**
 * 基于单链表的 LRU 缓存
 *
 * @param <T>
 */
public class LRUBaseLinkedList<T> {
    private static final int DEFAULT_CAPACITY = 10;

    // 带头结点链表,头结点永远不存储数据
    private Node<T> mHead = new Node<>(null, null);
    private int mCapacity;  // 链表的容量
    private int mLength;    // 链表的数据量

    public LRUBaseLinkedList() {
        mCapacity = DEFAULT_CAPACITY;
        mLength = 0;
    }

    public LRUBaseLinkedList(int capacity) {
        mCapacity = capacity;
        mLength = 0;
    }

    /**
     * 加入缓存列表
     * 先检查链表是否有这个元素。
     * 如果没有这个元素,再看缓存是否满了,如果没满就直接插入到头结点,如果满了就删除尾结点,再插入头结点
     * 如果有这个元素,就先删除这个结点,再插入到头结点
     *
     * 时间复杂度 O(n)
     * 空间复杂度 O(1)
     *
     * @param data 加入缓存数据
     */
    public void add(T data) {

        // 检查链表是否有这个结点,并且把前置结点取出来
        Node<T> preNode = findPreData(data);

        if (preNode == null) {
            if (mLength >= mCapacity) {  // 缓存已经存满了,就删除尾结点
                deleteLastNode();
            }
            insertToHead(data);

        } else {    // 缓存没满,就删除这个结点,再插入头结点
            deleteNextNode(preNode);
            insertToHead(data);
        }
    }

    /**
     * 找到目标值的前置结点
     * 时间复杂度 O(n)
     * 空间复杂度 O(1)
     *
     * @param value 目标值
     * @return 返回前置结点
     */
    private Node<T> findPreData(T value) {
        Node<T> p = mHead;

        while (p.next != null) {
            if (value.equals(p.next.data)) {
                return p;
            }
            p = p.next;
        }
        return null;    // 没有找到这个结点
    }

    private void insertToHead(T value) {
        Node<T> node = new Node<>(value, null);

        insertToHead(node);
    }

    /**
     * 插入链表头
     * 时间复杂度 O(1)
     * 空间复杂度 O(1)
     *
     * @param node 待插入的元素
     */
    private void insertToHead(Node<T> node) {
        node.next = mHead.next;
        mHead.next = node;
        mLength++;
    }

    /**
     * 删除当前结点的下一个结点
     * 时间复杂度 O(1)
     * 空间复杂度 O(1)
     *
     * @param preNode 目标结点
     */
    private void deleteNextNode(Node<T> preNode) {
        preNode.next = preNode.next.next;
        mLength--;
    }

    /**
     * 删除链表的最后一个结点
     * 时间复杂度 O(n)
     * 空间复杂度 O(1)
     */
    private void deleteLastNode() {
        if (mHead.next == null) return;

        Node<T> p = mHead;
        // 得到倒数第二个结点
        while (p.next.next != null) {
            p = p.next;
        }
        p.next = null;
        mLength--;
    }

    @Override
    public String toString() {
        Node<T> p = mHead;
        StringBuilder sb = new StringBuilder();
        while (p.next != null) {
            sb.append(p.next.data);
            sb.append(" -> ");
            p = p.next;
        }
        sb.append("null");
        return sb.toString();
    }

    private class Node<K> {
        private K data;
        private Node<K> next;

        public Node(K data, Node<K> next) {
            this.data = data;
            this.next = next;
        }

        public K getData() {
            return data;
        }
    }

    public static void main(String[] args) {
        LRUBaseLinkedList list = new LRUBaseLinkedList();
        Scanner scanner = new Scanner(System.in);

        while (true) {
            list.add(scanner.nextInt());
            System.out.println(list);
        }
    }
}
基于数组实现的 LRU 缓存淘汰算法
public class LRUBasedArray<T> {
    private static final int DEFAULT_CAPACITY = 10;

    private T[] mData;
    private int mCapacity;  // 链表的容量
    private int mCount;    // 链表的数据量

    public LRUBasedArray() {
        mCapacity = DEFAULT_CAPACITY;
        mData = (T[]) new Object[mCapacity];
        mCount = 0;
    }

    public LRUBasedArray(int capacity) {
        mCapacity = capacity;
        mData = (T[]) new Object[mCapacity];
        mCount = 0;
    }

    public void add(T data) {
        int index = findByValue(data);
        // 缓存中没有这个元素
        if (index == -1) {
            if (mCount == mCapacity) {
                mCount--;
            }
            insertToHead(data);

        } else {
            for (int i = index; i > 0; i--) {
                mData[index] = mData[index - 1];
            }
            mData[0] = data;
        }
    }

    private int findByValue(T data) {
        for (int i = 0; i < mCount; i++) {
            if (mData[i] == data) {
                return i;
            }
        }
        return -1;
    }

    private void insertToHead(T data) {
        for (int i = mCount; i > 0; i--) {
            mData[i] = mData[i - 1];
        }
        mData[0] = data;
        mCount++;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        for (int i = 0; i < mCount - 1; i++) {
            sb.append(mData[i]).append(", ");
        }
        sb.append(mData[mCount - 1]).append("]");
        return sb.toString();
    }

    public static void main(String[] args) {
        LRUBasedArray array = new LRUBasedArray();
        Scanner scanner = new Scanner(System.in);

        while (true) {
            array.add(scanner.nextInt());
            System.out.println(array);
        }
    }
}

如果字符串是通过单链表来存储的,那应该如何判断是一个回文串呢?

/**
 * 判断回文
 * 时间复杂度 O(n)
 * 空间复杂度 O(1)
 *
 * @return 是否是回文字符串
 */
public boolean palindrome() {
    if (mHead == null) return false;

    Node<K> fast = mHead;
    Node<K> slow = mHead;

    // 只有一个元素
    if (slow.next == null) {
        return true;
    }

    while (slow.next != null && fast.next != null && fast.next.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    // 现在 slow 一定是中间点
    if (fast.next != null) { // 偶数个
        slow = slow.next;
    }

    // 翻转链表
    Node reverse = inverseLinkList(slow);

    while (slow != null && reverse != null) {
        if (slow.data != reverse.data) {
            return false;
        }
        slow = slow.next;
        reverse = reverse.next;
    }

    return true;
}

/**
 * 无头结点的链表翻转
 * 时间复杂度 O(n)
 * 空间复杂度 O(1)
 *
 * @param p 链表的结点
 * @return 返回翻转后的结点
 */
private Node<K> inverseLinkList(Node<K> p) {
    Node<K> pre = null; // 保存前置结点
    Node<K> reverse = mHead;    // 保存翻转结点
    Node<K> next = null;    // 保存下一个结点

    while (reverse != p) {
        next = reverse.next;

        reverse.next = pre;
        pre = reverse;
        reverse = next;
    }
    reverse.next = pre;

    return reverse;
}