链表专题三

207 阅读8分钟

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

题目链接

  1. 删除排序链表中的重复元素II leetcode-cn.com/problems/re…
  2. 分隔链表 leetcode-cn.com/problems/pa…
  3. 复制带随机指针的链表 leetcode-cn.com/problems/co…
  4. 设计循环队列 leetcode-cn.com/problems/de…
  5. 设计循环双端队列 leetcode-cn.com/problems/de…

题解及分析

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

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

这道题是链表折磨专题二 - 掘金 (juejin.cn)最后一道题的拓展。两道题不同的点在于,这一道题要求“只留下不同的数字”,那么我们不只是需要找出值重复的节点,同时也要注意将链表节点连接的顺序

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

    const dummyNode = new ListNode(0, head)
    let cur = dummyNode
    // 首先找到重复的节点值
    while(cur.next && cur.next.next) {
        if(cur.next.val === cur.next.next.val) {
            // 注意之后的若干个节点可能都是同个值
            const val =cur.next.val
            while(cur.next && cur.next.val === val) {
                cur.next = cur.next.next
            }
        } else {
            cur = cur.next
        }
    }
    return dummyNode.next
};

分隔链表

给你一个链表的头节点head和一个特定值x,请你对链表进行分隔,使得所有小于x的节点都出现在大于或等于x的节点之前。
你应当保留两个分区中每个节点的初始相对位置。

这道题的需要维护两个链表,分别用来存储值小于x和值大于x的节点,遍历并分类完所有节点后,将两个链表分别组转即可

var partition = function(head, x) {
    // 维护两个链表,用来存储大小值节点
    let less = new ListNode(0)
    lessHead = less
    let more = new ListNode(0)
    moreHead = more
    
    // 遍历链表进行分类
    while(head) {
        if(head.val < x) {
            lessHead.next = head
            lessHead = lessHead.next
        }else {
            moreHead.next = head
            moreHead = moreHead.next
        }
        head = head.next
    }
    组装链表
    moreHead.next = null
    lessHead.next = more.next
    return less.next
};

复制带随机指针的链表

给你一个长度为n的链表,每个节点包含一个额外增加的随机指针random,该指针可以指向链表中的任何节点或空节点。
构造这个链表的深拷贝。深拷贝应该正好由n个全新节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有X和Y两个节点,其中X.random --> Y 。那么在复制链表中对应的两个节点x和y,同样有x.random --> y 。
返回复制链表的头节点。
用一个由n个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:
val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从0到n-1);如果不指向任何节点,则为null 。
你的代码只接受原链表的头节点head作为传入参数。

这道题在普通的链表节点中加入了新的random指针,这个指针指向随机的一个其他节点或者为空,我们实现深拷贝的时候需要考虑:当前复制节点的random会不会指向一个尚未被创建的节点

思路一:维护map来记录每一个链表的头节点,当random指向的节点未被创建时,则递归的创建一个

var copyRandomList = function(head, cachedNode = new Map()) {
    if (head === null) {
        return null;
    }
    if(!cachedNode.has(head)) {
        // map中不存在某个头节点时,向map添加这个头节点
        cachedNode.set(head, {val: (head.val)})
        // 利用js的引用关系,对节点的next和random做更新
        Object.assign(cachedNode.get(head), { 
            next: copyRandomList(head.next, cachedNode), 
            random: copyRandomList(head.random, cachedNode)
        })
    }
    return cachedNode.get(head)
}

思路二:在每一个原节点后面接入一个复制后的节点,最后再将复制的节点拆出去形成新的链表,这么做的优势在于不需要考虑random指向的节点是否存在的情况

var copyRandomList = function(head) {
    if (head === null) {
        return null;
    }
    // 在每个原节点后方插入一个复制节点
    for(let node = head; node !== null; node = node.next.next ) {
        const newNode = new Node(node.val, node.next, null)
        node.next = newNode
    }
    // 定位random,这时候复制节点的random需要指向原节点random的复制节点
    for(let node = head; node !== null; node = node.next.next) {
        const nodeNew = node.next;
        nodeNew.random = (node.random !== null) ? node.random.next : null;
    }
    // 重新组装复制后的链表,先保存第二个节点作为头节点
    const headNew = head.next
    for(let node = head; node !== null; node = node.next) {
        const newNode = node.next
        node.next = node.next.next
        newNode.next = newNode.next ? newNode.next.next : null
    }
    return headNew
}

设计循环队列

设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
你的实现应该支持如下操作:
MyCircularQueue(k): 构造器,设置队列长度为k。
Front: 从队首获取元素。如果队列为空,返回-1。
Rear: 获取队尾元素。如果队列为空,返回-1。
enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
isEmpty(): 检查循环队列是否为空。
isFull(): 检查循环队列是否已满。

题目表现的很专业也很复杂,但实际上重点在循环这两个字上。

思路一:维护一个数组来实现循环队列。我们需要维护以下的几个相关变量:

  • 头部节点下标
  • 计数器
  • 列表的最大长度
  • 数据存储的数组 一个定长列表的尾部下标可以通过(头部节点下标+计数器-1)%列表的最大长度来获得,原理如下图

image.png

var MyCircularQueue = function(k) {
    this.frontIndex = 0
    this.count = 0
    this.capacity = k
    this.queue = Array(k)
};

MyCircularQueue.prototype.enQueue = function(value) {
    if(this.isFull() || value === undefined) return false
    this.queue[(this.frontIndex + this.count) % this.capacity] = value
    this.count ++ 
    return true
};

MyCircularQueue.prototype.deQueue = function() {
    if(this.isEmpty()) return false 
    this.queue[this.frontIndex] = undefined
    // 头指针前移一位,对总长度取mod保证溢出时正确取值
    this.frontIndex = (this.frontIndex + 1) % this.capacity
    this.count -= 1
    return true
};

MyCircularQueue.prototype.Front = function() {
    const result = this.queue[this.frontIndex]
    return result === undefined ? -1 : result
};

MyCircularQueue.prototype.Rear = function() {
    const rear = this.queue[(this.frontIndex + this.count -1) % this.capacity]
    return rear === undefined ? -1 : rear
};

MyCircularQueue.prototype.isEmpty = function() {
    return this.count === 0
};

MyCircularQueue.prototype.isFull = function() {
    return this.count === this.capacity
};

思路二:链表维护。链表只依赖next指针确定与另一个节点的关系,根本不需要考虑顺序问题,因此可以忽略数组中对头下表的维护

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

var MyCircularQueue = function(k) {
    // 维护指向尾节点的指针,用以返回尾值
    this.front = this.rear = null
    this.count = 0
    this.capacity = k
};

MyCircularQueue.prototype.enQueue = function(value) {
    if(this.isFull() || value === undefined) return false
    const newNode = new ListNode(value, null)
    if(this.isEmpty()) {
        this.front = newNode
        this.rear = newNode
    } else {
        this.rear.next = newNode
        this.rear = this.rear.next
    }
    this.count ++ 
    return true
};

MyCircularQueue.prototype.deQueue = function() {
    if(this.isEmpty()) return false 
    this.front = this.front.next
    this.count -= 1
    return true
};

MyCircularQueue.prototype.Front = function() {
    return this.count ? this.front.val : -1
};

MyCircularQueue.prototype.Rear = function() {
    return this.count ? this.rear.val : -1
};

MyCircularQueue.prototype.isEmpty = function() {
    return this.count === 0
};

MyCircularQueue.prototype.isFull = function() {
    return this.count === this.capacity
};

设计循环双端队列

设计实现双端队列。 你的实现需要支持以下操作:


MyCircularDeque(k):构造函数,双端队列的大小为k。
insertFront():将一个元素添加到双端队列头部。 如果操作成功返回 true。
insertLast():将一个元素添加到双端队列尾部。如果操作成功返回 true。
deleteFront():从双端队列头部删除一个元素。 如果操作成功返回 true。
deleteLast():从双端队列尾部删除一个元素。如果操作成功返回 true。
getFront():从双端队列头部获得一个元素。如果双端队列为空,返回 -1。
getRear():获得双端队列的最后一个元素。 如果双端队列为空,返回 -1。
isEmpty():检查双端队列是否为空。
isFull():检查双端队列是否满了。

这道题目和上一道题相比,多了'需要从头部插入元素'和'需要从头部删除元素',两个要求实际上的实现差不多,但是由于需要考虑从头部插入元素和从尾部删除元素,需要考虑的情景变多了

思路一:维护数组来处理。

var MyCircularDeque = function(k) {
    this.front = 0
    this.count = 0
    this.capacity = k
    this.queue = Array(k)
};

MyCircularDeque.prototype.insertFront = function(value) {
    if(this.count === this.capacity) return false
    this.queue = [value, ...this.queue]
    this.count += 1
    this.front = 0
    return true
};

MyCircularDeque.prototype.insertLast = function(value) {
    if(this.count === this.capacity) return false
    const index = this.front + this.count -1 >= 0 ? (this.front + this.count)% this.capacity : 0
    this.queue[index] = value
    this.count += 1
    return true
};

MyCircularDeque.prototype.deleteFront = function() {
    if(this.isEmpty()) return false
    this.queue.splice(0, 1)
    this.count --
    return true
};

MyCircularDeque.prototype.deleteLast = function() {
    if(this.isEmpty()) return false
    this.queue[(this.front + this.count -1) % this.capacity] = undefined
    this.count --
    return true
};

MyCircularDeque.prototype.getFront = function() {
    return this.queue[this.front] === undefined ? -1 : this.queue[this.front]
};

MyCircularDeque.prototype.getRear = function() {
    return this.queue[(this.front + this.count -1) % this.capacity] === undefined ? -1 : this.queue[(this.front + this.count -1) % this.capacity]
};

MyCircularDeque.prototype.isEmpty = function() {
    return this.count === 0
};

MyCircularDeque.prototype.isFull = function() {
    return this.count === this.capacity
};

思路二:双指针链表解法。唯一要注意的是计数器为0时的插入和删除需要对头指针和尾指针进行处理

/**
 * @param {number} k
 */

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

var MyCircularDeque = function(k) {
    this.front = this.rear = null
    this.count = 0
    this.capacity = k
};

MyCircularDeque.prototype.insertFront = function(value) {
    if(this.isFull()) return false
    const newNode = new ListNode(value, this.front)
    this.front = newNode
    // 如果尾节点为空时插入元素,需要把尾指针重新定位
    if(!this.rear) {
        this.rear = this.front
        while(this.rear.next) {
            this.rear = this.rear.next
        }
    }
    this.count++
    return true
};

MyCircularDeque.prototype.insertLast = function(value) {
    if(this.isFull()) return false
    if(!this.count) {
        // 如果链表为空时插入元素,需要把尾指针和头指针指向同个节点
        this.front = this.rear = new ListNode(value)
    } else {
        this.rear.next = new ListNode(value)
        this.rear = this.rear.next
    }
    this.count++
    return true
};

MyCircularDeque.prototype.deleteFront = function() {
    if(this.isEmpty()) return false
    this.front = this.front.next
    this.count--
    // 如果链表删除后为空,需要把尾指针和头指针重置
    if(!this.count) this.rear = this.front
    return true
};

MyCircularDeque.prototype.deleteLast = function() {
    if(this.isEmpty()) return false
    let cache = this.count - 1 
    /**
     * 如果链表中仅有一个节点,那么直接将头尾节点置空
     * 如果链表多个节点,那么删除后重新定位尾节点
     */
    if(!cache) {
        this.front = this.rear = null
    } else {
        let newRear = this.front
        while(--cache) {
            newRear = newRear.next || null
        }
        newRear.next = null
        this.rear = newRear
    }
    this.count--
    return true
};

MyCircularDeque.prototype.getFront = function() {
    return this.front? this.front.val : -1
};

MyCircularDeque.prototype.getRear = function() {
    return this.rear ? this.rear.val : -1
};

MyCircularDeque.prototype.isEmpty = function() {
    return this.count === 0
};

MyCircularDeque.prototype.isFull = function() {
    return this.count === this.capacity
};

题目总结

  • 1道题没有太多的变化,但需要考虑多个重复数据的情景
  • 2道题可以近似的看作快速排序的链表实现
  • 3道题有点类似于双向链表,需要考虑场景问题
  • 4,5道题属于队列处理类题目。这两道题都可以考虑使用数组或者链表的方式来处理

上期文章

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