剑指Offer(专项突破版)刷题笔记 | 第四章 链表

184 阅读8分钟

4.1 链表的基础知识

特点

  • 可以实现灵活的内存动态管理
  • 遍历的时间复杂度为O(n)O(n)

单向链表节点

public class ListNode{
    public int val;
    public ListNode next;
    
    public ListNode(int val){
        this.val = val;
    }
}

4.2 哨兵节点

用于简化创建或删除链表头节点操作的代码

链表插入操作

public ListNode append(ListNode head,int val){

    //dummy是哨兵
    ListNode dummy = new ListNode(0);
    dummy.next = head;

    ListNode newNode = new ListNode(val);
    ListNode node = dummy;//这样无需判断head是否为null了
    while(node.next != null){
        node =node.next;
    }

    node.next = newNode;
    return head;
}

链表删除操作

public ListNode delete(ListNode head,int val){
    ListNode dummy = new ListNode(0);
    dummy.next = head;

    ListNode node = dummy;
    while (node.next != null){
        if(node.next.val == val){
            node = node.next.next;
            break;
        }
        node = node.next;
    }
    return dummy.next;
}

4.3 双指针

前后双指针

两指针的相对位置确定后不改变,同时移动两个指针。用于查找链表的倒数第k个节点

Q21:删除倒数第k个节点

题目(中等):删除链表中的倒数第k个节点,要求只能遍历一次

示例 1:

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:

输入:head = [1], n = 1
输出:[]
public ListNode removeNthFromEnd(ListNode head,int k){
    ListNode dummy = new ListNode(0);
    dummy.next = head;

    ListNode front = dummy;
    ListNode back = dummy;

    for (int i = 0; i < k; i++) {
        front = front.next;
    }//两者位置差了k,front到倒数第一时,back到倒数k+1的位置

    while(front.next != null){//退出循环时,front停在最后一个节点,back停在要删除节点的前一节点
        front = front.next;
        back = back.next;
    }
    back.next = back.next.next;
    return dummy.next;
}

快慢双指针

设置两个指针的移动速度不同,用于寻找链表的分位点

Q22:链表中环的入口节点

题目(中等):如果一个链表中包含环,那么应该如何找到环的入口节点?从链表的头节点开始顺着next指针方向进入环的第1个节点为环的入口节点

解题思路

  • 获得环中的一个节点(利用快慢指针)
private ListNode getNodeInLoop(ListNode head){
    if(head ==null || head.next == null){
        return null;
    }
    //每次多走一步,再次相遇快指针一定是多走了环元素个数的整数倍
    ListNode slow = head;
    ListNode fast = head;

    while(fast != null && fast.next != null){
        slow = slow.next;
        fast = fast.next.next;

        if(slow == fast){
            return slow;
        }
    }
    return null;
}
  • getNodeInLoop中,若当慢指针走k步后相遇,快指针走2k,两者的差值k(此时慢指针位置为k+1,快指针位置为2k+1)必定是环中节点数目的倍数,这时引入node指向链表头(位置为1),把之前的慢指针和node指针当作前后指针(两者相差k),因此两指针相遇的节点正好是入口节点。
public ListNode detectCycle(ListNode head){
    ListNode inLoop = getNodeInLoop(head);
    if(inLoop == null){
        return null;
    }

    ListNode node = head;
    while(node != inLoop){
        node = node.next;
        inLoop = inLoop.next;
    }
    return node;
}

Q23:两个链表的第1个重合节点

题目(简单):输入两个单向链表,请问如何找到它们的第一个重合节点。

示例 1:

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A[4,1,8,4,5],链表 B[5,0,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

示例 2:

输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A[2,6,4],链表 B[1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。

解题思路

  1. 将最后一个重合节点与其中一个单向链表的头节点相连,问题转化同Q22求入口节点
  2. 利用栈从后往前判断(会用到额外的空间)
  3. 先的到两个单向链表的长度,将短的那个链表指针先前移两者的长度差,使二者同时到达重合节点。
public ListNode getIntersectionNode(ListNode headA,ListNode headB){
    int count1 = countList(headA);
    int count2 = countList(headB);
    int delta = Math.abs(count1-count2);
    ListNode longer = count1 > count2 ? headA : headB;
    ListNode shorter = count1 < count2 ? headA : headB;
    for (int i = 0; i < delta; i++) {
        longer = longer.next;
    }

    while(longer != shorter){
        longer = longer.next;
        shorter = shorter.next;
    }
    return shorter;
}
//计算单向链表长度
public int countList(ListNode head){
    int count = 0;
    while(head != null){
        count++;
        head = head.next;
    }
    return count;
}

Q24:反转链表

题目(简单):定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点

示例 1:

输入: head = [1,2,3,4,5]
输出: [5,4,3,2,1]

解题思路

需要三个指针来记录现在,前,后节点。

public ListNode reverseList(ListNode head){
    ListNode cur = head;
    ListNode prev = null;
    
    while(cur != null){//退出循环的时候cur=null,prev才是最后要求返回的
        ListNode next = cur.next;//保存后一节点
        
        cur.next = prev;//指向前一节点(反转)
        
        prev = cur;//保存当前节点作为下一次反转的前一节点
        cur = next;//当前节点后移
    }
    return prev;
}

Q25:链表中的数字相加

题目(中等):给定两个表示非负整数的单向链表,请问如何实现这两个整数的相加并且把它们的和仍然用单向链表表示?链表中的每个节点表示整数十进制的一位,并且头节点对应整数的最高位数而尾节点对应整数的个位数

示例1:

输入: l1 = [7,2,4,3], l2 = [5,6,4]
输出: [7,8,0,7]

PS:不要忘记考虑溢出的情况

解题思路

因为需要从尾节点对齐开始相加,可以先将链表进行反转

public ListNode addTwoNumbers(ListNode headA, ListNode headB){
    headA = reverseList(headA);
    headB = reverseList(headB);

    ListNode dummy = new ListNode(0);
    ListNode sumNode = dummy;

    int flag = 0;
    while (headA != null || headB != null){
        //两个数相加取个位
        int sum = ((headA == null ? 0 : headA.val) + (headB == null ? 0 : headB.val) + flag) % 10;
        //进位标志
        flag = ((headA == null ? 0 : headA.val) + (headB == null ? 0 : headB.val) + flag) / 10;

        sumNode.next = new ListNode(sum);
        sumNode = sumNode.next;
        headA = headA == null ? null : headA.next;
        headB = headB == null ? null : headB.next;
    }
    if(flag == 1){
        sumNode.next = new ListNode(1);
    }
    return reverseList(dummy.next);
}

Q26:重排链表

题目(中等):给定一个链表,链表中节点的顺序是L0>L1>L2>...>Ln1>LnL_0->L_1->L_2->...->L_{n-1}->L_n,请问如何重排链表是节点的顺序变成L0>Ln>L1>Ln1>L2>Ln2>...L_0->L_n->L_1->L_{n-1}->L_2->L_{n-2}->...

示例 2:

输入: head = [1,2,3,4,5]
输出: [1,5,2,4,3]

解题思路

  1. 将链表分成两半(快慢指针)
  2. 反转后一链表
  3. 两链表穿插连接
public ListNode reorderList(ListNode head){
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    ListNode fast = dummy;
    ListNode slow = dummy;

    //分成两个子链,无论奇偶总是前一子链较长
    while(fast != null && fast.next != null){//退出循环时fast在尾节点的后一节点位置
        slow = slow.next;
        fast = fast.next;
        if (fast.next != null){
            fast = fast.next;
        }
    }
    ListNode head2 = slow.next;
    slow.next = null;
    ListNode head1 = dummy.next;
    head2 = reverseList(head2);
    //穿插连接
    ListNode node = dummy;
    while(head1 != null || head2 != null){
        dummy.next = head1;
        head1 = head1.next;

        dummy = dummy.next;

        if(head2 != null) {
            dummy.next = head2;
            head2 = head2.next;
        }
        dummy = dummy.next;
    }
    return node.next;
}

Q27:回文链表

题目(简单):如何判断一个链表是不是回文?要求解法的时间复杂度是O(n)O(n),并且不得使用超过O(1)O(1)的辅助空间。如果一个链表是回文,那么链表的节点序列从前往后看和从后往前看是相同的

示例 1:

输入: head = [1,2,3,3,2,1]
输出: true
public boolean isPalindrome(ListNode head){
    if(head == null || head.next == null) return true;//空或者只有一个元素时返回true

    ListNode slow = head;
    ListNode fast = head.next;//为了使慢指针停在重复回文的末尾

    while(fast != null && fast.next != null){
        slow = slow.next;
        fast = fast.next.next;
    }
    ListNode head2 = slow.next;
    if(fast.next != null){
        head2 = slow.next.next;
    }
    slow.next = null;

    return equals(head2,reverseList(head));
}
//判断是否相等
private boolean equals(ListNode head1,ListNode head2){
    while (head1 != null){
        if(head1.val == head2.val){
            return false;
        }
        head1 = head1.next;
        head2 = head2.next;
    }
    return head1 == null && head2 == null;
}

双向链表和循环链表

Q28:展开多级双向链表

题目(中等):在一个多级双向链表中,节点除了有两个指针分别指向前后两个节点,还有一个指针指向它的子链表,并且子链表也是个双向链表,它的节点也有指向子链表的指针。请将这样的多级双向链表展平成普通的双向链表,即所有节点都没有子链表

输入:head = [1,2,3,4,5,6,null,null,null,7,8,9,10,null,null,11,12]
输出:[1,2,3,7,8,11,12,9,10,4,5,6]
解释:

输入的多级列表如下图所示:

扁平化后的链表如下图:

解题思路

  1. 没有子链的node节点时,往下走即可,尾节点tail就是node节点
  2. 遇到有子链的node节点时,将node节点和node.child、childTail和node.next分别创建前后指针关系
public Node flatten(Node head){
    flattenGetTail(head);
    return head;
}

private Node flattenGetTail(Node head){
    Node node = head;
    Node tail = null;


    while(node != null){
        //保存node的下一节点
        Node next = node.next;
        if(node.child != null){
            Node child = node.child;
            //当前node节点和child节点创建连接
            node.next = node.child;
            child.prev = node;
            //node的child指针设置为null
            node.child = null;

            Node childTail = flattenGetTail(child);
            //创建后指针
            childTail.next = next;
            //如果node后面还有元素,创建前指针
            if(next != null){
                next.prev = childTail;
            }
            tail = childTail;
        }
        else{
            tail = node;
        }
        node = next;
    }
    return tail;
}

Q29:排序的循环链表

题目(中等):在一个循环链表中节点的值递增排序,请设计一个算法在该循环链表中插入节点,并保证插入节点之后的循环链表仍然是排序的

输入:head = [3,4,1], insertVal = 2
输出:[3,4,1,2]
解释:在上图中,有一个包含三个元素的循环有序列表,你获得值为 3 的节点的指针,我们需要向表中插入元素 2。
新插入的节点应该在 1 和 3 之间,插入之后,整个列表如上图所示,最后返回节点 3 。
public ListNode insert(ListNode head,int insertVal){
    ListNode node = new ListNode(insertVal);
    
    if(head == null){
        head = node;
        head.next = head;
    }else if(head.next == null){
        head.next = node;
        node.next = head;
    }else{
        insertCore(head,node);
    }
    return head;
}

private void insertCore(ListNode head,ListNode node){
    ListNode cur = head;
    ListNode next = head.next;
    
    ListNode biggest = head;
    //找到最大值的位置
    while(!(cur.val <= node.val && cur.val >= node.val) && next != head){
        cur = next;
        next = next.next;
        if(cur.val >= biggest.val){
            biggest = cur;
        }
    }
    
    if(cur.val <= node.val && cur.val >= node.val){//在最大最小范围之间
        cur.next = node;
        node.next = next;
    }else{
        node.next = biggest.next;
        biggest.next = node;
    }
}

快慢指针分链表有以下几种方式供选择

  1. 用到哨兵情况
  • fastslow开始都指向dummy,配合while判断条件为fast != null && fast.next != null
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode fast = dummy;
ListNode slow = dummy;
while(fast.next != null && fast.next.next != null){
    slow = slow.next;
    fast = fast.next.next;
}
  • fastslow开始都指向dummy,配合while判断条件为fast != null && fast.next != null
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode fast = dummy;
ListNode slow = dummy;
while(fast != null && fast.next != null){
    slow = slow.next;
    fast = fast.next.next;
}

以下代码也可以实现同样的分割效果,但是fast最后会停在链表尾节点

ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode fast = dummy;
ListNode slow = dummy;
while(fast != null && fast.next != null){//退出循环时fast在尾节点的后一节点位置
    slow = slow.next;
    fast = fast.next;
    if (fast.next != null){
        fast = fast.next;
    }
}
  1. 不用使用哨兵,直接指向头节点情况采用相同方法分析