算法专题(链表)

161 阅读6分钟

链表的合并

真题描述:将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有结点组成的。示例:

输入:1->2->4, 1->3->4 输出:1->1->2->3->4->4
/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
const mergeTwoLists = function(l1, l2) {
  // 定义头结点,确保链表可以被访问到
  let head = new ListNode()
  // cur 这里就是咱们那根“针”
  let cur = head
  // “针”开始在 l1 和 l2 间穿梭了
  while(l1 && l2) {
      // 如果 l1 的结点值较小
      if(l1.val<=l2.val) {
          // 先串起 l1 的结点
          cur.next = l1
          // l1 指针向前一步
          l1 = l1.next
      } else {
          // l2 较小时,串起 l2 结点
          cur.next = l2
          // l2 向前一步
          l2 = l2.next
      }
      
      // “针”在串起一个结点后,也会往前一步
      cur = cur.next 

  }
  
  // 处理链表不等长的情况
  cur.next = l1!==null?l1:l2
  // 返回起始结点
  return head.next
};

链表结点的删除

dummy节点应用

真题描述:给定一个排序链表,删除所有含有重复数字的结点,只保留原始链表中 没有重复出现的数字。 示例 1:

  • 输入: 1->2->3->3->4->4->5

  • 输出: 1->2->5 示例 2:

  • 输入: 1->1->1->2->3

  • 输出: 2->3

思路分析

我们先来分析一下这道题和上道题有什么异同哈:相同的地方比较明显,都是删除重复元素。不同的地方在于,楼上我们删到没有重复元素就行了,可以留个“独苗”;但现在,题干要求我们只要一个元素发生了重复,就要把它彻底从链表中干掉,一个不留。
这带来了一个什么问题呢?我们回顾一下前面咱们是怎么做删除的:在遍历的过程中判断当前结点和后继结点之间是否存在值相等的情况,若有,直接对后继结点进行删除:
这个过程非常自然,为啥?因为咱们要删除某一个目标结点时,必须知道它的前驱结点。在上图中,我们本来就是站在前驱结点的位置,对其后继结点进行删除,只需要将前驱结点的 next 指针往后挪一位就行了。
但是现在,咱们要做的事情变成了把前驱和后继一起删掉,前面两个值为1的结点要一起狗带才行,起始结点直接变成了第三个: \

如果继续沿用刚才的思路,我们会发现完全走不通。因为我们的 cur 指针就是从图中第一个结点出发开始遍历的,无法定位到第一个结点的前驱结点,删除便无法完成。
其实在链表题中,经常会遇到这样的问题:链表的第一个结点,因为没有前驱结点,导致我们面对它无从下手。这时我们就可以用一个 dummy 结点来解决这个问题。 所谓 dummy 结点,就是咱们人为制造出来的第一个结点的前驱结点,这样链表中所有的结点都能确保有一个前驱结点,也就都能够用同样的逻辑来处理了。 dummy 结点能够帮助我们降低链表处理过程的复杂度,处理链表时,不设 dummy 结点思路可能会打不开;设了 dummy 结点的话,就算不一定用得上,也不会出错。所以笔者个人非常喜欢用 dummy 结点。有心的同学可能也会注意到,在本节的第一题“链表的合并”中,其实也有 dummy 结点的身影。
回到这道题上来,我们首先要做的就是定义一个 dummy 结点,指向链表的起始位置:
这样一来,如果想要删除两个连续重复的值为 1 的结点,我们只需要把 dummy 结点的 next 指针直接指向 2:
如此一来,就大功告成啦~
注意:由于重复的结点可能不止一个两个,我们这里需要用一个 while 循环来反复地进行重复结点的判断和删除操作。

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
const deleteDuplicates = function(head) {
    // 极端情况:0个或1个结点,则不会重复,直接返回
    if(!head || !head.next) {
        return head
    }
    // dummy 登场
    let dummy = new ListNode() 
    // dummy 永远指向头结点
    dummy.next = head   
    // cur 从 dummy 开始遍历
    let cur = dummy 
    // 当 cur 的后面有至少两个结点时
    while(cur.next && cur.next.next) {
        // 对 cur 后面的两个结点进行比较
        if(cur.next.val === cur.next.next.val) {
            // 若值重复,则记下这个值
            let val = cur.next.val
            // 反复地排查后面的元素是否存在多次重复该值的情况
            while(cur.next && cur.next.val===val) {
                // 若有,则删除
                cur.next = cur.next.next 
            }
        } else {
            // 若不重复,则正常遍历
            cur = cur.next
        }
    }
    // 返回链表的起始结点
    return dummy.next;
};

快慢指针

真题描述:给定一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例:

  • 给定一个链表: 1->2->3->4->5, 和 n = 2.
  • 当删除了倒数第二个结点后,链表变为 1->2->3->5.

说明:给定的 n 保证是有效的。

/**
 * @param {ListNode} head
 * @param {number} n
 * @return {ListNode}
 */
const removeNthFromEnd = function(head, n) {
    // 初始化 dummy 结点
    const dummy = new ListNode()
    // dummy指向头结点
    dummy.next = head
    // 初始化快慢指针,均指向dummy
    let fast = dummy
    let slow = dummy

    // 快指针闷头走 n 步
    while(n!==0){
        fast = fast.next
        n--
    }
    
    // 快慢指针一起走
    while(fast.next){
        fast = fast.next
        slow = slow.next
    }
    
    // 慢指针删除自己的后继结点
    slow.next = slow.next.next
    // 返回头结点
    return dummy.next
};

237. 删除链表中的节点

leetcode-cn.com/problems/de…

链表的反转

206. 反转链表

leetcode-cn.com/problems/re…

328. 奇偶链表

leetcode-cn.com/problems/od…

链表排序

148. 排序链表

leetcode-cn.com/problems/so…

环形链表

圆圈中最后剩下的数字

题目
0,1,...,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。\

其实这就是著名的约瑟夫环问题,下面是这个问题产生的背景,一个有趣的故事:
据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决?Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

www.conardli.top/docs/dataSt… 思路

解法1:用链表模拟环

  • 用链表模拟一个环

  • 模拟游戏场景

  • 记录头节点的前一个节点current,以保证我们找到的要删除的节点是current.next

  • 每次循环m次找到目标节点删除,直到链表只剩下一个节点

  • 时间复杂度O(m*n) 空间复杂度O(n) 解法2:用数组模拟

  • 每次计算下标,需要考虑末尾条件 解法3:数学推导

  • f(n) = (f(n-1)+m)%n 即 f(n,m) = (f(n-1,m)+m)%n

  • 使用递归求解 边界条件为 n=1 时间复杂度 1>2>3 易理解程度 1>2>3

    // 解法1
    function LastRemaining_Solution(n, m) {
      if (n < 1 || m < 1) {
        return -1;
      }
      const head = { val: 0 }
      let current = head;
      for (let i = 1; i < n; i++) {
        current.next = { val: i }
        current = current.next;
      }
      current.next = head;

      while (current.next != current) {
        for (let i = 0; i < m - 1; i++) {
          current = current.next;
        }
        current.next = current.next.next;
      }
      return current.val;
    }

    // 解法2
    function LastRemaining_Solution(n, m) {
      if (n < 1 || m < 1) {
        return -1;
      }
      const array = [];
      let index = 0;
      for (let i = 0; i < n; i++) {
        array[i] = i;
      }
      while (array.length > 1) {
        index = (index + m) % array.length - 1;
        if (index >= 0) {
          array.splice(index, 1);
        } else {
          array.splice(array.length - 1, 1);
          index = 0;
        }
      }
      return array[0];
    }

    // 解法3
    function LastRemaining_Solution(n, m) {
      if (n < 1 || m < 1) {
        return -1;
      } else {
        return joseoh(n, m);
      }

    }

    function joseoh(n, m) {
      if (n === 1) {
        return 0;
      }
      return (joseoh(n - 1, m) + m) % n;
    }

141. 环形链表

leetcode-cn.com/problems/li…

剑指 Offer II 029. 排序的循环链表

leetcode-cn.com/problems/4u…

双向链表

138. 复制带随机指针的链表

leetcode-cn.com/problems/co…

剑指 Offer II 028. 展平多级双向链表

leetcode-cn.com/problems/Qv…

160. 相交链表

leetcode-cn.com/problems/in…

234. 回文链表

leetcode-cn.com/problems/pa…