算法通关第一村-链表白银挑战(Java)

算法通关第一村-链表白银挑战(Java)


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

  • 剑指offer52::这是经典的链表问题:输入两个链表找出它们的第一个公共子节点,例如下面的两个链表:
  • 在这里插入图片描述
  • 两个链表的头节点都是已知的,相交后成为一个单链表,但相交的位置未知,并且相交之前的节点数也是未知的,请设计算法找到两个链表的合并点。

1.1 哈希和集合

  •   //1.1 哈希和集合
          public static LinkListNode findFirstCommonNodeBySet(LinkListNode headA, LinkListNode headB){
              Set<LinkListNode> setA = new HashSet<>();
              while(headA != null){//先将一个链表元素全部存到Map里
                  setA.add(headA);
                  headA = headA.next;
              }
      
              while(headB != null){//然后一边遍历第二个链表headB,一边检测Hash中是否存在当前节点
                  if(setA.contains(headB)){
                      return headB;
                  }
                  headB = headB.next;
              }
              return null;
          }
    

1.2 使用栈

  •   //1.2 使用栈
          // 直到找出最晚出栈的那一组,需要两个O(n)的空间
          //stackA.peek() 是Java中Stack类的一个方法。当你调用这个方法时,
          // 它会返回栈顶的元素,但不从栈中移除它。这与 stackA.pop() 不同,
          // 后者会移除并返回栈顶的元素。
          public static LinkListNode findFirstCommonNodeByStack(LinkListNode headA, LinkListNode headB){
              //使用两个栈,分别将两个链表的节点入两个栈
              Stack<LinkListNode> stackA = new Stack();
              Stack<LinkListNode> stackB = new Stack();
              while(headA != null){
                  stackA.push(headA);
                  headA = headA.next;
              }
              while(headB != null){
                  stackB.push(headB);
                  headB = headB.next;
              }
              LinkListNode preNode = null;//指向当前两个栈中元素相等的节点
              while(stackB.size() > 0 && stackA.size() > 0){
                  if(stackB.peek() == stackA.peek()){//,如果两个栈的栈顶元素相等,则分别出栈
                      preNode = stackA.pop();
                      stackB.pop();
                  }else{//此时两个链表出现分叉
                      break;
                  }
              }
              return preNode;
          }
    

1.3拼接两个字符串(换表双指针)

  •   //1.3拼接两个字符串
          public LinkListNode findFirstCommonNode(LinkListNode pHead1, LinkListNode pHead2){
              if(pHead1 == null || pHead2 == null){
                  return null;
              }
              LinkListNode p1 = pHead1;
              LinkListNode p2 = pHead2;
              while(p1 != p2){
                  p1 = p1.next;
                  p2 = p2.next;
                  if(p1 != p2){
                      //一个链表访问完了,就跳到另一个链表继续访问
                      if(p1 == null){
                          p1 = pHead2;
                      }
                      if(p2 == null){
                          p2 = pHead1;
                      }
                  }
              }
              return p1;
          }
    
  • 接下来解释一下,循环体里为什么需要加一个判断 if(p1 != p2)。简单来说,如果序列不存在交集的时候会陷入死循环,例如 list1是1 2 3,list2是 4 5,很明显,如果不加判断,list1 和 liste2 会不断循环,出不来。


1.4差和双指针

  • 假如公共子节点一定存在第一轮遍历,假设 La 的长度为 L1,Lb 的长度为 L2。则 | L2-L1 | 就是两个链表的差值。第二轮遍历,链表长的先走 | L2-L1 |,然后两个链表同时向前走,节点一样的时候就是公共节点了。

  •   //1.4差和双指针
          public LinkListNode findFirstCommonNode2(LinkListNode pHead1, LinkListNode pHead2){
              if(pHead1 == null || pHead2 == null){
                  return null;
              }
              LinkListNode p1 = pHead1;
              LinkListNode p2 = pHead2;
              int length1 = 0, length2 = 0;
              //分别计算两个链表的长度
              while(pHead1 != null){
                  pHead1 = pHead1.next;
                  length1++;
              }
              while(pHead2 != null){
                  pHead2 = pHead2.next;
                  length2++;
              }
              //长的链表先走 sub步
              if(length1 > length2){
                  int sub = length1 - length2;
                  while(sub > 0){
                      p1 = p1.next;
                      sub--;
                  }
              }
              if(length1 < length2){
                  int sub = length2 - length1;
                  while(sub > 0){
                      p2 = p2.next;
                      sub--;
                  }
              }
              //同时遍历两个链表
              while(p1 != p2){
                  p1 = p1.next;
                  p2 = p2.next;
              }
              return p1;
          }
    

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

  • LeetCode234:判断一个链表是否为回文链表:

  • 基本的全部压栈解法:

  •   public static boolean isPalindromeSequence(LinkListNode head){
              Stack<LinkListNode> stack = new Stack();
              LinkListNode curNode = head;
              while(curNode != null){//将链表节点存放到栈中
                  stack.push(curNode);
                  curNode = curNode.next;
              }
              while(head != null){//一边出栈,一边遍历链表,进行值的比较
                  if(stack.peek().val == head.val){
                      stack.pop();
                      head = head.next;
                  }else{
                      return false;
                  }
              }
              return true;
          }
    

3.合并有序序列

3.1合并两个有序序列

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

  •   public static LinkListNode MergeTwoLinkedLists(LinkListNode list1, LinkListNode list2){
              //-1 只是一个示例值,它并不代表任何实际意义,只用于作为虚拟头节点的值
              LinkListNode preHead = new LinkListNode(-1);
              LinkListNode prev = preHead;
              while(list1 != null && list2 != null){
                  if(list1.val <= list2.val){
                      prev.next = list1;
                      list1 = list1.next;
                  }else{
                      prev.next = list2;
                      list2 = list2.next;
                  }
                  prev = prev.next;
              }
              //最多只有一个还未被合并完,直接接上去就行
              prev.next = list1 == null ? list2 : list1;
              return preHead.next;
          }
    

3.2合并K个有序链表

  • 合并K个链表,先将前两个合并,之后再将后面的逐步合并进来,只要将两个合并的写清楚,合并K个就容易很多。

  •   //合并k个有序链表
          public LinkListNode MergeTwoLinkedLists(LinkListNode[] lists){
              LinkListNode res = null;
              for (LinkListNode list : lists) {//快捷键iter回车
                  res = MergeTwoLinkedLists(res, list);
              }
              return res;
          }
    

3.3一道很无聊的好题

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

在这里插入图片描述

  •   //一道很无聊的好题
          public LinkListNode mergeInBetween(LinkListNode list1, int a, int b, LinkListNode list2) {
              LinkListNode pre1 = list1, post1 = list1, post2 = list2;
              int i = 0, j = 0;
              while (pre1 != null && post1 != null && j < b) {
                  if (i != a - 1) {//pre1指向下标为a的前一个节点
                      pre1 = pre1.next;
                      i++;
                  }
                  if (j != b) {
                      post1 = post1.next;
                      j++;
                  }
              }
              post1 = post1.next;//post1指向下标为b的后一个节点
              //寻找1ist2的尾节点
              while (post2.next != null) {
                  post2 = post2.next;
              }
              //链1尾接链2头,链2尾接链1后半部分的头
              pre1.next = list2;
              post2.next = post1;
              return list1;
          }
    

4.双指针专题

4.1寻找中间节点

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

  • 示例1
    输入:[1, 2, 3, 4, 5]
    输出:此列表的中间结点 3
    示例2
    输入:[1, 2, 3, 4, 5, 6]
    输出:此列表的中间结点 4
  • 这个问题用经典的快慢指针,用两个指针 slow 与 fast一起遍历链表。slow 一次走一步,fast 一次走两步。那么当 fast到达链表的末尾时, slow必然位于中间

  •   //寻找中间节点
          public LinkListNode FindMiddleNode(LinkListNode head){
              LinkListNode fast, slow;
              fast = slow = head;
              while(fast != null && fast.next != null){
                  slow = slow.next;
                  fast = fast.next.next;
              }
              return slow;
          }
    

4.2寻找倒数第K个节点

  • 输入一个链表,输出该链表中倒数第K个节点。从1开始计数,即链表的尾节点是倒数第1个节点
    示例
    给定一个链表:1->2->3->4->5, 和 k=2
    返回链表 4->5
  • 这里也可以使用快慢指针,我们先将 fast 向后遍历到第 k+1 个节点,slow 仍然指向链表的第一个节点,此时指针 fast 和 slow 二者之间刚好间隔 k 个节点。之后两个指针同步向后走,当 fast 走到链表的尾部空节点时,slow 指针刚好指向链表的倒数第 K个节点。

  • 需要注意的是,链表的长度可能小于 K,寻找位置的时候必须判断 fast 是否为null。

  •   //寻找倒数第K个节点
          public LinkListNode getKthFromEnd(LinkListNode head, int k){
              LinkListNode slow = head, fast = head;
              while(fast != null && k > 0){
                  fast = fast.next;
                  k--;
              }
              while(fast != null){
                  fast = fast.next;
                  slow = slow.next;
              }
              return slow;
          }
    

4.3旋转链表

  • LeetCode61:给你一个链表的头节点head, 旋转链表,将链表每个节点向右移动 K 个位置。

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

在这里插入图片描述

  • 思路:因为 K 有可能大于链表长度,所以首先取一下链表长度 length,然后 k = k % length,如果k ==0,则不用旋转,直接返回头结点,否则:

    1. 让快指针 fast先走 K 步;
    2. 快指针和慢指针一起走;
    3. 快指针走到链表尾部时,慢指针所在位置刚好是要断开的地方,把快指针的结点指向原链表头部,保存一下慢指针指向的节点的下一个节点,然后慢指针指向的节点断开和下一节点的联系。
    4. 返回结束时慢指针指向节点的下一个节点。
  •   //旋转链表
          public LinkListNode rotateRight(LinkListNode head, int k){
              if(head == null || k == 0){
                  return head;
              }
              LinkListNode slow = head, fast = head;
              LinkListNode temp = head;
              int length = 0;
              while(head != null){//得到链表的长度
                  head = head.next;
                  length++;
              }
              if(k % length == 0){
                  return temp;
              }
              //fast 从头节点向后走
              //这里使用取模运算,是为了防止 K 大于 length 的情况
              //例如,如果length=5, 那么 k=2 和 k=7 效果是一样的
              while((k % length) > 0){
                  fast = fast.next;
                  k--;
              }
              //当快指针走到链表尾节点时
              while(fast.next != null){
                  fast = fast.next;
                  slow = slow.next;
              }
              LinkListNode res = slow.next;
              slow.next = null;
              fast.next = temp;
              return res;
          }
    

5. 删除链表元素专题

  • • LeetCode 237:删除某个链表中给定的(非末尾)节点。传入函数的唯一参数为要被删除的节点。
    • LeetCode 1474.删除链表 M 个节点之后的 N个节点。
    •LeetCode 83 存在一个按升序排列的链表,请你删除所有重复的元素,使每个元素只出现一次。
    •LeetCode 82 存在一个按升序排列的链表,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中没有重复出现的数字。
    我们在链表基本操作部分介绍了删除的方法,至少需要考虑删除头部,删除尾部和中间位置三种情况的处理。而上面这些题目就是这个删除操作的进一步拓展。

5.1删除特定节点

  • LeetCode203:给你一个链表的头节点 head 和一个整数 val,请你删除链表中所有满足Node.val == val 的节点,并返回新的头节点。

  • 示例1:
    输入:head = [1, 2, 3, 4, 5, 6], val = 6
    输出:[1, 2, 3, 4, 5]

在这里插入图片描述

  • 对于删除,我们注意到首元素的处理方式与后面的不一样。为此,我们可以先创建一个虚拟节点 dummyHead,使其指向 Head,即 dummyHead.next = next,这样就不用单独处理首节点了。

  • 完整步骤:

      1. 我们创建一个虚拟链表头 dummyHead, 使其指向 Head。
      2. 开始循环链表寻找目标元素,注意要通过 cur.next.val 来判断。
      3. 如果找到目标元素,就使用 cur.next = cur.next.next 来删除。
      4. 注意最后返回的时候要用 dummyHead.next, 而不是 dummyHead。
  •   //删除特定节点
          public LinkListNode DeleteSpecificNode(LinkListNode head, int val){
              LinkListNode dummyHead = new LinkListNode(0);
              dummyHead.next = head;
              LinkListNode cur = dummyHead;
              while(cur.next != null){
                  if(cur.next.val == val){
                      cur.next = cur.next.next;
                  }else{
                      cur = cur.next;
                  }
              }
              return dummyHead.next;
          }
    

5.2删除倒数第n个节点

  • LeetCode 19. 删除链表的倒数第 N 个节点,并且返回链表的头节点。

  • 示例1:
    输入:head = [1, 2, 3, 4, 5], n = 2
    输出:[1, 2, 3, 5]

在这里插入图片描述

  • 方法1:计算链表长度

  • 首先从头节点开始对链表进行一次遍历,得到链表的长度 length,随后我们再从头节点开始对链表进行一次遍历,当遍历到第 length-n+1 个节点时,它就是我们需要删除的节点。

  •   //删除倒数第n个节点
          //计算链表长度
          public LinkListNode removeNthFromEnd(LinkListNode head, int n){
              LinkListNode dummyHead = new LinkListNode(0);
              dummyHead.next = head;
              LinkListNode cur = dummyHead;
              int length = 0;
              while(head != null){
                  head = head.next;
                  length ++;
              }
              for(int i = 1; i < length-n+1; ++i){
                  cur = cur.next;
              }
              cur.next = cur.next.next;
              return dummyHead.next;
          }
    
  • 方法2:双指针

  • 我们定义 first 和 second 两个指针,first 先走 n 步,然后 second 再开始走,当 first 走到队尾的时候,second 就是我们要的节点。

  •   //双指针
          public LinkListNode removeNthFromEnd2(LinkListNode head, int n){
              LinkListNode dummyHead = new LinkListNode(0);
              dummyHead.next = head;
              LinkListNode first = dummyHead, second = dummyHead;
              for(int i = 1; i <= n; i++){
                  first = first.next;
              }
              while(first.next != null){
                  first = first.next;
                  second = second.next;
              }
              second.next = second.next.next;
              return dummyHead.next;
          }
    

5.3删除重复元素

  • 关于结点删除的题:
    LeetCode 83 存在一个按升序排列的链表,请你删除所有重复的元素,使每个元素只出现一次。
    LeetCode 82 存在一个按升序排列的链表,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中没有重复出现的数字。
    两个题其实是一个,区别就是一个要将出现重复的保留一个,一个是只要重复都不要了,处理起来略有差别。LeetCode 1836是在82的基础上将链表改成无序的了,难度要增加不少,感兴趣的话可以自己研究一下。
5.3.1重复元素保留一个
  • LeetCode 83 存在一个按升序排列的链表,给你这个链表的头节点 head,请你删除所有重复的元素,使每个元素只出现一次。返回同样按升序排列的结果的链表。

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

在这里插入图片描述

  • 由于给定的链表是排好序的,因此重复的元素在链表中出现的位置是连续的,因此我们只需要对链表进行一次遍历,就可以删除重复的元素。具体地,我们从指针cur 指向链表的头节点,随后开始对链表进行遍历。如果当前 cur 与cur.next 对应的元素相同,那么我们就将cur.next 从链表中移除;否则说明链表中已经不存在其它与cur 对应的元素相同的节点,因此可以将 cur 指向cur.next。当遍历完整个链表之后,我们返回链表的头节点即可。另外要注意的是 当我们遍历到链表的最后一个节点时,cur.next 为空节点,此时要加以判断。

  •   //删除重复元素
          //重复元素保留一个
          public LinkListNode deleteDuplicates(LinkListNode head){
              if(head == null){
                  return head;
              }
              LinkListNode cur = head;
              while(cur.next != null){
                  if(cur.val == cur.next.val){
                      cur.next = cur.next.next;
                  }else{
                      cur = cur.next;
                  }
              }
              return head;
          }
    
5.3.2重复元素都不要
  • LeetCode 82 存在一个按升序排列的链表,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中没有重复出现的数字。

  • 示例1:
    输入:head = [1, 2, 3, 3, 4, 4, 5]
    输出:[1, 2, 5]
  • 当一个都不要是,链表只要直接对 cur.next 以及 cur.next.next 两个node进行比较就行了,这里要注意两个node都可能为空,稍加判断就行了。

  •   //重复元素都不要
          public LinkListNode deleteDuplicates2(LinkListNode head){
              if(head == null){
                  return head;
              }
              LinkListNode dummyHead = new LinkListNode(0);
              dummyHead = head;
              LinkListNode cur = dummyHead;
              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 dummyHead;
          }
      
    
          if(head == null){
              return head;
          }
          LinkListNode dummyHead = new LinkListNode(0);
          dummyHead = head;
          LinkListNode cur = dummyHead;
          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 dummyHead;
      }
    
    
    
    
  • 28
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

种一棵树leng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值