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.

提示:

各函数的调用总次数不超过 20000

思路

  • 使用一个额外的 minStack,栈顶元素为当前栈中最小的值。
  • 在对栈进行 push 入栈和 pop 出栈操作时,同样需要对 minStack 进行入栈出栈操作,从而使 minStack 栈顶元素一直为当前栈中最小的值。
  • 在进行 push 操作时,需要比较入栈元素和当前栈中最小值,将值较小的元素 push 到 minStack 中。

day27_leetcode

代码实现

/**
* 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;
}

}

快速选择解法

思路

day27_大顶堆_02

代码实现

/**
* 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]

思路

我们必然需要有序数据结构,本题的核心思路是使用两个优先级队列

中位数是有序数组最中间的元素算出来的对吧,我们可以把「有序数组」抽象成一个倒三角形,宽度可以视为元素的大小,那么这个倒三角的中部就是计算中位数的元素对吧:

day27_队列_03

然后我把这个大的倒三角形从正中间切成两半,变成一个小倒三角和一个梯形,这个小倒三角形相当于一个从大到小的有序数组,这个梯形相当于一个从小到大的有序数组。

中位数就可以通过小倒三角和梯形顶部的元素算出来对吧?嗯,你联想到什么了没有?它们能不能用优先级队列表示?小倒三角不就是个大顶堆嘛,梯形不就是个小顶堆嘛,中位数可以通过它们的堆顶元素算出来

day27_大顶堆_04

梯形虽然是小顶堆,但其中的元素是较大的,我们称其为​​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。

问题很容易发现,看下当前两个堆中的数据:

day27_大顶堆_05

抽象点说,我们的梯形和小倒三角都是由原始的大倒三角从中间切开得到的,那么梯形中的最小宽度要大于等于小倒三角的最大宽度,这样它俩才能拼成一个大的倒三角对吧?

也就是说,不仅要维护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;
}
}