双向链表和双向循环链表

双向链表简介

单向链表只有一个方向,结点只有一个后继指针 next 指向后面的结点。而双向链表,顾名思义,它支持两个方向,每一个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。java

img

从上图中能够看出来,双向链表须要额外的两个空间来存储后继结点和前驱结点的地址。因此,若是存储一样多的数据,双向链表要比单链表占用更多的内存空间。虽然两个指针比较浪费存储空间,但能够支持双向遍历,这样也带来了双向链表操做的灵活性。那相比单链表,双向链表适合解决哪一种问题呢?node

从结构上来看,双向链表能够支持 O(1) 时间复杂度的状况下找到前驱结点,正是这样的特色,也使双向链表在某些状况下的插入、删除等操做都要比单链表简单、高效数组

双向链表的增删改查操做

1. 插入操做缓存

  • 头部插入:时间复杂度O(1)
  • 尾部插入:时间复杂度O(1)
  • 指定位置后面插入:时间复杂度O(1)
  • 指定位置前面插入:时间复杂度O(1) ---注意和单向链表的区别

2. 删除操做安全

删除操做的时间复杂度和插入操做的时间复杂度相似。数据结构

  • 删除头部节点:时间复杂度O(1)
  • 删除尾部节点:时间复杂度O(1)
  • 删除值等于某个数的节点:时间复杂度O(n)
  • 删除某个具体节点:O(1)

关于删除操做,这边作下说明。测试

在实际的软件开发中,从链表中删除一个数据无外乎这两种状况:this

  1. 删除结点中“值等于某个给定值”的结点;线程

  2. 删除给定指针指向的结点。指针

对于第一种状况,不论是单链表仍是双向链表,为了查找到值等于给定值的结点,都须要从头结点开始一个一个依次遍历对比,直到找到值等于给定值的结点,而后再经过我前面讲的指针操做将其删除。

尽管单纯的删除操做时间复杂度是 O(1),但遍历查找的时间是主要的耗时点,对应的时间复杂度为 O(n)。根据时间复杂度分析中的加法法则,删除值等于给定值的结点对应的链表操做的总时间复杂度为 O(n)。

对于第二种状况,咱们已经找到了要删除的结点,可是删除某个结点 q 须要知道其前驱结点,而单链表并不支持直接获取前驱结点,因此,为了找到前驱结点,咱们仍是要从头结点开始遍历链表,直到 p->next=q,说明 p 是 q 的前驱结点。

可是对于双向链表来讲,这种状况就比较有优点了。由于双向链表中的结点已经保存了前驱结点的指针,不须要像单链表那样遍历。因此,针对第二种状况,单链表删除操做须要 O(n) 的时间复杂度,而双向链表只须要在 O(1) 的时间复杂度内就搞定了!

除了插入、删除操做有优点以外,对于一个有序链表,双向链表的按值查询的效率也要比单链表高一些。由于,咱们能够记录上次查找的位置 p,每次查询时,根据要查找的值与 p 的大小关系,决定是往前仍是日后查找,因此平均只须要查找一半的数据。

如今,你有没有以为双向链表要比单链表更加高效呢?这就是为何在实际的软件开发中,双向链表尽管比较费内存,但仍是比单链表的应用更加普遍的缘由。若是你熟悉 Java 语言,你确定用过 LinkedHashMap 这个容器。若是你深刻研究 LinkedHashMap 的实现原理,就会发现其中就用到了双向链表这种数据结构。

3. 更新操做

  • 更新指定节点:时间复杂度O(1)
  • 将链表中值等于某个具体值的节点更新:时间复杂度O(n)

4. 查询操做

  • 时间复杂度:O(n)

双向链表的Java代码实现

public class TwoWayLinkedList<E> {


    public static void main(String[] args) {
        TwoWayLinkedList<Integer> list = new TwoWayLinkedList<>();
        //尾部插入,遍历链表输出
        System.out.println("尾部插入[1-10]");
        for (int i = 1; i <= 10; i++) {
            list.addLast(Integer.valueOf(i));
        }
        list.printList();
        //头部插入,遍历链表输出
        System.out.println("头部插入[1-10]");
        for (int i = 1; i <= 10; i++) {
            list.addFirst(Integer.valueOf(i));
        }
        list.printList();
        //在指定节点后面插入
        System.out.println("在头节点后面插入[100]");
        list.addAfter(100, list.head);
        list.printList();
        System.out.println("在头节点前面插入[100]");
        list.addBefore(100, list.head);
        list.printList();
        System.out.println("在尾节点前面插入[100]");
        list.addBefore(100, list.tail);
        list.printList();
        System.out.println("在尾节点后面插入[100]");
        list.addAfter(100, list.tail);
        list.printList();

        System.out.println("------------删除方法测试-----------");
        System.out.println("删除头节点");
        list.removeFirst();
        list.printList();
        System.out.println("删除尾节点");
        list.removeLast();
        list.printList();
        System.out.println("删除指定节点");
        list.removeNode(list.head.next);
        list.printList();
    }


    private Node head;
    private Node tail;

    public TwoWayLinkedList() {
    }

    public TwoWayLinkedList(E data) {
        Node node = new Node<>(data, null,null);
        head = node;
        tail = node;
    }

    public void printList() {
        Node p = head;
        while (p != null && p.next != null) {
            System.out.print(p.data + "-->");
            p = p.next;
        }
        if (p != null) {
            System.out.println(p.data);
        }
    }

    public void addFirst(E data) {
        Node newNode = new Node(data,null ,head);
        if(head!=null){
            head.pre = newNode;
        }
        head = newNode;
        if (tail == null) {
            tail = newNode;
        }
    }

    public void addLast(E data) {
        Node newNode = new Node(data, tail,null);
        if (tail == null) {
            head = newNode;
            tail = newNode;
        } else {
            tail.next = newNode;
            tail = newNode;
        }
    }

    /**
     * @param data
     * @param node node节点必须在链表中
     */
    public void addAfter(E data, Node node) {
        if (node == null) {
            return;
        }
        Node newNode = new Node(data, node,node.next);
        if(node.next!=null){
            node.next.pre = newNode;
        }
        node.next = newNode;
        if (tail == node) {
            tail = newNode;
        }
    }

    public void addBefore(E data, Node node) {
        if (node == null) {
            return;
        }
        if(node==head){
            addFirst(data);
        }else {
            Node newNode = new Node(data,node.pre,node);
            node.pre.next = newNode;
            node.pre = newNode;
        }
    }

    public void removeFirst() {
        if (head == null) {
            return;
        }
        if (head == tail) {
            head = null;
            tail = null;
        } else {
            if(head.next!=null){
                head.next.pre = null;
            }
            head = head.next;
        }
    }

    public void removeLast() {
        if (tail == null) {
            return;
        }
        if (head == tail) {
            head = null;
            tail = null;
        } else {
            if(tail.pre!=null){
                tail.pre.next = null;
                Node p = tail.pre;
                tail.pre = null;
                tail = p;
            }
        }
    }

    public void removeNode(Node node) {
        if (node == null) {
            return;
        }
        if(node==head){
            removeFirst();
        }
        if(node==tail){
            removeLast();
        }
        node.pre.next = node.next;
        node.next.pre = node.pre;
    }

    private static class Node<E> {
        E data;
        Node pre;
        Node next;

        public Node(E data, Node pre, Node next) {
            this.data = data;
            this.pre = pre;
            this.next = next;
        }
    }

}

双向链表的JDK实现

JDK中的LinkedList就是一个双向链表。咱们能够直接拿来使用,或者作简单的封装。

package com.csx.algorithm.link;

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.Set;
import java.util.function.Predicate;

public class SinglyLinkedList2<E> {

    private LinkedList<E> list;

    public SinglyLinkedList2() {
        this.list = new LinkedList<>();
    }

    public SinglyLinkedList2(E data){
        Set<E> singleton = Collections.singleton(data);
        this.list = new LinkedList<>(singleton);
    }

    public SinglyLinkedList2(Collection<? extends E> c){
        this.list = new LinkedList<>(c);
    }

    // ----------------------------------新增方法---------------------------------------

    public void addFirst(E data){
        list.addFirst(data);
    }

    public void addLast(E data){
        list.addLast(data);
    }
    // 在链表末尾添加
    public boolean add(E date){
        return list.add(date);
    }

    public boolean addAll(Collection<? extends E> collection){
        return list.addAll(collection);
    }

    public boolean addBefore(E data,E succ){
        int i = list.indexOf(succ);
        if(i<0){
            return false;
        }
        list.add(i,data);
        return true;
    }

    public boolean addAfter(E data,E succ){
        int i = list.indexOf(succ);
        if(i<0){
            return false;
        }
        if((i+1)==list.size()){
            list.addLast(data);
            return true;
        }else {
            list.add(i+1,data);
            return true;
        }
    }
    // ---------------------------------- 删除方法---------------------------------------
    // 删除方法,默认删除链表头部元素
    public E remove(){
        return list.remove();
    }
    // 删除方法,删除链表第一个元素
    public E removeFirst(){
        return list.removeFirst();
    }
    // 删除方法,删除链表最后一个元素
    public E removeLast(){
        return list.removeLast();
    }
    // 删除链表中第一次出现的元素,成功删除返回true
    // 对象相等的标准是调用equals方法相等
    public boolean remove(E data){
        return list.remove(data);
    }
    // 逻辑和remove(E data)方法相同
    public boolean removeFirstOccur(E data){
        return list.removeFirstOccurrence(data);
    }
    // 由于LinkedList内部是双向链表,因此时间复杂度和removeFirstOccur相同
    public boolean removeLastOccur(E data){
        return list.removeLastOccurrence(data);
    }
    // 批量删除方法
    public boolean removeAll(Collection<?> collection){
        return list.removeAll(collection);
    }
    // 按照条件删除
    public boolean re(Predicate<? super E> filter){
        return list.removeIf(filter);
    }
    // ----------------------------- 查询方法----------------------------
    // 查询链表头部元素
    public E getFirst(){
        return list.getFirst();
    }
    // 查询链表尾部元素
    public E getLast(){
        return list.getLast();
    }
    // 查询链表是否包含某个元素
    // 支持null判断
    // 相等的标准是data.equals(item)
    public boolean contains(E data){
        return list.contains(data);
    }
    public boolean containsAll(Collection<?> var){
        return list.containsAll(var);
    }

}

仍是作下提醒,LinkedList并非线程安全的。若是须要保证线程安全,须要你本身作同步控制。

双向循环链表

其实就是将头节点的前趋指针指向尾节点,将尾节点的后驱指针指向头节点。

img

数组和链表的比较

img

不过,数组和链表的对比,并不能局限于时间复杂度。并且,在实际的软件开发中,不能仅仅利用复杂度分析就决定使用哪一个数据结构来存储数据。

数组简单易用,在实现上使用的是连续的内存空间,能够借助 CPU 的缓存机制,预读数组中的数据,因此访问效率更高。而链表在内存中并非连续存储,因此对 CPU 缓存不友好,没办法有效预读。

数组的缺点是大小固定,一经声明就要占用整块连续内存空间。若是声明的数组过大,系统可能没有足够的连续内存空间分配给它,致使“内存不足(out of memory)”。若是声明的数组太小,则可能出现不够用的状况。这时只能再申请一个更大的内存空间,把原数组拷贝进去,很是费时。链表自己没有大小的限制,自然地支持动态扩容,我以为这也是它与数组最大的区别。

除此以外,若是你的代码对内存的使用很是苛刻,那数组就更适合你。由于链表中的每一个结点都须要消耗额外的存储空间去存储一份指向下一个结点的指针,因此内存消耗会翻倍。并且,对链表进行频繁的插入、删除操做,还会致使频繁的内存申请和释放,容易形成内存碎片,若是是 Java 语言,就有可能会致使频繁的 GC(Garbage Collection,垃圾回收)。因此,在咱们实际的开发中,针对不一样类型的项目,要根据具体状况,权衡到底是选择数组仍是链表。

相关文章
相关标签/搜索