数据结构

数据结构

1.链表

  1. 单链表
// 创建链表
const int N = 1e6 + 10
// e[N] 表示value -> 数据 我是以int 类型举例, 你可以用其他的比如struct, double
// ne[N] 表示next的位置
// idx 相当于指针
// head 表示头指针 
// 以-1结尾
int head, e[N], ne[N], idx;

void init() {
    idx = 0;
    // 头指针指向空 即为 -1
	head = -1;
}

// 头插入
void add_to_head(int x) {
    e[idx] = x;
    ne[idx] = head;
    head = idx++;
}

// 中间插入 在插入的第k个下标后插入
void add(int k, int x) {
    e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx++;
}

// 删除k 后面一个数
// k与上面一样
void remove(int k) {
    ne[k] = ne[ne[k]];
}


对于上面k是什么?

下面图片给予解释

Snipaste_2023-05-09_19-47-31!](…/…/Snipaste_2023-05-09_19-47-31.png)

这题上面有

输入数据:
10
H 9
I 1 1
D 1
D 0
H 6
I 3 6
I 4 5
I 4 5
I 3 4
D 6
输出数据:
6 4 6 5

参考代码:

#include<bits/stdc++.h>
using namespace std;

const int N = 1e6 + 10;
// 下面函数基本上就是对于单调栈的相关操作
int head, e[N], ne[N], idx;
int n, k, x;
char a;
void init() {
    head = -1;
    idx = 0;
}

void add_to_head(int x) {
    e[idx] = x;
    ne[idx] = head;
    head = idx++;
}

void add(int k, int x) {
    e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx++;
}

void remove(int k) {
    ne[k] = ne[ne[k]];
}

int main() {
    // 初始化链表
    init();
    // n 个测试数据
    cin >> n;
    while(n--) {
        // 判断第一个字符是什么
        cin >> a;
        // 对应相关操作
        switch(a) {
            case 'H': 
                cin >> x;
                add_to_head(x);
                break;
            case 'I':
                cin >> k >> x;
                add(k - 1, x);
                 break;
            case 'D':
                cin >> k;
                if (!k) head = ne[head];
                else remove(k - 1);
                break;
        }
    }
	// 遍历链表
    for (int i = head; i != -1; i = ne[i]) {
        printf("%d ", e[i]);
    }
    printf("\n");
    system("pause");
    return 0;
}

2.双链表

  • 就是可以从前往后也可以从后往前
const int N = 1e6 + 10;
// l[N] 存放左边的next
// r[N] 存放右边的next
int e[N], l[N], r[N], idx;
// 你可能会发现与上面不同的是head没有了
// 那么头指针去哪里了呢?
// 我们这里偷了个懒 把l方向的head 用0号下标来表示, r 方向的head用1号下标来表示

void init() {
    r[0] = 1;
    l[1] = 0;
    // 0 的右边为1
    // 1 的左边为0
}
// 在第k个插入的元素后添加x
void add(int k, int x) {
    e[idx] = x;
    r[idx] = r[k];
    l[idx] = l[k];
    
    l[r[k]] = idx;
    r[k] = idx++;
    // 最后两个不可反过来
}

// 这里为什么只有有插入, 而且没有做插入呢?
// 你可以想想


// 输出第k个插入元素
void remove(int k) {
    l[r[k]] = l[k];
    r[l[k]] = r[k];
}
  • 对于左插入只需调用add(l[k], x); 即为右插入

Snipaste_2023-05-09_20-05-25!](…/…/Snipaste_2023-05-09_20-05-25.png)

测试数据:
10
R 7
D 1
L 3
IL 2 10
D 3
IL 2 7
L 8
R 9
IL 4 7
IR 2 2
输出数据:
8 7 7 3 2 9

参考代码:

#include<bits/stdc++.h>
using namespace std;

const int N = 1e6 + 10; 
int n;
int e[N], r[N], l[N], idx;
char a;
int k, x;

void init() {
    r[0] = 1;
    l[1] = 0;
    idx = 2;
}

void add(int k, int x) {
    e[idx] = x;
    r[idx] = r[k];
    l[idx] = k;
    l[r[k]] = idx;
    r[k] = idx++;
}

void remove(int k) {
    l[r[k]] = l[k];
    r[l[k]] = r[k];
}


int main() {
    init();
    scanf("%d", &n);
    while(n--) {
        cin >> a;
        switch(a) {
            case 'L':
                cin >> x;
                add(0, x);
                break;
            case 'R':
                cin >> x;
                add(l[1], x);
                break;
            case 'D':
                cin >> k;
                remove(k +  1);
                break;
            case 'I':
                cin >> a;
                if (a == 'L') {
                    cin >> k >> x;
                    add(l[k + 1], x);
                }else {
                    cin >> k >> x;
                    add(k + 1, x);
                }
        }
    }
    // 为什么这个遍历与单链表不一样???
    for (int i = r[0]; i != 1; i = r[i]) {
        printf("%d ", e[i]);
    }
   
    return 0;
}

链表补充

  1. 单链表变成循环链表
// 对一个元素进行闭环操作
// 不需要head
ne[idx] = idx;
// 这是我自己想的不确定对于不对
// 如果是不初始化head 在最后一个元素来了后进入下一个就是head, 但e[head] 没有东西
// 所以不妨直接删除head
  1. 双链表变成循环链表
// 其实与单链表是类似的处理
// 对第一个元素
r[idx] = idx;
l[idx] = idx;
// 这样应该就可以了, (不确定对错)

3.栈(stack)

const int N = 1e6 + 10;
// sta 表示一个栈
// tt 表示一个类似指针 指向栈顶的下标
int sta[N], tt;

// 插入
void push(int x) {
    sta[tt++] = x; 
}

// 删除
void pop() {
    tt--;
}
// 取出栈顶元素
int top() {
    return sta[tt - 1];
}

// 判断是否为空
bool isempty() {
    return tt == 0;
}

// 栈的长度
int size() {
    return tt;
}

​ 给个例子:

Snipaste_2023-05-09_20-15-05!](…/…/Snipaste_2023-05-09_20-15-05.png)

输入数据:
10
push 5
query
push 6
pop
query
pop
empty
push 4
query
empty
输出数据:
5
5
YES
4
NO

​ 参考代码:

#include<bits/stdc++.h>
using namespace std;

const int N = 1e6 + 10;
int sta[N], tt, n, t;
string s;

// 是否为空
bool isempty() {
    return tt == 0;
}

// 增加元素
void add(int x) {
    sta[tt++] = x;
}

// 删除元素
void pop() {
    tt--;
}

// 长度
int size() {
    return tt;
}
// 栈顶元素
int top() {
    return sta[tt - 1];
}

int main()
{
    scanf("%d", &n);
    while(n --) {
        cin >> s;
        if (s == "push") {
            scanf("%d", &t);
            add(t);
        }else if (s == "pop") {
            pop();
        }else if (s == "empty") {
            if (isempty()) {
                printf("YES\n");
            }else {
                printf("NO\n");
            }
        }else {
            printf("%d\n", top());
        }
    }
    system("pause");
    return 0;
}

4.双端队列

queue只是deque的一部分, 对于模拟队列来说可以模拟双端队列来实现

对于priority_queue来说是没法直接模拟的

可以通过模拟队列加sort来实现不过时间复杂度比较高

可以换成堆来实现相对来说时间复杂度就低了许多

const int N = 1e6 + 10;

// 用que来表示队列
// tt 表示队尾
// hh 表示对头
// 为什么要令他们等于N / 2 呢?
// 因为要push_front()

int que[N], tt = N / 2, hh = N / 2;

// 尾插
void push_back(int x) {
    que[tt++] = x;
}

// 头插
void push_front(int x) {
    que[hh--] = x;
} 

// 尾删
void pop_back() {
    tt--;
}

// 头删
void pop_front() {
    hh++;
}

// 判断对否为空
bool isempty() {
    return tt == hh;
}

// 队的长度
int size() {
	return tt - hh;
}

// 输出尾顶部元素
int back() {
    return que[tt - 1];
}

// 输出头部元素
int front() {
    return que[hh];
}

Snipaste_2023-05-09_20-29-57!](…/…/Snipaste_2023-05-09_20-29-57.png)

输入:
10
push 6
empty
query
pop
empty
push 3
push 4
pop
query
push 6
输出:
NO
6
YES
4

参考代码:

#include <iostream>
#include<deque>
using namespace std;

const int N = 1e6 + 10;
int que[N], hh, tt;
int n, t;
string s;

void push(int x) {
    que[tt++] = x; 
}

void pop() {
    hh++;
}

bool isempty() {
    return hh == tt;
}

int front() {
    return que[hh];
}

int main()
{   
    cin >> n;
    while ( n--) {
        cin >> s;
        if (s == "push") {
            cin >> t;
            push(t);
        }else if (s == "pop") {
            pop();
        }else if (s == "empty") {
            if (isempty()) {
                printf("YES\n");
            }else {
                printf("NO\n");
            }
        }else {
            printf("%d\n", front());
        }
    }
    
    system("pause");
    return 0;
}

5.栈的应用->单调栈

Snipaste_2023-05-09_21-15-11!](…/…/Snipaste_2023-05-09_21-15-11.png)

  1. 暴力做法O(n^2)
for (int i = 0; i < n; i++) {
    int j = i - 1;
    for (; j >= 0; j--) {
        if (q[j] < q[i]) {
            break;
        }
    }
    if (j >= 0) {
        printf("%d ", q[j]);
    }else {
        printf("-1 ");
    }
}

n = 1e5 所以会time limit error

那么这么优化呢?

不妨把暴力过程写出来, 来看哪些数据一定是没有用的, 那么我们就可以直接去除

20230509213124!](…/…/20230509213124.png)

20230509213116!](…/…/20230509213116.png)

最终, 我们发现了可以将之前的数据保存到一个单调递增的栈中

// 参考代码
#include<bits/stdc++.h>
using namespace std;

const int N = 1e5 + 10;
int n, sta[N], tt, q[N];

void push(int x) {
    sta[tt++] = x;
}

void pop() {
    tt--;
}

int top() {
    return sta[tt - 1];
}

bool isempty() {
    return tt == 0;
}

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%d", q + i);
    }
    for (int i = 0; i < n; i++) {
        // 如果 栈顶的元素比q[i] 大就将栈顶元素删除
        while(!isempty() && top() >= q[i]) pop();
        // 如果栈空了就说明栈中元素均大于等于q[i], 在q[i] 前面没有小于q[i] 的值
        if (isempty()) {
            printf("-1 ");
        }else {
            printf("%d ", top());
        }
        // 将q[i] 加入到栈中
        push(q[i]);
    }
    system("pause");
    return 0;
}

6.队的应用->单调队列

Snipaste_2023-05-09_21-37-33!](…/…/Snipaste_2023-05-09_21-37-33.png)

与上题类似

首先用暴力做法, 然后再爆力的基础上, 进行优化

// 暴力
int l = 0, r = k - 1;
for (int i = r; i < n; i++) {
    int mi = inf;// inf = 2^31 - 1
    for (int j = l; j <= r; j++) {
        mi = min(mi, q[j]);
    }
    printf("%d ", mi);
    l++, r++;
}

时间复杂度O(nk)// k 没有说多少默认1e6

20230510083501!](…/…/20230510083501.jpg)

// 参考代码
// 注意: que保存的是q[i] 中的i 不是q[i]
#include<bits/stdc++.h>
using namespace std;

const int N = 1e6 + 10;
int n, q[N], k;
int que[N], tt, hh;
// 保存下标
void push(int x) {
    que[tt++] = x;
}
// 尾部删除
void pop_back() {
    tt--;
}
// 头删除
void pop_front() {
    hh++;
}

bool isempty() {
    return hh == tt;
}

int size() {
    return tt - hh;
}
// 查队头
int front() {
    return q[que[hh]];
}
// 查队尾
int back() {
    return q[que[tt - 1]];
}



int main() {
    scanf("%d%d", &n, &k);
    for (int i = 0; i < n; i++) {
        scanf("%d", q + i);
    }

    for (int i = 0; i < n; i++) {
        // 这就是为什么要保存i, i - k + 1 > que[hh] // 可以判断长度是否>= k 不然无法判断
        if (tt > hh && i - k + 1 > que[hh]) pop_front();
        // 这里一定要用尾删除, 所以无法用que来保存q[i]
        // 为什么不能用头删除
        // 8 3
		// 1 3 -1 -3 5 3 6 7
        // 后面会出现5 你可以去试一试
        while(!isempty() && back() >= q[i]) pop_back();
        push(i);
        // 当i >= k - 1 也就是第一个滑动窗口满了的时候, 后面开始输出
        if (i >= k - 1) printf("%d ", front());
    }
    puts("");
    // 后面是写大于的
    // 思想一样
    hh = tt = 0;
    for (int i = 0; i < n; i++) {
        if (tt > hh && i - k + 1 > que[hh]) pop_front();
        while(!isempty() && back() <= q[i]) pop_back();
        push(i);
        if (i >= k - 1) printf("%d ", front());
    }


    system("pause");
    return 0;
}

7. KMP 算法

简介:

​ kmp 算法主要应用与有两个字符串之间匹配的问题上面

假设存在字符串s1 = “abababab”, s2 = “aba” 求所有s1中出现子串s2的初始下标

首先还是先暴力做法:

for (int i = 0; s1[i]; i++) {
    if (s1[i] == s2[0]) {
        int j = 1, l = i + 1;
        for (; s2[j]; j++, l++) {
            if (s2[j] != s1[l]) {
                break;
            }
        }
        if (s2[j] == '\0') {
            printf("%d ", i);
        }
    }
}

时间复杂度O(nm)

一般n, m 范围为10^5 会超时

优化:

​ 那么如果s2 的前i个和后i个字符串一样的

​ 字符串"abcabc"

​ 前3个与后3个是一样的

​ 那么当匹配到最后一个字符‘c’后 我们可以将i 向后移动3格而不是一格这就是kmp算法的优化

我们定义: 
ne[]数组存放就是以i 结尾向后移动的格子数量比如上面的ne[5] = 3;

20230512152314!](…/…/20230512152314.png)

对应代码:

for (int i = 1, j = 0; p[i]; i++ ) {
    // 这个理解起来有点难
    // 下面有解释
    while(j && p[i] != q[j + 1]) j = ne[j];
    // while() 循环退出两种情况 
    if (p[i] == q[j + 1]) j++;
    // 匹配完成
    // 为什么是i - k 下面有解释
    if (j == m) {
        printf("%d", i - k);
        // 最后一个元素他可以让i 向前多少
        // 可以不写, 但是这样可以减少时间
        j = ne[j];
    }
}
// 乍一看这个代码好像也是O(n ^ 2) 其实是O(n) 证明我也不会
// 求ne代码 
// 就是q对q自己求子串的过程
// ne[1] = 0 这不用过多解释, 所以从二开始
for (int i = 2, j = 0; q[i]; i++) {
    while(j && q[i] != q[j + 1]) j = ne[j];
    if (q[i] == q[j + 1]) j++;
    ne[i] = j;
}

20230512155013!](…/…/20230512155013.jpg)

002!](…/…/002.png)

8.trie树

trie树可以用来存放字符串出现次数和插入一个字符串

// N 代表所有字符串中 字符的数量不超过N, 不是字符串的数量不超过N
const int N = 1e6 + 10;
// 假设字符串全为小写字母
// son表示存放单词的数组, 其实是一个链表(我之前不是这么想的, 会出现一个bug在后面会说)
// idx 与链表的差不多;
// cnt[idx]记录的是以 idx 结尾出现的字符串的个数
// 如果没有要个数可以用bool
int son[N][26], idx, cnt[N];
// 因为全为小写字母 所以只需要26就行
// 最保险的做法是 son[N][128]
// 当然所需要的空间更大

假设现在有字符串“abcdef" “abceff” “abfgh” 需要插入到trie中

先写代码:

void insert(char * str) {
    int p = 0;
    for(int i = 0; str[i]; i++) {
		int u = str[i] - 'a';
        // 因为idx 初始化为0 所以一定要用++idx 而不能用idx++
        // 如果没有son[p][u] 我就开辟一条路来
        if (!son[p][u]) son[p][u] = ++idx;
        // p = son[p][u] 相当于链表了
        p = son[p][u];
    }
    // 因为p他是惟一的 所以cnt[p]是唯一的
    cnt[p] ++;
}
// 这也可以解释N 就比如每个字符串中字符对应不等idx就会一种加一

解释:

Snipaste_2023-05-12_16-26-49!](…/…/Snipaste_2023-05-12_16-26-49.png)

Snipaste_2023-05-12_16-26-58!](…/…/Snipaste_2023-05-12_16-26-58.png)

是不是觉得很奇怪???

这是啥?

我之前想直接son[26][26]不就行了何必这么麻烦

但是有bug

比如插入 abc

你查询abcc 发现是有的

接下来就是查询操作了

int query(char str[]) {
    int p = 0;
    
    for (int i = 0; str[i]; i++) {
        int u = str[i] - 'a';
    	// 就是说str[i] 下一个字符没得了
        // 那肯定没有对应的字符串了
        // return 0;
        if (!son[p][u]) return 0;
        p = son[p][u];
    }
    // 返回cnt[p] 就是以p结尾的字符串
    return cnt[p];
}

Snipaste_2023-05-12_16-43-08!](…/…/Snipaste_2023-05-12_16-43-08.png)

输入:
5
I abc
Q abc
Q ab
I ab
Q ab
输出:
1
0
1

参考代码:

#include<bits/stdc++.h>
using namespace std;

const int N = 1e5 + 10;
int son[N][26], idx, cnt[N];
int n;
char str[N];

void insert() {
    int p = 0;
    for (int i = 0; str[i]; i++) {
        int u = str[i] - 'a';
        if (!son[p][u]) son[p][u] = ++idx;
        p = son[p][u];
    }
    cnt[p]++;
}

int query() {
    int p = 0;
    for (int i = 0; str[i]; i++) {
        int u = str[i] - 'a';
        if (!son[p][u]) return 0;
        p = son[p][u];
    }
    return cnt[p];
}

int main() {
    cin >> n;
    while(n --) {
        // 这是一个技巧
        // 读入%s来接收%c 可以去除空格
        char op[2];
        cin >> op >> str;
        if (op[0] == 'I') {
            insert();
        }else {
            cout << query() << endl;
        }
    }
    return 0;
}

9.并查集

什么是并查集呢?

[请自行百度](算法学习笔记(1) : 并查集 - 知乎 (zhihu.com))

并查集最最重要的就是find

// find函数为返回x所在的集合的顶部
// 同时也使用了路径压缩
// 定义每个集合的root的p[root]为其下标
int find(int x) {
    if (x != p[x]) p[x] = find(p[x]);
    return p[x];
}

01!](…/…/01.png)

  1. 如何将一个集合和另外一个集合变成一个集合
// 假设要合并 a b 所在集合
p[find(a)] = find(b);
// 就是将 a 的root 的头改为 b 的root的头

Snipaste_2023-05-12_17-07-46!](…/…/Snipaste_2023-05-12_17-07-46.png)

输入:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出:
Yes
No
Yes

参考代码:

#include<bits/stdc++.h>
using namespace std;

const int N = 1e5 + 10;
int q[N];
int n, m;

int find(int x) {
    if (x != q[x]) q[x] = find(q[x]);
    return q[x];
}

int main() {
    cin >> n >> m;
    // 题目说一开始这n个点在不同集合
	// 全部为i = q[i]即可, 说明他们在不同集合
    // 这里我之前以为他们全部在为0 的集合里, 后面发现不对
    // 因为我没有看初始化
    for (int i = 0; i < n; i++) {
        q[i] = i;
    }
    while(m --) {
        char op[2];
        int a, b;
        cin >> op >> a >> b;
        if (op[0] == 'M') {
            q[find(a)] = find(b);
        }else {
            if (find(a) == find(b)) {
                puts("Yes");
            }else {
                puts("No");
            }
        }
    }
    return 0;
}

同时, 还可以用并查集维护一些元素

比如: 集合里面元素的个数, 集合元素到root的距离等?

10.堆

主要介绍手写堆而不是priority_queue() (这是c++的)

手搓堆最最最最最…最主要的就是down() and up() 一个是上浮, 一个是下沉

堆 一定是完全二叉树

哪什么是完全二叉树呢?

请自行百度

还有一个概念就是满二叉树

[请自行百度](百度一下,你就知道 (baidu.com))

// 授之以鱼不如授之以渔, 请去百度

这里是以小根堆举例:

最小小根堆):根结点的键值是所有堆结点键值中最小者。

就是:他的左子树和右子树均大于等于自己本身

你可以猜猜右子树是啥?

// 用 heap[]数组来存放堆
// cnt 表示已经用了多少的元素
// u 是下标
void down(int u) {
    // t 是他本身, 左子树root, 右子树root的最小值的下标
    int t = u; 
    // cnt 是从1开始 如果是0    2 * 0 = 0 那么根的左子树和root在同一点所以从一开始
    // 左子树下标为 2u
    // 有子树下标为 2u + 1 在完全二叉树应该有讲
    if (u * 2 <= cnt && heap[u * 2] < heap[u]) t = u * 2;
    if (u * 2 + 1 <= cnt && heap[u * 2 + 1] < heap[u]) t = u * 2 + 1;
    // if u != t 说明左右子树其中有一个更小
    if (u != t) {
        swap(heap[u], heap[t]);
        // 在调用t 开始的down
        down(t);
    }
}

// 不多做解释
// 比较简单
void up(int u) {
    while (u >> 1 && heap[u >> 1] > heap[u]) {
        swap(heap[u >> 1], heap[u]);
        u >>= 1;
    }
}

Snipaste_2023-05-11_17-59-16!](…/…/Snipaste_2023-05-11_17-59-16.png)

// 创建堆
cin >> n;
for (int i = 0; i < n; i++) {
    cin >> heap[i];
}
// 这里就可以创建堆
// 时间复杂度O(n)
// n >> 1 就是倒数第二层 从倒数第二层开始去依次down() 
// down 可以将该子树变成最小堆
for (int i = (n >> 1); i; i--) {
    down(i);
}

Snipaste_2023-05-12_18-16-25!](…/…/Snipaste_2023-05-12_18-16-25.png)

参考代码:

#include<bits/stdc++.h>
using namespace std;

const int N = 1e5 + 10;
int n, m;
// sz 就是cnt
// 本来想写size 你可以试一试为啥不行
int h[N], sz;

void down(int u) {
    int t = u;
    if (u * 2 <= sz && h[u * 2] < h[t]) t = u * 2;
    if (u * 2 + 1 <= sz && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
    if (u != t) {
        swap(h[u], h[t]);
        down(t);
    }
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n ; i++) {
        scanf("%d", h + i);
    }
    sz = n;
    // 创建堆
    for (int i = n / 2; i; i--) {
        down(i);
    }
    while(m --) {
        printf("%d ", h[1]);
        h[1] = h[sz];
        sz--;
        down(1);
    }
    return 0;
}

ap[u >> 1], heap[u]);
u >>= 1;
}
}


[外链图片转存中...(img-6bFRW9zM-1683897425195)]!](../../Snipaste_2023-05-11_17-59-16.png)

```c
// 创建堆
cin >> n;
for (int i = 0; i < n; i++) {
    cin >> heap[i];
}
// 这里就可以创建堆
// 时间复杂度O(n)
// n >> 1 就是倒数第二层 从倒数第二层开始去依次down() 
// down 可以将该子树变成最小堆
for (int i = (n >> 1); i; i--) {
    down(i);
}

[外链图片转存中…(img-i6g0tKtF-1683897425195)]!](…/…/Snipaste_2023-05-12_18-16-25.png)

参考代码:

#include<bits/stdc++.h>
using namespace std;

const int N = 1e5 + 10;
int n, m;
// sz 就是cnt
// 本来想写size 你可以试一试为啥不行
int h[N], sz;

void down(int u) {
    int t = u;
    if (u * 2 <= sz && h[u * 2] < h[t]) t = u * 2;
    if (u * 2 + 1 <= sz && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
    if (u != t) {
        swap(h[u], h[t]);
        down(t);
    }
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n ; i++) {
        scanf("%d", h + i);
    }
    sz = n;
    // 创建堆
    for (int i = n / 2; i; i--) {
        down(i);
    }
    while(m --) {
        printf("%d ", h[1]);
        h[1] = h[sz];
        sz--;
        down(1);
    }
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值