day27

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 中。

Untitled

代码实现

/**
 * 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
pushed 是 popped 的排列。

思路

使用一个栈来模拟压入弹出操作。每次入栈一个元素后,都要判断一下栈顶元素是不是当前出栈序列 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;
    }

}

快速选择解法

思路

Untitled

代码实现

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

思路

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

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

Untitled

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

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

Untitled

梯形虽然是小顶堆,但其中的元素是较大的,我们称其为large,倒三角虽然是大顶堆,但是其中元素较小,我们称其为small

当然,这两个堆需要算法逻辑正确维护,才能保证堆顶元素是可以算出正确的中位数,我们很容易看出来,两个堆中的元素之差不能超过 1

因为我们要求中位数嘛,假设元素总数是n,如果n是偶数,我们希望两个堆的元素个数是一样的,这样把两个堆的堆顶元素拿出来求个平均数就是中位数;如果n是奇数,那么我们希望两个堆的元素个数分别是n/2 + 1n/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函数的时候,我们比较一下largesmall的元素个数,谁的元素少我们就加到谁那里,如果它们的元素一样多,我们默认加到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。

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

Untitled

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

也就是说,不仅要维护largesmall的元素个数之差不超过 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,作为交换,把large堆顶的元素再插入到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;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猿小羽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值