day27
剑指 Offer 09. 用两个栈实现队列
力扣题目链接
题目
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
示例 1:
输入:
["CQueue","appendTail","deleteHead","deleteHead"]
[[],[3],[],[]]
输出:[null,null,3,-1]
示例 2:
输入:
["CQueue","deleteHead","appendTail","appendTail","deleteHead","deleteHead"]
[[],[],[5],[2],[],[]]
输出:[null,-1,null,null,5,2]
提示:
1 <= values <= 10000
最多会对 appendTail、deleteHead 进行 10000
思路
https://camo.githubusercontent.com/fd6467534e8105f2873582ad721daa575763449314fcaf3e9eb3d327a820e91b/68747470733a2f2f63732d6e6f7465732d313235363130393739362e636f732e61702d6775616e677a686f752e6d7971636c6f75642e636f6d2f33656132383062352d626537642d343731622d616337362d6666303230333834333537632e676966
代码实现
/**
* https://leetcode-cn.com/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof/
*
* @author xiexu
* @create 2022-04-19 22:15
*/
public class _剑指Offer09_用两个栈实现队列 {
}
class CQueue {
Stack<Integer> in = null;
Stack<Integer> out = null;
public CQueue() {
in = new Stack<>();
out = new Stack<>();
}
// 在队尾插入整数
public void appendTail(int value) {
in.push(value);
}
// 在队头删除整数
public int deleteHead() {
if (out.isEmpty()) {
while (!in.isEmpty()) {
out.push(in.pop());
}
}
if (out.isEmpty()) {
return -1;
}
return out.pop();
}
}
剑指 Offer 30. 包含min函数的栈
力扣题目链接
题目
定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
示例:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.min(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.min(); --> 返回 -2.
提示:
思路
- 使用一个额外的 minStack,栈顶元素为当前栈中最小的值。
- 在对栈进行 push 入栈和 pop 出栈操作时,同样需要对 minStack 进行入栈出栈操作,从而使 minStack 栈顶元素一直为当前栈中最小的值。
- 在进行 push 操作时,需要比较入栈元素和当前栈中最小值,将值较小的元素 push 到 minStack 中。
代码实现
/**
* https://leetcode-cn.com/problems/bao-han-minhan-shu-de-zhan-lcof/
*
* @author xiexu
* @create 2022-04-19 22:29
*/
public class _剑指Offer30_包含min函数的栈 {
}
class MinStack {
Stack<Integer> dataStack = null;
Stack<Integer> minStack = null;
public MinStack() {
dataStack = new Stack<>();
minStack = new Stack<>();
}
public void push(int x) {
dataStack.push(x);
if (minStack.isEmpty()) {
minStack.push(x);
} else {
int min = Math.min(minStack.peek(), x);
minStack.push(min);
}
}
public void pop() {
dataStack.pop();
minStack.pop();
}
public int top() {
return dataStack.peek();
}
public int min() {
return minStack.peek();
}
}
剑指 Offer 31. 栈的压入、弹出序列
力扣题目链接
题目
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。
示例 1:
输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
输出:true
解释:我们可以按以下顺序执行:
push(1), push(2), push(3), push(4), pop() -> 4,
push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1
示例 2:
输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2]
输出:false
解释:1 不能在 2
提示:
0 <= pushed.length == popped.length <= 1000
0 <= pushed[i], popped[i] < 1000
思路
使用一个栈来模拟压入弹出操作。每次入栈一个元素后,都要判断一下栈顶元素是不是当前出栈序列 popped 的第一个元素,如果是的话则执行出栈操作并将 popped 往后移一位,继续进行判断。
代码实现
/**
* https://leetcode-cn.com/problems/zhan-de-ya-ru-dan-chu-xu-lie-lcof/
*
* @author xiexu
* @create 2022-04-19 22:48
*/
public class _剑指Offer31_栈的压入_弹出序列 {
public boolean validateStackSequences(int[] pushed, int[] popped) {
int n = pushed.length;
// 使用一个栈来模拟入栈出栈操作
Stack<Integer> stack = new Stack<>();
for (int pushIndex = 0, popIndex = 0; pushIndex < n; pushIndex++) {
stack.push(pushed[pushIndex]);
while (popIndex < n && !stack.isEmpty() && stack.peek() == popped[popIndex]) {
stack.pop();
popIndex++;
}
}
return stack.isEmpty();
}
}
剑指 Offer 40. 最小的k个数
力扣题目链接
题目
输入整数数组 arr
,找出其中最小的 k
个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
示例 1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]
示例 2:
输入:arr = [0,1,2,1], k = 1
输出:[0]
限制:
0 <= k <= arr.length <= 10000
0 <= arr[i] <= 10000
大顶堆解法
思路
大小为 K 的最小堆
- 复杂度:O(NlogK) + O(K)
- 特别适合处理海量数据
维护一个大小为 K 的最小堆过程如下:使用大顶堆。在添加一个元素之后,如果大顶堆的大小大于 K,那么将大顶堆的堆顶元素去除,也就是将当前堆中值最大的元素去除,从而使得留在堆中的元素都比被去除的元素来得小。
应该使用大顶堆来维护最小堆,而不能直接创建一个小顶堆并设置一个大小,企图让小顶堆中的元素都是最小元素。
Java 的 PriorityQueue 实现了堆的能力,PriorityQueue 默认是小顶堆,可以在在初始化时使用 Lambda 表达式 (o1, o2) -> o2 - o1 来实现大顶堆。其它语言也有类似的堆数据结构。
代码实现
/**
* https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/
*
* @author xiexu
* @create 2022-04-19 23:04
*/
public class _剑指Offer40_最小的k个数_大顶堆 {
public int[] getLeastNumbers(int[] arr, int k) {
if (k > arr.length || k <= 0) {
return new int[]{};
}
// 构造大顶堆,如果是(o1 - o2)则默认是小顶堆
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);
for (int i : arr) {
maxHeap.add(i);
if (maxHeap.size() > k) {
maxHeap.poll();
}
}
int[] res = new int[maxHeap.size()];
int index = 0;
for (Integer num : maxHeap) {
res[index++] = num;
}
return res;
}
}
快速选择解法
思路
代码实现
/**
* https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/
*
* @author xiexu
* @create 2022-04-19 23:04
*/
public class _剑指Offer40_最小的k个数_快速选择 {
public int[] getLeastNumbers(int[] arr, int k) {
if (k > arr.length || k <= 0) {
return new int[]{};
}
quick_sort(arr, 0, arr.length - 1, k);
int[] res = new int[k];
for (int i = 0; i < k; i++) {
res[i] = arr[i];
}
return res;
}
private int quick_sort(int[] nums, int l, int r, int k) {
if (l == r) {
return nums[l];
}
int x = nums[l + r >> 1];
int i = l - 1;
int j = r + 1;
while (i < j) {
do i++; while (nums[i] < x);
do j--; while (nums[j] > x);
if (i < j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
// 左半边个数
int sl = j - l + 1;
if (k <= sl) {
return quick_sort(nums, l, j, k);
} else {
return quick_sort(nums, j + 1, r, k - sl);
}
}
}
剑指 Offer 41. 数据流中的中位数
力扣题目链接
题目
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
例如,
[2,3,4] 的中位数是 3
[2,3] 的中位数是 (2 + 3) / 2 = 2.5
设计一个支持以下两种操作的数据结构:
-
void addNum(int num)
- 从数据流中添加一个整数到数据结构中。 -
double findMedian()
- 返回目前所有元素的中位数。
示例 1:
输入:
["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]
示例 2:
输入:
["MedianFinder","addNum","findMedian","addNum","findMedian"]
[[],[2],[],[3],[]]
输出:[null,null,2.00000,null,2.50000]
思路
我们必然需要有序数据结构,本题的核心思路是使用两个优先级队列。
中位数是有序数组最中间的元素算出来的对吧,我们可以把「有序数组」抽象成一个倒三角形,宽度可以视为元素的大小,那么这个倒三角的中部就是计算中位数的元素对吧:
然后我把这个大的倒三角形从正中间切成两半,变成一个小倒三角和一个梯形,这个小倒三角形相当于一个从大到小的有序数组,这个梯形相当于一个从小到大的有序数组。
中位数就可以通过小倒三角和梯形顶部的元素算出来对吧?嗯,你联想到什么了没有?它们能不能用优先级队列表示?小倒三角不就是个大顶堆嘛,梯形不就是个小顶堆嘛,中位数可以通过它们的堆顶元素算出来。
梯形虽然是小顶堆,但其中的元素是较大的,我们称其为large
,倒三角虽然是大顶堆,但是其中元素较小,我们称其为small
。
当然,这两个堆需要算法逻辑正确维护,才能保证堆顶元素是可以算出正确的中位数,我们很容易看出来,两个堆中的元素之差不能超过 1。
因为我们要求中位数嘛,假设元素总数是n
,如果n
是偶数,我们希望两个堆的元素个数是一样的,这样把两个堆的堆顶元素拿出来求个平均数就是中位数;如果n
是奇数,那么我们希望两个堆的元素个数分别是n/2 + 1
和n/2
,这样元素多的那个堆的堆顶元素就是中位数。
根据这个逻辑,我们可以直接写出findMedian
函数的代码:
class MedianFinder {
private PriorityQueue<Integer> large;
private PriorityQueue<Integer> small;
public MedianFinder() {
// 小顶堆
large = new PriorityQueue<>();
// 大顶堆
small = new PriorityQueue<>((a, b) -> {
return b - a;
});
}
public double findMedian() {
// 如果元素不一样多,多的那个堆的堆顶元素就是中位数
if (large.size() < small.size()) {
return small.peek();
} else if (large.size() > small.size()) {
return large.peek();
}
// 如果元素一样多,两个堆堆顶元素的平均数是中位数
return (large.peek() + small.peek()) / 2.0;
}
public void addNum(int num) {
// 后文实现
}
}
现在的问题是,如何实现addNum
方法,维护「两个堆中的元素之差不能超过 1」这个条件呢?
这样行不行?每次调用addNum
函数的时候,我们比较一下large
和small
的元素个数,谁的元素少我们就加到谁那里,如果它们的元素一样多,我们默认加到large
里面:
// 有缺陷的代码实现
public void addNum(int num) {
if (small.size() >= large.size()) {
large.offer(num);
} else {
small.offer(num);
}
}
看起来好像没问题,但是跑一下就发现问题了,比如说我们这样调用:
addNum(1)
,现在两个堆元素数量相同,都是 0,所以默认把 1 添加进large
堆。
addNum(2)
,现在large
的元素比small
的元素多,所以把 2 添加进small
堆中。
addNum(3)
,现在两个堆都有一个元素,所以默认把 3 添加进large
中。
调用findMedian
,预期的结果应该是 2,但是实际得到的结果是 1。
问题很容易发现,看下当前两个堆中的数据:
抽象点说,我们的梯形和小倒三角都是由原始的大倒三角从中间切开得到的,那么梯形中的最小宽度要大于等于小倒三角的最大宽度,这样它俩才能拼成一个大的倒三角对吧?
也就是说,不仅要维护large
和small
的元素个数之差不超过 1,还要维护large
堆的堆顶元素要大于等于small
堆的堆顶元素。
维护large
堆的元素大小整体大于small
堆的元素是本题的难点,不是一两个 if 语句能够正确维护的,而是需要如下技巧:
// 正确的代码实现
public void addNum(int num) {
if (small.size() >= large.size()) {
small.offer(num);
large.offer(small.poll());
} else {
large.offer(num);
small.offer(large.poll());
}
}
简单说,想要往large
里添加元素,不能直接添加,而是要先往small
里添加,然后再把small
的堆顶元素加到large
中;向small
中添加元素同理。
为什么呢 ? 稍加思考可以想明白,假设我们准备向large
中插入元素:
- 如果插入的
num
小于small
的堆顶元素,那么我们把num
留在small
堆里,为了保证两个堆的元素数量之差不大于 1,作为交换,把small
堆顶的元素再插入到large
堆里。 - 如果插入的
num
大于large
的堆顶元素,那么我们把num
留在large
的堆里,为了保证两个堆的元素数量之差不大于 1,作为交换,把l
arge堆顶的元素再插入到small堆里。 - 这样就巧妙地保证了
large
堆整体大于small
堆,且两个堆的元素之差不超过 1,那么中位数就可以通过两个堆的堆顶元素快速计算了。
至此,整个算法就结束了,addNum
方法时间复杂度 O(logN),findMedian
方法时间复杂度 O(1)。
代码实现
/**
* https://leetcode-cn.com/problems/shu-ju-liu-zhong-de-zhong-wei-shu-lcof/
*
* @author xiexu
* @create 2022-04-19 23:32
*/
public class _剑指Offer41_数据流中的中位数 {
}
class MedianFinder {
private PriorityQueue<Integer> large; // 小顶堆
private PriorityQueue<Integer> small; // 大顶堆
public MedianFinder() {
// 默认就是小顶堆
large = new PriorityQueue<>();
// 大顶堆
small = new PriorityQueue<>((a, b) -> {
return b - a;
});
}
// 添加一个数字
public void addNum(int num) {
if (small.size() >= large.size()) {
small.offer(num);
large.offer(small.poll());
} else { // small.size() < large.size()
large.offer(num);
small.offer(large.poll());
}
}
// 计算当前添加的所有数字的中位数
public double findMedian() {
// 如果元素不一样多,多的那个堆的堆顶元素就是中位数
if (large.size() < small.size()) {
return small.peek();
} else if (large.size() > small.size()) {
return large.peek();
}
// 如果元素一样多,两个堆堆顶元素的平均数是中位数
return (large.peek() + small.peek()) / 2.0;
}
}