链表

90 阅读5分钟

链表

存放一个数据的同时保存下一个数据在内存中的地址,通过地址找到下一个数据,存储地址是不连续的,散乱的。

单链表

单链表的结构

每个节点存放的是:val(值)和next(下一个节点的指向)。

image.png

查找

单向链表的查找操作通常是这样的:

  1. 从头节点进入,开始比对节点的值,如果不同则通过指针进入下一个节点
  2. 重复上面的动作,直到找到相同的值,或者节点的指针指向null 链表的查找性能与数组一样,都是时间复杂度为O(n).

插入删除

链表与数组最大的不同就在于删除、插入的性能优势,由于链表是非连续的内存,所以不用像数组一样在插入、删除操作的时候需要进行大面积的成员位移,比如在a、b节点之间插入一个新节点c,链表只需要:

  1. a断开指向b的指针,将指针指向c
  2. c节点将指针指向b,完毕 这个插入操作仅仅需要移动一下指针即可,插入、删除的时间复杂度只有O(1).

image.png

读取性能

链表相比之下也有劣势,那就是读取操作远不如数组,数组的读取操作之所以高效,是因为它是一块连续内存,数组的读取可以通过寻址公式快速定位,而链表由于非连续内存,所以必须通过指针一个一个节点遍历.

比如,对于一个数组,我们要读取第三个成员,我们仅需要arr[2]就能快速获取成员,而链表则需要从头部节点进入,在通过指针进入后续节点才能读取.

应用场景

由于双向链表的存在,单向链表的应用场景比较少,因为很多场景双向链表可以更出色地完成.

但是单向链表并非无用武之地,在以下场景中依然会有单向链表的身影:

撤销功能,这种操作最多见于各种文本、图形编辑器中,撤销重做在编辑器场景下属于家常便饭,单向链表由于良好的删除特性在这个场景很适用 实现图、hashMap等一些高级数据结构

代码实现

public class SinglyLinkedList<T> {
    // 链表本身的头节点
    Node<T> head;
    // 链表的大小
    int size;
    // 初始化头节点
    SinglyLinkedList() {
        head = new Node<T>(null, null);
    }
    public class Node<T> {
        T data;
        Node<T> next;
        Node(T data, Node next) {
            this.data = data;
            this.next = next;
        }
    }

    /**
     * 头插法
     * @param t
     */
    public void addFirstNode(T t) {
        // 新节点,还没有指向
        Node<T> tNode = new Node<>(t, null);
        Node<T> first = head;
        // 判断是否为空链表,如果为空,直接插入
        if (first.next == null) {
            first.next = tNode;
            // 大小加一
            size++;
            return;
        }
        // 先让当前节点指向头节点的下一个节点,防止下一个节点丢失
        tNode.next = first.next;
        // 再让头节点指向当前节点
        first.next = tNode;
        size++;
    }

    public void addLastNode(T t) {
        // 新节点,还没有指向
        Node<T> tNode = new Node<>(t, null);
        Node<T> first = head;
        // 判断是否为空链表,如果为空,直接插入
        if (first.next == null) {
            first.next = tNode;
            // 大小加一
            size++;
            return;
        }
        // 循环遍历链表,看是不是尾部
        while (first.next != null) {
            first = first.next; // 让当前节点变成下一个节点
        }
        first.next = tNode;
        size++;
    }
}

/**
 * 指定位置插入
 * @param index
 * @param t
 */
public void addNode(int index, T t){
    // 新节点,还没有指向
    Node<T> tNode = new Node<>(t, null);
    Node<T> first = head;
    // 判断是否为空链表,如果为空,直接插入
    if (first.next == null) {
        first.next = tNode;
        // 大小加一
        size++;
        return;
    }
    // 判断只要下标不是0那就证明,还没到应该插入的位置,index--,下标从0开始
    while (index == 0) {
        first = first.next;
        index--;
    }
    Node<T> temp = new Node<>(null, null);
    temp.next = first.next;
    first.next = tNode;
    tNode.next = temp.next;
    temp.next = null;
    size++;
}

/**
 * 删除
 * @param data
 */
public void remove(T data) {
    // 新节点,还没有指向
    Node<T> tNode = new Node<>(data, null);
    Node<T> first = head;
    // 判断是否为空链表
    if (first.next == null) {
        System.out.println("没有元素可删除");
        return;
    }
    while (first.next != null) {
        if (first.next.data.equals(data)) {
            // 当前节点指向的节点已被删除,所以后移
            first.next = first.next.next;
            first.next.next = null;
            size--;
        }
        first = first.next;
    }
}

双向链表

我们上文已经提到,单向链表的应用场景并不多,而真正在生产环境中被广泛运用的正是双向链表.

双向链表与单向链表相比有何特殊之处?

image.png

我们看到双向链表多了一个新的指针prev指向节点的前一个节点,当然由于多了一个指针,所以双向链表要更占内存.

别小看双向链表多了一个前置指针,在很多场景里由于多了这个指针,它的效率更高,也更加实用.

比如编辑器的「undo/redo」操作,双向链表就更加适用,由于拥有前后指针,用户可以自由得进行前后操作,如果这个是一个单向链表,那么用户需要遍历链表这时的时间复杂度是O(n).

真正生产级应用中的编辑器采用的数据结构和设计模式更加复杂,比如Word就是采用Piece Table数据结构加上Command queue模式实现「undo/redo」的,不过这是后话了.

代码实现

public class DoubleLinkedList<T> {

    Node<T> head;

    int size = 0;

    DoubleLinkedList() {
        head = new Node<>(null,null,null);
    }

    class Node<T> {
        T val;

        Node<T> pre;

        Node<T> next;

        Node(T val, Node pre, Node next) {
            this.val = val;
            this.pre = pre;
            this.next = next;
        }
    }

    /**
     * 头插法
     * @param val
     */
    public void addFirstNode(T val) {
        Node<T> tNode = new Node<>(val, null, null);
        Node<T> first = head;
        if (first.next == null && first.pre == null) {
            tNode.pre = first;
            first.next = tNode;
            size++;
            return;
        }
        // 先让当前节点指向头节点的下一个节点,防止后续节点丢失
        tNode.next = first.next;
        // 让下一个节点pre指向当前节点
        first.next.pre = tNode;
        first.next = tNode;
        // 当前节点的pre指向头
        tNode.pre = first;
        size++;
    }

    /**
     * 尾插法
     * @param val
     */
    public void addLastNode(T val) {
        Node<T> tNode = new Node<>(val, null, null);
        Node<T> first = head;
        if (first.next == null) {
            tNode.pre = first;
            first.next = tNode;
            size++;
            return;
        }
        while (first.next != null) {
            first = first.next;
        }
        first.next = tNode;
        tNode.pre = first;
        size++;
    }

    /**
     * 指定位置插入
     * @param index
     * @param val
     */
    public void addNode(int index, T val) {
        Node<T> tNode = new Node<>(val, null, null);
        Node<T> first = head;
        if (first.next == null) {
            tNode.pre = first;
            first.next = tNode;
            size++;
            return;
        }
        // 判断是否到这个位置了,如果到了index就为0,没到index就--,然后后移
        while (index > 0) {
            first = first.next;
            index--;
        }
        Node<T> temp = new Node<>(null,null, null);
        temp.next = first.next;
        first.next = tNode;
        temp.next.pre = tNode;
        tNode.next = temp.next;
        tNode.pre = first;
        temp.next = null;
        size++;
    }

    public void list() {
        Node last = head;
        if(last.next == null) {
            //空链表
            System.out.println("链表为空");
            return ;
        }

        while(last.next!=null) {
            System.out.println(last.next);
            last = last.next;
        }
    }
}

循环链表

循环链表,顾名思义,他就是将单向链表的尾部指针指向了链表头节点:

image.png

循环链表一个应用场景就是操作系统的分时问题,比如有一台计算机,但是有多个用户使用,CPU要处理多个用户的请求很可能会出现抢占资源的情况,这个时候计算机会采取分时策略来保证每个用户的使用体验.

双向循环链表

双向循环链表就是循环链表和双向链表的结合。