白银-链表的几道经典问题解法

文章介绍了使用不同方法解决链表问题的算法,包括利用哈希映射或双指针找到两个链表的第一个公共节点,判断链表是否为回文结构,以及合并两个或多个有序链表。此外,还涵盖了删除链表元素的策略,如删除特定节点、倒数第k个节点、一定间隔后的节点以及重复元素。
摘要由CSDN通过智能技术生成

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

这个问题第一反应就是可以使用map或者set来做,先遍历其中一条链表并存入map/set中,再遍历另外一条链表通过contains()来找到第一个公共子节点。

另外一个就是用双指针来解决,两个指针,一个指向A,一个指向B。两个指针同时移动,其中一个指针移动到最后时,将其指向另一条链表;另外一个指针同理。当两个指针都改变指向后,两个指针已经处于同一起点,往后遍历,第一个相等的就是需要的答案。

1.1.集合
public static ListNode findFirstCommonNodeByMap(ListNode pHead1, ListNode pHead2) {
        if (pHead1 == null || pHead2 == null) {
            return null;
        }
        ListNode current1 = pHead1;
        ListNode current2 = pHead2;

        HashMap<ListNode, Integer> hashMap = new HashMap<ListNode, Integer>();
        while (current1 != null) {
            hashMap.put(current1, null);
            current1 = current1.next;
        }

        while (current2 != null) {
            if (hashMap.containsKey(current2))
                return current2;
            current2 = current2.next;
        }

        return null;
    }
1.2.双指针
 public static ListNode get(ListNode la, ListNode lb){
        ListNode cur1 = la;
        ListNode cur2 = lb;
        while(cur1 != cur2){
            cur1 = cur1 == null ? lb : cur1.next;
            cur2 = cur2 == null ? la : cur2.next;
        }
        return cur1;
     }

还可以使用差和双指针和栈来解决。

差和双指针:计算两条链表的长度差k,长的先移动k步。在往后找到第一个相等的节点。

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

这个题第一时间能想到的就是用list集合来做,把所有元素存入list集合中,再进行首尾比较,一但出现不等,那就不是回文序列。但是教官提示我们面试的时候不能这么干,所以使用另外的方法。

除此之外,可以使用栈和快慢双指针来解决。

2.1.全部入栈
  public static boolean isPalindromeByAllStack(ListNode head) {
        ListNode temp = head;
        Stack<Integer> stack = new Stack();
        //把链表节点的值存放到栈中
        while (temp != null) {
            stack.push(temp.val);
            temp = temp.next;
        }
        //然后再出栈
        while (head != null) {
            if (head.val != stack.pop()) {
                return false;
            }
            head = head.next;
        }
        return true;
    }
2.2.快慢双指针

几个关键点:

  • slow每次移动一步,fast每次移动两步,当fast指针到达链表的末尾时,slow指针就会在链表的中间

  • 在寻找中间节点的过程中顺便反转前半部分链表,pre指向slow的前一个节点,prepre指向slow的前一个节点。再改变完指向后不要忘记更新pre和prepre(后移)

  • 如果链表的长度是奇数,fast指针会停在最后一个节点上,此时slow指针指向的是链表的中间节点,需要将slow指针向后移动一步,跳过中间节点。

  • 最后只需要将反转后的前半部分与后半部分进行比较即可

 public static boolean isPalindromeByTwoPoints(ListNode head) {
        if (head == null || head.next == null) {
            return true;
        }
        ListNode fast = head,slow = head;
        ListNode pre = head, prepre = null;
        // 找到中间节点并反转链表前半部分
        while(fast != null && fast.next != null){
            pre = slow; // slow的前驱
            slow = slow.next;
            fast = fast.next.next;
            pre.next = prepre; // 反转
            prepre = pre; //后移
        }
        // 节点数为奇数时,跳过中间节点,此时fast指向最后一个节点
        if(fast != null) slow = slow.next;
        //比较前半部分后后半部分是否相等
        while (slow != null && pre != null){
            if(slow.data != pre.data) return false;
            slow = slow.next;
            pre = pre.next;
        }
        return true;

    }

3.合并有序链表

3.1.合并两个有序链表

3.1.1.直接求解
 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;
    }
3.1.2递归
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        if(list1 == null) return list2;
        if(list2 == null) return list1;
        if(list1.val < list2.val){
            list1.next = mergeTwoLists(list1.next,list2);
            return list1;
        }
        else{
            list2.next = mergeTwoLists(list1,list2.next);
            return list2;
        }
    }
3.2.合并k个链表

只要会合并两个链表,就只需要加个循环就可以合并k个链表了。

package com.ljw.javademo.day1;

import java.util.ArrayList;
import java.util.List;

/**
 * Created with IntelliJ IDEA.
 *
 * @Auther: ljw
 * @Date: 2023/07/18/18:58
 * @Description:
 */
public class 合并k个链表 {
    public static void main(String[] args) {
        ListNode head1 = new ListNode(1);
        head1.next = new ListNode(3);

        ListNode head2 = new ListNode(1);
        head2.next = new ListNode(2);

        ListNode head3 = new ListNode(1);
        head3.next = new ListNode(7);

        List<ListNode> listNodes = new ArrayList<>();
        listNodes.add(head1);
        listNodes.add(head2);
        listNodes.add(head3);
        ListNode listNode = mergeKList(listNodes);
        travelListNode(listNode);

    }
    public static ListNode mergeKList(List<ListNode> listNodes){
        ListNode res = null;
        for(ListNode listNode : listNodes){
            res = mergeTwoLists(res,listNode);
        }
        return res;

    }
    public static ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        if(list1 == null) return list2;
        if(list2 == null) return list1;
        if(list1.data < list2.data){
            list1.next = mergeTwoLists(list1.next,list2);
            return list1;
        }
        else{
            list2.next = mergeTwoLists(list1,list2.next);
            return list2;
        }
    }

    public static void travelListNode(ListNode head){
        while(head != null){
            System.out.print(head.data+" ");
            head = head.next;
        }
    }
}

3.3.合并链表

关键点:找到a的前驱,b的后继,list2的尾节点

public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2) {
      // 需要保留a的前驱和b的后继
      ListNode pre = list1;
      ListNode post1 = list1;
      // list2的尾部节点
      ListNode post2 = list2;
      int i = 0,j = 0;
      while(pre != null && post1 != null && j < b){
          // 找到a的前驱
…      post1 = post1.next;
      // 找到list2的尾节点
      while(post2.next != null){
          post2 = post2.next;
      }
      pre.next = list2;
      post2.next = post1;
      return list1;

    }

4.双指针专题

4.1.链表的中间节点

image-20230719104024350

快慢双指针:慢指针移动一步,快指针移动两步,当快指针到尾部时,慢指针刚好到达中间节点。

public ListNode middleNode(ListNode head) {
        ListNode slow = head,fast = head;
        while(fast != null && fast.next != null){
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }
4.2.链表中的倒数第k个节点

image-20230719104427197

首先想到了暴力求解,先遍历链表求得链表长度L,再根据k和L找到答案,但是这样需要遍历两次链表。因此我们使用快慢双指针来解决。fast先移动k步,slow指向头节点,然后同时移动,当fast为null时,slow刚好指向了需要的答案。

注意:链表的长度可能小于k,所以移动时要判断fast是否为空。

快慢双指针
 public ListNode getKthFromEnd(ListNode head, int k) {
        // 暴力解决需要遍历两次链表
        // ListNode cur = head;
        // int length=0;
        // while(cur!=null){
        //     length++;
        //     cur=cur.next;
        // }
        // if(length==1 || length==k){
        //     return head;
        // }
        // cur=head;
        // for(int i =1;i<length-k;i++){
        //     cur=cur.next;
        // }
        // ListNode res = cur.next;
        // return res;

        //双指针
        ListNode fast=head;
        ListNode slow=head;
        //头指针前移k步
        while(fast != null && k > 0){
            fast = fast.next;
            k--;
        }
        while(fast != null){
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }
4.3.旋转链表

image-20230719110719104

通过观察,可以发现相当于将原链表断成两部分,我们只需要找到断开的节点,再修改指向就可以了。要找到这个断开点,就是跟上一题基本一样,区别点在于上一题是要找到这个断开点的后继,这个是要找到断开点。所以在fast那里会有所区别:到最后一个节点判断条件为fast.next != null,到null为fast != null

还有一点k可能大于链表长度,所以需要求链表长度并取余进行判断。同时k等于链表长度时不需要旋转

public ListNode rotateRight(ListNode head, int k) {
        if(head == null || k == 0) return head;
        ListNode temp = head;
        ListNode slow = head;
        ListNode fast = head;
        int length = 0;
        // 求链表长度
        while(head != null){
            head = head.next;
            length++;
        }
        // k等于链表长度时,不需要旋转
        if((k % length == 0)) return temp;
        // fast先移动k步
        while((k % length) > 0){
            fast = fast.next;
            k--;
        }
        // 同时移动
        while(fast.next != null){
            fast = fast.next;
            slow = slow.next;
        }
        // 此时已经找到了要断开的地方,修改指向即可
        ListNode res = slow.next;
        slow.next = null;
        fast.next = temp;
        return res;
    }

5.删除链表元素专题

5.1.删除链表中的特定节点

对于链表要删除某一结点时,需要知道该节点的前驱pre和后继节点next。然后让pre.next = next就完成了删除。

**注意:**头节点需要做特殊处理,所以通过创建一个指向头节点的虚拟节点来简化操作

public ListNode removeElements(ListNode head, int val) {
        ListNode res = new ListNode(0,head);
        ListNode cur = res;
        while(cur.next != null){
            if(cur.next.val == val){
                cur.next = cur.next.next;
            }
            else{
                cur = cur.next;
            }
        }
        return res.next;
}
5.2.删除链表的·倒数第K个节点

在双指针专题中已经学会了如何让找到倒数第k个节点:快慢指针,这个题不过是多了一步删除操作。

public ListNode removeNthFromEnd(ListNode head, int n) {
    
        ListNode fast = head;
        ListNode temp = new ListNode(0);
        temp.next = head;
        ListNode slow = temp;
        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 temp.next;
    }
5.3.删除链表M个节点后的第N个节点

力扣要会员。。

public static ListNode delete(ListNode head,int m,int n){
        ListNode dummyHead = new ListNode(0,head);
        ListNode cur = dummyHead;
        int count = 0;
        while(true){
            // 移动m
            count = m;
            while (count > 0 && cur != null){
                cur = cur.next;
                count--;
            }
            if(cur == null) break;;
            // 删除n个节点
            // 走到这儿的时候cur是要删除的n个节点的前面那个节点
            count = n;
            ListNode forward = cur;
            while (count > 0 && forward != null){
                forward = forward.next;
                count--;
            }
            if (forward == null) {
                cur.next = null;
                break;
            }
            cur.next = forward.next;
        }
        return dummyHead.next;

    }
5.4.删除重复元素
5.4.1.重复元素保留一个

从头节点开始遍历,当前节点与后继相等时删除,不等则将当前节点后移。

cur和cur.next比较

public ListNode deleteDuplicates(ListNode head) {
        if(head == null) return head;
        ListNode cur = head;
        while(cur.next != null){
            if(cur.val == cur.next.val){
                cur.next = cur.next.next;
            }
            else{
                cur = cur.next;
            }
        }
        return head;
    }
5.4.2.重复元素一个不要

cur.next和cur.next.next比较,头节点可能会被删除,使用虚拟头节点

public ListNode deleteDuplicates(ListNode head) {
        if (head == null) {
            return head;
        }
        ListNode dummy = new ListNode(0, head);
        ListNode cur = dummy;
        while(cur.next != null && cur.next.next != null){
            if(cur.next.val == cur.next.next.val){
                int x =cur.next.val;
                while(cur.next != null && cur.next.val == x){
                    cur.next = cur.next.next;
                }
            }
            else{
                cur = cur.next;
            }
        }
        return dummy.next;
    }
    }
    ListNode dummy = new ListNode(0, head);
    ListNode cur = dummy;
    while(cur.next != null && cur.next.next != null){
        if(cur.next.val == cur.next.next.val){
            int x =cur.next.val;
            while(cur.next != null && cur.next.val == x){
                cur.next = cur.next.next;
            }
        }
        else{
            cur = cur.next;
        }
    }
    return dummy.next;
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值