链表刷题总结

链表刷题总结


做了一段时间的链表题,多多少少也看了一些优秀题解,链表解题技巧无非就以下几种:

  1. 朴素解法,head = head.next遍历链表来解决问题;
  2. 双指针,甚至是三指针,在很多的链表题中发挥很大的作用;
  3. 快慢指针,快指针和慢指针以不同的速度同时遍历链表;
  4. 递归,大多数链表题都能用递归来解决;
  5. 哑节点dummy,或者是哨兵节点,可以简化链表的极端操作;

朴素解法就不总结了,下面来说说我这几天对其他节点的感受和做过的题。

双指针


对于链表的题目,双指针的应用太多了。不论是删除链表的倒数第N个节点,还是回文链表,都会用到双指针。因为链表的随机访问效率差,当我们想要处于某个位置的节点的时,都需要去从头遍历,所以使用额外的指针来记录节点位置是非常有必要的。

  • 删除链表的倒数第N个节点,对于这个问题,使用双指针beforeafter,距离N个节点,即before先走N步,然后afterbefore一起走。当before到达链尾时,after指向倒数第N个节点。

    while (n-- > 0){
        before = before.next;
    }
    while (before.next != null){ // 结束循环后slow指向需要删除节点的前一个节点
        before = before.next;
        after = after.next;
    }
    
  • 回文链表:双指针分别指向头尾,像中间靠近。

    int front = 0;      // 双指针分别指向头尾,向中间靠经
    int back = vals.size() - 1;
    while (front < back){
        if (!vals.get(front).equals(vals.get(back))){  // 使用equals
        	return false;
         }
         front++;
         back--;
    }
    
  • 旋转链表:本质上是找到倒数第K个节点,但是K有可能大于链表的长度,所以可以先考虑将链表闭合,再找到相应位置断开这个环。

    // 记录链表节点数
    int n;                 
    for (n = 1; old_tail.next != null; n++){
        old_tail = old_tail.next;
    }
    
    // 形成闭环
    old_tail.next = head;  
    
    ListNode new_tail = head;
    for (int i = 0; i < n - k%n - 1; i++){
        new_tail = new_tail.next;
    }
    ListNode new_head = new_tail.next;
    
    // 断开
    new_tail.next = null;
    return new_head;
    
  • 两两交换链表中的节点:使用三个指针,需要交换的节点firstNodesecondNode,还有firstNode的前一个节点prevNode

    while ((head != null) && (head.next != null)) {
        ListNode firstNode = head;
        ListNode secondNode = head.next;
    	// 进行交换
    	prevNode.next = secondNode;
    	firstNode.next = secondNode.next;
    	secondNode.next = firstNode;
       	// 对prevNode和head重新赋值,准备下一次的交换 
        prevNode = firstNode;
        head = firstNode.next; // jump
    }
    

链表中用到指针的地方非常多,同时在草稿纸上进行画图对解题也非常有帮助。

快慢指针


我觉得快慢指针其实也就是双指针的一种,只不过快慢指针是同时以不同的速度对链表进行遍历,来解决相关的问题。

  • 链表的中间节点:
    • 解决这个问题,我们当然可以对链表进行两次遍历。第一次遍历记录链表节点的总数,然后再遍历到链表的中点位置即可。
    • 如果使用快慢指针呢?我们将快指针fast的速度设为2,即每次走两步,将慢指针slow的速度设为1,每次走一步。这样,当fast走到末尾时,slow正好走到中间,完美解决题目。
    • 思考:这个和上面找到倒数第N个节点的不同在哪?
      • 使用双指针,两个指针不同时出发,这个时候我们对于先出发的指针需要先走多少步是已知的。
      • 而对于这个题来说,链表的总结点我们不知道,所以我们并不知道要先走多少步,所以快慢指针同时出发,因为fast的速度是slow的两倍,所以当fast走到末尾时,slow正好在中间。
  • 环形链表
    • 使用快慢指针解决环形链表问题,如果是环形链表,则在某一时刻快指针会追上慢指针,如果不是,快指针会先一步到达链表尾部,退出循环。
    • 当我们需要找到环形链表的环头时,将快指针的速度设为2,慢指针速度设为1,当快慢指针相遇时,它们不一定在环的起始节点,所以我们在找到了相遇节点后,该如何找到环开始的位置呢?
      • 数学思想:快指针的速度是慢指针的2倍,所以快指针走的路程是慢指针走的路程的两倍;
      • 在分段画图分析后,我们可以得到这样的结论:head出发与meet相遇,两指针的速度一样,相遇时即为环的起点。

相关题目

递归


使用递归函数,避免复杂的更改指针变量指向操作,使解题更加简单。

相关题目

  • (力扣)21.合并两个有序链表

    • 将两链表的头的值进行比较,较小的节点的next为下一层递归的结果,本级递归返回两链表头较小的节点

      if (l1.val < l2.val) {
          l1.next = mergeTwoLists(l1.next, l2);
          return l1;
      }else {
          l2.next = mergeTwoLists(l1, l2.next);
          return l2;
      }
      
  • (力扣)23.合并K个升序链表

    • 归并排序,我们将链表数组分成两个,分别对小链表数组进行合并。
    • 通过递归将链表一直分割,直到链表只有1个时,此时链表是有序的,然后将两个有序的链表进行合并;
      • 先对子列表进行递归,返回对列表内排序好的头节点;
      • 然后再对当前的两个链表进行合并,合并调用上面21题的函数。
  • (力扣)24.两两交换链表中的节点

    • 终止条件

      • 当递归到链表为空或者只有1个元素时,无法进行交换,递归终止;
    • 返回值

      • 已经完成交换的子列表的头结点
    • 单次递归过程

      • 将待交换的两个节点设置为firstNodesecondNodefirstNode交换后在后面,所以firstNode.next指向下一层递归返回的子链表,而secondNodefirstNode位置交换,所以secondNode.next指向firstNode

        ListNode firstNode = head;
        ListNode secondNode = head.next;
        
        // 交换
        firstNode.next  = swapPairs(secondNode.next);
        secondNode.next = firstNode;
        
        // 返回
        return secondNode;
        
  • (力扣)203.移除链表元素

    • 递归边界,head == null

    • 先调用递归函数删除后面的目标元素,即head.next = removeElements(head.next, val);

    • 再判断当前节点是否等于目标元素

      • 相等,返回head.next;
      • 不相等,返回head
      public ListNode removeElements(ListNode head, int val) {
          if (head == null){
              return null;
          }
          head.next = removeElements(head.next, val);
          return head.val == val ? head.next : head;
      }
      
  • (力扣)206.反转链表

    • 递归边界

      • 当递归到链表为空或者只有1个元素时,不需要反转,递归终止;
    • 返回值

      • 返回已经反转好的子链表头节点
    • 本级任务

      • 将当前节点的下一节点的next指向当前节点
      public ListNode reverseList(ListNode head) {
          if (head == null || head.next == null) return head;
          ListNode p = reverseList(head.next);
          head.next.next = head;
          head.next = null;
          return p;
      }
      
  • (力扣)234.回文链表

    • cur先到尾节点,由于递归的特性再从后往前进行比较;

    • front是递归函数外的指针;

    • 如果cur.val != front.val,返回false

    • 否则,front向前移动并返回true

      本质上是同时在正向和逆向迭代,利用递归的特性,可以实现链表逆向迭代,翻转链表的递归方法就是利用这个思想。

  • (力扣)328.奇偶链表

    • 终止条件

      • 当递归到链表只有两个节点时,不需要再进行奇偶分离再合并,递归终止。
    • 返回值

      • 返回奇链表的尾节点。
    • 单次递归过程

      • 进行奇偶分离odd.next = even.next; even.next = odd.next.next;后,进入下一层递归。

哑节点


设置dummy节点,避免对链表第1个节点进行单独讨论。

相关题目

总结


第一次这样写刷题总结,还在摸索当中,凑合看吧,多写几次肯定会有进步,加油!

TO BE CONTINUED...
posted @ 2020-09-20 15:37  Haloya  阅读(137)  评论(0编辑  收藏  举报