链表专题二

173 阅读5分钟

这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战 | 创作学习持续成长,夺宝闯关赢大奖 - 掘金 (juejin.cn)

题目链接

  1. K 个一组翻转链表 leetcode-cn.com/problems/re…
  2. 旋转链表 leetcode-cn.com/problems/ro…
  3. 两两交换链表中的节点 leetcode-cn.com/problems/sw…
  4. 删除链表的倒数第 N 个结点 leetcode-cn.com/problems/re…
  5. 删除排序链表中的重复元素 leetcode-cn.com/problems/re…

题解及分析

K 个一组翻转链表

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
进阶:
你可以设计一个只使用常数额外空间的算法来解决此问题吗?
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

链表反转可以参考过往的文章 链表折磨专题一 - 掘金 (juejin.cn)

思路一:

  1. 我们在链表头插入一个虚拟节点,循环每个节点并用变量tail标识出每次翻转的最后一个节点,head和tail可以圈出我们需要翻转的部分。
  2. 定义一个pre节点,一开始指向表头,每一次翻转之后,我们把翻转完的链表段装回pre的位置,而pre则移到tail的位置,而head则指向tail的下一个节点 需要注意的是:
  • head指向tail下一个节点的原因在于,head的作用是表示出下一段需要翻转的链表段
  • pre指向tail的原因是,pre用于标识出更新后的链表段需要组装在链表中的位置
let reverse = (head, tail) => {
    // 这个函数用于反转链表
    let prev = tail.next;
    let p = head;
    while (prev !== tail) {
        const nex = p.next;
        p.next = prev;
        prev = p;
        p = nex;
    }
    return [tail, head];
}
let reverseKGroup = (head, k) => {
    // 在列表头生成一个虚拟节点
    const vertical = new ListNode(0);
    vertical.next = head;
    let pre = vertical;
    while(head) {
        // tail用来标识每一次翻转的最后一个节点
        let tail = pre;
        // tail的值通过循环k来确认
        for(let i = 0; i < k; ++i) {
            tail = tail.next;
            if(!tail) {
                // 没有tail时说明剩余链表不满足k值,返回剩余的链表
                return vertical.next;
            }
        }
        // next用来定位尾部的下一位
        const next = tail.next;
        [head, tail] = reverse(head, tail)
        // 重新组装链表
        pre.next = head;
        tail.next = next;
        pre = tail;
        head = tail.next;
    }
    return vertical.next
};

思路二

  1. 声明一个虚拟节点插入链表头部,
  2. 每次翻转k位链表,翻转完之后直接装入链表的指定部分
// 另一种写法
let reverseKGroup = (head, k) => {
    if(!head) return null
    let ret = new ListNode(-1, head)
    let pre = ret
    do{
        // 直接对某段链表进行翻转
        pre.next = reverse(pre.next, k)
        // 翻转之后,将链表直接装入原链表
        for(let i = 0; i < k && pre; i++) {
            pre = pre.next
        }
        if(!pre) break
    }while(1)
    return ret.next
}
let reverse = (head, n) => {
    let pre = head
    let cur = head
    let count = n
    // 用于校验翻转长度是否大于链表长度
    while(--n && pre) {
        pre = pre.next
    }
    if(!pre) return head
    pre = null
    while(count--) {
        [cur.next, pre, cur] = [pre, cur, cur.next]
    }
    head.next = cur
    return pre
}

旋转链表

给你一个链表的头节点head,旋转链表,将链表每个节点向右移动k个位置。

这道题实际上难度并不高,题意上就很明显能看出是一个链表环型题目,那么优化点也就在于如何判断出链表到底需要扭转哪些节点,换言之即是k对链表长度mod求余,余下的即是需要旋转的节点。

var rotateRight = function(head, k) {
    if(!head || !head.next || !k) {
        return head
    }
    // 计算链表的长度
    let cur = head
    let n = 1
    while(cur.next) {
        cur = cur.next
        n++
    }
    /**
     *判断需要扭转的节点有哪些
     *如果mod后长度和链表长度相等,代表不需要修改链表
     */
    let diff = n - k % n
    if(diff === n) {
        return head
    }
    // 链表成环
    cur.next = head
    // 查找操作完成后的尾节点
    while(diff) {
        cur = cur.next
        diff--
    }
    // 重新组装链表
    const ret = cur.next
    cur.next = null
    return ret
};

两两交换链表中的节点

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

一般这种题目的解法有迭代和递归两种

思路一:迭代

var swapPairs = function(head) {
    if(!head || !head.next) return head

    const dummyHead = new ListNode()
    dummyHead.next = head
    
    let temp = dummyHead
    // 循环条件是下一个节点和下下个节点是否存在
    while(temp.next && temp.next.next ) {
        const node1 = temp.next
        const node2 = temp.next.next
        // 交换节点引用
        temp.next = node2
        node1.next = node2.next
        node2.next = node1
        temp = node1
    }
    return dummyHead.next
};

思路二:递归

var swapPairs = function(head) {
    if (head === null|| head.next === null) {
        return head;
    }
    
    // 用一个新的变量保存反转节点的下一个节点
    const cache = head.next
    // 每次将需要翻转的节点传入
    // 这个函数实际上只是调换了cache.next(即head.next.next)和head.next的引用顺序
    head.next = swapPairs(cache.next)
    cache.next = head
    return cache
};

删除链表的倒数第N个结点

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

这道题的问题在于,我们不知道链表的长度。那么大致的解法分为两种,一种是遍历循环链表,确认链表长度后再删除对应的节点;另一种则不考虑去获得链表的长度

思路一:将链表转换为数组,用处理完数组后再转为链表

var removeNthFromEnd = function(head, n) {
    let newArr = []
    let dummy = new ListNode()
    let newList = dummy
    // 循环链表,维护数组来存储节点值
    while(head){
        newArr.push(head.val)
        head = head.next
    }
    newArr.splice(newArr.length - n, 1)
    // 将数组转换成链表
    for(let i = 0; i < newArr.length ;i++){
        newList.next = new ListNode(newArr[i]);
        newList = newList.next;
    }
    return dummy.next
};

思路二:数组中直接存储链表的头节点,利用数组方法来修改头节点的指向(好家伙直接卡js数组的bug啊) 与上个解法的区别在于,这个写法用数组维护链表的头节点

var removeNthFromEnd = function(head, n) {
    
    const dummyNode = new ListNode(0, head)
    const arr = new Array()
    let pushList = dummyNode
    while (pushList != null) {
        arr.push(pushList)
        pushList = pushList.next
    }
    // 链表长度和n等同时删去第一个
    if(n === arr.length) return head.next
    arr[arr.length - n -1].next = arr[arr.length - n -1].next.next
    return dummyNode.next
};

思路三:堆栈 利用堆栈找出倒数第n个节点,然后与剩余的节点连接

var removeNthFromEnd = function(head, n) {
    const dummy = new ListNode(0, head)
    const stack = new Array()
    let pushList = dummy
    while (pushList != null) {
        stack.push(pushList)
        pushList = pushList.next
    }
    // 弹出堆栈中的倒数n个值,找到被删除节点的前一个节点
    for (let i = 0; i < n; i++) {
        stack.pop()
    }
    let peek = stack[stack.length - 1]
    peek.next = peek.next.next
    return dummy.next
};

思路四:快慢指针 快指针先移动n个单位,然后同步移动慢指针和快指针,最后修改慢指针的指向

var removeNthFromEnd = function(head, n) {
    if(!head || !n) return head

    let fast = head
    let slow = head

    while(n) {
        fast = fast.next
        n--
    }

    if(!fast) {
        return head.next
    }

    while(fast.next) {
        slow = slow.next
        fast = fast.next
    }

    slow.next = slow.next.next
    return head
};

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

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

由于链表已经被排序,所以相同的值必定在相邻的两个节点,那么遍历链表,找到相同值的节点,再修改节点的指向就可以了。

var deleteDuplicates = function(head) {
    if(!head) return head

    let cur = head
    while(cur.next) {
        if(cur.val === cur.next.val) {
            cur.next = cur.next.next
        } else {
            cur = cur.next
        }
    }
    return head
};

题目总结

  • 1,2,3道题属于链表翻转类题目,难点在于如何确认翻转的位置以及对临界情况的处理,需要考虑临界情况是否能够省略余下的步骤
  • 4,5道题属于节点删除类题目。4题难点在于如果需要在忽略链表长度的情况下对某个节点进行操作,那么我们可以利用堆栈的特性/快慢指针来处理。5题只要直接遍历即可。

上期文章

链表折磨专题一 - 掘金 (juejin.cn)