算法分析之栈和队列

算法相关数据结构总结:

序号数据结构文章
1动态规划动态规划之背包问题——01背包
动态规划之背包问题——完全背包
动态规划之打家劫舍系列问题
动态规划之股票买卖系列问题
动态规划之子序列问题
算法(Java)——动态规划
2数组算法分析之数组问题
3链表算法分析之链表问题
算法(Java)——链表
4二叉树算法分析之二叉树
算法分析之二叉树遍历
算法分析之二叉树常见问题
算法(Java)——二叉树
5哈希表算法分析之哈希表
算法(Java)——HashMap、HashSet、ArrayList
6字符串算法分析之字符串
算法(Java)——字符串String
7栈和队列算法分析之栈和队列
算法(Java)——栈、队列、堆
8贪心算法算法分析之贪心算法
9回溯Java实现回溯算法入门(排列+组合+子集)
Java实现回溯算法进阶(搜索)
10二分查找算法(Java)——二分法查找
11双指针、滑动窗口算法(Java)——双指针
算法分析之滑动窗口类问题

一、栈和队列

队列是先进先出,栈是先进后出。
在这里插入图片描述

1. 栈
(1)栈的基础知识

Java Stack类,栈是Vector的一个子类,实现后进先出的栈。

Stack stackA = new Stack();

栈是一种 “特殊” 的线性存储结构,因此栈的具体实现有以下两种方式:

  1. 顺序栈:采用顺序存储结构可以模拟栈存储数据的特点,从而实现栈存储结构;
  2. 链栈:采用链式存储结构实现栈结构;

注意:

Java中用来表达栈的功能(push/pop/peek),更适用的是使用双端队列接口Deque,并用实现类ArrayDeque/LinkedList来进行初始化。

Deque<Integer> stack = new ArrayDeque<>();
Deque<Integer> stack = new LinkedList<>();

不用Stack至少有以下两点原因

  1. 性能上来说应该使用Deque代替Stack。Stack和Vector都是线程安全的,其实多数情况下并不需要做到线程安全,因此没有必要使用Stack。毕竟保证线程安全需要上锁,有额外的系统开销。
  2. Stack从Vector继承是个历史遗留问题,JDK官方已建议优先使用Deque的实现类来代替Stack。Stack从Vector继承的一个副作用是,暴露了set/get方法,可以进行随机位置的访问,这与Stack只能从尾巴上进行增减的本意相悖。

相比LinkedList,ArrayDeque会略胜一筹,不过差别通常可以忽略。建议使用ArrayDeque

ArrayDeque的方法

// 添加元素
addFirst() 在数组前面添加元素
addLast() 在数组后面添加元素
offerFirst() 在数组前面添加元素,并返回是否添加成功
offerLast(E e) 在数组后天添加元素,并返回是否添加成功
// 删除元素
removeFirst()删除第一个元素,并返回删除元素的值,如果元素为null,将抛出异常
pollFirst()删除第一个元素,并返回删除元素的值,如果元素为null,将返回null
removeLast()删除最后一个元素,并返回删除元素的值,如果为null,将抛出异常
pollLast()删除最后一个元素,并返回删除元素的值,如果为null,将返回null
removeFirstOccurrence(Object o) 删除第一次出现的指定元素
removeLastOccurrence(Object o) 删除最后一次出现的指定元素
// 获取元素
getFirst() 获取第一个元素,如果没有将抛出异常
getLast() 获取最后一个元素,如果没有将抛出异常
// 队列操作
add(E e) 在队列尾部添加一个元素
offer(E e) 在队列尾部添加一个元素,并返回是否成功
remove() 删除队列中第一个元素,并返回该元素的值,如果元素为null,将抛出异常(其实底层调用的是removeFirst())
poll()  删除队列中第一个元素,并返回该元素的值,如果元素为null,将返回null(其实调用的是pollFirst())
element() 获取第一个元素,如果没有将抛出异常
peek() 获取第一个元素,如果返回null
// 栈操作
push(E e) 栈顶添加一个元素
pop(E e) 移除栈顶元素,如果栈顶没有元素将抛出异常
// 其他
size() 获取队列中元素个数
isEmpty() 判断队列是否为空
iterator() 迭代器,从前向后迭代
descendingIterator() 迭代器,从后向前迭代
contain(Object o) 判断队列中是否存在该元素
toArray() 转成数组
clear() 清空队列
clone() 克隆(复制)一个新的队列
(2)栈的函数
Stack<Integer> stack = new Stack<Integer>();//建栈
stack.push(Element);//进栈
stack.pop();//出栈
stack.peek();//取栈顶值(不出栈)
stack.isEmpty();//判断栈是否为空
2. 队列
(1)队列的基础知识

LinkedList类实现了Queue接口,因此我们可以把LinkedList当成Queue来用。

Queue<String> queue = new LinkedList<String>();

队列存储结构的实现有以下两种方式:

  1. 顺序队列:在顺序表的基础上实现的队列结构;
  2. 链队列:在链表的基础上实现的队列结构;
(2)队列的函数
添加:queue.offer() queue.add()
删除队列第一个元素:queue.poll()返回null queue.remove()返回异常
查询队列头部元素:peek()返回null element()返回异常
(3)优先队列

PriorityQueue,即优先队列。优先队列的作用是能保证每次取出的元素都是队列中权值最小的。堆保证每次插入都排好序。

Java中PriorityQueue默认是小顶堆,可以通过传入自定义的Comparator函数来实现大顶堆。

PriorityQueue<Integer> queue = new PriorityQueue<>()

大顶堆的写法:

匿名内部类型的写法:

PriorityQueue<Integer> queue = new PriorityQueue<Integer>(new Comparator<Integer>(){
            public int compare(Integer n1, Integer n2){
                return n2-n1; 
            }
        });

lamdba表达式的写法:

PriorityQueue<Integer> queue = new PriorityQueue<>((x,y) -> (y-x));

优先队列的方法:

创建:PriorityQueue<Integer> queue = new PriorityQueue<Integer>(new Comparator<Integer>()
add()offer() 向优先队列中插入元素
element()peek() 获取但不删除队首元素
remove()poll() 获取并删除队首元素

二、leetcode例题讲解栈和队列问题

1. 基础题目
232. 用栈实现队列

leetcode题目链接:232. 用栈实现队列

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):

实现 MyQueue 类:

  • void push(int x) 将元素 x 推到队列的末尾
  • int pop() 从队列的开头移除并返回元素
  • int peek() 返回队列开头的元素
  • boolean empty() 如果队列为空,返回 true ;否则,返回 false

说明:

  • 你只能使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
  • 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

解题思路:

使用栈来模式队列的行为,如果仅仅用一个栈,是一定不行的,所以需要两个栈一个输入栈,一个输出栈,这里要注意输入栈和输出栈的关系。

下面动画模拟以下队列的执行过程如下:

执行语句:

queue.push(1);
queue.push(2);
queue.pop(); 注意此时的输出栈的操作
queue.push(3);
queue.push(4);
queue.pop();
queue.pop();注意此时的输出栈的操作
queue.pop();
queue.empty();

在这里插入图片描述

在push数据的时候,只要数据放进输入栈就好,但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入),再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。

最后如何判断队列为空呢?如果进栈和出栈都为空的话,说明模拟的队列为空了。

Java代码实现

class MyQueue {
    Stack<Integer> stack1, stack2;

    public MyQueue() {
        stack1 = new Stack();  // 负责进栈
        stack2 = new Stack();  // 负责出栈
    }
    
    public void push(int x) {
        stack1.push(x);
    }
    
    public int pop() {
        // 如果stack2为空,那么将stack1中的元素全部放到stack2中
        if (stack2.isEmpty()){
            while (!stack1.isEmpty()){
                stack2.push(stack1.pop());
            }
        }
        // 然后出栈
        return stack2.pop();
    }
    
    public int peek() {
        // 如果stack2为空,那么将stack1中的元素全部放到stack2中
        if (stack2.isEmpty()){
            while (!stack1.isEmpty()){
                stack2.push(stack1.pop());
            }
        }
        // 然后取stack2栈顶元素
        return stack2.peek();
    }
    
    public boolean empty() {
        return stack1.isEmpty() && stack2.isEmpty();
    }
}
225. 用队列实现栈

leetcode题目链接:225. 用队列实现栈

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。

实现 MyStack 类:

  • void push(int x) 将元素 x 压入栈顶。
  • int pop() 移除并返回栈顶元素。
  • int top() 返回栈顶元素。
  • boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。

注意:

  • 你只能使用队列的基本操作 —— 也就是 push to back、peek/pop from front、size 和 is empty 这些操作。
  • 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。

解题思路:

队列是先进先出的规则,把一个队列中的数据导入另一个队列中,数据的顺序并没有变,并没有变成先进后出的顺序。

所以用栈实现队列, 和用队列实现栈的思路还是不一样的,这取决于这两个数据结构的性质。

但是依然还是要用两个队列来模拟栈,只不过没有输入和输出的关系,而是另一个队列完全用又来备份的!

如下面动画所示,用两个队列que1和que2实现队列的功能,que2其实完全就是一个备份的作用,把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。

queue.push(1);        
queue.push(2);        
queue.pop();   // 注意弹出的操作       
queue.push(3);        
queue.push(4);       
queue.pop();  // 注意弹出的操作    
queue.pop();    
queue.pop();    
queue.empty(); 

在这里插入图片描述

两个队列实现栈的Java代码实现:

两个队列Queue实现,一般使用LinkedList。

class MyStack {
    // // 两个队列Queue实现,一般使用LinkedList
    // Queue<Integer> queue1, queue2;

    Deque<Integer> que1;

    public MyStack() {
        queue1 = new LinkedList<>();  // 和栈中保持一样元素的队列
        queue2 = new LinkedList<>();  // 辅助队列

    }
    
    public void push(int x) {
        queue2.offer(x); // 先放在辅助队列中
        while (!queue1.isEmpty()){
            queue2.offer(queue1.poll());
        }
        Queue<Integer> queueTemp;
        queueTemp = queue1;
        queue1 = queue2;
        queue2 = queueTemp; // 最后交换queue1和queue2,将元素都放到queue1中
    }
    
    public int pop() {
        return queue1.poll();
    }
    
    public int top() {
        return queue1.peek();
    }
    
    public boolean empty() {
        return queue1.isEmpty();
    }
}

一个队列实现栈的Java代码:

一个队列实现,双端队列Deque,Queue 中的 add、poll、peek等效于 Deque 中的 addLast、pollFirst、peekFirst都可以使用。

class MyStack {

    // 一个队列实现,双端队列Deque
    // Deque 接口继承了 Queue 接口
    // 所以 Queue 中的 add、poll、peek等效于 Deque 中的 addLast、pollFirst、peekFirst
    Deque<Integer> que1;

    public MyStack() {
        que1 = new ArrayDeque<>();
    }
    
    public void push(int x) {
        que1.addLast(x);  // 进来直接加到最后面
    }
    
    public int pop() {
        int size = que1.size();
        size--;
        while (size-- > 0) {
            que1.addLast(que1.peekFirst());
            que1.pollFirst();
        }

        int res = que1.pollFirst();
        return res;
    }
    
    public int top() {
        return que1.peekLast();
    }
    
    public boolean empty() {
        return que1.isEmpty();
    }
}
2. 栈的经典问题
(1) 括号匹配问题
20. 有效的括号

leetcode题目链接:20. 有效的括号

给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。

示例一:

输入:s = "()"
输出:true

示例二:

输入:s = "()[]{}"
输出:true

示例三:

输入:s = "([)]"
输出:false

解题思路:

先来分析一下 这里有三种不匹配的情况,

  1. 第一种情况,字符串里左方向的括号多余了 ,所以不匹配。
  2. 第二种情况,括号没有多余,但是 括号的类型没有匹配上。
  3. 第三种情况,字符串里右方向的括号多余了,所以不匹配。

使用栈来存储左括号对应的右括号。然后遇到右括号和栈顶元素相同则出栈。

第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false

第二种情况:遍历字符串匹配的过程中,发现栈里没有要匹配的字符。所以return false

第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号return false

左括号和右括号全都匹配了呢,就是字符串遍历完之后,栈是空的,就说明全都匹配了。

Java代码实现:

class Solution {
    public boolean isValid(String s) {
        Stack<Character> stack = new Stack<>();
        for(int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            // 碰到左括号,就把相应得右括号入栈
            if (ch == '(') {
                stack.push(')');
            }else if(ch == '[') {
                stack.push(']');
            }else if(ch == '{') {
                stack.push('}');
            }else if(stack.isEmpty() || stack.pop() != ch) { //栈空,说明右括号没找到对应得左括号,栈顶元素不是匹配的字符
                return false;
            }
        }
        // 遍历完栈不空,则说明左括号没有对应的右括号
        return stack.isEmpty();
    }
}
(2) 字符串去重问题
1047. 删除字符串中的所有相邻重复项

leetcode题目链接: 1047. 删除字符串中的所有相邻重复项

给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。

在 S 上反复执行重复项删除操作,直到无法继续删除。

在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。

示例一:

输入:"abbaca"
输出:"ca"
解释:
例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"

解题思路:

要删除相邻相同元素,其实也是匹配问题,相同左元素相当于左括号,相同右元素就是相当于右括号,匹配上了就删除。

那么再来看一下本题:可以把字符串顺序放到一个栈中,然后如果相同的话 栈就弹出,这样最后栈里剩下的元素都是相邻不相同的元素了。

Java代码实现:

用栈来实现匹配问题,最后需要将栈里的元素转为字符串。

class Solution {
    public String removeDuplicates(String s) {
        // 用栈来存储元素
        Stack<Character> stack = new Stack<>();
        for(int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            if(stack.isEmpty() || stack.peek() != ch) {  // 栈空或栈顶元素等于ch
                stack.push(ch);
            }else {
                stack.pop();
            }
        }
        String res = "";
        while (!stack.isEmpty()) {  // 剩下的都是不重复元素
            res = stack.pop() + res;
        }
        return res;
    }
}

用字符串当栈。

class Solution {
    public String removeDuplicates(String s) {
        // 用字符串直接做栈
        StringBuffer res = new StringBuffer();
        // top为 res 的长度
        int top = -1;
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            // 当 top > 0,即栈中有字符时,当前字符如果和栈中字符相等,弹出栈顶字符,同时 top--
            if (top >= 0 && res.charAt(top) == c) {
                res.deleteCharAt(top);
                top--;
            // 否则,将该字符入栈,同时top++
            } else {
                res.append(c);
                top++;
            }
        }
        return res.toString();
    }
}

不使用StringBuffer,StringBuilder太慢。

class Solution {
    public String removeDuplicates(String s) {
        // 不用StringBuffer
        char[] str = s.toCharArray();
        int top = -1;
        for (int i = 0; i < s.length(); i++) {
            if (top == -1 || str[top] != str[i]) {
                str[++top] = str[i];
            } else {
                top--;
            }
        }
        return String.valueOf(str, 0, top + 1);
    }
}
(3)逆波兰表达式问题
150. 逆波兰表达式求值

leetcode题目链接:150. 逆波兰表达式求值

根据 逆波兰表示法,求表达式的值。

有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。

说明:

  • 整数除法只保留整数部分。
  • 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。

示例一:

输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9

示例二:

输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6

解题思路:

这和1047. 删除字符串中的所有相邻重复项 是差不错的,只不过本题不要相邻元素做消除了,而是做运算!

使用栈将数字存到栈里面,遇到运算符的时候进行两个数的计算。

Java实现代码:

(注意在leetcode中,由于jdk版本不同,不能使用==来判断字符串等于运算符,需要使用equals)

class Solution {
    public int evalRPN(String[] tokens) {
        Stack<Integer> stack = new Stack<>();
        for(String s : tokens) {
            if("+".equals(s) || "-".equals(s) || "*".equals(s) || "/".equals(s)) {  // jdk版本问题,不能使用==
                int num1 = stack.pop();
                int num2 = stack.pop();
                if("+".equals(s)) stack.push(num2 + num1);
                if("-".equals(s)) stack.push(num2 - num1);
                if("*".equals(s)) stack.push(num2 * num1);
                if("/".equals(s)) stack.push(num2 / num1);
            } else {
                stack.push(Integer.valueOf(s));
            }
        }
        int res = stack.peek();
        return res;
    }
}
3. 队列的经典问题
(1)滑动窗口最大值问题
239. 滑动窗口最大值

leetcode题目链接:239. 滑动窗口最大值

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值。

示例一:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

解题思路:

这是使用单调队列的经典题目。

设计单调队列的时候,pop,和push操作要保持如下规则:

  • pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作

  • push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止

保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。

利用双端队列手动实现单调队列

用一个单调队列来存储对应的下标,每当窗口滑动的时候,直接取队列的头部指针对应的值放入结果集即可
单调队列类似 (tail -->) 3 --> 2 --> 1 --> 0 (–> head) (右边为头结点,元素存的是下标)

Java实现代码:

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        ArrayDeque<Integer> deque = new ArrayDeque<>();
        int n = nums.length;
        int[] res = new int[n - k + 1];
        int idx = 0;
        for(int i = 0; i < n; i++) {
            // 根据题意,i为nums下标,是要在[i - k + 1, i] 中选到最大值,只需要保证两点
            // 1.队列头结点需要在[i - k + 1, i]范围内,不符合则要弹出
            while(!deque.isEmpty() && deque.peek() < i - k + 1){
                deque.poll();
            }
            // 2.既然是单调,就要保证每次放进去的数字要比末尾的都大,否则也弹出
            while(!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
                deque.pollLast();
            }

            deque.offer(i);

            // 因为单调,当i增长到符合第一个k范围的时候,每滑动一步都将队列头节点放入结果就行了
            if(i >= k - 1){
                res[idx++] = nums[deque.peek()];
            }
        }
        return res;

    }
}
(2)求前 K 个高频元素问题
347. 前 K 个高频元素

leetcode题目链接:347. 前 K 个高频元素

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

示例一:

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

示例二:

输入: nums = [1], k = 1
输出: [1]

解题思路:

这道题目主要涉及到如下三块内容:

  • 要统计元素出现频率
  • 对频率排序
  • 找出前K个高频元素

首先统计元素出现的频率,这一类的问题可以使用map来进行统计。

然后是对频率进行排序,这里我们可以使用一种 容器适配器就是优先级队列。

什么是优先级队列呢?

其实就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。

而且优先级队列内部元素是自动依照元素的权值排列。

们要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。

寻找前k个最大元素流程如图所示:(图中的频率只有三个,所以正好构成一个大小为3的小顶堆,如果频率更多一些,则用这个小顶堆进行扫描)

Java代码实现:

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        int[] result = new int[k];
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int num : nums) {
            map.put(num, map.getOrDefault(num, 0) + 1);
        }

        Set<Map.Entry<Integer, Integer>> entries = map.entrySet();
        // 根据map的value值正序排,相当于一个小顶堆
        PriorityQueue<Map.Entry<Integer, Integer>> queue = new PriorityQueue<>((o1, o2) -> o1.getValue() - o2.getValue());
        for (Map.Entry<Integer, Integer> entry : entries) {
            queue.offer(entry);
            if (queue.size() > k) {
                queue.poll();
            }
        }
        for (int i = k - 1; i >= 0; i--) {
            result[i] = queue.poll().getKey();
        }
        return result;
    }
}

简单写法:

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
         HashMap<Integer,Integer> hashMap = new HashMap<>();
         for(int m : nums)
         {
             hashMap.put(m,hashMap.getOrDefault(m,0)+1);
         }
         PriorityQueue<Integer> queue = new PriorityQueue<>((a,b) -> (hashMap.get(b)-hashMap.get(a)));
         for(int key : hashMap.keySet())
         {
             queue.offer(key);
         }
         int res[] = new int[k];
         for(int i=0;i<k;i++)
         {
             res[i] = queue.poll();
         }
         return res;
    }
}

三、其它算法分析

1. 动态规划之背包问题——01背包

动态规划之背包问题——01背包

2. 动态规划之背包问题——完全背包

动态规划之背包问题——完全背包

3. 动态规划之子序列问题

动态规划之子序列问题

4. 算法分析之数组问题

算法分析之数组问题

5. 算法分析之链表问题

算法分析之链表问题

6. 算法分析之哈希表

算法分析之哈希表

7. 算法分析之字符串

算法分析之字符串

8. 算法分析之栈和队列

算法分析之栈和队列

参考:

代码随想录:栈和队列

算法(Java)——栈、队列、堆

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值