707.设计双向链表

707.设计双向链表

链表简介

  • 链表(LinkedList) 是一种线性表,但不是顺序表,因为它是通过节点直接的相互引用相互联系起来的。
  • 由于不必按顺序存储,链表在插入和删除的时候可以达到O(1)的复杂度,比顺序表快的多。但是查找一个节点或者访问特定编号的节点则需要O(n)的时间。
  • 使用链表结构可以克服数组链表需要预先知道数据大小的缺点,充分利用计算机的内存空间,实现灵活的动态管理,但是链表失去了数组随机读取的优点,且链表由于增加了节点指针域,空间开销较大
  • 单链表结构如下图,但是参考LinkedList的实现思路,本文将会对双向链表进行总结:

下面是leetcode上面关于链表的题目及解题思路。

题目描述

设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。

在链表类中实现这些功能:

  • get(index) :获取链表中第 index 个节点的值。如果索引无效,则返回-1。
  • addAtHead(val) :在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
  • addAtTail(val) :将值为 val 的节点追加到链表的最后一个元素。
  • addAtIndex(index,val) :在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
  • deleteAtIndex(index) :如果索引 index 有效,则删除链表中的第 index 个节点。

我的思路是:

  • 参考LinkedList的实现,利用双向链表设计,难度并不是特别大。下面是双向链表的结构图,链表由许多的节点组成。每个节点都包含指向上一节点的prev,指向下一节点的next,和存储数据的item。
  • 如果一个节点的prev为null,那么这个节点就是首节点,如果一个节点的next为null,那这个节点就是尾节点。
  • LinkedList将first指向头节点,将last指向尾节点,如果头节点为空,则链表为空。
  • 双向链表相对于单向链表来说,增加了size属性,记录节点的个数,而且除了next,还增加了前向指针prev,向前遍历更加容易。

  • 关于循环列表,即在双向链表的基础上,让尾节点的next指向头节点,让头节点的prev指向尾节点,变成一个环。

代码实现

class MyLinkedList {
    int size = 0;
    Node first;
    Node last;
    /* determine the scope of delete operation*/
    public void elementRangeCheck(int index) {
        if(index <0 || index >= size)
            throw new IndexOutOfBoundsException();
    }
    /* determine the scope of add operation*/
    public void positionRangeCheck(int index) {
        if(index < 0 && index > size)
            throw new IndexOutOfBoundsException();
    }

    /**
     * Initialize your data structure here.
     */
    public MyLinkedList() {
    }
    /**
     * get node by specified index
     */
    private Node getNode(int index) {
        if (index > (size >> 1)) {
            Node temp = last;
            for (int i = size - 1; i > index; i--) {
                temp = temp.prev;
            }
            return temp;
        } else {
            Node temp = first;
            for (int i = 0; i < index; i++) {
                temp = temp.next;
            }
            return temp;
        }
    }
    /**
     * Get the value of the index-th node in the linked list. If the index is invalid, return -1.
     */
    public int get(int index) {
        try{
            elementRangeCheck(index);
        }catch (IndexOutOfBoundsException e){
            return -1;
        }
        return (Integer) getNode(index).item;
    }
    /**
     * Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list.
     */
    public void addAtHead(int val) {
        final Node f = first;
        final Node newNode = new Node(null, val, f);
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
        first = newNode;
        size++;
    }

    /**
     * Append a node of value val to the last element of the linked list.
     */
    public void addAtTail(int val) {
        final Node l = last;
        final Node newNode = new Node(l, val, null);
        if (l == null)
            first = newNode;
        else
            l.next = newNode;

        last = newNode;
        size++;
    }

    /**
     * Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted.
     */
    public void addAtIndex(int index, int val) {
        positionRangeCheck(index);
        if (index == size) {
            addAtTail(val);
            return;
        }
        final Node currNode = getNode(index);
        final Node preNode = currNode.prev;
        final Node newNode = new Node(preNode, val, currNode);
        currNode.prev = newNode;
        if (preNode==null))
            first = newNode;
        else
            preNode.next = newNode;
        size++;
    }

    /**
     * Delete the index-th node in the linked list, if the index is valid.
     */
    public void deleteAtIndex(int index) {
        elementRangeCheck(index);
        final Node succ = getNode(index);
        final Node prev = succ.prev;
        final Node next = succ.next;
        succ.next = null;
        succ.prev = null;
        if (prev==null) {
            first = next;
        } else {
            prev.next = next;
        }

        if (next==null)
            last = prev;
        else
            next.prev = prev;
        size--;
    }

    private static class Node {
        int item;//store value;
        Node next;//point to next node;
        Node prev;//point to prev node;

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

力扣官方题解提供了一种使代码简化的方式,忽视表头和表尾的边界条件提供哨兵(sentinel)对象(哑对象),让删除的代码更加简洁。https://leetcode-cn.com/problems/design-linked-list/solution/she-ji-lian-biao-by-leetcode/
具体的操作就是:

  • 设置一个代表null的对象,但是它拥有和其他节点对象相同的属性,它也有前驱和后继。
  • 这样一来,链表中每一处对null的引用都用对哨兵对象的引用来代替。这样的骚操作将双向链表转变为有哨兵对象的双向循环链表
  • 假设哨兵对象为nil,此时nil.next指向表头,nil.prev指向表尾,表尾的next和表头的prev同时指向nil
  • 这样一来,原先的first就可以用nil.next来代替。
  • 再不忽略边界条件的情况下,我们删除操作往往是
    • 判断该元素是否为first节点,如果是的话,就让first指向该元素的下一位;如果不是,就让该元素的前一节点指向下一节点。
    • 如果该元素不是尾节点的话,就让该元素的下一个节点的prev指向该元素的上一个节点。如果是的话,就让last指向该元素的上一个节点。
  • 但是如果我们运用哨兵对象,忽略边界条件,思路可以是下面这样:
    • 直接让该元素上一节点的next指向该元素的next。
    • 直接让该元素下一节点的prev指向该元素的prev。

虽然哨兵可以降低某些操作的渐进时间界,例如在插入和删除的时候会节约O(1)的时间。但是,应该尽量慎用,因为哨兵对象将会消耗额外的存储空间。

参考:《算法导论》

posted @ 2020-01-16 20:07  天乔巴夏丶  阅读(332)  评论(0编辑  收藏  举报