链表

155 阅读6分钟

链表反转

反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

迭代法:

  • 需要一个变量保存cur后的节点,因为中间会修改cur.next的指向
  • 循环结束时,pre为反转后链表的第一个节点,cur为反转链表部分的后一个节点
var reverseList = function(head) {
  let pre = null, cur = head, next = null
  while (cur) {
    next = cur.next
    cur.next = pre
    pre = cur
    cur = next
  }
  return pre
};

递归法:

假设head后的节点已经反转了,只需要将head添加到反转后的链表的尾部

head.next.next为子链表翻转后的最后一个节点,将head节点添加在其后面

var reverseList = function(head) {
  if (head === null || head.next === null) return head
  const res = reverseList(head.next)
  head.next.next = head
  head.next = null
  return null
};

反转链表II

给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。

反转逻辑同反转链表类似,但是需要知道链表反转部分的前一个节点和后一个节点

后一个节点就是反转逻辑中的cur, 前一个节点则需要额外变量保存

var reverseBetween = function(head, left, right) {
  let dummy = new ListNode(-1)
  dummy.next = head
  let nodeBeforeLeft = dummy
  // 找到整数left对应的前一个节点
  for (let i = 0; i < left - 1; i++) {
    nodeBeforeLeft = nodeBeforeLeft.next
  }

  // 反转left到right部分的链表
  let pre = null, next = null, cur = nodeBeforeLeft.next
  for (let i = 0; i < right - left + 1; i++) {
    next = cur.next
    cur.next = pre
    pre = cur
    cur = next
  }

  // 反转后的链表部分接入到原链表
  nodeBeforeLeft.next.next = cur
  nodeBeforeLeft.next = pre

  return dummy.next
};

k个一组翻转链表

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k **的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

解法一:

将链表中的节点保存到数组中,按照翻转后的顺序依次访问。

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

空间复杂度:O(n)O(n),因为需要额外的数组保存节点

var reverseKGroup = function(head, k) {
  // 保存数组
  const arr = []
  let cur = head
  while (cur) {
    arr.push(cur)
    cur = cur.next
  }

  // 按组翻转链表
  const n = Math.floor(arr.length / k)
  let dummy = new ListNode(-1)
  dummy.next = head
  cur = dummy
  for (let i = 0; i < n; i++) {
    for (let j = k - 1; j >= 0; j--) {
      cur.next = arr[j + k * i]
      cur = cur.next
    }
  }
  
  // 处理链表未被整除部分
  for (let i = 0; i < arr.length - n * k; i++) {
    cur.next = arr[n * k + i]
    cur = cur.next
  }
  // 当节点长度刚好被k整除时,此时的cur为节点,需要手动将其next置为null
  if (cur) cur.next = null
  return dummy.next
};

解法二:

与反转链表II的解法类似,但是会循环多组,注意在一组链表反转后,p0需要相应的更新

function getListLength(head) {
  if (!head) return 0
  let length = 0
  while (head) {
    length++
    head = head.next
  }
  return length
}

var reverseKGroup = function(head, k) {
  let dummy = new ListNode(-1, head), p0 = dummy
  const length = getListLength(head)
  const n = Math.floor(length / k) 
  for (let i = 0; i < n; i++) {
    let cur = p0.next, pre = null, next = null
    // k个一组翻转链表
    for (let j = 0; j < k; j++) {
      next = cur.next
      cur.next = pre
      pre = cur
      cur = next 
    }
    const tmp = p0.next // 保存p0的下一个位置
    p0.next.next = cur 
    p0.next = pre
    p0 = tmp // 更新p0
  }
  return dummy.next
};

快慢指针

链表的中间节点

给你单链表的头结点 head ,请你找出并返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

var middleNode = function(head) {
  let slow = head, fast = head
  while (fast && fast.next) {
    slow = slow.next
    fast = fast.next.next
  }
  return slow
};

环形链表

给你一个链表的头节点 head ,判断链表中是否有环。 如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。

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

解法分析:

使用快指针(一次走两步)、慢指针(一次走一步),快指针每次比慢指针多走一步,如果存在环的话,那么快慢指针必定会在环中相遇。

而且快慢指针相遇时,慢指针肯定没有走完全部环。因为假如慢指针走了环的一圈的话,则快指针肯定比慢指针多走了环的一圈的距离,快慢指针肯定已经相遇了。

var hasCycle = function(head) {
  if (!head) return false
  let slow = head, fast = head
  while (fast && fast.next) {
    slow = slow.next
    fast = fast.next.next
    if (slow === fast) {
      return true
    }
  }
  return false
};

环形链表II

给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环.不允许修改 链表.

解法分析:

假设链表头到环入口的距离为a,快慢指针相遇的地方正序距环入口的距离为b,快慢指针相遇的地方逆序距环入口的距离为c。正序意思是按照指针的指向顺序。

快指针走的距离:a+b+k(b+c),k>0a + b + k(b + c), k > 0

慢指针走的距离:a+ba + b

那么:

a+b+k(b+c)=2(a+b)a+b=k(b+c)a=(k1)(b+c)+ca+b+k(b+c) = 2(a+b) a + b = k(b + c) a = (k - 1)(b + c) + c

从上面的推导可以看出:当快慢指针相遇时,一个指针从相遇点,一个指针从链表头,两者同时前进,那么这两个指针必定是在环的入口相遇。由此代码如下:

var detectCycle = function(head) {
  if (!head || !head.next) return null
  let slow = head, fast = head
  while (fast && fast.next) {
    slow = slow.next
    fast = fast.next.next
    if (slow === fast) break
  }
  if (!fast || !fast.next) return null // 链表无环
  let res = head
  while (res !== fast) {
    res = res.next
    fast = fast.next
  }
  return res
};

重排链表

给定一个单链表 L **的头节点 head ,单链表 L 表示为:

L0 → L1 → … → Ln - 1 → Ln

请将其重新排列后变为:

L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → …

不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换

解法分析:

将链表从中间分为两段,后半段先进行反转,然后依次从两段链表中取出节点进行重新排列。

var reorderList = function(head) {
  if (!head || !head.next) return head
  const midNode = midList(head) // 获取中间节点
  const head2 = reverseList(midNode) // 链表后半段反转

  let first = head, second = head2, first_next = null, second_next = null
  while (second.next) {
    first_next = first.next
    second_next = second.next
    first.next = second
    second.next = first_next
    first = first_next
    second = second_next
  }
  return head
};

链表删除

删除链表中的节点

给你一个需要删除的节点 node 。你将 无法访问 第一个节点  head

链表的所有值都是 唯一的,并且保证给定的节点 node 不是链表中的最后一个节点

解法分析:注意本题只给了要删除的节点node,并未给链表head, 所以无法获取node的前一个节点。但是链表中的值是不同的,所以可以将node.next的值给node,把node.next删除掉。

var deleteNode = function(node) {
  node.val = node.next.val
  node.next = node.next.next
};

删除链表的倒数第n个节点

给你一个链表,删除链表的倒数第 n **个结点,并且返回链表的头结点

解法分析:

  • 倒数第n个节点,该节点可能是head节点,因此需要使用哨兵节点,确保第一个节点是存在的。
  • 如果先获取链表长度,再正向遍历到 长度-n ,将该节点删除。此方法需要两次扫描链表
  • 一次扫描的话,可以使用快慢指针,相距n,当快指针为null时,慢指针自然指向倒数第n个节点
var removeNthFromEnd = function (head, n) {
  let dummy = new ListNode(-1, head)
  let fast = head, slow = head, pre = dummy
  for (let i = 0; i < n; i++) {
    fast = fast.next
  }
  while (fast) {
    pre = pre.next
    slow = slow.next
    fast = fast.next
  }
  // slow此时指向倒数第n个节点
  pre.next = slow.next
  slow = null
  return dummy.next
}

解法优化:

最关键的是我们需要获得倒数第n+1个节点,因此上面的解法引入了pre变量。但其实pre变量不是必须的,修改循环的条件即可。

var removeNthFromEnd = function (head, n) {
  let dummy = new ListNode(-1, head)
  let fast = dummy, slow = dummy
  for (let i = 0; i < n; i++) {
    fast = fast.next
  }
  while (fast.next) {
    slow = slow.next
    fast = fast.next
  }
  // slow此时指向倒数第n+1个节点
  slow.next = slow.next.next
  return dummy.next
}

删除排序链表中的重复元素

给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。

// 复杂写法
var deleteDuplicates = function(head) {
  if (!head) return null
  let cur = head
  while (cur) {
    let next = cur.next
    while (next) {
      if (next.val === cur.val){
        next = next.next
      } else {
        break
      }
    }
    cur.next = next
    cur = next
  }
  return head
};

// 优雅写法
var deleteDuplicates = function(head) {
  if (!head) return null
  let cur = head
  while (cur.next) {
    if (cur.next.val === cur.val) {
      cur.next = cur.next.next
    } else {
      cur = cur.next
    }
  } 
  return head
};

删除排序链表中的重复元素II

给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。

解法分析:

  • head节点可能会被删除,需要在头部添加哨兵节点,其值小于head节点的值
  • 所有重复数字的节点均被删除,表示cur也可能被删除,因此需要保存pre变量,表示无重复数字链表的尾部。
// 复杂写法
var deleteDuplicates = function(head) {
  if (!head) return null
  let dummy = new ListNode(head.val - 1, head)
  let pre = dummy, cur = head, next = null
  while (cur) {
    next = cur.next
    while (next) {
      if (next.val === cur.val) {
        next = next.next
      } else {
        break
      }
    }
    if (next === cur.next) {
      pre = cur
      cur = next
    } else {
      pre.next = next
      cur = next
    }
  }
  return dummy.next
};

// 优雅写法
var deleteDuplicates = function(head) {
  if (!head) return null
  let dummy = new ListNode(head.val - 1, head)
  let cur = dummy
  // 两个一组判断
  while (cur.next && cur.next.next) {
    let val = cur.next.val
    if (cur.next.next.val === val) {
      while (cur.next && cur.next.val === val) {
        cur.next = cur.next.next
      }
    } else {
      cur = cur.next
    }
  } 
  return dummy.next
};