剑指 offer 第 2 天
阅读原文时间:2023年07月08日阅读:2

第 2 天

剑指 Offer 06. 从尾到头打印链表

输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。

示例 1:

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

限制:

0 <= 链表长度 <= 10000

题解思路:辅助栈、反向填充、递归

辅助栈:利用栈先进后出的性质,实现反向节点

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public int[] reversePrint(ListNode head) {
        Stack<ListNode> stack = new Stack<ListNode>();
        ListNode temp = head;
        while (temp != null) {
            stack.push(temp);
            temp = temp.next;
        }
        int size = stack.size();
        int[] print = new int[size];
        for (int i = 0; i < size; i++) {
            print[i] = stack.pop().val;
        }
        return print;
    }
}

反向填充:反向填充数组下标

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public int[] reversePrint(ListNode head) {
        ListNode currNode = head;
        int len = 0;
        while (currNode != null) {
            len ++;
            currNode = currNode.next;
        }
        int[] print = new int[len];

        for (int i = len-1; i >= 0; i --) {
            print[i] = head.val;
            head = head.next;
        }
        return print;
    }
}

递归:利用递归走到链表最后结点

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public int[] reversePrint(ListNode head) {
        ArrayList<Integer> tmp = new ArrayList<Integer>();
        recur(head, tmp);
        int[] res = new int[tmp.size()];
        for(int i = 0; i < res.length; i++)
            res[i] = tmp.get(i);
        return res;
    }
    void recur(ListNode head, ArrayList<Integer> tmp) {
        if(head == null) return;
        recur(head.next, tmp);
        tmp.add(head.val);
    }
}

剑指 Offer 24. 反转链表

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

示例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

限制:

0 <= 节点个数 <= 5000

题解思路:辅助栈、递归、迭代

辅助栈:利用栈结构先进后出的特点反转链表

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null) {
            return head;
        }
        ListNode curr = head;
        Stack<ListNode> stack = new Stack<>();
        while (curr != null) {
            stack.push(curr);
            curr = curr.next;
        }
        ListNode listNode = stack.pop();
        ListNode res = listNode;
        while (!stack.isEmpty()) {
            listNode.next = stack.pop();
            listNode = listNode.next;
            listNode.next = null;
        }
        return res;
    }
}

递归:通过递归利用系统栈反转链表,递归到最后一个节点中止,途中完成 head.next.next = head 的交换

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode newHead = reverseList(head.next);
        head.next.next = head;
        head.next = null;
        return newHead;
    }
}

迭代:通过头插法再次重建链表

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {
            // 暂存后续节点
            ListNode next = curr.next;
            // 修改节点指向
            curr.next = prev;
            // 暂存当前头节点
            prev = curr;
            // 访问下一个节点
            curr = next;
        }
        return prev;
    }
}

反转链表的题目不止于此,目前常考察的还有区间反转、多次区间反转等

92. 反转链表 II

给你单链表的头指针 head 和两个整数 leftright ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表

示例 1:

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

示例 2:

输入:head = [5], left = 1, right = 1
输出:[5]

提示:

  • 链表中节点数目为 n
  • 1 <= n <= 500
  • -500 <= Node.val <= 500
  • 1 <= left <= right <= n

进阶: 你可以使用一趟扫描完成反转吗?

题解思路:两次遍历、一次遍历

两次遍历:先断链,然后反转中间部分,再接上链表

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode reverseBetween(ListNode head, int left, int right) {
        // 使用额外头节点
        ListNode dummyNode = new ListNode(-1);
        dummyNode.next = head;
        ListNode pre = dummyNode;

        // 得到left的前一个节点
        for (int i = 0; i < left-1; i ++) {
            pre = pre.next;
        }

        // 得到right节点
        ListNode rightNode = pre;
        for (int i = 0; i < right-left+1; i ++) {
            rightNode = rightNode.next;
        }

        // 截断链表
        ListNode leftNode = pre.next;

        // 保存rightNode后续节点
        ListNode curr = rightNode.next;

        // 截断链接
        pre.next = null;
        rightNode.next = null;

        // 反转链表
        reverseLinkedList(leftNode);

        // 恢复连接
        pre.next = rightNode;
        leftNode.next = curr;
        return dummyNode.next;
    }
    // 反转链表
    private void reverseLinkedList(ListNode head) {
        ListNode curr = head;
        ListNode pre = null;
        while (curr != null) {
            ListNode next = curr.next;
            curr.next = pre;
            pre = curr;
            curr = next;
        }
    }
}

复杂度: 时间 O(n) 空间 O(1)

一次遍历:头插法建立链表的方式完成反转

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode reverseBetween(ListNode head, int left, int right) {
        ListNode dummyNode = new ListNode(-1);
        dummyNode.next = head;
        ListNode pre = dummyNode;
        for (int i = 0; i < left - 1; i ++) {
            pre = pre.next;
        }
        ListNode curr = pre.next;
        // 步骤记熟即可
        for (int i = 0; i < right-left; i ++) {
            // 保存后续节点
            ListNode next = curr.next;
            // 改变当前节点指向
            curr.next = next.next;
            // 改变next节点指向
            next.next = pre.next;
            // 改变pre节点指向
            pre.next = next;
        }
        return dummyNode.next;

    }
}

复杂度: 时间 O(n) 空间 O(1)

25. K 个一组翻转链表

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。

k 是一个正整数,它的值小于或等于链表的长度。

如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

进阶:

  • 你可以设计一个只使用常数额外空间的算法来解决此问题吗?
  • 你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

示例 1:

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

示例 2:

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

示例 3:

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

示例 4:

输入:head = [1], k = 1
输出:[1]

提示:

  • 列表中节点的数量在范围 sz
  • 1 <= sz <= 5000
  • 0 <= Node.val <= 1000
  • 1 <= k <= sz

题解思路:多次区间反转

多次区间反转:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode dummyNode = new ListNode(-1);
        dummyNode.next = head;
        // pre 代表待翻转链表的前驱
        ListNode pre = dummyNode;
        // end 代表待翻转链表的末尾
        ListNode end = dummyNode;
        while (end.next != null) {
            for (int i = 0; i < k && end != null; i ++) {
                end = end.next;
            }
            if (end == null) {
                break;
            }
            // 区间反转类似,保存反转前节点,反转后接上
            ListNode start = pre.next;
            ListNode next = end.next;
            end.next = null;
            pre.next = reverse(start);
            start.next = next;
            pre = start;
            end = pre;

        }
        return dummyNode.next;
    }
    private ListNode reverse(ListNode head) {
        ListNode pre = null;
        ListNode curr = head;
        while (curr != null) {
            ListNode next = curr.next;
            curr.next = pre;
            pre = curr;
            curr = next;
        }
        return pre;
    }

}

复杂度:时间 O(n) 空间 O(1)

剑指 Offer 35. 复杂链表的复制

请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null

示例 1:

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

示例 2:

输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]

示例 3:

输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]

示例 4:

输入:head = []
输出:[]
解释:给定的链表为空(空指针),因此返回 null。

提示:

  • -10000 <= Node.val <= 10000
  • Node.random 为空(null)或指向链表中的节点。
  • 节点数目不超过 1000 。

解题思路:哈希表复制、节点拆分

哈希表复制:利用HashMap先存储一次全部节点,再将其取出重建链表即可

/*
// Definition for a Node.
class Node {
    int val;
    Node next;
    Node random;

    public Node(int val) {
        this.val = val;
        this.next = null;
        this.random = null;
    }
}
*/
class Solution {
    public Node copyRandomList(Node head) {
        if (head == null) {
            return head;
        }
        // key存放原节点,value存放待复制节点
        Map<Node, Node> map = new HashMap<>();
        // 节点复制val
        for (Node cur = head; cur != null; cur = cur.next) {
            map.put(cur, new Node(cur.val));
        }
        // 遍历链表,利用key中原节点信息填充value节点中的next和random
        for (Node cur = head; cur != null; cur = cur.next) {
            // 取出并赋值
            map.get(cur).next = map.get(cur.next);
            map.get(cur).random = map.get(cur.random);
        }
        return map.get(head);
    }
}

复杂度:时间 O(n) 空间 O(n)

节点拆分:在每个节点相邻位置创建一个相同节点,先复制 next 和 val 域,再遍历一次复制random域,最后拆分两条链表

/*
// Definition for a Node.
class Node {
    int val;
    Node next;
    Node random;

    public Node(int val) {
        this.val = val;
        this.next = null;
        this.random = null;
    }
}
*/
class Solution {
    public Node copyRandomList(Node head) {
        if (head == null) {
            return null;
        }

        Node cur = head;
        // 1. 复制各节点,并构建拼接链表
        while(cur != null) {
            Node tmp = new Node(cur.val);
            tmp.next = cur.next;
            cur.next = tmp;
            cur = tmp.next;
        }
        // 2. 构建各新节点的 random 指向
        cur = head;
        while(cur != null) {
            if(cur.random != null)
                cur.next.random = cur.random.next;
            cur = cur.next.next;
        }
        // 3. 拆分两链表
        cur = head.next;
        Node pre = head, res = head.next;
        while(cur.next != null) {
            pre.next = pre.next.next;
            cur.next = cur.next.next;
            pre = pre.next;
            cur = cur.next;
        }
        pre.next = null; // 单独处理原链表尾节点
        return res;      // 返回新链表头节点
    }
}

复杂度:时间 O(n) 空间 O(1)