# 链表:指针操作有点乱?这些技巧要记好!

在这里插入图片描述

# 链表不难,但是容易乱

当我们遇到链表类型的面试题时,一般都不会太难。但是很容易写错,因为经常容易搞错链表的指向关系,并且忽略对边界的处理。本节我们就以常见的链表题给大家演示几个常用的小技巧,当遇到比较复杂的题目时,还可以画图辅助梳理链表的指向关系

  1. 假头
  2. 新链表
  3. 双指针
  4. 拆分成多个链表再合并

后面演示用到的链表类定义如下

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;
    }
}

# 假头和新链表

链表有些题目还是需要如果想追求高性能,还是会用到一些稍微复杂的算法。但如果你在笔试阶段实在想不到更高效的算法。就直接重新构造一个新链表返回,但是时间复杂度和空间复杂度会比较高。如将链表排序,合并N个有序链表

# 排序链表

题目来源:LeetCode 148 在这里插入图片描述 给你链表的头结点 head ,请将其按升序排列并返回排序后的链表 。

我们直接遍历这个链表,将值存下来,然后重新构建

public ListNode sortList(ListNode head) {
    if (head == null) {
        return null;
    }
    List<Integer> resultList = new ArrayList<>();
    while (head != null) {
        resultList.add(head.val);
        head = head.next;
    }
    Collections.sort(resultList);
    // 构建链表
    ListNode tempHead = null, newHead = null;
    for (int i = 0; i < resultList.size(); i++) {
        if (i == 0) {
            newHead = new ListNode(resultList.get(i));
            tempHead = newHead;
        } else {
            ListNode listNode = new ListNode(resultList.get(i));
            newHead.next = listNode;
            newHead = newHead.next;
        }
    }
    return tempHead;
}

可以看到,我们要对头节点的操作进行特判,还是比较麻烦的。此时我们可以构建一个假头(命名为dummy),放在要返回的头节点的前面,这样当我们对链表进行增加,删除等操作时,就非常方便

public ListNode sortList(ListNode head) {
    if (head == null) {
        return null;
    }
    List<Integer> resultList = new ArrayList<>();
    while (head != null) {
        resultList.add(head.val);
        head = head.next;
    }
    Collections.sort(resultList);
    ListNode dummy = new ListNode();
    ListNode dummyTemp = dummy;
    for (Integer item : resultList) {
        ListNode listNode = new ListNode(item);
        dummy.next = listNode;
        dummy = dummy.next;
    }
    return dummyTemp.next;
}

# 反转链表

题目来源:LeetCode 206

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

在这里插入图片描述 如果我们基于原来的链表进行操作还是比较麻烦的。如果换一种思路,重新构建一个新链表然后返回,实现过程是不是一下就清晰起来了?

遍历旧的链表,然后将旧链表的节点不断设置为新链表的头节点,最后返回的新链表就是反转后的。为了简化操作过程,我们还是弄一个假头

请添加图片描述

public ListNode reverseList(ListNode head) {
    ListNode dummy = new ListNode();
    while (head != null) {
        ListNode curNode = head;
        head = head.next;
		// 下面2行代码为将旧链表的节点设置为新链表的头节点
        curNode.next = dummy.next;
        dummy.next = curNode;
    }
    return dummy.next;
}

# 双指针

链表中的双指针操作一般有两种,一种是比较常规的用法,如用2个指针记录2个链表的移动过程。另一种是快慢指针,能实现很多有意思的操作。

# 合并2个有序链表

题目来源:LeetCode 21

在这里插入图片描述 我们使用双指针,标记2个链表的移动,然后构建新链表

public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    if (l1 == null) {
        return l2;
    }
    if (l2 == null) {
        return l1;
    }
    ListNode dummy = new ListNode();
    ListNode tempDummy = dummy;
    while (l1 != null && l2 != null) {
        if (l1.val < l2.val) {
            dummy.next = l1;
            l1 = l1.next;
        } else {
            dummy.next = l2;
            l2 = l2.next;
        }
        dummy = dummy.next;
    }
    // l1链表遍历完了,把l2剩余的节点加上去
    if (l1 == null) {
        dummy.next = l2;
    }
    // l2链表遍历完了,把l1剩余的节点加上去
    if (l2 == null) {
        dummy.next = l1;
    }
    return tempDummy.next;
}

# 删除链表的倒数第n个节点

在这里插入图片描述 题目来源:LeetCode 19

大部分人都能很快想到解法,先遍历一遍得到链表的长度,再遍历一遍删除节点。

有没有可能通过一次遍历就做到呢?

用快慢2个指针即可做到,快指针先走n步,然后快慢指针一起走。当快指针到达链表的尾部时,慢指针正好是倒数第n+1个节点,重新设置倒数第n+1个节点的next指针即可

链表长度是是n,删除倒数第n个节点这种情况怎么处理呢?

上面说到,我们先找倒数第n+1个节点,然后重新设置next指针。但是当链表长度为n时,倒数第n+1个节点不存在,为了保持逻辑的统一性,我们只需要加一个假头即可,和前面的反转链表一个道理,这样处理起来比较容易,代码也比较容易理解

public ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode dummy = new ListNode(0, head);
    ListNode slow = dummy;
    ListNode fast = dummy;
    for (int i = 0; i <= n; i++) {
        fast = fast.next;
    }
    while (fast != null) {
        fast = fast.next;
        slow = slow.next;
    }
    slow.next = slow.next.next;
    return dummy.next;
}

# 判断链表是否是回文链表

请添加图片描述 当一个链表正着读和反着读都一样时,则这个链表是回文链表,否则不是回文链表。

我们同样可以用快慢指针来解决这种类型的题目。慢指针每次走1步,快指针每次都2步。当快指针到头的时候,慢指针正好在链表的中点。

此时同时移动链表的头节点指针和慢节点指针,当2者的值不同时,说明这个链表不是回文链表

public boolean isPalindrome(ListNode head) {
    ListNode pre = new ListNode();
    ListNode slow = head;
    ListNode fast = head;
    while (fast != null && fast.next != null) {
        ListNode node = new ListNode(slow.val, pre.next);
        pre.next = node;
        slow = slow.next;
        fast = fast.next.next;
    }
    if (fast != null) {
        slow = slow.next;
    }
    pre = pre.next;
    while (slow != null) {
        if (slow.val != pre.val) {
            return false;
        }
        slow = slow.next;
        pre = pre.next;
    }
    return true;
}

# 判断链表是否有环

题目来源:LeetCode 141 在这里插入图片描述 如上图链表,-4节点到2节点就形成了一个环

我们只需要把访问过的节点都记录下来,每次访问一个节点就判断这个节点是否被访问过即可。

记录这个节点是否访问的方法有很多种

  1. 给ListNode增加一个属性visit,默认为false,访问过则置为true,将新访问到的ListNode的visit为true时,说明链表有环
  2. 用HashMap存储访问过的ListNode,注意不要重写hashCode和equals,这样只有同一个对象才能被判断为相等,当新访问到的ListNode在HashMap中存在时,说明链表有环
  3. 将访问过的ListNode放到Set中(相同的ListNode会自动去重哈),如果将新访问的ListNode放入Set中,set的数量没有增加,说明链表有环
public boolean hasCycle(ListNode head) {
    Map<ListNode, Boolean> listNodeMap = new HashMap();
    while (head != null) {
        if (listNodeMap.containsKey(head)) {
            return true;
        }
        listNodeMap.put(head, null);
        head = head.next;
    }
    return false;
}

当然最高效的方式还使用快慢指针,快指针每次走一步,慢指针每次走两步,如果链表有环,则快慢指针迟早会相遇。

public boolean hasCycle(ListNode head) {
    ListNode slow = head;
    ListNode fast = head;
    while (fast!= null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
        if (fast == slow) {
            return true;
        }
    }
    return false;
}

# 拆分成多个链表再合并

# 链表的奇偶重排

题目描述: 在这里插入图片描述 题目来源:BM14(牛客网) 思路:直接基于原链表移动比较复杂,直接新建2个链表,一个插入链表的奇数位节点,一个插入链表的偶数位节点,最后将这2个链表合并起来即可

public class Solution {

    public ListNode oddEvenList (ListNode head) {
        if (head == null) {
            return null;
        }
        ListNode oddHead = new ListNode(0);
        ListNode oddTail = oddHead;
        ListNode evenHead = new ListNode(0);
        ListNode evenTail = evenHead;
        for (int i = 1; head != null; i++) {
            if ((i & 1) == 1) {
                oddTail.next = head;
                oddTail = head;
            } else {
                evenTail.next = head;
                evenTail = head;
            }
            head = head.next;
        }
        evenTail.next = null;
        oddTail.next = evenHead.next;
        return oddHead.next;
    }
}