算法基础课 --- 数据结构

目录

正文

单链表

在算法题目中,我们需要快速的得到答案,因此,单链表在实现的过程中使用数组实现,记录下一个位置。

题目:

实现一个单链表,链表初始为空,支持三种操作:

  1. 向链表头插入一个数;
  2. 删除第 k 个插入的数后面的数;
  3. 在第 k 个插入的数后插入一个数。

现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。

注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 11 个插入的数,第 22 个插入的数,…第 n 个插入的数。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:

  1. H x,表示向链表头插入一个数 x。
  2. D k,表示删除第 k 个插入的数后面的数(当 k 为 0 时,表示删除头结点)。
  3. I k x,表示在第 k 个插入的数后面插入一个数 x(此操作中 k 均大于 0)。
输出格式

共一行,将整个链表从头到尾输出。

数据范围

1 ≤ M ≤ 100000
所有操作保证合法。

输入样例:
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
笔记

操作 D 0 理解错了,卡了一会儿
我误解为了重置单链表,但实际上只是删除头结点指向的元素

#include <iostream>

using namespace std;

const int N = 100010;

int head, e[N], ne[N], idx;

void init()
{
    head = -1;
    idx = 0;
}

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

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

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

int main()
{
    int m;
    scanf("%d", &m);
    
    init();
    
    while (m -- )
    {
        char op[2];
        scanf("%s", op);
        if (*op == 'H')
        {
            int x;
            scanf("%d", &x);
            add_head(x);
        }
        else if (*op == 'D')
        {
            int k;
            scanf("%d", &k);
            if (k) delete_k(k - 1);
            else head = ne[head];
        }
        else
        {
            int k, x;
            scanf("%d%d", &k, &x);
            insert_k(k - 1, x);
        }
    }
    
    int p = head;
    while (p != -1)
    {
        printf("%d ", e[p]);
        p = ne[p];
    }
    
    return 0;
}

双链表

实现一个双链表,双链表初始为空,支持 55 种操作:

  1. 在最左侧插入一个数;
  2. 在最右侧插入一个数;
  3. 将第 k 个插入的数删除;
  4. 在第 k 个插入的数左侧插入一个数;
  5. 在第 k 个插入的数右侧插入一个数

现在要对该链表进行 M 次操作,进行完所有操作后,从左到右输出整个链表。

注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 11 个插入的数,第 22 个插入的数,…第 n 个插入的数。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:

  1. L x,表示在链表的最左端插入数 x。
  2. R x,表示在链表的最右端插入数 x。
  3. D k,表示将第 k 个插入的数删除。
  4. IL k x,表示在第 k 个插入的数左侧插入一个数。
  5. IR k x,表示在第 k 个插入的数右侧插入一个数。
输出格式

共一行,将整个链表从左到右输出。

数据范围

1 ≤ M ≤ 100000
所有操作保证合法。

输入样例:
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
笔记

刚开始操作符用的是 char
改为用 string 读,最后解决了读入的问题

#include <iostream>

using namespace std;

const int N = 100010;

int e[N], l[N], r[N], idx;

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

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

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

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

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

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

int main()
{
    int m;
    scanf("%d", &m);
    
    init();
    
    while (m -- )
    {
        string op;
        cin >> op;
        
        if (op == "L")
        {
            int x;
            scanf("%d", &x);
            add_left(x);
        }
        else if (op == "R")
        {
            int x;
            scanf("%d", &x);
            add_right(x);
        }
        else if (op == "D")
        {
            int k;
            scanf("%d", &k);
            delete_k(k + 1);
        }
        else if (op == "IL")
        {
            int k, x;
            scanf("%d%d", &k, &x);
            insert_left(k + 1, x);
        }
        else
        {
            int k, x;
            scanf("%d%d", &k, &x);
            insert_right(k + 1, x);
        }
    }
    
    int p = r[0];
    while (p != 1)
    {
        printf("%d ", e[p]);
        p = r[p];
    }
}

看了题解以后,发现往 k 的左边插入,其实就是往 k 的左结点的右边插入,可以省去很多定义的操作

#include <iostream>

using namespace std;

const int N = 100010;

int e[N], l[N], r[N], idx;

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

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

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

int main()
{
    int m;
    scanf("%d", &m);
    
    init();
    
    while (m -- )
    {
        string op;
        cin >> op;
        
        if (op == "L")
        {
            int x;
            scanf("%d", &x);
            insert(0, x);
        }
        else if (op == "R")
        {
            int x;
            scanf("%d", &x);
            insert(l[1], x);
        }
        else if (op == "D")
        {
            int k;
            scanf("%d", &k);
            delete_k(k + 1);
        }
        else if (op == "IL")
        {
            int k, x;
            scanf("%d%d", &k, &x);
            insert(l[k + 1], x);
        }
        else
        {
            int k, x;
            scanf("%d%d", &k, &x);
            insert(k + 1, x);
        }
    }
    
    int p = r[0];
    while (p != 1)
    {
        printf("%d ", e[p]);
        p = r[p];
    }
}

实现一个栈,栈初始为空,支持四种操作:

  1. push x – 向栈顶插入一个数 x;
  2. pop – 从栈顶弹出一个数;
  3. empty – 判断栈是否为空;
  4. query – 查询栈顶元素。

现在要对栈进行 M 个操作,其中的每个操作 33 和操作 44 都要输出相应的结果。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令为 push xpopemptyquery 中的一种。

输出格式

对于每个 emptyquery 操作都要输出一个查询结果,每个结果占一行。

其中,empty 操作的查询结果为 YESNOquery 操作的查询结果为一个整数,表示栈顶元素的值。

数据范围

1 ≤ M ≤ 100000,
1 ≤ x ≤ 10^9
所有操作保证合法。

输入样例:
10
push 5
query
push 6
pop
query
pop
empty
push 4
query
empty
输出样例:
5
5
YES
4
NO
笔记
#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int stk[N], tt = -1;

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

void pop()
{
    tt -- ;
}

void query()
{
    printf("%d\n", stk[tt]);
}

void empty()
{
    if (tt == -1) printf("YES\n");
    else printf("NO\n");
}

int main()
{
    int m;
    scanf("%d", &m);
    
    while (m -- )
    {
        string op;
        cin >> op;
        if (op == "push")
        {
            int x;
            scanf("%d", &x);
            push(x);
        }
        else if (op == "pop") pop();
        else if (op == "query") query();
        else empty();
    }
    
    return 0;
}

队列

实现一个队列,队列初始为空,支持四种操作:

  1. push x – 向队尾插入一个数 x;
  2. pop – 从队头弹出一个数;
  3. empty – 判断队列是否为空;
  4. query – 查询队头元素。

现在要对队列进行 M 个操作,其中的每个操作 33 和操作 44 都要输出相应的结果。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令为 push xpopemptyquery 中的一种。

输出格式

对于每个 emptyquery 操作都要输出一个查询结果,每个结果占一行。

其中,empty 操作的查询结果为 YESNOquery 操作的查询结果为一个整数,表示队头元素的值。

数据范围

1 ≤ M ≤ 100000,
1 ≤ x ≤ 10^9
所有操作保证合法。

输入样例:
10
push 6
empty
query
pop
empty
push 3
push 4
pop
query
push 6
输出样例:
NO
6
YES
4
笔记

三目运算符小心越界,最好打个括号

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int m;
int que[N], tt, hh;

int main()
{
    cin >> m;
    
    while (m -- )
    {
        string op;
        cin >> op;
        
        if (op == "push")
        {
            int x;
            cin >> x;
            que[tt ++ ] = x;
        }
        else if (op == "pop") hh ++ ;
        else if (op == "query") cout << que[hh] << endl;
        else cout << ((hh == tt)? "YES" : "NO") << endl;
    }
    
    return 0;
}

KMP

给定一个字符串 S,以及一个模式串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。

模式串 P 在字符串 S 中多次作为子串出现。

求出模式串 P 在字符串 S 中所有出现的位置的起始下标。

输入格式

第一行输入整数 N,表示字符串 P 的长度。

第二行输入字符串 P。

第三行输入整数 M,表示字符串 S 的长度。

第四行输入字符串 S。

输出格式

共一行,输出所有出现位置的起始下标(下标从 0 开始计数),整数之间用空格隔开。

数据范围

1 ≤ N ≤ 10^5
1 ≤ M ≤ 10^6

输入样例:
3
aba
5
ababa
输出样例:
0 2
笔记

ne[N] 表示的是每次匹配失败,最少应该把子串往前移动几位

#include <iostream>

using namespace std;

const int N = 1e5 + 10, M = 1e6 + 10;

int n, m, ne[N];
char p[N], s[M];

int main()
{
    cin >> n >> p + 1 >> m >> s + 1;
    
    for (int i = 2, j = 0; i <= n; i ++ )
    {
        while (j && p[i] != p[j + 1]) j = ne[j];
        if (p[i] == p[j + 1]) j ++ ;
        ne[i] = j;
    }
    
    for (int i = 1, j = 0; i <= m; i ++ )
    {
        while (j && s[i] != p[j + 1]) j = ne[j];
        if (s[i] == p[j + 1]) j ++ ;
        if (j == n) printf("%d ", i - n), j = ne[j];
    }
    
    return 0;
}

Trie

维护一个字符串集合,支持两种操作:

  1. I x 向集合中插入一个字符串 x;
  2. Q x 询问一个字符串在集合中出现了多少次。

共有 N 个操作,所有输入的字符串总长度不超过 105105,字符串仅包含小写英文字母。

输入格式

第一行包含整数 N,表示操作数。

接下来 N 行,每行包含一个操作指令,指令为 I xQ x 中的一种。

输出格式

对于每个询问指令 Q x,都要输出一个整数作为结果,表示 x 在集合中出现的次数。

每个结果占一行。

数据范围

1 ≤ N ≤ 2∗10^4

输入样例:
5
I abc
Q abc
Q ab
I ab
Q ab
输出样例:
1
0
1
笔记

son[N][26] 中存储的是子结点的编号

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int son[N][26], cnt[N], idx;
char str[N];

void insert(char str[])
{
    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(char str[])
{
    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()
{
    int n;
    scanf("%d", &n);
    
    while (n -- )
    {
        char op[2];
        scanf("%s%s", op, str);
        
        if (op[0] == 'I') insert(str);
        else printf("%d\n", query(str));
    }
    
    return 0;
}

并查集

并查集中的操作都是近似 O ( 1 ) O(1) O(1) 的。

用到了路径优化,做法是每次合并时将结点指向根节点。

在这里插入图片描述

题目:

一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。

现在要进行 m 个操作,操作共有两种:

  1. M a b,将编号为 a 和 b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
  2. Q a b,询问编号为 a 和 b 的两个数是否在同一个集合中;
输入格式

第一行输入整数 n 和 m。

接下来 m 行,每行包含一个操作指令,指令为 M a bQ a b 中的一种。

输出格式

对于每个询问指令 Q a b,都要输出一个结果,如果 a 和 b 在同一集合内,则输出 Yes,否则输出 No

每个结果占一行。

数据范围

1 ≤ n, m ≤ 10^5

输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
Yes
No
Yes
笔记

题解中的 find(x) 很精妙
返回的是祖先结点
同时压缩路径,递归的让路径上的每个节点都指向祖先结点

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int p[N];

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

int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    
    for (int i = 0; i < n; i ++ ) p[i] = i;
    
    while (m -- )
    {
        char op[2];
        int a, b;
        scanf("%s%d%d", op, &a, &b);
        if (op[0] == 'M') p[find(a)] = find(b);
        else
        {
            if (find(a) == find(b)) puts("Yes");
            else puts("No");
        }
    }
    
    return 0;
}

输入一个长度为 n 的整数数列,从小到大输出前 m 小的数。

输入格式

第一行包含整数 n 和 m。

第二行包含 n 个整数,表示整数数列。

输出格式

共一行,包含 m 个整数,表示整数数列中前 m 小的数。

数据范围

1 ≤ m ≤ n ≤ 10^5,
1 ≤ 数列中元素 ≤ 10^9

输入样例:
5 3
4 5 1 3 2
输出样例:
1 2 3
笔记

四则运算的优先级高于位运算
建堆时只需要对前 n / 2 n/2 n/2 的数据进行 down 操作,且该操作的时间复杂度不是 O ( n l o g n ) O(nlogn) O(nlogn) 而是 O ( n ) O(n) O(n)(具有严格数学证明)

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int hp[N], idx;

void down(int x)
{
    int t = x;
    if ((x << 1) <= idx && hp[x << 1] < hp[t]) t = x << 1;
    if ((x << 1 | 1) <= idx && hp[x << 1 | 1] < hp[t]) t = x << 1 | 1;
    if (t != x)
    {
        swap(hp[t], hp[x]);
        down(t);
    }
}

int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    
    for (int i = 1; i <= n; i ++ ) scanf("%d", &hp[i]);
    idx = n;
    
    for (int i = n / 2; i; i -- ) down(i);
    
    while (m -- )
    {
        printf("%d ", hp[1]);
        hp[1] = hp[idx];
        idx -- ;
        down(1);
    }
    
    return 0;
}

哈希表

维护一个集合,支持如下几种操作:

  1. I x,插入一个整数 x;
  2. Q x,询问整数 x 是否在集合中出现过;

现在要进行 N 次操作,对于每个询问操作输出对应的结果。

输入格式

第一行包含整数 N,表示操作数量。

接下来 N 行,每行包含一个操作指令,操作指令为 I xQ x 中的一种。

输出格式

对于每个询问指令 Q x,输出一个询问结果,如果 x 在集合中出现过,则输出 Yes,否则输出 No

每个结果占一行。

数据范围

1 ≤ N ≤ 10^5
−10^9 ≤ x ≤ 10^9

输入样例:
5
I 1
I 2
I 3
Q 2
Q 5
输出样例:
Yes
No
笔记

开放寻址法
如果当前位置上有人,则找下一个位置,直到找到空的位置(解决冲突的某一种办法)

#include <iostream>
#include <cstring>

using namespace std;

const int N = 200003, null = 0x3f3f3f3f;

int h[N];

int find(int x)
{
    int k = (x % N + N) % N;
    while (h[k] != null && h[k] != x) 
    {
        k ++ ;
        if (k == N) k = 0;
    }
    return k;
}

int main()
{
    int n;
    scanf("%d", &n);
    
    memset(h, 0x3f, sizeof h);
    
    while (n -- )
    {
        char op[2];
        int x;
        scanf("%s%d", op, &x);
        int k = find(x);
        if (*op == 'I') h[k] = x;
        else
        {
            if (h[k] != null) puts("Yes");
            else puts("No");
        }
    }
    
    return 0;
}

链地址法:
h[N] 存储的是链表的头结点

#include <iostream>
#include <cstring>

using namespace std;

const int N = 100003;

int h[N], e[N], ne[N], idx;

void insert(int x)
{
    int k = (x % N + N) % N;
    e[idx] = x; 
    ne[idx] = h[k];
    h[k] = idx ++ ;
}

bool query(int x)
{
    int k = (x % N + N) % N;
    for (int i = h[k]; i != -1; i = ne[i])
        if (x == e[i]) return true;
    return false;
}

int main()
{
    int n;
    scanf("%d", &n);
    
    memset(h, -1, sizeof h);
    
    while (n -- )
    {
        char op[2];
        int x;
        scanf("%s%d", op, &x);
        if (*op == 'I') insert(x);
        else
        {
            if (query(x)) puts("Yes");
            else puts("No");
        }
    }
    
    return 0;
}
of h);
    
    while (n -- )
    {
        char op[2];
        int x;
        scanf("%s%d", op, &x);
        int k = find(x);
        if (*op == 'I') h[k] = x;
        else
        {
            if (h[k] != null) puts("Yes");
            else puts("No");
        }
    }
    
    return 0;
}

链地址法:
h[N] 存储的是链表的头结点

#include <iostream>
#include <cstring>

using namespace std;

const int N = 100003;

int h[N], e[N], ne[N], idx;

void insert(int x)
{
    int k = (x % N + N) % N;
    e[idx] = x; 
    ne[idx] = h[k];
    h[k] = idx ++ ;
}

bool query(int x)
{
    int k = (x % N + N) % N;
    for (int i = h[k]; i != -1; i = ne[i])
        if (x == e[i]) return true;
    return false;
}

int main()
{
    int n;
    scanf("%d", &n);
    
    memset(h, -1, sizeof h);
    
    while (n -- )
    {
        char op[2];
        int x;
        scanf("%s%d", op, &x);
        if (*op == 'I') insert(x);
        else
        {
            if (query(x)) puts("Yes");
            else puts("No");
        }
    }
    
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值