经典面试题
推荐刷题顺序:
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);
*/