线性表基础:队列(二)经典面试题

经典面试题

推荐刷题顺序:
LeetCode #86 分隔链表
LeetCode #138 复制带随机指针的链表
LeetCode #622 设计循环队列
LeetCode #641 设计双端循环队列
LeetCode #1670 设计前中后队列
LeetCode #933 最近请求次数

一、链表复习题

1.1 LeetCode #86 分隔链表

题目描述:
给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。

你应当 保留 两个分区中每个节点的初始相对位置。
在这里插入图片描述

解题思路:

  • 使用两个链表,一个用于插入小于x的元素,一个用于插入大于等于x的元素,最后合并两个链表即可。

在这里插入图片描述

  • 创建两个虚拟头节点big、small,代表两条链表
    big插入大于等于3的元素,small插入小于3的元素

  • 定义三个指针,用来进行比较,连接操作

    • b:指针big链表中最后一个元素
    • s:指向small链表中最后一个元素
    • cur:当前处理的元素
  • 如果cur指针指向的节点值小于3,将节点连接到small中
    否则将节点连接到big中,然后将cur指针移动到下一位,循环操作

在这里插入图片描述

  • 当cur指针指向null的时候说明整个链表都处理完了
    此时将small链表的尾节点指向big链表的头节点
    big链表的尾节点指向null,最后返回small链表的头节点即可

在这里插入图片描述
在这里插入图片描述

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    
    
    public ListNode partition(ListNode head, int x) {
    
    
        ListNode big = new ListNode(0);
        ListNode small = new ListNode(0);
        // 指针big链表中最后一个元素
        ListNode b = big;
        // 指向small链表中最后一个元素
        ListNode s = small;
        ListNode cur = head;
        while (cur != null) {
    
    
            if (cur.val >= x) {
    
    
                b.next = cur;
                b = b.next;
            } else {
    
    
                s.next = cur;
                s = s.next;
            }
            cur = cur.next;
        }
        s.next = big.next;
        b.next = null;

        return small.next;
    }
}

1.2 LeetCode #138 复制带随机指针的链表

题目描述:
给你一个长度为 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 作为传入参数。
在这里插入图片描述

解题思路:

  • 这里可以使用一个小技巧对节点进行复制,在每个节点后面复制一个相同的节点,串成单向链表,将原本A -> B -> C 的复制成A -> A’ -> B -> B’ -> C -> C’ 。
  • 然后将复制节点中的随机指针域向后移动一位,这样复制节点的随机指针域,就指向了随机指针的复制节点。
  • 最后将复制的节点拆下来即可。

在这里插入图片描述

第一步复制:
在这里插入图片描述

刚复制好的节点,它的随机指针指向的,和原节点随机指针指向的是同一个节点
在这里插入图片描述

第二步将每个复制节点的随机指针向后移动一位,即指向了对应节点的复制节点

在这里插入图片描述

在这里插入图片描述

最后将原来的节点和复制的节点拆成两个链表即可
在这里插入图片描述

/*
// Definition for a Node.
class Node {
    int val;
    Node next;
    Node random;

    public Node(int val) {
        this.val = val;
        this.next = null;
        this.random = null;
    }
}
*/

class Solution {
    
    
    public Node copyRandomList(Node head) {
    
    
        if (head == null) {
    
    
            return null;
        }

        // 第一步复制链表
        Node cur = head;
        while (cur != null) {
    
    
            // 复制节点,random指针可以不复制
            Node copyNode = new Node(cur.val);

            // 拼接节点
            copyNode.next = cur.next;
            cur.next = copyNode;
            cur = copyNode.next;
        }
        // 处理复制节点中的随机指针
        cur = head;
        while (cur != null) {
    
    
            if (cur.random != null) {
    
    
                cur.next.random = cur.random.next;
            }
            cur = cur.next.next;
        }
        // 将复制节点拆出来
        Node copyHead = head.next;
        Node copy = null;
        cur = head;
        while (cur != null) {
    
    
            // 复制节点
            copy = cur.next;
            // 原链表删除复制的节点
            cur.next = copy.next;

            cur = cur.next;

            // 复制节点连接下一个复制节点
            if (cur != null) {
    
    
                copy.next = cur.next;
            }
        }
        return copyHead;
    }
}

二、队列的封装与使用

2.1 LeetCode #622 设计循环队列

题目描述:
设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。

循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。

你的实现应该支持如下操作:

  • MyCircularQueue(k): 构造器,设置队列长度为 k 。
  • Front: 从队首获取元素。如果队列为空,返回 -1 。
  • Rear: 获取队尾元素。如果队列为空,返回 -1 。
  • enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
  • deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
  • isEmpty(): 检查循环队列是否为空。
  • isFull(): 检查循环队列是否已满。

在这里插入图片描述

解题思路:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

class MyCircularQueue {
    
    
    int[] arr;
    int head, tail, count, capacity;

    public MyCircularQueue(int k) {
    
    
        arr = new int[k];
        capacity = k;
        head = tail = count = 0;
    }

    // 入队
    public boolean enQueue(int value) {
    
    
        if (isFull()) {
    
    
            return false;
        }
        arr[tail++] = value;
        tail = tail % capacity;
        count++;
        return true;
    }

    // 出队
    public boolean deQueue() {
    
    
        if (isEmpty()) {
    
    
            return false;
        }
        head = (head + 1) % capacity;
        count--;
        return true;
    }

    public int Front() {
    
    
        if (isEmpty()) {
    
    
            return -1;
        }
        return arr[head];
    }

    public int Rear() {
    
    
        if (isEmpty()) {
    
    
            return -1;
        }
        return arr[(tail - 1 + capacity) % capacity];
    }

    public boolean isEmpty() {
    
    
        return count == 0;
    }

    public boolean isFull() {
    
    
        return count == capacity;
    }
}

/**
 * Your MyCircularQueue object will be instantiated and called as such:
 * MyCircularQueue obj = new MyCircularQueue(k);
 * boolean param_1 = obj.enQueue(value);
 * boolean param_2 = obj.deQueue();
 * int param_3 = obj.Front();
 * int param_4 = obj.Rear();
 * boolean param_5 = obj.isEmpty();
 * boolean param_6 = obj.isFull();
 */

2.2 LeetCode #641 设计双端循环队列

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

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

在这里插入图片描述

头部可以出队也可以入队
尾部可以出队也可以入队
一般叫:Deque:double ended queue

注意头指针指向的位置是有元素的,尾指针指向的位置是没有元素的

解题思路:

和上一题一样,新增两个操作:

  • 从队首删除元素
  • 从队尾添加元素

在这里插入图片描述
在这里插入图片描述

class MyCircularDeque {
    
    
    int[] arr;
    int head, tail, count, capacity;

    /** Initialize your data structure here. Set the size of the deque to be k. */
    public MyCircularDeque(int k) {
    
    
        arr = new int[k];
        capacity = k;
        head = tail = count = 0;
    }

    /** Adds an item at the front of Deque. Return true if the operation is successful. */
    public boolean insertFront(int value) {
    
    
        if (isFull()) {
    
    
            return false;
        }
        head = (head - 1 + capacity) % capacity;
        arr[head] = value;
        count++;
        return true;
    }

    /** Adds an item at the rear of Deque. Return true if the operation is successful. */
    public boolean insertLast(int value) {
    
    
        if (isFull()) {
    
    
            return false;
        }
        arr[tail++] = value;
        tail = tail % capacity;
        count++;
        return true;
    }

    /** Deletes an item from the front of Deque. Return true if the operation is successful. */
    public boolean deleteFront() {
    
    
        if (isEmpty()) {
    
    
            return false;
        }
        head = (head + 1) % capacity;
        count--;
        return true;
    }

    /** Deletes an item from the rear of Deque. Return true if the operation is successful. */
    public boolean deleteLast() {
    
    
        if (isEmpty()) {
    
    
            return false;
        }
        tail = (tail - 1 + capacity) % capacity;
        count--;
        return true;
    }

    /** Get the front item from the deque. */
    public int getFront() {
    
    
        if (isEmpty()) {
    
    
            return -1;
        }
        return arr[head];
    }

    /** Get the last item from the deque. */
    public int getRear() {
    
    
        if (isEmpty()) {
    
    
            return -1;
        }
        return arr[(tail - 1 + capacity) % capacity];
    }

    /** Checks whether the circular deque is empty or not. */
    public boolean isEmpty() {
    
    
        return count == 0;
    }

    /** Checks whether the circular deque is full or not. */
    public boolean isFull() {
    
    
        return count == capacity;
    }
}

/**
 * Your MyCircularDeque object will be instantiated and called as such:
 * MyCircularDeque obj = new MyCircularDeque(k);
 * boolean param_1 = obj.insertFront(value);
 * boolean param_2 = obj.insertLast(value);
 * boolean param_3 = obj.deleteFront();
 * boolean param_4 = obj.deleteLast();
 * int param_5 = obj.getFront();
 * int param_6 = obj.getRear();
 * boolean param_7 = obj.isEmpty();
 * boolean param_8 = obj.isFull();
 */

2.3 LeetCode #1670 设计前中后队列

题目描述:
请你设计一个队列,支持在前,中,后三个位置的 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] 。

在这里插入图片描述

解题思路:

  • 使用两个链表实现的双端队列构成一个前中后队列,其中一个存放前半部分元素,一个存放后半部分元素。
  • 需要始终保证内部两个队列元素个数的平衡
    • 当整个队列元素个数为偶数时,左队列个数 = 右队列个数
    • 当整个队列元素个数为奇数时,左队列个数 = 右队列个数+1
  • 当添加或删除元素引起队列中元素变化的时候,需要动态平衡内部两个队列的元素数量,使得中间的元素始终处于固定的位置(如维持在第二个链表头部、或第一个链表尾部)。

以添加操作为例,前中后队列中有5个元素,内部左边队列有3个,右边有2个
在这里插入图片描述

当向整个队列头部添加一个元素6的时候,会将其添加到leftArray的头部
在这里插入图片描述

此时左边队列的数量 比 右边队列的数量 多2个,这个时候我们就需要平衡左右两个队列,将左队列的尾部元素,移动到右队列的头部
在这里插入图片描述

接下来我会首先实现一个基于链表的双端队列,然后在基于两个双端队列的基础上实现前中后队列:

class FrontMiddleBackQueue {
    
    
    // 首先内部类实现一个基于链表的双端队列

    // 双向节点,实现四个方法,便于后面操作
    // 向前添加一个节点
    // 向后添加一个节点
    // 删除前一个节点
    // 删除后一个节点
    class Node {
    
    
        int val;
        Node pre;
        Node next;

        public Node(int val) {
    
    
            this.val = val;
        }
		
		//当前节点前面插入一个新的节点
        public void insertPre(int val) {
    
    
            Node newNode = new Node(val);
            newNode.next = this;
            newNode.pre = this.pre;
            if (this.pre != null) {
    
    
                this.pre.next = newNode;
            }
            this.pre = newNode;
        }
		
		//当前节点后面插入新节点
        public void insertNext(int val) {
    
    
            Node newNode = new Node(val);
            newNode.next = this.next;
            newNode.pre = this;
            if (this.next != null) {
    
    
                this.next.pre = newNode;
            }
            this.next = newNode;
        }
		
		// 删除当前节点的前一个节点
        public void deletePre() {
    
    
            Node needDelete = this.pre;
            if (needDelete != null) {
    
    
                this.pre = needDelete.pre;
                if (needDelete.pre != null) {
    
    
                    needDelete.pre.next = this;
                }
            }
        }

		// 删除当前节点的后一个节点
        public void deleteNext() {
    
    
            Node needDelete = this.next;
            if (needDelete != null) {
    
    
                this.next = needDelete.next;
                if (needDelete.next != null) {
    
    
                    needDelete.next.pre = this;
                }
            }
        }
    }

    // 链表实现的双端队列
    class Deque {
    
    
        Node head, tail;
        int count;

        public Deque() {
    
    
            // 设置虚拟头、尾节点
            head = new Node(-1);
            tail = new Node(-1);
            head.next = tail;
            tail.pre = head;
            count = 0;
        }

        public boolean insertFront(int value) {
    
    
            head.insertNext(value);
            count++;
            return true;
        }

        public boolean insertLast(int value) {
    
    
            tail.insertPre(value);
            count++;
            return true;
        }

        public int deleteFront() {
    
    
            if (isEmpty()) {
    
    
                return -1;
            }
            int val = head.next.val;
            head.deleteNext();
            count--;
            return val;
        }

        public int deleteLast() {
    
    
            if (isEmpty()) {
    
    
                return -1;
            }
            int val = tail.pre.val;
            tail.deletePre();
            count--;
            return val;
        }

        public boolean isEmpty() {
    
    
            return count == 0;
        }

        public int size() {
    
    
            return count;
        }
    }

    // 前中后队列
    Deque left;
    Deque right;

    public FrontMiddleBackQueue() {
    
    
        left = new Deque();
        right = new Deque();
    }

    // 向队首添加元素
    public void pushFront(int val) {
    
    
        left.insertFront(val);
        rebalance();
    }

    // 平衡左右队列
    private void rebalance() {
    
    
        if (left.size() < right.size()) {
    
    
            // 左边队列小于右边队列数量,则将右边队首元素 移到 左边队列队尾
            left.insertLast(right.deleteFront());
        } else if (left.size() == right.size() + 2) {
    
    
            // 左边比右边多两个,才需要将左边队尾元素 移到 右边队列队首
            right.insertFront(left.deleteLast());
        }
    }

    // 向队中添加元素
    public void pushMiddle(int val) {
    
    
        if (left.size() > right.size()) {
    
    
            right.insertFront(left.deleteLast());
        }
        left.insertLast(val);
    }

    // 向队尾添加元素
    public void pushBack(int val) {
    
    
        right.insertLast(val);
        rebalance();
    }

    // 队首弹出元素
    public int popFront() {
    
    
        if (isEmpty()) {
    
    
            return -1;
        }
        int val = left.deleteFront();
        rebalance();
        return val;
    }

    // 队中弹出元素
    public int popMiddle() {
    
    
        if (isEmpty()) {
    
    
            return -1;
        }

        int val = left.deleteLast();
        rebalance();
        return val;
    }

    // 队尾弹出元素
    public int popBack() {
    
    
        if (isEmpty()) {
    
    
            return -1;
        }
        if (right.isEmpty()) {
    
    
            // 刚好只有一个元素队话,右边队列是空的
            return left.deleteLast();
        }
        int val = right.deleteLast();
        rebalance();
        return val;
    }

    // 判断队列是否为空
    public boolean isEmpty() {
    
    
        // 因为我们定义,左边队列最多比右边队列多一个,所以左边队列元素个数为0,右边肯定也为0
        return left.size() == 0;
    }
}

/**
 * Your FrontMiddleBackQueue object will be instantiated and called as such:
 * FrontMiddleBackQueue obj = new FrontMiddleBackQueue();
 * obj.pushFront(val);
 * obj.pushMiddle(val);
 * obj.pushBack(val);
 * int param_4 = obj.popFront();
 * int param_5 = obj.popMiddle();
 * int param_6 = obj.popBack();
 */

2.4 LeetCode #933 最近请求次数

题目描述:
写一个 RecentCounter 类来计算特定时间范围内最近的请求。

请你实现 RecentCounter 类:

  • RecentCounter() 初始化计数器,请求数为 0 。
  • int ping(int t) 在时间 t 添加一个新请求,其中 t 表示以毫秒为单位的某个时间,并返回过去 3000 毫秒内发生的所有请求数(包括新请求)。确切地说,返回在 [t-3000, t] 内发生的请求数。

保证 每次对 ping 的调用都使用比之前更大的 t 值。
在这里插入图片描述

解题思路:

  • 使用队列对过程进行模拟。
  • 每次入队一次请求的时间后,不断比较请求的时间和队首的时间
    • 当请求的时间-队首的时间>3000,说明队首元素过期,弹出过期的元素,循环比较队首元素,直到所有过期的都弹出为止。
    • 最后返回队列大小即可。

如图,当请求时间为3001毫秒时,有效的时间范围是[1,3001]
当请求时间为3002毫秒时,有效的时间范围是[2,3002],此时1就要移除
在这里插入图片描述

class RecentCounter {
    
    

    public RecentCounter() {
    
    

    }
    
    //直接用Java内置队列
    Queue<Integer> queue = new LinkedList();

    public int ping(int t) {
    
    
        queue.offer(t);
        
        while ((t - queue.peek()) > 3000) {
    
    
            //将队首过期的元素弹出
            queue.poll();
        }
        return queue.size();
    }
}

/**
 * Your RecentCounter object will be instantiated and called as such:
 * RecentCounter obj = new RecentCounter();
 * int param_1 = obj.ping(t);
 */

猜你喜欢

转载自blog.csdn.net/weixin_41947378/article/details/115283759