leetcode刷题——链表

189 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第14天

leetcode-21--合并两个有序链表

Question:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。(`l1 `和` l2 `均按 非递减顺序 排列)

示例: 输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,5,4]

Method 1:递归

思路:已知两个升序的链表

数据结构:单链表

使用递归前,最好做一个建模:

l1[0]+merge(l1[1:],l2),l1[0]<l2[0] l2[0]+merge(l1,l2[1:]),otherwise 边界情况:

l1l2都为空 l1l2为空 为了能够更好地理解递归的过程,把链表对象键值对的形式写出来

const l1 = {
  val: 1,
  next: {
    val: 2,
    next: { val: 4, next: null },
  },
};

const l2 = {
  val: 1,
  next: {
    val: 3,
    next: { val: 4, next: null },
  },
};

基于此我们再看代码...

// 我们需要辅助链表 =》 构造链表
const ListNode = function (val, next) {
  this.val = val || 0;
  this.next = next || null;
};

const mergeTwoLists = (l1, l2) => {
  // 递归函数
  const recursion = (newListNode, l1, l2) => {
    // 1 如果链表 l1 l2 都为空,那就结束本次递归
    if (!l1 && !l2) {
      return;
    }
    // 2 如果 l1 或 l2 链表为空,将非空连接加到新链表后面,也结束本次递归
    if (!l1 || !l2) {
      newListNode.next = l1 || l2;
      return;
    }
    // 3 再创建一个辅助链表,用来获取新链表的下一个新节点
    // 这里配合点 4 来看比较容易懂
    newListNode.next = new ListNode();
    newListNode = newListNode.next;

    // 4 排序,同时将采纳了的链表往后挪一位
    if (l1.val >= l2.val) {
      newListNode.val = l2.val;
      l2 = l2.next;
    } else {
      newListNode.val = l1.val;
      l1 = l1.next;
    }
    // 5 继续下一次递归
    recursion(newListNode, l1, l2);
  };

  // 初始化一个新链表(作为结果链表)
  const newListNode = new ListNode();
  // 处理这个链表,和 l1、l2
  recursion(newListNode, l1, l2);
  // 返回结果链表
  return newListNode.next; // 重点:我们初始化的时候,有一个没用的链表,记得往后挪一位
};

通过上面的递归解法,我们理清思路后,可以优化递归的代码:

const mergeTwoLists = function (l1, l2) {
  if (l1 === null) {
    return l2;
  } else if (l2 === null) {
    return l1;
  } else if (l1.val < l2.val) {
    l1.next = mergeTwoLists(l1.next, l2);
    return l1;
  } else {
    l2.next = mergeTwoLists(l1, l2.next);
    return l2;
  }
};

时空复杂度:O(n + m)

Method 2:迭代 + 双指针

思路很像递归的思路:

初始化一个新链表 归并排序

const ListNode = function (val, next) {
  this.val = val || 0;
  this.next = next || null;
};
const mergeTwoLists = function (l1, l2) {
  const prehead = new ListNode(); // 创建一个哨兵节点(链表)
  let prev = prehead; // 初始化指针 prev 指向哨兵节点
  while (l1 && l2) {
    if (l1.val <= l2.val) {
      prev.next = l1; // 这里同时改变了 prehead.next, prev 仅仅是辅助指针
      l1 = l1.next;
    } else {
      prev.next = l2;
      l2 = l2.next;
    }
    prev = prev.next; // 将辅助指针 prev 移动到下一位(当前的 prehead.next)
  }
  prev.next = l1 || l2; // 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接通过指针将链表的末尾指向未合并完的链表即可
  return prehead.next; // 返回哨兵节点(链表)之后的所有节点
};

时间复杂度:O(n + m),空间复杂度:O(1)

leetcode-141--环形链表

Question:给定一个链表,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪next指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数pos来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果pos是 -1,则在该链表中没有环。注意:pos不作为参数进行传递,仅仅是为了标识链表的实际情况。

如果链表中存在环,则返回true。 否则,返回false

进阶:

你能用 O(1)(即,常量)内存解决此问题吗?

Method 1:暴力法

思路:

cur1每遍历到一个节点,就让cur2从头遍历之前所有节点 如果cur2走到cur1,所用的步数二者一样,则相遇点不是入环点 如果cur2走到cur1,用的步数二者不一样,则相遇点是入环点,cur1cur2多走一个环

时间复杂度为O(n^2),空间复杂度为O(1)

const hasCycle = (head) => {
  let cur1 = head; // cur1指针指向链表节点
  let step1 = 0; // 初始化cur1指针走的步数
  while (cur1) {
    step1++; // 走一步
    let cur2 = head; // 然后创建cur2指针开始从头遍历
    let step2 = 0; // 初始化cur2指针走的步数
    while (cur2) {
      step2++; // 走一步
      if (cur1 == cur2) {
        // cur1和cur2的元素相同
        if (step1 == step2) {
          // 如果走的步数一样,即走到了cur1这里
          break; // 退出内层while
        } else {
          // 相遇但步数不一样
          return true; // cur2多走了几步(一个环)又回到了cur1,说明链表有环
        }
      }
      cur2 = cur2.next; // cur2向后移动一步
    }
    cur1 = cur1.next; // cur1向后移动一步
  }
  return false;
};

Method 2:利用 JSON.stringify() 不能序列化含有循环引用的结构

const hasCycle = function (head) {
  try {
    JSON.stringify(head);
    return false;
  } catch (err) {
    return true;
  }
};

时间复杂度:O(n),空间复杂度:O(n)

Method 3:标志法

给每个已遍历过的节点加标志位,遍历链表,当出现一个节点已被标志时,则证明单链表有环

const hasCycle = function (head) {
  while (head) {
    if (head.flag) return true;
    head.flag = true;
    head = head.next;
  }
  return false;
};

时间复杂度:O(n),空间复杂度:O(n)

emmmm,这个方法写的总感觉哪里不对劲,虽然能顺利通过。(head.flag哪来的?这么添加属性真的可以嘛?你品,你细品)

顺便附上题目定义的链表节点

/\*\*

- Definition for singly-linked list.
- function ListNode(val) {
- this.val = val;
- this.next = null;
- }
  \*/

/\*\*

- @param {ListNode} head
- @return {boolean}
  \*/

Method 4:哈希表

思路:参考标志法的思路

哈希表存遍历过的节点,每遍历一个节点,都查看哈希表是否存在当前节点,如果存在,则说明链表有环 如果不存在,则存入哈希表,继续遍历 时间复杂度为O(n),空间复杂度为O(n)

const hasCycle = (head) => {
  let map = new Map();
  while (head) {
    if (map.has(head)) return true;
    map.set(head, true); // 存的是节点的地址引用,而不是节点值
    head = head.next;
  }
  return false;
};

Method 5:快慢指针(双指针法)

思路:类似 “追及问题”

两个人在环形跑道上赛跑,同一个起点出发,一个跑得快一个跑得慢,在某一时刻,跑得快的必定会追上跑得慢的,只要是跑道是环形的,不是环形就肯定追不上。

快、慢指针,从头节点出发 慢指针每次走一步,快指针每次走两步,不断比较它们指向的节点的值 如果节点值相同,说明有环。如果不同,继续循环。

const hasCycle = (head) => {
  let fast = head;
  let slow = head;
  while (fast) {
    if (fast.next == null) return false;
    slow = slow.next;
    fast = fast.next.next;
    if (slow == fast) return true;
  }
  return false;
};

时间复杂度:O(n),空间复杂度:O(1)