链表折磨四

153 阅读7分钟

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

题目链接

  1. 设计前中后队列 leetcode-cn.com/problems/de…
  2. 返回倒数第k个节点 leetcode-cn.com/problems/kt…
  3. 链表中倒数第k个节点 leetcode-cn.com/problems/li…
  4. 复杂链表的复制 leetcode-cn.com/problems/fu…
  5. 删除中间节点 leetcode-cn.com/problems/de…

题解及分析

设计前中后队列

请你设计一个队列,支持在前,中,后三个位置的 push 和 pop 操作。
请你完成 FrontMiddleBack 类:
FrontMiddleBack() 初始化队列。
void pushFront(int val) 将val添加到队列的最前面。
void pushMiddle(int val) 将val添加到队列的正中间。
void pushBack(int val) 将val添加到队里的最后面。
int popFront() 将最前面的元素从队列中删除并返回值,如果删除之前队列为空,那么返回-1。
int popMiddle() 将正中间的元素从队列中删除并返回值,如果删除之前队列为空,那么返回-1。
int popBack() 将最后面的元素从队列中删除并返回值,如果删除之前队列为空,那么返回-1。
请注意当有两个中间位置的时候,选择靠前面的位置进行操作。比方说:
将6添加到 [1, 2, 3, 4, 5] 的中间位置,结果数组为[1, 2, 6, 3, 4, 5]。
从[1, 2, 3, 4, 5, 6]的中间位置弹出元素,返回3,数组变为[1, 2, 4, 5, 6]。

这道题是前两道题的升级链表折磨专题三 - 掘金 (juejin.cn) 与上一道题相比,多了'将元素插入到正中间'和'从正中间删除元素'两个要求,用数组比较容易处理,但是要考虑正中间元素的边界情况 思路一:用链表来处理。在确认正中间的元素时,我们可以用快慢指针的方式来处理,详情参考链表折磨专题一 - 掘金 (juejin.cn),感谢leetcode题解,我已经忘了有这玩意儿

function ListNode(val, next) {
    this.val = (val===undefined ? 0 : val)
    this.next = (next===undefined ? null : next)
}

var FrontMiddleBackQueue = function() {
    // 在表头插入虚拟节点,规避存在链表长度为0或者为1的尴尬
    this.front = new ListNode(-1)
};

FrontMiddleBackQueue.prototype.pushFront = function(val) {
    const newNode = new ListNode(val, this.front.next)
    this.front.next = newNode
};

FrontMiddleBackQueue.prototype.pushMiddle = function(val) {
    /**
     * 快慢指针找出中间元素
     * 注意fast的初始值,如果和slow对齐,那么slow最终取值达不到题目要求的'前一位'
     */ 
    let slow = this.front
    let fast = this.front.next
    while(fast && fast.next) {
        slow = slow.next
        fast = fast.next.next
    }
    let next = slow.next
    slow.next = new ListNode(val, next)
};

FrontMiddleBackQueue.prototype.pushBack = function(val) {
    let node = this.front
    while(node.next) {
        node = node.next
    }
    node.next = new ListNode(val)
};

FrontMiddleBackQueue.prototype.popFront = function() {
    const next = this.front.next
    if(!next) return this.front.val
    this.front.next = next.next
    return next.val
};

FrontMiddleBackQueue.prototype.popMiddle = function() {
    let slow = this.front
    let fast = this.front.next
    if(!fast) return this.front.val
    while(fast && fast.next) {
        fast = fast.next.next
        if(fast) {
            slow = slow.next
        }
    }
    // 注意取值和删除的元素
    let next = slow.next
    slow.next = slow.next.next
    return next.val
};

FrontMiddleBackQueue.prototype.popBack = function() {
    if(!this.front.next) return this.front.val
    let node = this.front
    while(node.next && node.next.next) {
        node = node.next
    }
    const next = node.next
    node.next = null
    return next.val
};

思路二:维护两个数组来处理。维护左右两个数组,控制其中元素的数量以保持中间元素在右边数组的第一位,以此来控制元素的取值

  • 首先需要确认中间元素在哪个数组中。我们假定中间元素在右数组,那么右数组的长度应该大于等于左数组的长度
  • 每次给左右两边的数组加入或者删去元素,我们都需要同时维护两个数组,保证左右数组的长度相等或者只差一
  • 删除时,右数组如果为空,那么左边数组必定也为空;左数组为空,那么右数组只有一个元素或者为空
var FrontMiddleBackQueue = function() {
    this.leftArray = []
    this.rightArray = []
};

FrontMiddleBackQueue.prototype.pushFront = function(val) {
    this.leftArray.unshift(val)
    // 保证中间值在右边的数组
    if(this.leftArray.length > this.rightArray.length) {
        this.rightArray.unshift(this.leftArray.pop())
    }
};

FrontMiddleBackQueue.prototype.pushMiddle = function(val) {
    // 右边的数组一定比左边大,因此相等取右,不等取左
    if(this.leftArray.length === this.rightArray.length) {
        this.rightArray.unshift(val)
    } else {
        this.leftArray.push(val)
    }
};

FrontMiddleBackQueue.prototype.pushBack = function(val) {
    this.rightArray.push(val)
    // 除开只有一个元素的情景,正常情况下右边会比左边多一个中间元素
    if(this.rightArray.length === this.leftArray.length + 2) {
        this.leftArray.push(this.rightArray.shift())
    }
};

FrontMiddleBackQueue.prototype.popFront = function() {
    /**
     * 右数组可以判断是否为空
     * 左数组为空,那么正常情况下只有一个元素
    */
    if(!this.rightArray.length) return -1
    if(!this.leftArray.length) return this.rightArray.shift()
    let ret = this.leftArray.shift()
    if(this.leftArray.length + 1 < this.rightArray.length) {
        this.leftArray.push(this.rightArray.shift())
    }
    return ret
};

FrontMiddleBackQueue.prototype.popMiddle = function() {
    if(!this.rightArray.length) return -1
    if(!this.leftArray.length) return this.rightArray.shift()
    if(this.leftArray.length === this.rightArray.length) {
        this.rightArray.unshift(this.leftArray.pop())
    }
    return this.rightArray.shift()
};

FrontMiddleBackQueue.prototype.popBack = function() {
    if(!this.rightArray.length) return -1
    if(!this.leftArray.length) return this.rightArray.shift()
    let ret = this.rightArray.pop()
    if(this.leftArray.length > this.rightArray.length) {
        this.rightArray.unshift(this.leftArray.pop())
    }
    return ret    
};

顺便纪念下leetcode第一次执行用时击败了100%的其他方式

image.png

返回倒数第k个节点

实现一种算法,找出单向链表中倒数第k个节点。返回该节点的值。

这道题目题意很简单,但解法不少。下面除开遍历算出节点总数求解的方法,聊聊其他解法。

思路一:栈
借助栈的filo特性来解答这一类题目。

  • 遍历链表,把节点都推入栈中
  • 接下来遍历k,依次弹出栈顶的元素,直至取得第k个值
var kthToLast = function(head, k) {
    const stack = []
    while(head) {
        stack.push(head)
        head = head.next
    }
    let newNode = null
    while(k > 0) {
        newNode = stack.pop()
        k--
    }
    return newNode.val
};

思路二:快慢指针
如果快指针比慢指针固定快k个节点,那快指针到达最后一个节点时,慢指针刚好指向倒数第k个节点

  • 先声明一个快指针,向前走k个单位
  • 声明一个指向链表头部的慢指针
  • 循环链表,同时移动快慢指针,直到快指针触底
var kthToLast = function(head, k) {
    let fast = head
    let slow = head
    while(--k > 0) {
        fast = fast.next
    }
    while(fast && fast.next) {
        slow = slow.next
        fast = fast.next
    }
    return slow.val
};

思路三:递归
*这个解法不通过...但本地跑是ok的?
递归的特性是反向执行,即:在触及边界条件之前,不停的调用自身,直到触及边界条件为止
那我们把边界条件设置为k次呢?

  • 我们设定一个变量来记录遍历的次数
  • 设定对应的判断条件,否则递归自身
let count = 0
var kthToLast = function(head, k) {
    if(head === null) return 0
    const num = kthToLast(head.next, k)
    count++
    if(k === count) {
        return head.val
    } 
    return num
}

链表中倒数第k个节点

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个节点是值为4的节点。

这道题和上一题一样,只是返回的结果从值变成了节点,这里就不作题解了

复杂链表的复制

请实现copyRandomList函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个next指针指向下一个节点,还有一个random指针指向链表中的任意节点或者null。

这道题和链表折磨专题三 - 掘金 (juejin.cn)第三题一样,这里就不作题解了

删除中间节点

若链表中的某个节点,既不是链表头节点,也不是链表尾节点,则称其为该链表的「中间节点」。
假定已知链表的某一个中间节点,请实现一种算法,将该节点从链表中删除。
例如,传入节点c(位于单向链表a->b->c->d->e->f中),将其删除后,剩余链表为a->b->d->e->f

思路:
实际上链表中是没有删除这个概念的。所谓移除链表中的某个节点,本质是将该节点的上一个节点的next指针的从指向当前节点,修改为指向当前节点的下一个节点
那么题目的问题就变成:我们如何让当前节点的上一个节点的next指针跳过当前节点,指向下一个节点?
这是一个题目的思维定式陷阱,实际上,我们并不需要跳过当前节点,我们需要做的是把当前指针的下一个节点替换掉当前节点

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

题目总结

这几道题目中比较值得关注的是快慢指针。
实际上快慢指针可以用来处理很多场景:环问题,中间值问题,相距n个单位的链表取值问题等。

上期文章

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