链表经典问题之双指针和节点

1.两个链表第一个公共子节点

这是一道经典的链表问题,剑指offer52 先看一下题目:输入两个链表,找出它们的第一个公共节点。例如下面的两个链表:

image (8)

两个链表的头结点都是已知的,相交之后成为一个单链表,但是相交的位置未知,并且相交之前的结点数也是未知的,请设计算法找到两个链表的合并点。

首先要清晰一个概念:

一个结点只能有一个后继

看下面两个图

image-20230903231233373

上面第一个图是满足单链表要求的,因为我们说链表要求环环相扣,核心是一个结点只能有一个后继,但不代表一个结点只能有一个被指向。第一个图中,c1被a2和b3同时指向,这是没关系的。这就好比法律倡导一夫一妻,你只能爱一个人,但是可以都多个人爱你。
第二图就不满足要求了,因为c1有两个后继a5和b4。

解法一:暴力遍历(不推荐但是需要注意一点)

   public static ListNode findFirstCommonNodeByFor(ListNode pHead1, ListNode pHead2) {
        ListNode pointerA = pHead1;
        ListNode pointerB;
        while (pointerA != null) {
            pointerA = pointerA.next;
            pointerB = pHead2; //这里需要每一次进行内部遍历的时候把pointerB从头结点开始
            while (pointerB != null) {
                if (pointerA.val == pointerB.val) {
                    return pointerB;
                } else {
                    pointerB = pointerB.next;
                }
            }
        }
        return null;
    }

​ 这里注意这句pointerB = pHead2必须重置指针回到头结点

解法二:使用栈

整体思路就是逆袭思维,当栈是后入先出,利用此特性实现了单向链表的逆向寻找

这里需要使用两个栈,分别将两个链表的结点入两个栈,然后分别出栈,如果相等就继续出栈,一直找到最晚出栈的那一组。这种方式需要两个O(n)的空间,所以在面试时不占优势,但是能够很好锻炼我们的基础能力

public static ListNode findFirstCommonNodeByStack(ListNode pHead1, ListNode pHead2) {
    Stack<ListNode> stackA = new Stack();
    Stack<ListNode> stackB = new Stack();
    while (pHead1 != null) {
        stackA.add(pHead1);
        pHead1 = pHead1.next;
    }
    while (pHead2 != null) {
        stackB.add(pHead2);
        pHead2 = pHead2.next;
    }
    ListNode tem = null;
    while (stackA.size() > 0 && stackB.size() > 0) {
        if (stackA.peek() == stackB.peek()) {
            stackB.pop();
            tem = stackA.pop();
            continue;
        } else {
            return tem;
        }
    }
    return null;
}

解法三:利用集合或者哈希

public static ListNode findFirstCommonNodeBySet(ListNode pHead1, ListNode pHead2) {
        Set<ListNode> set = new HashSet<>();
        while (pHead1 != null) {
            set.add(pHead1);
            pHead1 = pHead1.next;
        }

        while (pHead2 != null) {
            if (set.contains(pHead2))
                return pHead2;
            pHead2 = pHead2.next;
        }
        return null;
    }

解法四:双指针

1.连接双表法

核心思路是连接两个链表,分别以a开始和b开始,最后某一点开始,总会是公共节点

从方法作为新建链表的优化可以一个链表遍历完以后直接指向新链表

需要注意的是循环体内的if判断,当没有公共节点时,两个指针都指向null,此时不需要再次改变指针指向,即停止循环

  if (headA == null || headB == null) {
            return null;
        }

        ListNode p1 = headA;
        ListNode p2 = headB;
        while (p1 != p2) {
            p1 = p1.next;
            p2 = p2.next;
            if (p1 != p2) {
                if (p1 == null) {
                    p1 = headB;
                }
                if (p2 == null) {
                    p2 = headA;
                }
            }
        }
        return p1;

2.求出链表之间相差的长度

让La-Lb,多出的长度就是公共节点前的长度差,让较长的-较短的求出的值,让长的先走这个值的步数,就可以相遇

这种方法依然是基于求出两个链表的长度,因此不推荐,下面是优化

public ListNode findFirstCommonNode(ListNode pHead1, ListNode pHead2) {
    if(pHead1==null || pHead2==null){
        return null;
    }
    ListNode current1=pHead1;
    ListNode current2=pHead2;
    int l1=0,l2=0;
    //分别统计两个链表的长度
    while(current1!=null){
        current1=current1.next;
        l1++;
    }

    while(current2!=null){
        current2=current2.next;
        l2++;
    }
    current1=pHead1;
    current2=pHead2;
    int sub=l1>l2?l1-l2:l2-l1;
    //长的先走sub步
    if(l1>l2){
        int a=0;
        while(a<sub){
            current1=current1.next;
            a++;
        }   
    }

    if(l1<l2){
        int a=0;
        while(a<sub){
            current2=current2.next;
            a++;
        }   
    }
    //同时遍历两个链表
    while(current2!=current1){
        current2=current2.next;
        current1=current1.next;
    } 

    return current1;

优化

  ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode a = headA;
        ListNode b = headB;
       while(a!=b){
           if(a==null){
               a=headB;
           }else a=a.next;
           if(b==null){
               b=headA;
           }else b=b.next;
       }return a;
    }

2.判断链表是否为回文序列

LeetCode234,这也是一道简单,但是很经典的链表题,判断一个链表是否为回文链表。

示例1:

输入: 1->2->2->1

输出: true

进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?

方法一:通过压入栈检查一半序列

 public static boolean isPalindromeByAllStack(ListNode head) {
        ListNode headNode = head;
        Stack<Integer> stack1 = new Stack<>();
        while (head != null) {
            stack1.add(head.val);
            head = head.next;
        }
        head = headNode;
        int i = 0;
        for (; i < stack1.size() / 2; i++) {
            if (stack1.peek() != head.val) {
                break;
            }
        }
        return i == stack1.size() / 2;

    }

方法二:双指针(快慢指针)

整个流程可以分为以下五个步骤:

  1. 找到前半部分链表的尾节点。
  2. 反转后半部分链表。
  3. 判断是否回文。
  4. 恢复链表。
  5. 返回结果。

3 合并有序链表

数组中我们研究过合并的问题,链表同样可以造出两个或者多个链表合并的问题。 两者有相似的地方,也有不同的地方,你能找到分别是什么吗?

LeetCode21 将两个升序链表合并为一个新的升序链表并返回,新链表是通过拼接给定的两个链表的所有节点组成的

方法一:直接连接

  public static ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode newHead = new ListNode(-1);
        ListNode res = newHead;
        while (list1 != null || list2 != null) {

            if (list1 != null && list2 != null) {//都不为空的情况
                if (list1.val < list2.val) {
                    newHead.next = list1;
                    list1 = list1.next;
                } else if (list1.val > list2.val) {
                    newHead.next = list2;
                    list2 = list2.next;
                } else { //相等的情况,分别接两个链
                    newHead.next = list2;
                    list2 = list2.next;
                    newHead = newHead.next;
                    newHead.next = list1;
                    list1 = list1.next;
                }
                newHead = newHead.next;
            } else if (list1 != null && list2 == null) {
                newHead.next = list1;
                list1 = list1.next;
                newHead = newHead.next;
            } else if (list1 == null && list2 != null) {
                newHead.next = list2;
                list2 = list2.next;
                newHead = newHead.next;
            }
        }
        return res.next;
  }

上面这种方式能完成基本的功能,但是所有的处理都在一个大while循环里,代码过于臃肿,我们可以将其变得苗条一些:

 public static ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode newnode = new ListNode(-1);
        ListNode newhead = newnode;
        while (list1!=null&&list2!=null){
                if(list1.val>list2.val){
                    newnode.next = list2;
                    list2 = list2.next;
                }else if(list1.val<list2.val){
                    newnode.next = list1;
                    list1 = list1.next;
                }else {
                    newnode.next = list1;
                    list1 = list1.next;
                }
                newnode = newnode.next;}
            while (list1!=null){
                newnode.next = list1;
                list1 = list1.next;
                newnode = newnode.next;

            }while (list2!=null){
                newnode.next = list2;
                list2=list2.next;
                newnode = newnode.next;
            }
        return newhead.next;
}

进一步分析,我们发现两个继续优化的点,

1.一个是上面第一个大while里有三种情况,我们可以将其合并成两个,也就是说,当节点相同的时候,不论连接list1或者list2,都可以,因此直接将第三种情况放进else中

2.第二个优化是后面两个小的while循环,这两个while最多只有一个会执行,而且由于链表只要将链表头接好,后面的自然就接上了,因此循环都不用写,也就是这样:

public static ListNode mergeTwoListsMoreSimple(ListNode l1, ListNode l2) {
        ListNode prehead = new ListNode(-1);
        ListNode prev = prehead;
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                prev.next = l1;
                l1 = l1.next;
            } else {
                prev.next = l2;
                l2 = l2.next;
            }
            prev = prev.next;
        }
        // 最多只有一个还未被合并完,直接接上去就行了,这是链表合并比数组合并方便的地方
        prev.next = l1 == null ? l2 : l1;
        return prehead.next;
    }

4. 合并K个链表

合并k个链表,有多种方式,例如堆、归并等等。如果面试遇到,最好先将前两个合并,之后再将后面的逐步合并进来,这样的的好处是只要将两个合并的写清楚,合并K个就容易很多,现场写最稳妥

两种循环迭代方式,需要注意的是每次调用合并两个链表需要与之前的res进行合并

方法见上

public static ListNode mergeKLists(ListNode[] lists) {
    ListNode res = null;
    for (int i = 0; i < lists.length; i++) res = mergeTwoListsMoreSimple(res,lists[i]);
    /for (ListNode list : lists) res = mergeTwoListsMoreSimple(res, list);
    
}

5.一道很无聊的好题

LeetCode1669:给你两个链表 list1 和 list2 ,它们包含的元素分别为 n 个和 m 个。请你将 list1 中下标从a到b的节点删除,并将list2 接在被删除节点的位置。

ListNode nodea = null;
ListNode nodeb =list2;
ListNode heada = list1;
while (list2.next!=null){
    list2 = list2.next;
}
  while (list1!=null){
      if(a--==1)nodea = list1;
      if(b--==-1)break;//目标节点的下一个
      list1 = list1.next;
  }
  list2.next = list1;
  nodea.next = nodeb;
  return heada;

6.双指针专题

在数组里我们介绍过双指针的思想, 可以简单有效的解决很多问题,而所谓的双指针只不过是两个变量而已。在链表中同样可以使用双指针来轻松解决一部分算法问题。这类题目的整体难度不大,但是在面试中出现的频率很高,我们集中看一下。

1.寻找中间结点

LeetCode876 给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

在这里需要分析两种情况,链表的节点数量为奇数或者链表的节点数量为偶数

image-20230908151203662

有人的思路是,判断list.next.next!=null时,执行,循环,分为两种情况,这样用两个情况就可以做,但是更简单的方法是只需要让list在偶数情况下,再走一次,刚好为null时

此时slow刚好指向4,因此while条件可以是:list!=null&&list.next!=null,保证了一定可以走到最后一个无论节点数量为奇数或者偶数

 public ListNode middleNode(ListNode head) {

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

2寻找倒数第K个元素

输入一个链表,输出该链表中倒数第k个节点。本题从1开始计数,即链表的尾节点是倒数第1个节点。

示例

给定一个链表: 1->2->3->4->5, 和 k = 2.

返回链表 4->5.

 public static ListNode getKthFromEnd(ListNode head, int k) {
        ListNode fast = head;
        ListNode slow = head;
        while (fast != null && k > 0) {
            fast = fast.next;
            k--;
        }
        while (fast != null) {
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }

本题需要注意的是当fast走到null时候停止循环,此时slow的指针刚好指向倒数第k个节点当节点数量为1,或者k大于节点数量时依然适用

3.旋转链表

Leetcode61.先看题目要求:给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。

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

输出:[4,5,1,2,3]

image-20230910202112127

易错:k>节点数时候的情况

错误写法:

    public static ListNode rotateRight(ListNode head, int k) {
if (head == null || k == 0) {
            return head;
        }
        ListNode tem = head;
        ListNode fast = head;
        ListNode slow = head;
        ListNode newHead;
        int length = 0;
        while (tem != null ) {
            tem = tem.next;
            length++;
        }
        if (k % length == 0) {
            return head;
        }
        while (fast!=null&&k>0){
            k--;
            fast=fast.next;

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

        newHead = slow.next;
        slow.next = null;
        if(fast!=null)
        fast.next = head;
        return newHead;
}

if(fast!=null) 每当fast指针移动以后,都必须判断一下,否则可能会出现空指针异常

并且当k>节点数量时,代码无法得到结果

注意:与第一道题倒数第k个节点不同,这里快指针是fast.next,可能会造成空指针异常,因此这里条件不可以是fast!=null,使用k对长度取余可以很巧妙的让k的长度小于总数量,从而不会造成越界

正确写法:

 public static ListNode rotateRight(ListNode head, int k) {
        if (head == null || k == 0) {
            return head;
        }
        ListNode tem = head;
        ListNode fast = head;
        ListNode slow = head;
        ListNode newHead;
        int length = 0;
        while (tem != null ) {
            tem = tem.next;
            length++;
        }
        if (k % length == 0) {
            return head;
        }
        while (k%length!=0){
            k--;
            fast=fast.next;

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

        newHead = slow.next;
        slow.next = null;
        fast.next = head;
        return newHead;

方法二:

通过连接成环得到长度n,再到n-k处解环,但是注意的是,当重新计算时,解环的地方要么是n-k-1(原来的头结点开始),或者是n-k,(原来尾节点开始)

代码实现:

 public static ListNode rotateRight(ListNode head, int k) {
        if (head == null || k == 0 || head.next == null) {
            return head;
        }

        int len = 1;
        ListNode tem = head;
        while (tem.next != null) {
            tem = tem.next;
            len++;
        }

        int add = len - k % len;
        if (add == len) {
            return head;
        }
        tem.next = head;
        while (add-- > 0) {
            tem = tem.next;

        }
        ListNode res = tem.next;
        tem.next = null;
        return res;

7.1删除特定结点

先看一个简单的问题,LeetCode 203:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回新的头节点 。

   public ListNode removeElements(ListNode head, int val) {

     ListNode dummyHead = new ListNode(-1);
        dummyHead.next = head;
        head = dummyHead;
        while (head.next!=null){
            if(head.next.val==val){
                head.next = head .next.next;
            }else{
                head = head.next;}
        }return dummyHead.next;
    }

此处需要注意的一点是:if后的else条件,因为当删除节点时,不需要移动指针向下一个,两个原因

1.倘若有两个连续重复元素,会漏删

2.倘若在最后一个节点,会导致空指针异常

7.2删除特定结点

LeetCode 19. 删除链表的倒数第 N 个节点

方法一:利用双指针求解

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点

双指针:也就是找到倒数第n+1个节点但是需要注意头结点

 public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummyHead = new ListNode(0);
        dummyHead.next = head;
        ListNode tem = dummyHead;
        ListNode slow = dummyHead;
        ListNode fast = dummyHead;
        while (n-- >0){
            fast = fast.next;
        }
        while (fast.next!=null){
            fast= fast.next;
            slow=slow.next;
        }slow.next = slow.next.next;
        return dummyHead.next;


    }

方法2:利用栈

 public ListNode removeNthFromEnd(ListNode head, int n) {

         ListNode dummy = new ListNode(0);
        Stack<ListNode>stack = new Stack<>();
        dummy.next = head;
        ListNode tem = dummy;
        while (tem!=null){
            stack.push(tem);
            tem = tem.next;
        }
        for (int i = 0; i < n; i++) {
           stack.pop();
        }
stack.peek().next = stack.peek().next.next;
        return dummy.next;

    }

7.3 删除重复元素

1.给定一个已排序的链表的头 head删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表

image-20230911222233268

这道题需要注意的是while(内的防止空指针异常的条件)in.next!=nulltem.next!=null

  public ListNode deleteDuplicates(ListNode head) {
   if(head==null){return head;}
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        ListNode in ;
        ListNode tem = dummy;
        while (tem.next!=null&&tem.next.next!= null) {
            if (tem.next.val == tem.next.next.val) {
                in = tem.next;
                while (in.next!=null&&in.val == in.next.val) {
                    in = in.next;
                }
                tem.next = in.next;

            } else {
                tem = tem.next;
            }
        }
        return dummy.next;
    }

两道题思路基本相同,不同的是第二个保留一个只需要考虑重复情况,保留第一个出现的元素

2.给定一个已排序的链表的头 head删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表

image-20230911222314450

 public ListNode deleteDuplicates(ListNode head) {
if(head==null)return head;
        ListNode tem = head;
        while(tem.next!=null){
            if(tem.val==tem.next.val){
                tem.next = tem.next.next;
            }else {
                tem=tem.next;
            }
        }
        return head;
    }