剑指 offer (2) -- 链表篇

上一篇文章中对剑指 offer 中数组相关的题目进行了概括,这一篇文章是链表篇。一样地,若是各位大佬发现程序有什么 bug 或其余更巧妙的思路,欢迎交流学习。java

6. 从尾到头打印链表

题目描述node

输入一个链表的头节点,从尾到头打印链表的每一个节点的值。算法

这里能够用显式栈,或者递归来实现,都比较简单,也就很少作解释了。数组

递归实现bash

public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    if(listNode == null){
        return new ArrayList<>();
    }
    ArrayList<Integer> list = printListFromTailToHead(listNode.next);
    list.add(listNode.val);
    return list;
}
复制代码

栈实现dom

public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    ArrayList<Integer> list = new ArrayList<Integer>();
    if(listNode == null){
        return list;
    }
    Deque<Integer> stack = new LinkedList<>();
    ListNode node = listNode;
    while(node != null) {
        stack.push(node.val);
        node = node.next;
    }
    
    while(!stack.isEmpty()) {
        list.add(stack.pop());
    }
    return list;
}
复制代码

18. 删除链表的节点

题目一描述oop

在 O(1) 时间内删除链表指定节点。给定单链表的头节点引用和一个节点引用,要求在 O(1) 时间内删除该节点。学习

解题思路ui

通常来讲,要在单向链表中删除指定节点,须要获得被删除节点的前驱节点。但这须要从头节点开始顺序查找,时间复杂度确定不是 O(1) 了,因此须要换一种思路。spa

咱们能够将后继节点的值赋值给要删除的指定节点,再删除下一个节点,如此也一样实现了删除指定节点的功能。可是还须要注意两种特殊状况:

  • 第一种是要删除的节点是头节点,这时还须要对链表的头结点进行更新;
  • 第二种是要删除的节点是尾节点,它没有下一个节点,这时就只能从头节点开始顺序查找要删除节点的前驱节点了。

代码实现

public Node deleteNode(Node head, Node node) {
    if (head == null || node == null) {
        return head;
    }
    if (head == node) {
        // 要删除的节点是头节点
        return head.next;
    } else if (node.next == null) {
        // 要删除的节点是尾节点
        Node cur = head;
        while (cur.next != node) {
            cur = cur.next;
        }
        cur.next = null;
    } else {
        // 要删除的节点在链表中间
        ListNode nextNode = node.next;
        node.val = nextNode.val;
        node.next = nextNode.next;
    }
    return head;
}
复制代码

这里除了最后一个节点,其余节点均可以在 O(1) 时间内删除,只有要删除的节点是尾节点时,才须要对链表进行遍历,因此,整体的时间复杂度仍是 O(1)

题目二描述

在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5。

解题思路

这里要删除排序链表中的重复节点,因为头节点也可能被删除,因此须要对头节点特殊处理,或者添加一个虚拟节点。这里选择使用虚拟节点。

因为这里须要判断当前节点和下一个节点的值,因此循环中条件就是要判断当前节点和下一个节点均不能为空。若是这两个值不相等,则继续遍历。

若是不相等,则循环判断跳过连续重复的数个节点,最后 cur 指向这些重复节点的最后一个。因为重复节点不保留,因此须要让 pre.next 指向 cur.next,再更新 cur 为下一个节点 pre.next,进而继续判断。

代码实现

public Node deleteDuplication(Node head) {
    Node dummyHead = new Node(-1);
    dummyHead.next = head;

    Node pre = dummyHead;
    Node cur = head;
    while (cur != null && cur.next != null) {
        if (cur.value != cur.next.value) {
            pre = cur;
            cur = cur.next;
        } else {
            while (cur.next != null && cur.value == cur.next.value) {
                cur = cur.next;
            }
            pre.next = cur.next;
            cur = pre.next;
        }
    }

    return dummyHead.next;
}
复制代码

这里虽然有两层嵌套循环,但实际上只对链表遍历了一遍,因此其时间复杂度为 O(n)。另外只申请了一个虚拟节点,因此空间复杂度为 O(1)

22. 链表中倒数第 k 个节点

题目描述

输入一个链表,输出该链表中倒数第 k 个结点。(k 从 1 开始)

解题思路

这里能够定义两个指针。第一个指针从链表头开始遍历,向前移动 k - 1 步。而后从 k 步开始,第二个指针也开始从链表头开始遍历。

因为两个指针的距离为 k - 1,全部当第一个指针移动到链表的尾节点时,第二个指针正好移动到倒数第 k 个节点。

代码实现

public static ListNode findKthToTail(ListNode head, int k) {
    if (head == null || k <= 0) {
        return null;
    }

    ListNode fast = head;
    for (int i = 0; i < k - 1; i++) {
        if (fast.next == null) {
            return null;
        }
        fast = fast.next;
    }

    ListNode slow = head;
    while (fast.next != null) {
        fast = fast.next;
        slow = slow.next;
    }

    return slow;
}
复制代码

23. 链表中环的入口节点

题目描述

给一个链表,若其中包含环,请找出该链表的环的入口结点,不然,输出null。

解题思路

首先须要判断链表是否有环,可使用两个指针,同时从链表的头部开始遍历,一个指针一次走一步,一个指针一次走两步。若是快指针能追上慢指针,则表示链表有环;不然若是快指针走到了链表的末尾,表示没有环。

在找到环以后,定义一个指针指向链表的头节点,再选择刚才的慢指针从快慢指针的相遇节点开始,两个指针同时以每次一步向前移动,它们相遇的节点就是链表的入口节点。

代码实现

public ListNode EntryNodeOfLoop(ListNode pHead) {
    if(pHead == null || pHead.next == null) {
        return null;
    }
    
    ListNode slow = pHead.next;
    ListNode fast = slow.next;
    while(slow != fast) {
        if(fast == null || fast.next == null) {
            return null;
        }
        slow = slow.next;
        fast = fast.next.next;
    }
    
    ListNode p = pHead;
    while(slow != p) {
        slow = slow.next;
        p = p.next;
    }
    
    return slow;
}
复制代码

24. 反转链表

题目描述

输入一个链表,反转链表后,输出新链表的表头。

循环解决

思路以下图:

循环代码

public ListNode reverseList1(ListNode head) {
    ListNode newHead = null;
    ListNode cur = head;
    ListNode nex;
    while (cur != null) {
        nex = cur.next;
        
        cur.next = newHead;
        newHead = cur;
        // 记录
        cur = nex;
    }
    return newHead;
}
复制代码

递归解决

递归代码

public ListNode reverseList2(ListNode head) {
    if (head == null || head.next == null) {
        return head;
    }
    ListNode newHead = reverseList2(head.next);

    head.next.next = head;
    head.next = null;

    return newHead;
}
复制代码

25. 合并两个有序的链表

题目描述

输入两个单调递增的链表,输出两个链表合成后的链表,固然咱们须要合成后的链表知足单调不减规则。

循环解题

在使用循环时,首先须要肯定新链表的头节点,若是链表 first 的头节点的值小于链表 second 的头节点的值,那么链表 first 的头节点即是新链表的头节点。

而后循环处理两个链表中剩余的节点,若是链表 first 中的节点的值小于链表 second 中的节点的值,则将链表 first 中的节点添加到新链表的尾部,不然添加链表 second 中的节点。而后继续循环判断,直到某一条链表为空。

当其中一条链表为空后,只须要将另外一条链表所有连接到新链表的尾部。

思路图以下:

循环代码

public ListNode merge1(ListNode first, ListNode second) {
    if (first == null) {
        return second;
    }
    if (second == null) {
        return first;
    }
    ListNode p = first;
    ListNode q = second;
    ListNode newHead;
    if (p.val < q.val) {
        newHead = p;
        p = p.next;
    } else {
        newHead = q;
        q = q.next;
    }
    ListNode r = newHead;
    while (p != null && q != null) {
        if (p.val < q.val) {
            r.next = p;
            p = p.next;
        } else {
            r.next = q;
            q = q.next;
        }
        r = r.next;
    }
    if (p == null) {
        r.next = q;
    } else {
        r.next = p;
    }
    return newHead;
}
复制代码

递归解题

使用递归解决,比较简单。首先判断两条链表是否为空,若是 first 为空,则直接返回 second;若是 second 为空,则直接返回 first

接着判断链表 first 中节点的值和链表 second 中节点的值,若是 first 中节点的值较小,则递归地求 first.nextsecond 的合并链表,让 first.next 指向新的链表头节点,而后返回 first 便可。

另外一种状况相似,这里就再也不赘述了。

递归代码

public ListNode merge2(ListNode first, ListNode second) {
    if (first == null) {
        return second;
    }
    if (second == null) {
        return first;
    }

    if (first.val < second.val) {
        first.next = merge2(first.next, second);
        return first;
    } else {
        second.next = merge2(first, second.next);
        return second;
    }
}
复制代码

35. 复杂链表的复制

题目描述

输入一个复杂链表(每一个节点中有节点值,以及两个指针,一个指向下一个节点,另外一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。

解题思路

这能够分为三步来解决。第一步是根据原始链表的全部节点,将每一节点的复制节点连接到它的后面。

第二步设置复制出来的节点的特殊指针。若是原始链表的节点 p 的特殊指针指向节点 s,则复制出来的节点 cloned 的特殊指针就指向节点 s 的下一个节点。

第三部是将长链表拆分红两个链表,把全部偶数位置的节点链接起来就是新的复制出来的链表。

代码实现

public RandomListNode Clone(RandomListNode head) {
    cloneNodes(head);
    connectSiblingNode(head);
    return reconnectNodes(head);
}

private void cloneNodes(RandomListNode head) {
    RandomListNode p = head;
    while(p != null) {
        RandomListNode newNode = new RandomListNode(p.label);
        newNode.next = p.next;
        p.next = newNode;
        p = newNode.next;
    }
}

private void connectSiblingNode(RandomListNode head) {
    RandomListNode p = head;
    while(p != null) {
        RandomListNode cloned = p.next;
        if(p.random != null) {
            cloned.random = p.random.next;
        }
        p = cloned.next;
    }
}

private RandomListNode reconnectNodes(RandomListNode head) {
    RandomListNode p = head;
    
    RandomListNode newHead = null;
    RandomListNode tail = null;
    
    if(p != null) {
        tail = newHead = p.next;
        p.next = tail.next;
        p = p.next;
    }
    
    while(p != null) {
        tail.next = p.next;
        tail = tail.next;
        p.next = tail.next;
        p = p.next;
    }
    
    return newHead;
}
复制代码

36. 二叉搜索树与双向链表

题目描述

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能建立任何新的结点,只能调整树中结点指针的指向。

解题思路

这里将二叉搜索树转换为一个排序的双向链表,能够采用使用递归算法。

首先递归地转换左子树,返回其链表头节点,而后须要遍历该链表,找到链表的尾节点,这是为了和根节点相链接。须要让链表的尾节点的 right 指向根节点,让根节点的 left 指向链表的尾节点。

而后递归地转换右子树,返回其链表头节点,而后须要让根节点的 right 指向链表头节点,让链表的头节点指向根节点。

最后判断若是左子树转换的链表为空,则返回以 root 根节点为头节点的链表,不然返回以左子树最小值为头节点的链表。

代码实现

public TreeNode Convert(TreeNode root) {
    if(root == null) {
        return null;
    }
    
    TreeNode leftHead = Convert(root.left);
    TreeNode leftEnd = leftHead;
    while(leftEnd != null && leftEnd.right != null) {
        leftEnd = leftEnd.right;
    }
    if(leftEnd != null) {
        leftEnd.right = root;
        root.left = leftEnd;
    }
    
    TreeNode rightHead = Convert(root.right);
    if(rightHead != null) {
        root.right = rightHead;
        rightHead.left = root;
    }
    
    return leftHead == null ? root : leftHead;
}
复制代码

52. 两个链表的第一个公共节点

题目描述

输入两个链表,找出它们的第一个公共结点。

解题思路

对于两个链表,若是有公共节点,要不它们就是同一条链表,要不它们的公共节点必定在公共链表的尾部。

能够遍历两个链表获得它们的长度,而后在较长的链表上,先走它们的长度差的步数,接着同时在两个链表上遍历,如此找到的第一个节点就是它们的第一个公共节点。

代码实现

public ListNode findFirstCommonNode(ListNode first, ListNode second) {
    int length1 = getListLength(first);
    int length2 = getListLength(second);

    ListNode headLongList = first;
    ListNode headShortList = second;
    int diff = length1 - length2;

    if (length1 < length2) {
        headLongList = second;
        headShortList = first;
        diff = length2 - length1;
    }

    for (int i = 0; i < diff; i++) {
        headLongList = headLongList.next;
    }

    while (headLongList != null && headShortList != null) {
        if (headLongList == headShortList) {
            return headLongList;
        }
        headLongList = headLongList.next;
        headShortList = headShortList.next;
    }

    return null;
}

public int getListLength(ListNode head) {
    int length = 0;
    ListNode cur = head;
    while (cur != null) {
        length++;
        cur = cur.next;
    }
    return length;
}
复制代码
相关文章
相关标签/搜索