链表题目:设计链表

文章目录

  • 题目
    • 标题和出处
    • 难度
    • 题目描述
      • 要求
      • 示例
      • 数据范围
  • 解法一
    • 思路和算法
    • 代码
    • 复杂度分析
  • 解法二
    • 思路和算法
    • 代码
    • 复杂度分析

题目

标题和出处

标题:设计链表

出处:707. 设计链表

难度

5 级

题目描述

要求

设计链表的实现。你可以选择使用单向链表或双向链表。

单向链表中的结点应该具有两个属性: val \texttt{val} val next \texttt{next} next val \texttt{val} val 是当前结点的值, next \texttt{next} next 是指向下一个结点的指针/引用。

如果要使用双向链表,则还需要一个属性 prev \texttt{prev} prev 以指示链表中的上一个结点。假设链表中的结点下标从 0 \texttt{0} 0 开始。

实现 MyLinkedList \texttt{MyLinkedList} MyLinkedList 类:

  • MyLinkedList() \texttt{MyLinkedList()} MyLinkedList():初始化 MyLinkedList \texttt{MyLinkedList} MyLinkedList 对象。
  • int   get(int   index) \texttt{int get(int index)} int get(int index):获取链表中第 index \texttt{index} index 个结点的值。如果索引无效,则返回 -1 \texttt{-1} -1
  • void   addAtHead(int   val) \texttt{void addAtHead(int val)} void addAtHead(int val):在链表的第一个元素之前添加一个值为 val \texttt{val} val 的结点。添加后,新结点将成为链表的第一个结点。
  • void   addAtTail(int   val) \texttt{void addAtTail(int val)} void addAtTail(int val):将值为 val \texttt{val} val 的结点追加到链表的最后一个元素。
  • void   addAtIndex(int   index,   int   val) \texttt{void addAtIndex(int index, int val)} void addAtIndex(int index, int val):在链表中的第 index \texttt{index} index 个结点之前添加值为 val \texttt{val} val 的结点。如果 index \texttt{index} index 等于链表的长度,则该结点将附加到链表的末尾。如果 index \texttt{index} index 大于链表长度,则不会添加该结点。
  • void   deleteAtIndex(int   index) \texttt{void deleteAtIndex(int index)} void deleteAtIndex(int index):如果索引 index \texttt{index} index 有效,则删除链表中的第 index \texttt{index} index 个结点。

示例

示例 1:

输入:
["MyLinkedList",   "addAtHead",   "addAtTail",   "addAtIndex",   "get",   "deleteAtIndex",   "get"] \texttt{["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"]} ["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"]
[[],   [1],   [3],   [1,   2],   [1],   [1],   [1]] \texttt{[[], [1], [3], [1, 2], [1], [1], [1]]} [[], [1], [3], [1, 2], [1], [1], [1]]
输出:
[null,   null,   null,   null,   2,   null,   3] \texttt{[null, null, null, null, 2, null, 3]} [null, null, null, null, 2, null, 3]
解释:
MyLinkedList   linkedList   =   new   MyLinkedList(); \texttt{MyLinkedList linkedList = new MyLinkedList();} MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1); \texttt{linkedList.addAtHead(1);} linkedList.addAtHead(1);
linkedList.addAtTail(3); \texttt{linkedList.addAtTail(3);} linkedList.addAtTail(3);
linkedList.addAtIndex(1,   2); \texttt{linkedList.addAtIndex(1, 2);} linkedList.addAtIndex(1, 2); // 链表变为 1 → 2 → 3 \texttt{1} \rightarrow \texttt{2} \rightarrow \texttt{3} 123
linkedList.get(1); \texttt{linkedList.get(1);} linkedList.get(1); // 返回 2 \texttt{2} 2
linkedList.deleteAtIndex(1); \texttt{linkedList.deleteAtIndex(1);} linkedList.deleteAtIndex(1); // 现在链表是 1 → 3 \texttt{1} \rightarrow \texttt{3} 13
linkedList.get(1); \texttt{linkedList.get(1);} linkedList.get(1); // 返回 3 \texttt{3} 3

数据范围

  • 0 ≤ index,   val ≤ 1000 \texttt{0} \le \texttt{index, val} \le \texttt{1000} 0index, val1000
  • 请不要使用内置的 LinkedList \texttt{LinkedList} LinkedList
  • 最多调用 2000 \texttt{2000} 2000 get \texttt{get} get addAtHead \texttt{addAtHead} addAtHead addAtTail \texttt{addAtTail} addAtTail addAtIndex \texttt{addAtIndex} addAtIndex deleteAtIndex \texttt{deleteAtIndex} deleteAtIndex

解法一

思路和算法

使用单向链表时,维护伪头结点 pseudoHead \textit{pseudoHead} pseudoHead 和链表的长度 size \textit{size} size。伪头结点的作用是方便各项操作,并不是链表的实际头结点,链表的实际头结点为伪头结点的后一个结点。链表的长度为链表的结点数,不包括伪头结点,初始时 size = 0 \textit{size} = 0 size=0

对于 get \textit{get} get 方法,只有当参数 index \textit{index} index 满足 0 ≤ index < size 0 \le \textit{index} < \textit{size} 0index<size 时才是有效的下标,当下标无效时返回 − 1 -1 1。当下标有效时,从伪头结点开始向后移动 index + 1 \textit{index} + 1 index+1 次,定位到下标为 index \textit{index} index 的结点,将该结点的值返回。

对于 addAtHead \textit{addAtHead} addAtHead 方法,需要创建一个值为参数 val \textit{val} val 的结点 addNode \textit{addNode} addNode,将该结点添加到伪头结点和实际头结点之间。依次执行 addNode . next : = pseudoHead . next \textit{addNode}.\textit{next} := \textit{pseudoHead}.\textit{next} addNode.next:=pseudoHead.next pseudoHead . next : = addNode \textit{pseudoHead}.\textit{next} := \textit{addNode} pseudoHead.next:=addNode,即完成结点的添加操作,添加结点之后需要将 size \textit{size} size 的值加 1 1 1

对于 addAtTail \textit{addAtTail} addAtTail 方法,需要创建一个值为参数 val \textit{val} val 的结点 addNode \textit{addNode} addNode,将该结点添加到尾结点之后。遍历链表,定位到尾结点 node \textit{node} node,令 node . next : = addNode \textit{node}.\textit{next} := \textit{addNode} node.next:=addNode,即完成结点的添加操作,添加结点之后需要将 size \textit{size} size 的值加 1 1 1

对于 addAtIndex \textit{addAtIndex} addAtIndex 方法,只有当参数 index \textit{index} index 满足 0 ≤ index ≤ size 0 \le \textit{index} \le \textit{size} 0indexsize 时才是有效的下标,当下标无效时不添加结点。当下标有效时,需要创建一个值为参数 val \textit{val} val 的结点 addNode \textit{addNode} addNode,从伪头结点开始向后移动 index \textit{index} index 次,定位到下标为 index − 1 \textit{index} - 1 index1 的结点 node \textit{node} node,即待添加结点的前一个结点。依次执行 addNode . next : = node . next \textit{addNode}.\textit{next} := \textit{node}.\textit{next} addNode.next:=node.next node . next : = addNode \textit{node}.\textit{next} := \textit{addNode} node.next:=addNode,即完成结点的添加操作,添加结点之后需要将 size \textit{size} size 的值加 1 1 1

对于 deleteAtIndex \textit{deleteAtIndex} deleteAtIndex 方法,只有当参数 index \textit{index} index 满足 0 ≤ index < size 0 \le \textit{index} < \textit{size} 0index<size 时才是有效的下标,当下标无效时不删除结点。当下标有效时,从伪头结点开始向后移动 index \textit{index} index 次,定位到下标为 index − 1 \textit{index} - 1 index1 的结点 node \textit{node} node,即待删除结点的前一个结点。需要删除的结点是 node \textit{node} node 的后一个结点,将 node \textit{node} node next \textit{next} next 指针更改为指向待删除结点的后一个结点即可完成删除,执行 node . next : = node . next . next \textit{node}.\textit{next} := \textit{node}.\textit{next}.\textit{next} node.next:=node.next.next,即完成结点的删除操作,删除结点之后需要将 size \textit{size} size 的值减 1 1 1

代码

class MyLinkedList {
    Node pseudoHead;
    int size;

    public MyLinkedList() {
        pseudoHead = new Node(0);
        size = 0;
    }
    
    public int get(int index) {
        if (index >= size) {
            return -1;
        }
        Node node = pseudoHead;
        for (int i = 0; i <= index; i++) {
            node = node.next;
        }
        return node.val;
    }
    
    public void addAtHead(int val) {
        Node addNode = new Node(val);
        addNode.next = pseudoHead.next;
        pseudoHead.next = addNode;
        size++;
    }
    
    public void addAtTail(int val) {
        Node addNode = new Node(val);
        Node node = pseudoHead;
        while (node.next != null) {
            node = node.next;
        }
        node.next = addNode;
        size++;
    }
    
    public void addAtIndex(int index, int val) {
        if (index > size) {
            return;
        }
        Node addNode = new Node(val);
        Node node = pseudoHead;
        for (int i = 0; i < index; i++) {
            node = node.next;
        }
        addNode.next = node.next;
        node.next = addNode;
        size++;
    }
    
    public void deleteAtIndex(int index) {
        if (index >= size) {
            return;
        }
        Node node = pseudoHead;
        for (int i = 0; i < index; i++) {
            node = node.next;
        }
        node.next = node.next.next;
        size--;
    }
}

class Node {
    int val;
    Node next;

    public Node(int val) {
        this.val = val;
    }
}

复杂度分析

  • 时间复杂度:构造方法的时间复杂度是 O ( 1 ) O(1) O(1),方法 get \textit{get} get 的时间复杂度是 O ( n ) O(n) O(n),方法 addAtHead \textit{addAtHead} addAtHead 的时间复杂度是 O ( 1 ) O(1) O(1),方法 addAtTail \textit{addAtTail} addAtTail 的时间复杂度是 O ( n ) O(n) O(n),方法 addAtIndex \textit{addAtIndex} addAtIndex 的时间复杂度是 O ( n ) O(n) O(n),方法 deleteAtIndex \textit{deleteAtIndex} deleteAtIndex 的时间复杂度是 O ( n ) O(n) O(n),其中 n n n 是链表的长度。
    构造方法初始化伪头结点和链表长度,时间复杂度是 O ( 1 ) O(1) O(1)
    方法 addAtHead \textit{addAtHead} addAtHead 直接在伪头结点后面添加结点,不需要遍历链表,时间复杂度是 O ( 1 ) O(1) O(1)
    方法 addAtTail \textit{addAtTail} addAtTail 需要遍历链表定位到尾结点然后添加结点,时间复杂度是 O ( n ) O(n) O(n)
    其余方法都需要在下标有效的情况下遍历链表,定位到指定下标的结点,然后进行返回结点值、添加结点或删除结点的操作,遍历链表的最坏情况下的时间复杂度都是 O ( n ) O(n) O(n),遍历链表之后的操作的时间复杂度是 O ( 1 ) O(1) O(1),因此执行一个方法的时间复杂度是 O ( n ) O(n) O(n)

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是链表的长度。需要创建长度为 n n n 的链表。

解法二

思路和算法

使用双向链表时,维护伪头结点 pseudoHead \textit{pseudoHead} pseudoHead、伪尾结点 pseudoTail \textit{pseudoTail} pseudoTail 和链表的长度 size \textit{size} size。伪头结点和伪尾结点的作用是方便各项操作,并不是链表的实际头结点和实际尾结点,链表的实际头结点为伪头结点的后一个结点,链表的实际尾结点为伪尾结点的前一个结点(只有当链表不为空时才存在实际头结点和实际尾结点)。初始时 pseudoHead \textit{pseudoHead} pseudoHead pseudoTail \textit{pseudoTail} pseudoTail 相邻。链表的长度为链表的结点数,不包括伪头结点和伪尾结点,初始时 size = 0 \textit{size} = 0 size=0

和单向链表相比,双向链表多了伪尾结点,遍历时可以向前移动或向后移动,因此每种功能的时间复杂度更优。对于 addAtHead \textit{addAtHead} addAtHead 方法和 addAtTail \textit{addAtTail} addAtTail 方法,时间复杂度都是 O ( 1 ) O(1) O(1)。对于 get \textit{get} get 方法、 addAtIndex \textit{addAtIndex} addAtIndex 方法和 deleteAtIndex \textit{deleteAtIndex} deleteAtIndex 方法,需要定位到下标为 index \textit{index} index 的结点或者相邻下标的结点,可以根据 index \textit{index} index size \textit{size} size 计算从头结点向后遍历和从尾结点向前遍历分别需要经过的结点数,选择需要经过的结点数较少的一种遍历方案。

在添加和删除结点时,需要对当前结点和前后相邻结点的 prev \textit{prev} prev next \textit{next} next 指针进行更新。

对于 get \textit{get} get 方法,只有当参数 index \textit{index} index 满足 0 ≤ index < size 0 \le \textit{index} < \textit{size} 0index<size 时才是有效的下标,当下标无效时返回 − 1 -1 1。当下标有效时,从伪头结点开始向后移动 index + 1 \textit{index} + 1 index+1 次或者从伪尾结点开始向前移动 size − index \textit{size} - \textit{index} sizeindex 次,定位到下标为 index \textit{index} index 的结点,将该结点的值返回。

对于 addAtHead \textit{addAtHead} addAtHead 方法,需要创建一个值为参数 val \textit{val} val 的结点 addNode \textit{addNode} addNode,将该结点添加到伪头结点和实际头结点之间。添加结点之后需要将 size \textit{size} size 的值加 1 1 1

对于 addAtTail \textit{addAtTail} addAtTail 方法,需要创建一个值为参数 val \textit{val} val 的结点 addNode \textit{addNode} addNode,将该结点添加到伪尾结点和实际尾结点之间。添加结点之后需要将 size \textit{size} size 的值加 1 1 1

对于 addAtIndex \textit{addAtIndex} addAtIndex 方法,只有当参数 index \textit{index} index 满足 0 ≤ index ≤ size 0 \le \textit{index} \le \textit{size} 0indexsize 时才是有效的下标,当下标无效时不添加结点。当下标有效时,需要创建一个值为参数 val \textit{val} val 的结点 addNode \textit{addNode} addNode,然后遍历到待添加结点的前后相邻结点,完成添加操作:

  • 从伪头结点开始向后移动,需要移动 index \textit{index} index 次,定位到下标为 index − 1 \textit{index} - 1 index1 的结点 node \textit{node} node,即待添加结点的前一个结点,然后将 addNode \textit{addNode} addNode 添加到 node \textit{node} node 的后面;

  • 从伪尾结点开始向前移动,需要移动 size − index \textit{size} - \textit{index} sizeindex 次,定位到下标为 index \textit{index} index 的结点 node \textit{node} node,即待添加结点的后一个结点(注意在添加结点之后,该结点的下标会后移一位变成 index + 1 \textit{index} + 1 index+1,因此在添加结点之前,下标为 index \textit{index} index 的结点即为待添加结点的后一个结点),然后将 addNode \textit{addNode} addNode 添加到 node \textit{node} node 的前面。

对于 deleteAtIndex \textit{deleteAtIndex} deleteAtIndex 方法,只有当参数 index \textit{index} index 满足 0 ≤ index < size 0 \le \textit{index} < \textit{size} 0index<size 时才是有效的下标,当下标无效时不删除结点。当下标有效时,从伪头结点开始向后移动 index \textit{index} index 次或者从伪尾结点开始向前移动 size − index − 1 \textit{size} - \textit{index} - 1 sizeindex1 次,定位到下标为 index − 1 \textit{index} - 1 index1 或者 index + 1 \textit{index} + 1 index+1 的结点,然后将下标为 index \textit{index} index 的结点删除。删除结点之后需要将 size \textit{size} size 的值减 1 1 1

代码

class MyLinkedList {
    Node pseudoHead;
    Node pseudoTail;
    int size;

    public MyLinkedList() {
        pseudoHead = new Node(0);
        pseudoTail = new Node(0);
        pseudoHead.next = pseudoTail;
        pseudoTail.prev = pseudoHead;
        size = 0;
    }
    
    public int get(int index) {
        if (index >= size) {
            return -1;
        }
        boolean fromHead = index + 1 <= size - index;
        Node node = fromHead ? pseudoHead : pseudoTail;
        if (fromHead) {
            for (int i = 0; i <= index; i++) {
                node = node.next;
            }
        } else {
            for (int i = size - 1; i >= index; i--) {
                node = node.prev;
            }
        }
        return node.val;
    }
    
    public void addAtHead(int val) {
        Node addNode = new Node(val);
        Node nextNode = pseudoHead.next;
        addNode.next = nextNode;
        nextNode.prev = addNode;
        pseudoHead.next = addNode;
        addNode.prev = pseudoHead;
        size++;
    }
    
    public void addAtTail(int val) {
        Node addNode = new Node(val);
        Node prevNode = pseudoTail.prev;
        addNode.prev = prevNode;
        prevNode.next = addNode;
        pseudoTail.prev = addNode;
        addNode.next = pseudoTail;
        size++;
    }
    
    public void addAtIndex(int index, int val) {
        if (index > size) {
            return;
        }
        Node addNode = new Node(val);
        boolean fromHead = index <= size - index;
        Node node = fromHead ? pseudoHead : pseudoTail;
        if (fromHead) {
            for (int i = 0; i < index; i++) {
                node = node.next;
            }
            Node nextNode = node.next;
            addNode.next = nextNode;
            nextNode.prev = addNode;
            node.next = addNode;
            addNode.prev = node;
        } else {
            for (int i = size - 1; i >= index; i--) {
                node = node.prev;
            }
            Node prevNode = node.prev;
            addNode.prev = prevNode;
            prevNode.next = addNode;
            node.prev = addNode;
            addNode.next = node;
        }
        size++;
    }
    
    public void deleteAtIndex(int index) {
        if (index >= size) {
            return;
        }
        boolean fromHead = index <= size - index - 1;
        Node node = fromHead ? pseudoHead : pseudoTail;
        if (fromHead) {
            for (int i = 0; i < index; i++) {
                node = node.next;
            }
            Node nextNode = node.next.next;
            node.next = nextNode;
            nextNode.prev = node;
        } else {
            for (int i = size - 1; i > index; i--) {
                node = node.prev;
            }
            Node prevNode = node.prev.prev;
            node.prev = prevNode;
            prevNode.next = node;
        }
        size--;
    }
}

class Node {
    int val;
    Node prev;
    Node next;

    public Node(int val) {
        this.val = val;
    }
}

复杂度分析

  • 时间复杂度:构造方法的时间复杂度是 O ( 1 ) O(1) O(1),方法 get \textit{get} get 的时间复杂度是 O ( n ) O(n) O(n),方法 addAtHead \textit{addAtHead} addAtHead 的时间复杂度是 O ( 1 ) O(1) O(1),方法 addAtTail \textit{addAtTail} addAtTail 的时间复杂度是 O ( 1 ) O(1) O(1),方法 addAtIndex \textit{addAtIndex} addAtIndex 的时间复杂度是 O ( n ) O(n) O(n),方法 deleteAtIndex \textit{deleteAtIndex} deleteAtIndex 的时间复杂度是 O ( n ) O(n) O(n),其中 n n n 是链表的长度。
    构造方法初始化伪头结点、伪尾结点和链表长度,时间复杂度是 O ( 1 ) O(1) O(1)
    方法 addAtHead \textit{addAtHead} addAtHead 直接在伪头结点后面添加结点,不需要遍历链表,时间复杂度是 O ( 1 ) O(1) O(1)
    方法 addAtTail \textit{addAtTail} addAtTail 直接在伪尾结点前面添加结点,不需要遍历链表,时间复杂度是 O ( 1 ) O(1) O(1)
    其余方法都需要在下标有效的情况下遍历链表,定位到指定下标的结点,然后进行返回结点值、添加结点或删除结点的操作,遍历链表的最坏情况下的时间复杂度都是 O ( n ) O(n) O(n),遍历链表之后的操作的时间复杂度是 O ( 1 ) O(1) O(1),因此执行一个方法的时间复杂度是 O ( n ) O(n) O(n)

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是链表的长度。需要创建长度为 n n n 的链表。

你可能感兴趣的:(数据结构和算法,#,链表,链表)