今天做两个有点难度的题。
1、295. 数据流的中位数
手写堆实现:
加入元素:
如何维护一个中位数?我们考虑一下堆的特点,大顶堆堆顶是一个最大值,小顶堆堆顶是一个最小值,那么,如果我们可以把数据流的数据按顺序地平均地存放在两个堆中,一个大顶堆,一个小顶堆,那么大顶堆和小顶堆的堆顶不就是中位数吗?
就像下图这样,我们依次加入数据流,最后需要形成这样的堆。
还需要考虑一个问题,我们怎样加入元素?肯定是加一下大顶堆,再加一下小顶堆,这样依次加入元素,但是能直接就加吗?
不可以的,因为我们的大顶堆需要存储小的那部分元素,小顶堆需要存储大的那部分元素。
这里需要一个巧妙地操作,比如我们每次要往大顶堆添加元素,先把这个元素加到小顶堆,然后把小顶堆的堆顶加到大顶堆中。
比如,此时来了个100,两个堆size相同,我们想加到大顶堆中
但是100很大,所以我们先加入小顶堆,然后小顶堆堆顶移到大顶堆
这样就实现了元素加入大顶堆并且大小有序。
加入代码如下:h1大顶堆,h2小顶堆。
if(h1.size == h2.size){
h2.offer(num);
h1.offer(h2.poll());
}
else{
h1.offer(num);
h2.offer(h1.poll());
}
返回答案
如果元素相同,返回堆头之和除以2;如果不同,那肯定是大顶堆h1多一个元素,直接返回就行。注意结果是double类型。
if(h1.size == h2.size){
return (h1.peek() + h2.peek())/2.0;
}
return 1.0*h1.peek();
这里我手写一下堆,复习一下堆的基本写法。
h1 = new Heap(10, true);
h2 = new Heap(10, false);
第二个参数是true代表大顶堆,是false代表小顶堆,我将大小顶堆代码合并了。
class MedianFinder {
Heap h1 = null;
Heap h2 = null;
public MedianFinder() {
h1 = new Heap(10, true);
h2 = new Heap(10, false);
}
public void addNum(int num) {
if(h1.size == h2.size){
h2.offer(num);
h1.offer(h2.poll());
}
else{
h1.offer(num);
h2.offer(h1.poll());
}
}
public double findMedian() {
if(h1.size == h2.size){
return (h1.peek() + h2.peek())/2.0;
}
return 1.0*h1.peek();
}
}
class Heap{
private int[] array;
int size;
Boolean m;
public Heap(int capacity, Boolean m) {
this.array = new int[capacity];
this.m = m;
}
public int peek(){
return size==0 ? -999 : array[0];
}
public void offer(int offered){
if(size == array.length) {
resize();
}
up(offered);
size++;
}
private void resize() {
int capacity = size + (size>>1);
int[] newArr = new int[capacity];
System.arraycopy(array, 0, newArr, 0, size);
array = newArr;
}
private void up(int offered) {
int child = size;
while(child > 0){
int parent = (child - 1) / 2;
if(m ? offered > array[parent] : offered < array[parent]){
array[child] = array[parent];
}
else {
break;
}
child = parent;
}
array[child] = offered;
}
public int poll(){
int top = array[0];
swap(0, size-1);
size--;
down(0);
return top;
}
private void down(int i) {
int lc = 2*i+1;
int rc = lc+1;
int now = i;
if(lc < size && (m ? array[lc] > array[now] : array[lc] < array[now])){
now = lc;
}
if(rc < size && (m ? array[rc] > array[now] : array[rc] < array[now])){
now = rc;
}
if(now != i){
swap(now, i);
down(now);
}
}
private void swap(int top, int bottom) {
int t = array[top];
array[top] = array[bottom];
array[bottom] = t;
}
}
利用优先队列:
优先队列底层就是堆嘛,所以直接用java提供的优先队列也行。( •̀ ω •́ )y
class MedianFinder {
PriorityQueue<Integer> q1;
PriorityQueue<Integer> q2;
public MedianFinder() {
q1 = new PriorityQueue<>((o1, o2)->o2-o1);
q2 = new PriorityQueue<>();
}
public void addNum(int num) {
if(q1.size() == q2.size()){
q2.offer(num);
q1.offer(q2.poll());
}
else{
q1.offer(num);
q2.offer(q1.poll());
}
}
public double findMedian() {
return (q1.size()==q2.size() ?
1.0*(q1.peek()+q2.peek())/2 :
1.0*q1.peek());
}
}
2、239. 滑动窗口最大值
第一想法,直接来个优先队列,但是想想不对,因为优先队列会将窗口中的数据排序,排完序之后窗口继续移动,那我就无法知道应该pop掉哪个元素了。
我们需要一个数据结构,这个数据结构和队列很像,它能push加入队尾,pop删除队头,getMaxValue获取最大值。但是并没有这种现成的数据结构。
实现上面需求的数据结构叫做单调队列。
单调队列中存着窗口中的元素,队列头就是当前窗口最大值,为什么能是最大值呢?因为这个队列并不是存储所有的元素,而是有选择的存,单调队列每次新加元素时,都会比较与队尾的值,如果队尾小,就一直pop,直到能添加进去,所以,如果此时新加的元素是最大值,那么它会pop掉所有队元素,成为队头。
所以,想要在单调队列中存活,需要满足两个条件:
- 在窗口的范围内
- 没有被新来的元素干掉,也就是不比新来的元素小
java中,我使用LinkedList,LinkedList实现了双端队列接口,用双端队列来模拟单调队列。
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
LinkedList<Integer> list = new LinkedList<>();
//nums.length-k+1
int len = nums.length;
int[] ans = new int[len-k+1];
int cnt = 0;
for(int i = 0; i < len; i++){
//弹出过时的
while(!list.isEmpty() && list.peek() < i-k+1){
list.poll();
}
//弹出末尾不够大的
while(!list.isEmpty() && nums[list.peekLast()] < nums[i]){
list.pollLast();
}
list.offer(i);
if(i >= k-1){
ans[cnt++] = nums[list.peek()];
}
}
return ans;
}
}