链表的应用

90 阅读5分钟

链表的合并

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

示例: 输入:1->2->4, 1->3->4 输出:1->1->2->3->4->4

分析: 将两个链表从头开始对比,根据对比的值依次插入新的链表,这里与其说是新的链表,其实就是一个人造的前驱节点,该节点的下一节点指向哪里是根据两个链表的对比来的

function ListNode(val) {
  this.val = val;
  this.next = null;
}
function mergeTwoLists(l1,l2){
  // 初始化一个链表
  let head = new ListNode();
  // 当前的链表,可以开始往链表上添加内容
  let cur = head;
  while (l1 && l2) {
    // 对比两个链表的值,如果l1值小,cur的next指向l1,并且将l1向前一步
    if(l1.val <= l2.val){
      cur.next = l1;
      l1 = l1.next;
    }else {
      cur.next = l2;
      l2 = l2.next;
    }
    // cur链表有值之后也要往前一步
    cur = cur.next;
  }
  // 链表如果不一样长,需要将cur.next指向剩下的链表
  cur.next = l1 === null ? l2 : l1;
  return cur;
}

链表的删除

真题描述:给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次 示例 1:

输入: 1->1->2

输出: 1->2

示例 2:

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

输出: 1->2->3

分析:删除某一节点,是将目标节点的前驱节点的next指向目标节点的后继节点;因为已有链表是排序好的,所以只需要将当前节点和下一节点进行比较就好

function deleteDuplicates(head){
      let cur = head;
      while (cur && cur.next) {
        // 如果当前节点和下一节点值相同,将当前节点的next指向下一节点的next
        if(cur.val === cur.next.val){
          cur.next = cur.next.next;
        }else {
          cur = cur.next;
        }
      }
      return head;
}

链表删除问题的延申

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

示例 1:

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

输出: 1->2->5

示例 2:

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

输出: 2->3

分析:这类问题也叫dummy问题,因为要删除的节点数不定,那么目标节点就不定,导致前驱节点也不知道,所以需要人为的造一个前驱节点

function deleteDuplicates(head){
      // 如果存在多个节点
      if(head && head.next){
        const dummy = new ListNode();
        // 指向链表的头节点
        dummy.next = head;
        // dummy的头节点val是空,所以从下一节点开始对比
        let cur = dummy;
        while (cur.next && cur.next.next) {
          if(cur.next.val === cur.next.next.val){
            // 因为会存在多个重复值,所以需要记录下当前值
            let val = cur.next.val;
            // 这里是判断cur.next是因为需要从当前节点开始删除,并且已经满足的后面两个节点是一样的
            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 保证是有效的。

解析:这里涉及到的是倒数第n个,链表不能倒着遍历,所以将问题转换为删除正数的第len-n-1个节点,之前说过删除节点需要定位目标节点的前驱节点,即len-n个节点;这里我们需要快指针去求链表的长度,慢指针就能定位目标节点;根据传入的n让快指针先走n步,然后快慢指针一起走,快指针走到最后的时候,慢指针所处的位置就是要删除节点的前驱节点

function removeNthFromEnd (head,n) {
      let dummy = new ListNode();
      dummy.next = head;
      // 设置快慢指针
      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;
}

链表的反转

真题描述:定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点

示例:

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

输出: 5->4->3->2->1->NULL

解析: 将链表进行反转即可,这里需要用到三个指针,cur表示目标节点,pre是前驱节点,next是后继节点;当目标节点的next指向前驱节点的时候就反转了;然后将三个指针往前移动,这是目标节点的后继节点是存在next中的,反转结束之后pre就会变成链表的头节点

function reverseList(head){
      let pre = null;
      let cur = head;
      // 当前节点遍历完整个链表会是null
      while (cur) {
        // 先将next存下来
        let next = cur.next;
        cur.next = pre;
        // 将pre往前走
        pre = cur;
        // cur往前走
        cur = next;
      }
      return pre;
}

局部反转一个链表

真题描述:反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。

示例:

输入: 1->2->3->4->5->NULL, m = 2, n = 4

输出: 1->4->3->2->5->NULL

解析: 很容易想到,找到需要反转的段,根据链表的反转就可以进行反转;只是需要将链表的前后段缓存下拉

const reverseBetween = function(head, m, n) {
      let dummy = new ListNode();
      // 先让p走到反转区间的初始位置
      let p = dummy;
      for(let i=0;i<m - 1;i++){
        p = p.next;
      }
      // 将反转区间前面的内容缓存起来
      let leftHead = p;
      // 用start表示需要反转区间的初始位置
      let start = leftHead.next;
      let pre = start;
      let cur = pre.next;
      // 开始反转
      for(let i=m;i<n;i++){
          let next = cur.next;
          cur.next = pre;
          prev = cur;
          cur = next;
      }
      // 得到pre是反转的
      leftHead.next = pre;
      start.next = cur;
      return dummy.next;
 }

环形链表

真题描述:给定一个链表,判断链表中是否有环。

解析: 对指针走过的节点都添加一个标记,下次走来的时候如果有标记代表是一个环

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

定位环的起点

​真题描述:给定一个链表,返回链表开始入环的第一个结点。 如果链表无环,则返回 null。

解析: 其实就是返回我们第一个打点的地方;快慢指针解决的思路:两个指针在同一点一起出发,slow一次走一步、fast 一次走两步。这样如果它们是在一个有环的链表里移动,一定有相遇的时刻

​function detectCycle(head){
      let index = 0;
      while (head) {
        if(head.flag){
          return head.index;
        }else {
          head.flag = true;
          head.index = index;
          head = head.next;
        }
      }
      return null;
 }