链表专题

1. 19.删除链表的倒数第 N 个结点 跳往力扣页面

1.1 栈

1.1.1 思路

题目要求删除倒数第 n 个节点,那么只需要将倒数第 n+1 个节点的 next 指针指向倒数第 n-1 个节点即可。

既然需要从末尾处开始寻找符合要求的节点,可以考虑将所有节点放入一个栈结构中,这样出栈的顺序不就是从链表末尾向前遍历的顺序吗?

1.1.2 使用栈结构完成
function removeNthFromEnd(head, n) {
  // 我们在创建了一个全新的头结点,想想为什么要这样做?这样做的好处是什么?
  let link = new ListNode(-1, head)
  let stack = []
  let currentNode = link
  while (currentNode) {
    stack.push(currentNode)
    currentNode = currentNode.next
  }
  for (let index = 1; index <= n + 1; index++) {
    let pre = stack.pop()
    // 被删除的前面一个节点  
    if (index === n + 1) { 
      pre.next = pre.next.next || null
      return link.next
    }
  }
  return link.next
}
1.1.3 解惑

Q: 我们在创建了一个全新的头结点,想想为什么要这样做?这样做的好处是什么?

A: 因为题目并没有说明,链表的头结点不会被删除。如果被删掉的节点刚刚是头结点,在出栈寻找倒数第 n-1 个节点时候,会提示错误。所以,我们不妨在链表的头结点之前接入一个新的节点,这样做完全避免了原链表的头结点被删除时,寻找倒数第 n-1 个节点提示错误的情况。当然你也可以不用这种方法,当栈的大小刚刚等于 n 的时候,将链表的头结点改为头结点的下一个节点即可!

function removeNthFromEnd(head, n) {
  let stack = []

  let currentNode = head
  while (currentNode) {
    stack.push(currentNode)
    currentNode = currentNode.next
  }
  // 特殊情况特殊处理
  if (n === stack.length) {
    head = head.next
    return head
  }
  let next = null
  for (let index = 1; index <= n + 1; index++) {
    let pre = stack.pop()
    if (index === n + 1) {
      pre.next = pre.next.next || null
      return link.next
    }
  }
  return head
}

1.2 双指针

1.2.1 思路

这道题目可以使用双指针解决,先设想定义了双指针 slow 和 fast ;如果两个指针正好间隔 n 个节点个数,那么指针 fast 指向链表末尾时,指针 slow 是不是刚刚指向链表的倒数第 N-1 个结点呢?

1.2.2 动画解释

1.2.3 代码的实现
function removeNthFromEnd(node, n) {
  let head = new ListNode(-1, node)
  let fast = head
  let slow = head
  // 快指针与慢指针间隔 n 个元素
  while (n >= 0) {
    fast = fast.next
    --n
  }
  // 快慢指针同事向前走,当fast指向空时,改变慢指针指向节点的 next 指向即可。
  while (fast) {
    fast = fast.next
    slow = slow.next
  }
  // 考虑到链表倒数第一个节点被删除这个特殊情况
  
  slow.next = slow.next.next || null
  return head.next
}

2. 92.反转链表 II 跳往力扣页面

2.1 栈

2.1.1 思路

题目要求翻转规定区间内部的节点,我们可以再次利用栈数据类型 后进先出 的特点,将区间内部的节点依次入栈。因为需要将指定区间的节点反转之后接入原链表中,所以我们需要找到区间的前一个节点和后一个节点。之后将栈中的节点依次出栈,并按照如下规则改变节点的指向关系:

(1)区间的前一个节点的 next 指向第一个出栈的节点。

(2)第 n 个出栈的节点的 next 指向第 n + 1 个出栈的节点(n>1)。

(3)最后一个出栈的节点的 next 指向区间的后一个节点。

2.2.3 解惑

问题一: 经过分析之后,在实现过程中需要找到区间的前一个和后一个节点,那如果翻转区间正好是将该链表的尾部位置和头部位置进行翻转时(比如 :请反转从位置 1 到位置 4 区间内的链表节点,链表为:2->3->4->5 ),区间的前一个节点是不是就不存在呢?

回答一:我们可以创建一个节点,让此节点的 next 指向原链表的头部。

2.2.3 代码实现

var reverseBetween = function (head, left, right) {
  // 创建一个栈空间,将符合条件的节点放入栈中。
  let stack = []
  // 创建一个虚拟头结点
  let link = new ListNode(-1, head)
  // 保存头结点内存地址
  let res = link
  
  let index = 1
  // 保存区间的前一个和后一个节点
  let pre = null
  let next = null
  // 我们动态在链表的基础上新增了一个头结点,所以,需要将区间整体右移1位
  left++
  right++
  // 将区间内部的节点放入栈空间 
  // 保存区间开始的前一个节点和区间结束的下一个节点
  while (link) {
    if (index === left - 1) {
      pre = link
    }
    if (index === right + 1) {
      next = link
    }

    link = link.next
    index++
    if (index >= left && index <= right) {
      // 区间内部的节点开始入栈
      stack.push(link)
    }
  }
  
  let current = null
  while (stack.length > 0) {
    // 出栈
    let node = stack.pop()
    // 根据规则改变节点的指向
    if (current === null) { 
      pre.next = node
    } else {
      current.next = node
    } 
    current = node
  } 
  // 最后一个出栈的节点的 next 指向区间的后一个节点。
  current.next = next
  return res.next
};

2.2 双指针-头插法

2.2.1 思路

  • 我们定义两个指针,分别称之为 g 和 p 。 我们首先将 g 移动到第一个要反转的节点的前面,将 p 移动到第一个要反转的节点的位置上。
  • 将 p 的下一个节点删除,然后添加到 g 节点的后面。将 p 的下一个节点更新为 p 的下一个节点的下一个节点。
  • 根据给定的区间,计算第二步重复的次数。

2.2.2 图解

2.2.3 代码实现

function reverseBetween(head, left, right) {
  let link = new ListNode(-99, head)
  let g = link
  let p = link
  let index = 1
  left++
  right++
  // 将指针 p 和指针 g 安置于合理的位置
  while (index < left - 1) {
    if (index < left - 1) {
      g = g.next
    }
    index++
  }
  p = g.next

  for (let i = 0; i < right - left; ++i) {
    // 保存指针 p 的下一个节点
    let deleted = p.next
    // 更新指针 p 的下一个节点
    p.next = p.next.next
    // p 的下一个节点放置于指针 g 节点的下一位 
    g.next = deleted
    // 将指针接入链表
    deleted.next = g.next
  }
  return link.next
}

3. 876.链表的中间结点 跳往力扣页面

3.1 栈

3.1.2 思路

我们可以维护栈(或者队列)的数据结构,具体步骤如下:

首先,先将链表中的所有节点依次入栈(或者进队);

然后,通过栈(或者队列)的总大小计算出中间位置;

最后,根据中间位置合理出栈(或者出队)。

3.1.2 代码实现

// 正式代码
var middleNode = function (head) {
  let stack = []
  let len = 0
  let curNode = head
  // 将所有节点入栈
  while (curNode) {
    stack.push(curNode)
    curNode = curNode.next
    len++
  }
  // 计算中间位置
  let middle = Math.ceil(len / 2)

  // 根据中间位置,合理出栈
  let top = null
  while (middle > 0) {
    top = stack.pop()
    middle--
  }
  return top
};

3.2 双指针

3.2.1 思路

举例:甲、乙两个人参加百米冲刺跑步,若甲的速度是乙速度的2倍,裁判一声令下,甲、乙同时开跑。当甲到达终点的时候,此时,乙应该在赛道的那个位置呢?毫无疑问,此时的乙应该正位于赛道的正中间

通过上述案例,再思考这道题目得到以下思路:

    1. 声明两个指针,分别命名为快指针和慢指针。
    2. 快指针的移动速度是慢指针移动速度的2倍,换言之快指针一次移动2位,慢指针一次移动1位。
    3. 当快指针移动到链表尾部时,返回慢指针指向的节点。

3.2.2 图解

3.2.3 代码实现

function middleNode(head) {
  let fast = head
  let slow = head
  while (fast && fast.next) {
    fast = fast?.next?.next || null
    slow = slow.next
  }
  return slow
}

4. 21.合并两个有序链表 跳往力扣页面

4.1 队列

4.1.1 思路

我们可以维护一个队列,具体步骤如下:

    • 首先,将两个链表中的所有节点排序放入一个队列中。
    • 然后,依次出队并改变指针指向。n个出队的节点的 next 指向第 n + 1 个出队的节点,其中n > 0

4.1.2 代码实现

function mergeTwoLists(list1, list2) {
    let queue = []
    let p = list1
    let q = list2
    while (p || q) {
      // 处理特殊情况
        if (!p && q) {
            queue.push(q)
            q = q.next
        } else if (!q && p) {
            queue.push(p)
            p = p.next
        }
        // 如果链表1中当前节点的值大于链表2中当前节点的值
        else if (p.val > q.val) {
            queue.push(q)
            q = q.next
        } else {
      	// 如果链表1中当前节点的值小于或等于链表2中当前节点的值
            queue.push(p)
            p = p.next
        }
    }
    let link = null
    let curent = null
    while (queue.length > 0) {
      // 依次出队
        let top = queue.shift()
      	// 第一个出队的节点为头结点,对头结点单独处理
        if (link === null) {
            curent = top
            link = top
        } else {
          // 第 n 个出队的节点的 next 指向第 n + 1 个出队的节点,其中n > 0。
            curent.next = top
            curent = top
        }
    }
    return link
};

4.2 双指针

4.2.1 思路

我们可以动态创建一个链表,然后依次遍历两个已有的链表,如果链表1当前节点的值小于链表2当前节点的值,那么将链表1当前节点追加到我们创建的链表中,反之也是如此。

4.2.2 图解

4.2.3 代码实现

function mergeTwoLists(list1, list2) {
  let p = list1
  let q = list2
  // 创建新链表
  let top = new ListNode(-1)
  let topLast = top
  while (p || q) {
    // 处理特殊情况
    if (!p && q) {
      topLast.next = q
      q = q.next
    } else if (p && !q) {
      topLast.next = p
      p = p.next
    }
    // 如果链表1当前节点的值小于链表2当前节点的值
    // 将链表1当前节点追加到我们创建的链表尾部
    else if (p.val < q.val) {
      topLast.next = p
      p = p.next
    } 
    // 将链表2当前节点追加到我们创建的链表尾部
    else {
      topLast.next = q
      q = q.next
    }
    // 更新新链表尾部的节点
    topLast = topLast.next
  }
  return top.next
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值