二、数据结构——单链表,双链表,栈,队列,单调栈,单调队列,KMP,Trie,并查集,堆,哈希表等内容。

news2024/9/25 23:20:10

        对于链表来说,由于new操作时间太长,因此,算法题中一般使用静态链表。

1.单链表

        采用数组实现单链表,可以直接开两个数据,一个数组存放数值,另外一个数据存放下一个元素(指针)。

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

向链表头插入一个数;删除第 k 个插入的数后面的数;
在第 k 个插入的数后插入一个数。
现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。

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

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

 const int N = 100010;
int c = 10;
 int value[N];
 int ne[N];
 int head,tal; // tal表示尾部元素在数组中的位置

 void init(){
    head = -1;
    tal = 0;
    fill_n(&ne[0], sizeof(ne) / sizeof(ne[0]), -1);
 }

 void insert_head(int x){
    value[tal] = x;    // 存放元素
    ne[tal] = head;    // x指向head指向的元素
    head = tal++;      // 新的head指向x
 }

 void insert_k(int k,int x){ // 在数组第k个位置后插入元素,注意不是链表
     value[tal] = x;         // 存放节点元素
     ne[tal] = ne[k];        // value[k]指向x
     ne[k] = tal++;          // x指向原value[k]下一个元素
 }

 void remove_k(int k){ // 此处没有释放空间,算法题一般不必考虑释放空间
    int tmp = ne[k];   // 保留删除前第k个元素的下一个位置
    ne[k] = ne[ne[k]]; // 第k个元素指向下一个元素的下个元素
    ne[tmp] = -1;     // 删除元素的下一个元素位置指向-1
 }

 int main(){
     int m;
     cin >> m;  // m次操作
     int k,x;   // k为位置,x为元素
     char O;    // 操作符
     init();    // 初始化
     while(m--){
        cin >> O;
        if(O == 'H'){cin >> x; insert_head(x);}  // 头差一个元素
        else if(O == 'D')   // 删除一个元素
        {
            cin >> k; 
            if(k == 0) head = ne[head];
            else remove_k(k-1);}
        else if(O == 'I'){cin >> k >> x; insert_k(k-1,x);} // 第k个位置插入一个元素
     }
    
     for(int i = head;i!=-1;i=ne[i])
        printf("%d ",value[i]);
    return 0;
 }

 2.双链表

双链表使用两个数组分别表示前驱和后继,并用1个数据存储值。

插入操作:头插入,需要改头指针

中间插入,需要防止断链 

尾部插入需注意改尾指针。

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

在最左侧插入一个数;
在最右侧插入一个数;
将第 k 个插入的数删除;
在第 k 个插入的数左侧插入一个数;
在第 k 个插入的数右侧插入一个数
现在要对该链表进行 M次操作,进行完所有操作后,从左到右输出整个链表。

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

代码实现:在代码实现中,左侧插入考虑了节点为第一个节点,右侧插入考虑了最后一个节点。同时,为了在O(1)时间复杂度完成尾插,设置了表尾指针。

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

const int N = 100010;
int value[N];
int l[N],r[N];
int head,tal,idx; // tal表示尾部元素在数组中的位置

void init(){
    idx = 0;
    fill_n(&l[0], sizeof(l) / sizeof(l[0]), -1); // 数组初始化为-1
    fill_n(&r[0], sizeof(r) / sizeof(r[0]), -1);
    head = -1;
    tal = -1;
 }
 
void insert_head(int x){
    value[idx] = x;    // 存放元素
    r[idx] = head;  // x指向第一个元素
    if(tal == -1) tal = idx; // 当前表为空,尾指针指向x
    else l[head] = idx;    // 表不为空,第一个元素的前驱指向x
    head = idx++;      // 新的head指向x
 }
 
 void insert_tal(int x){
    value[idx] = x;    // 存放元素
    l[idx] = tal;      // x的左指针指向最后一个元素
    if(head == -1) head = idx; // 表位空,头指针指向x
    else r[tal] = idx;  // 表不为空,最后一个元素的后继指向x
    tal = idx++;      // 新的tal指向x
 }

 
void insert_Rk(int k,int x){ // 在数组第k个位置后插入元素,注意不是链表
     value[idx] = x;         // 存放节点元素
     if(r[k] == -1) {l[idx] = tal; r[idx] = -1; r[tal] = idx;tal = idx++;} // 当前节点为最后一个节点
     else{
        l[idx] = k;        
        r[idx] = r[k];
        l[r[k]] = idx;
        r[k] = idx++;  
     }
 }
 
 void insert_Lk(int k,int x){ // 在数组第k个位置前插入元素,注意不是链表
     value[idx] = x;         // 存放节点元素
     if(l[k] == -1) {r[idx] = head; l[idx] = -1; l[head] = idx;head = idx++;} // 当前节点在第一个节点
     else{
        l[idx] = l[k];
        r[idx] = k;
        r[l[k]] = idx;
        l[k] = idx++;   
     }
 }

void remove_k(int k){ // 此处没有释放空间,算法题一般不必考虑释放空间
    if(head == k) head = r[k];
    if(tal == k) tal = l[k];
    l[r[k]] = l[k];
    r[l[k]] = r[k];
 }

int main(){
     int m;
     cin >> m;  // m次操作
     int k,x;   // k为位置,x为元素
     string O;    // 操作符
     init();    // 初始化
     while(m--){
        cin >> O;
        if(O == "L"){cin >> x; insert_head(x);}  // 头差一个元素
        else if(O == "R"){cin >> x; 
        insert_tal(x);
        } 
        else if(O == "D")   // 删除一个元素
        {
            cin >> k;
            remove_k(k-1);
        }
        else if(O == "IL"){
            cin >> k >> x;
            insert_Lk(k-1,x);
        } // 第k个位置插入一个元素
        else{cin >> k >> x; insert_Rk(k-1,x);}
     }

     for(int i = head;i!=-1;i=r[i])
        printf("%d ",value[i]);
    return 0;
 }

3.栈

        栈是先进后出的数据结构,top初始时指向0,代表指向下一个空位置,

  • 入栈时,先入元素,top++,
  • 出栈时,top--;再出元素。
  • top==0判空,top==n-1判满 
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 100010;
int sstack[N],top;

void init(){
    top = 0; // top 指向下一个空位置
}

void s_empty(){  // top指向0栈空,否则不空
    if(top) printf("NO\n");
    else printf("YES\n");
}

void push(int x){  // 入栈,元素先入栈,top++
    sstack[top++] = x;
}

int pop(){   // 出栈:top先减减,再出栈
    return sstack[--top];
}

int query(){  // 查询栈顶元素
    int id = top-1;
    return sstack[id];
}

int main(){
    int m;
    cin >> m;
    string op;
    int x;
    init();
    while(m--){
        cin >> op;
        if(op=="push") {cin >> x; push(x);}
        else if(op=="pop") int r = pop();
        else if(op == "empty") s_empty();
        else printf("%d\n",query());
    }
}

应用:表达式求值

算法思想:

使用一个数栈,一个符号栈,从左向右扫描表达式,若:

  • 符号栈为空,直接入栈 
  • 栈顶元素为(,直接入栈
  • 当前符号优先级大于栈顶元素,直接入栈
  • 否则从符号栈中弹出一个符号,并从数栈中弹出2个数字进行运算,直到满足相应入栈条件
#include <iostream>
#include <algorithm>
#include <stack>
#include <unordered_map>
#include <cstring>
using namespace std;

const int N = 100010;
stack<char> op;
stack<int> number;

void eval(){    // 实现运算
    int b = number.top(); // 栈是后进先出,第一个先出的元素是第二个操作数,第二个出的元素是第一个操作数
    number.pop();
    int a = number.top();
    number.pop();
    char c = op.top();    // 符号
    op.pop();
    if(c=='+') number.push(a+b);
    else if(c=='-') number.push(a-b);
    else if(c=='*') number.push(a*b);
    else number.push(a/b);
}

int main(){
    unordered_map <char,int> pr = {{'+',1},{'-',1},{'*',2},{'/',2}};
    string A; // 表达式
    cin >> A;
    int n,j;
    for(int i=0;i<A.size();i++){
        n = 0;
       if(isdigit(A[i])){  // 处理数字
            j = i;
            while(isdigit(A[j])) // 数字可能有多位
                n = 10 * n + (A[j++]-'0'); // 数字是字符形式,转变为整数
            number.push(n);
            i = j-1;
       }
        else if(op.empty()) op.push(A[i]); // 符号栈为空,直接入栈
        else{ 
            if(A[i]=='(') op.push(A[i]); // 栈顶元素为(,直接入栈
            else if(A[i]==')') {        // 当前符号为),从符号栈中弹出一个符号,并从数栈中弹出2个数字进行运算,直到遇到(
                while(op.top()!='(')
                    eval();
                op.pop();  // 弹出(
                }
            else if(pr[A[i]]>pr[op.top()]) op.push(A[i]); // 当前符号优先级大于栈顶元素直接入栈
            else{
                while(!op.empty() && pr[A[i]]<=pr[op.top()] && op.top()!='(') // 否则从符号栈中弹出一个符号,并从数栈中弹出2个数字进行运算,直到当前符号优先级大于栈顶元素
                    eval();
                op.push(A[i]); // 当前符号入栈
                }
    }}
    while(!op.empty()) eval(); // 计算剩余部分
    printf("%d ",number.top());
    return 0;
}

4.队列

为了解决假溢出问题,现在队列一般采用循环队列,舍弃一个存储单元

  • 初始化:r=f=0;即r指向队尾下一个空位置,f指向队首元素
  • 入队,先入元素,r=(r+1)%n
  •  出队,先出元素,f=(f+1)%n;
  • 判空:r==f;
  • 判满:(r+1)%n==f;
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 100010;
int Queue[N],r,f;

void init(){
    r = f = 0; // r指向下一个空位置,f指向队首元素
}

bool q_empty(){
    return r == f;
}

bool full(){
    return (r+1) % N == f;
}

void en_queue(int x){
    Queue[r] = x;
    r = (r+1)%N;
}

void de_queue(){
    f = (f+1)%N;
}

int query(){
    return Queue[f];
}

int main(){
    int m;
    cin >> m;
    string op;
    int x;
    init();
    while(m--){
        cin >> op;
        if(op=="push") {cin >> x; en_queue(x);}
        else if(op=="pop") printf("%d\n",de_queue());
        else if(op == "empty") {
            if(q_empty()) printf("YES");
            else printf("NO");
        }
        else printf("%d\n",query());
    }
}

5.单调栈

        单调栈的应用场景为给定一个长度为 N的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。

        算法分析:如果采用暴力解,则需要2重循环,时间复杂度为O(n^2)。降低时间复杂度可以采用单调栈的方式,即对于元素a[i],若栈中元素大于a[i],则退栈,这样,每个元素进战出栈1次,时间复杂度为O(n).

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

const int N = 100010;
int s[N];   // 栈
int top =0; // 栈顶指针

int main(){
    int n; // 整数序列长度
    int x; // 当前数x
    scanf("%d",&n);
    for(int i=0;i<n;i++){
        scanf("%d",&x);
        if(top == 0) {s[top++] =x;printf("-1 ");} // 当前栈为空,输出结果为-1
        else{
            while(s[top-1]>=x) top--;   // 如果栈顶元素大于当前元素,元素出栈
            if(top == 0) printf("-1 "); // 出栈后栈顶为空,输出-1
            else printf("%d ",s[top-1]); // 如果栈不为空,栈顶元素即为所求
            s[top++] = x;
        }
    }
    
}

6.单调队列

        单调队列的应用是找滑动窗口的最大值和最小值,这里的单调队列可以看做一个两头出一头进的双端队列,首先,使用队列实现滑动窗口,然后在队尾一侧使用栈构建单调队列,从而找最大值与最小值。

#include <iostream>
#include <algorithm>
using namespace std;
const int N= 1e6+10;
int q[N],a[N];   // q为双端队列,存储数组下标,a为数组
int r,f;        // 队列首尾指针


int main(){
    int n,k; // n为整数序列长度,k为窗口大小
    scanf("%d%d",&n,&k); // n为序列长度,k为滑动窗口大小
    for(int i=0;i<n;i++) scanf("%d",&a[i]);
    r=f=0;  // 初始化,f指向队首元素,r指向队尾元素的下一个空位置
    for(int i=0;i<n;i++){ // 最小值
    if(f<r && q[f]<i-k+1) f=(f+1)%N;  // 队列不空,且队首元素不在滑动窗口,队首元素出队
    while(a[i]<= a[q[r-1]] && f<r) r=(r-1)%N; // 队列不空且队尾元素大于当前元素,队尾元素出队
    q[r]= i;            // 当前元素入队
    r= (r+1)%N;         // 队尾指针++
    if(i>=k-1) printf("%d ",a[q[f]]); // i=k-1为第一个窗口,开始输出元素
        }
        printf("\n");
    r=f=0;
    for(int i=0;i<n;i++){ // 最大值
    if(f<r && q[f]<i-k+1) f=(f+1)%N;
    while(a[i]>= a[q[r-1]] && f<r) r=(r-1)%N;
    q[r]= i;
    r= (r+1)%N;
    if(i>=k-1) printf("%d ",a[q[f]]);
        }
        return 0;
}

7.kmp算法

        kmp算法是利用子串的性质降低暴力解的时间复杂度。利用子串的最长公共前后缀使每次前缀的位置移动到后缀的位置。

 

最后next数组为最长公共前后缀的长度+1 

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

const int N = 1e5+10;
const int M = 1e6+10;
char p[N],s[M];
int ne[N];

int main(){
    int n,m;
    cin >> n;
    cin >> p+1;
    cin >> m;
    cin >> s+1;
    int i,j;
    for(i=2,j=0;i<=n;i++){ // 构建next数组
        while(j && p[i]!=p[j+1]) j = ne[j]; // 当前不匹配,子串移动,while循环主要用于解决多个子串匹配的情形
        if(p[i]==p[j+1]) j++;  // 当前匹配,继续匹配
        ne[i] = j;        // 构建当前元素的next值
    }
    //for(i=0;i<n;i++) printf("%d ",ne[i]);
    //printf("\n");
    for(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); // 输出下标从0开始
            j = ne[j];  // 继续匹配下一个子串
        }
    }
    return 0;
}

8.trile树

         trile最重要的作用为高效的存储字符串,并能够很好的统计字符串出现的次数。其结构如下:

实例:

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

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

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

#include <iostream>

using namespace std;

const int N = 1e5+10;
int son[N][26]; // 存储子节点行号
int cnt[N];     // 字符串出现次数,注意,这是以字符串最后字符的创建为id
int idx = 0;    // 创建节点的id
char str[N];    // 创建节点

void insert_t(char str[]){
    int p = 0;
    int u;
    for(int i=0;str[i];i++){
        u = str[i] - 'a'; // 字符索引
        if(!son[p][u]) son[p][u] = ++idx; // 无节点,创建节点,值为创建时的id,可通过此id索引字符串,子节点也为对应的行
        p = son[p][u];   // 有子节点,进入子节点
    }
    cnt[p]++;  // 字符串出现次数+1
}

int query(char str[]){
    int p = 0;
    int u;
    for(int i=0;str[i];i++){
        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_t(str);
        else printf("%d\n",query(str));
    }
    return 0;
}

应用:最大异或对

在给定的 N 个整数 A1,A2 ......An 中选出两个进行 or (异或)运算,得到的结果最大是多少?

        算法思想:如果采用暴力解,则需要2重循环,时间复杂度为O(N^2)。分析异或运算过程,从最高位开始, 相同结果为0,不同结果为1,因此,我们可以以各位数字建立一颗二叉树,从而简化查找长度,查找某个数的最大异或的数时,从高位开始,尽量进入具有不同数的分支中

#include <iostream>

using namespace std;

const int N = 1e7+10;
int son[N][2];
int a[N];
int idx = 0;

void insert_t(int x){
    int p = 0;
    int s;
    for(int i=30;i>=0;i--){ // 创建trile树
        s = x >> i & 1; // 右移i位或1,即判断第i位是0还是1
        if(!son[p][s]) son[p][s] = ++idx; // 以创建节点的id作为子节点索引
        p = son[p][s]; // 进入子节点
    }
}

int query(int x){
    int p = 0;
    int s;
    int res = 0;
    for(int i=30;i>=0;i--){
        s = x >> i & 1; // 右移i位或1,即判断第i位是0还是1
        if(son[p][!s]) {res += 1 << i;p = son[p][!s];} // 异或运算,该位不同结果最大,进入该位不同的分支
        else p = son[p][s]; // 所有数该位相同,进入子树
    }
    return res;
}

int main(){
    int n;
    scanf("%d",&n);
    for(int i=0;i<n;i++){
        scanf("%d",&a[i]);
        insert_t(a[i]);
    }
    int res = 0;
    for(int i=0;i<n;i++){ res = max(res,query(a[i]));}
    printf("%d",res);
    return 0;
}

9.并查集

作用:在近似于O(1)的时间复杂度内完成以下2种操作:
1.将两个集合合并
2. 询问两个元素是否在一个集合当中
基本原理: 每个集合用一棵树来表示。树根的编号就是整个集合的编号。每个节点存储
它的父节点,p[x]表示x的父节点
问题1:如何判断树根: if (p[x] == x)
问题2:如何求x的集合编号: while (p[x] != x) x = p[x];优化:路径压缩

问题3:如何合并两个集合: px 是x的集合编号,py是y的集合编号。p[x] = y

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

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

  1. M a b,将编号为 a和 b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
  2. Q a b,询问编号为 a和 b的两个数是否在同一个集合中;
#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]);  // 路径压缩,x的父节点指向根
    return p[x];
}

int main(){
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) p[i] = i; // 初始化,每个节点指向自己的集合
    char op[2];
    int a,b;
    while(m--){
        scanf("%s%d%d",op,&a,&b);
        if(op[0]=='M') p[find(a)] = find(b); // 合并,a集合的根指向b
        else{
            if(find(a)==find(b)) printf("Yes\n"); // 同一集合
            else printf("No\n");
        }
    }
    return 0;
}

变体: 

给定一个包含 n 个点(编号为 1~ n)的无向图,初始时图中没有边
现在要进行 m个操作,操作共有三种
1. c a b ,在点a和点6之间连 条边,a和6可能相等
2. Q1 a b ,询问点 a 和点6 是否在同一个连通块中,a 和6可能相等
3.Q2 a ,询问点 a 所在连通块中点的数量 

        分析:此处需要求连通块中点的数量,因此需要额外维护一个信息,即每个集合元素的个数(只有根节点有意义)。

#include <iostream>
using namespace std;

const int N = 1e5+10;
int p[N];   // 父节点
int s[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=1;i<=n;i++) 
    {
        p[i] = i;
        s[i] = 1;
    }
    char op[5];
    int a,b;
    while(m--){
        scanf("%s",op);
        if(op[0]=='C') 
        {   
            scanf("%d%d",&a,&b);
            if(find(a)==find(b)) continue;
            s[find(b)] += s[find(a)];  // 集合节点数目增加
            p[find(a)] = find(b);
        }
        else if(op[1]=='1'){
            scanf("%d%d",&a,&b);
            if(find(a)==find(b)) printf("Yes\n");
            else printf("No\n");
        }
        else{
            scanf("%d",&a);
            printf("%d\n",s[find(a)]);
        }
    }
    return 0;
}

食物链问题

动物王国中有三类动物 4,B,C,这三类动物的食物链构成了有趣的环形。A吃B,B吃C,C吃A.
现有个动物,以1~ N 编号每个动物都是 A,B,C中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这 N个动物所构成的食物链关系进行描述:
第一种说法是 1XY,表示X和Y是同类
第二种说法是 2 XY,表示X吃Y
此人对 N 个动物,用上述两种说法,一句接一句地说出 K 句话,这 K 句话有的是真的,有的是假的
当一句话满足下列三条之一时,这句话就是假话,否则就是真话.
1.当前的话与前面的某些真的话冲突,就是假话
2.当前的话中 X或Y比N大,就是假话:
3.当前的话表示 X 吃 X,就是假话.
你的任务是根据给定的 N 和 K 句话,输出假话的总数 

算法思想:该问题为3个种群的关系问题,可以采用并查集处理。额外维护一个距离根节点的深度信息,若d[find(x)]-d[find(y)]模3为0,表明是同类。若d[find(x)]-d[find(y)]模3为1,则表明x是y的天敌。代码如下:

10.堆

如何手写一个堆?

1. 插入一个数               heap[ ++ size] = x; up(size);  插到末尾,自下向上调整堆
2.求集合当中的最小值  heap[1];(小根堆)
3. 删除最小值                heap[1] = heap[size]; szie -- ; down(1);
4.删除任意一个元素      heap[k] = heap[size]; size -- ; down(k); up(k)
5.修改任意一个元素      heap[k] = x; down(k); up(k);

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

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

const int N = 1e5+10;
int heap[N],hsize; // hsize表示堆的大小

void down(int i){  // 元素下沉操作
    int t;
    while(2*i <= hsize){  // 节点i(从1开始),其做孩子为2i,右孩子为2i+1,
        t = 2 * i;
        if(2*i+1 <= hsize && heap[2*i+1] < heap[2*i]) t++;  // 孩子中的较小者比根小,则交换
        //printf("%d ",heap[t]);
        if(heap[t]<heap[i]) {swap(heap[t],heap[i]);i = t;}  
        else break;
    }
}

int main(){
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",&heap[i]);
    hsize = n;
    for(int i=n/2;i>0;i--) down(i); // 从n/2开始下沉,表示从第n-1开始下沉,可实现在O(n)时间复杂度建堆
    while(m--){
        printf("%d ",heap[1]);
        swap(heap[1],heap[hsize]); // 将堆首元素输出
        hsize--;
        down(1); // 向下调整堆
    }
    return 0;
}

维护一个集合,初始时集合为空,支持如下几种操作
1.I x,插入一个数2;
2.PM ,输出当前集合中的最小值
3.DM,删除当前集合中的最小值 (数据保证此时的最小值唯一)
4.DK,删除第k个插入的数;
5.C k x ,修改第k个插入的数,将其变为 a
现在要进行N次操作,对于所有第 2 个操作,输出当前集合的最小值 

#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;

const int N = 1e5+10;
int heap[N],hp[N],ph[N],hsize; // ph[k]将插入的第k个元素对应到堆的索引j;hp[j]将堆中数组下标为i对应到第k个插入元素

void h_swap(int a,int b){   // 不仅需要交换堆元素,还需要维护额外的信息
    swap(ph[hp[a]],ph[hp[b]]); // 交换元素在堆中的索引
    swap(heap[a],heap[b]);  // 交换元素
    swap(hp[a],hp[b]);      // 交换堆索引对应插入元素的顺序
}

void down(int i){  // 自上而下调整堆
    int t;
    while(2*i <= hsize){
        t = 2 * i;
        if(2*i+1 <= hsize && heap[2*i+1] < heap[2*i]) t++;
        //printf("%d ",heap[t]);
        if(heap[t]<heap[i]) {h_swap(t,i);i = t;}
        else break;
    }
}

void up(int i){   // 自下而上调整堆
    while(i/2 && heap[i/2]>heap[i]){
        h_swap(i/2,i);
        i = i/2;
    }
}

int main(){
    int n;
    scanf("%d",&n);
    char op[5];
    int k,x;
    int c=0;    // 计数
    while(n--){
        scanf("%s",op);
        if(!strcmp(op,"I")){
            hsize++,c++;  // 索引++,堆大小++
            scanf("%d",&heap[hsize]);
            hp[hsize] =  c;  // 堆中数组下标为i对应到第k个插入元素
            ph[c] = hsize;   // 插入的第k个元素对应到堆的索引j
            up(hsize);   // 插入元素,自上而下调整堆
        }
        else if(!strcmp(op,"PM")) printf("%d\n",heap[1]); // 输出堆首元素
        else if(!strcmp(op,"DM")) { // 输出堆首元素
            h_swap(1,hsize);  // 交换堆首和堆尾元素
            hsize--;
            down(1);         // 自上而下调整堆
        }
        else if(!strcmp(op,"D")){
            scanf("%d",&k);  // 删除第k个插入的元素
            k = ph[k];    // 第k个插入的元素对应堆的索引
            h_swap(k,hsize); // 交换该元素和堆尾元素
            hsize--;
            down(k);up(k);  // 实际只会执行一种调整,为了书写方便
        }
        else{  // 更改第k个插入元素的值
            scanf("%d%d",&k,&x);
            k = ph[k];  // 第k个插入的元素对应堆的索引 
            heap[k] = x; // 更改值
            down(k);up(k); // 实际只会执行一种调整,为了书写方便
        }
    }
    return 0;
}

11.哈希表

        哈希表是一种高效的数据结构,它通过哈希函数将键映射到存储位置,从而实现快速的插入、删除和查找操作,理想情况下时间复杂度为O(1)。然而,在实际应用中可能会遇到冲突,即不同的键映射到同一位置。为解决这些冲突,哈希表采用了链地址法、开放地址法等方法。

实例:此处采用链地址法解决冲突

维护一个集合,支持如下几种操作
1.I x ,插入一个整数a;
2.Q x ,询问整数 是否在集合中出现过
现在要进行 N 次操作,对于每个询问操作输出对应的结果 

#include <iostream>

using namespace std;
const int N = 1e5+7;

typedef struct Node
{
    int data;
    Node* next;
    
};

Node* H[N];

void insert(int x){
    Node* node = new Node();
    node->data = x;
    node->next = NULL;
    int k = (x % N + N)%N; // 哈希函数映射位置,此处处理了负值的情况
    Node* p = H[k];
    if(!p) H[k] = node;  // 表位置为空,直接插入
    else{       // 位置不空,插入到表头
        node->next = p->next;
        p->next = node;
    }
}

void find(int x){
    int k = (x % N + N)%N;
    Node* p = H[k];
    while (p) // 表位置不空,进入查找
    {   
        if(p->data == x) break;
        p = p->next;
    }
    if(!p) printf("No\n");
    else printf("Yes\n");
}

int main(){
    char op[5]; // 运算符
    int x;  // 值
    int m; // m次操作
    scanf("%d",&m);
    while (m--)
    {
        scanf("%s%d",op,&x);
        if(op[0]=='I') insert(x);
        else find(x);
    }
    return 0;
}

字符串哈希

(1)字符串哈希值的表示

        将字符串哈希用p进制的数表示。例如:“ABCD”的值用H=ASCII(A)*p^3+ASCII(B)*p^2+ASCII(C)*p^1+ASCII(D)*p^0,然后字符串的哈希值为H%Q,其中当p=131或13331,Q=2^64时,不会发生冲突,此时unsigned long long类型的表示即为对应的哈希值。

(2)用处

        可以在O(1)的时间复杂度内求出[l,r]区间子串的哈希值。

12.STL库

在C++中,vector是一个动态数组,其大小可以在运行时更改。初始化vector的方式有多种,这取决于你想如何初始化它。以下是一些示例:

  1. 默认初始化​​​​​​​

std::vector<int> vec;

这将创建一个没有任何元素的空vector
2. 使用给定的大小初始化

std::vector<int> vec(10); // 创建一个大小为10的vector,所有元素默认初始化为0(对于int类型)

3.使用给定的大小和值初始化

std::vector<int> vec(10, 5); // 创建一个大小为10的vector,所有元素初始化为5

4.使用初始化列表初始化

std::vector<int> vec = {1, 2, 3, 4, 5}; // 创建一个包含5个元素的vector

5.通过复制另一个vector初始化

std::vector<int> vec1 = {1, 2, 3, 4, 5};
std::vector<int> vec2(vec1); // 创建一个与vec1相同的vector

在C++中,std::vector 提供了大量的成员函数来支持各种操作,包括访问元素、修改元素、管理大小和容量等。以下是一些常用的 std::vector 成员函数:

访问元素

  • at(size_type pos): 访问指定位置的元素,并进行边界检查。
  • operator[]: 通过索引访问元素,不进行边界检查(更快,但使用时要小心)。
  • front(): 访问第一个元素。
  • back(): 访问最后一个元素。

修改元素

  • push_back(const T& value): 在末尾添加一个元素。
  • pop_back(): 移除末尾的元素。
  • insert(iterator pos, const T& value): 在指定位置插入一个元素。
  • erase(iterator pos): 移除指定位置的元素。
  • erase(iterator first, iterator last): 移除一系列元素。
  • swap(vector& other): 交换两个向量的内容。
  • clear(): 清除所有元素。
  • emplace(const_iterator pos, args): 在指定位置构造并插入一个元素(C++11起)。
  • emplace_back(args): 在末尾构造并添加一个元素(C++11起)。

大小和容量

  • size(): 返回元素的数量。
  • empty(): 检查是否为空。
  • capacity(): 返回当前已分配的空间能容纳的元素数量。
  • resize(size_type n): 改变大小。
  • reserve(size_type n): 预留空间。
  • shrink_to_fit(): 请求移除未使用的容量(C++11起,非绑定性)。

其他

  • assign(size_type n, const T& value): 替换所有元素。
  • begin(): 返回指向第一个元素的迭代器。
  • end(): 返回指向最后一个元素之后位置的迭代器。
  • cbegin()cend(): 返回常量迭代器(C++11起)。
  • rbegin()rend(): 返回反向迭代器。
  • crbegin()crend(): 返回常量反向迭代器(C++11起)。
  • emplace_iteratorbegin(size_type)end(size_type): (C++20起)用于支持结构化绑定的新接口。

vector, 变长数组,倍增的思想
    size()  返回元素个数
    empty()  返回是否为空
    clear()  清空
    front()/back()
    push_back()/pop_back()
    begin()/end()
    []
    支持比较运算,按字典序

pair<int, int>
    first, 第一个元素
    second, 第二个元素
    支持比较运算,以first为第一关键字,以second为第二关键字(字典序)

string,字符串
    size()/length()  返回字符串长度
    empty()
    clear()
    substr(起始下标,(子串长度))  返回子串
    c_str()  返回字符串所在字符数组的起始地址

queue, 队列
    size()
    empty()
    push()  向队尾插入一个元素
    front()  返回队头元素
    back()  返回队尾元素
    pop()  弹出队头元素

priority_queue, 优先队列,默认是大根堆
    size()
    empty()
    push()  插入一个元素
    top()  返回堆顶元素
    pop()  弹出堆顶元素
    定义成小根堆的方式:priority_queue<int, vector<int>, greater<int>> q;

stack, 栈
    size()
    empty()
    push()  向栈顶插入一个元素
    top()  返回栈顶元素
    pop()  弹出栈顶元素

deque, 双端队列
    size()
    empty()
    clear()
    front()/back()
    push_back()/pop_back()
    push_front()/pop_front()
    begin()/end()
    []

set, map, multiset, multimap, 基于平衡二叉树(红黑树),动态维护有序序列
    size()
    empty()
    clear()
    begin()/end()
    ++, -- 返回前驱和后继,时间复杂度 O(logn)

    set/multiset
        insert()  插入一个数
        find()  查找一个数
        count()  返回某一个数的个数
        erase()
            (1) 输入是一个数x,删除所有x   O(k + logn)
            (2) 输入一个迭代器,删除这个迭代器
        lower_bound()/upper_bound()
            lower_bound(x)  返回大于等于x的最小的数的迭代器
            upper_bound(x)  返回大于x的最小的数的迭代器
    map/multimap
        insert()  插入的数是一个pair
        erase()  输入的参数是pair或者迭代器
        find()
        []  注意multimap不支持此操作。 时间复杂度是 O(logn)
        lower_bound()/upper_bound()

unordered_set, unordered_map, unordered_multiset, unordered_multimap, 哈希表
    和上面类似,增删改查的时间复杂度是 O(1)
    不支持 lower_bound()/upper_bound(), 迭代器的++,--

bitset, 圧位
    bitset<10000> s;
    ~, &, |, ^
    >>, <<
    ==, !=
    []

    count()  返回有多少个1

    any()  判断是否至少有一个1
    none()  判断是否全为0

    set()  把所有位置成1
    set(k, v)  将第k位变成v
    reset()  把所有位变成0
    flip()  等价于~
    flip(k) 把第k位取反

参考:常用代码模板2——数据结构 - AcWing

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1486524.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

klipper api测试脚本whconsole.py

1、whconsole.py简单介绍 whconsole.py用于测试klipper的对外接口api&#xff0c;其实是连接klipper的uds服务&#xff08;Unix Domain Socket&#xff09;&#xff0c;官方也有介绍API 服务器 - Klipper 文档。 需要注意是的whconsole.py脚本启动不能使用Python3&#xff0c;…

想要调用淘宝开放平台API,没有申请应用怎么办?

用淘宝自定义API接口可以访问淘宝开放平台API。 custom-自定义API操作 taobao.custom 公共参数 注册账号获取API请求地址 名称类型必须描述keyString是调用key&#xff08;必须以GET方式拼接在URL中&#xff09;secretString是调用密钥api_nameString是API接口名称&#xf…

【计算机网络】深度学习HTTPS协议

&#x1f493; 博客主页&#xff1a;从零开始的-CodeNinja之路 ⏩ 收录文章&#xff1a;【计算机网络】深度学习HTTPS协议 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 目录 一:HTTPS是什么二:HTTPS的工作过程三:对称加密四:非对称加密五:中间人攻击1…

精读《用 Babel 创造自定义 JS 语法》

1 引言 在写这次精读之前&#xff0c;我想谈谈前端精读可以为读者带来哪些价值&#xff0c;以及如何评判这些价值。 前端精读已经写到第 123 篇了&#xff0c;大家已经不必担心它突然停止更新&#xff0c;因为我已养成每周写一篇文章的习惯&#xff0c;而读者也养成了每周看一…

C++进阶(二) 多态

一、多态的概念 多态的概念&#xff1a;通俗来说&#xff0c;就是多种形态&#xff0c; 具体点就是去完成某个行为&#xff0c;当不同的对象去完成时会 产生出不同的状态。举个栗子&#xff1a;比如买票这个行为&#xff0c;当普通人买票时&#xff0c;是全价买票&#xff1b;学…

无人机镜头稳定的原理和相关算法

无人机的镜头稳定主要基于两个关键技术&#xff1a;镜头平衡技术和实时电子稳像。无人机镜头稳定的原理和相关算法主要是通过镜头平衡技术和实时电子稳像技术来保持摄像镜头的稳定性&#xff0c;从而拍摄出清晰、稳定的画面。无人机镜头稳定的原理主要是通过传感器和算法来实现…

基于SpringBoot的在线拍卖系统(附项目源码+论文)

摘要 在线拍卖系统&#xff0c;主要的模块包括管理员&#xff1b;首页、个人中心、用户管理、商品类型管理、拍卖商品管理、历史竞拍管理、竞拍订单管理、留言板管理、系统管理&#xff0c;用户&#xff1b;首页、个人中心、历史竞拍管理、竞拍订单管理、留言板管理&#xff0…

SSH教程

ssh 是远程连接的利器, 可以说凡是涉及到 linux 服务器, ssh 就是一个绕不开的话题. 本文作为一个教程, 尽可能详细的帮助读者设置 ssh, 并给出一些常用的 ssh 配置方法 (主要用于 linux 系统的远程登录和文件传输). 1. 简介 ssh 分为两个部分, sshd 服务端和 ssh 客户端. ssh…

2.2 mul、div、and、or乘除指令及所有寄存器英文名

汇编语言 1. mul乘指令 两个相乘的数&#xff0c;要么都是8位&#xff0c;要么都是16位 两个8位数相乘 一个默认放在al中&#xff0c;另一个放在8位reg或内存字节单元中8位乘法&#xff0c;结果默认放在ax中例如&#xff1a;计算100*10 100和10小于255&#xff0c;可以做8位…

优选算法|【双指针】|1089.复写零

目录 题目描述 题目解析 算法原理讲解 代码 题目描述 1089. 复写零 给你一个长度固定的整数数组 arr &#xff0c;请你将该数组中出现的每个零都复写一遍&#xff0c;并将其余的元素向右平移。 注意&#xff1a;请不要在超过该数组长度的位置写入元素。请对输入的数组 就…

基于图数据库构建知识图谱平台应用实践

▏摘要 中信证券基于分布式图数据库StellarDB&#xff0c;替代国外开源图数据库产品&#xff0c;打造全新的企业级知识图谱平台&#xff0c;应用于同一客户集团画像、科创板关联发现、风险事件报告、全球企业关联图谱、产业链图谱、投研图谱、反洗钱与稽核图谱、元数据图谱等应…

《程序员职业规划手册》学习笔记

《程序员职业规划手册》不是一本具体的书&#xff0c;而是由前阿里技术总监雪梅老师讲授的一个专栏课程&#xff0c;总共有20讲&#xff0c;内容基本都是图片和文字形式&#xff0c;也有对应的语音讲述。 回顾了下毕业工作的这几年&#xff0c;我买过很多学习课程&#xff0c;…

java八股文复习-----2024/03/03

1.接口和抽象类的区别 相似点&#xff1a; &#xff08;1&#xff09;接口和抽象类都不能被实例化 &#xff08;2&#xff09;实现接口或继承抽象类的普通子类都必须实现这些抽象方法 不同点&#xff1a; &#xff08;1&#xff09;抽象类可以包含普通方法和代码块&#x…

openGauss学习笔记-234 openGauss性能调优-系统调优-资源负载管理-资源管理准备-设置控制组

文章目录 openGauss学习笔记-234 openGauss性能调优-系统调优-资源负载管理-资源管理准备-设置控制组234.1 背景信息234.2 前提条件234.3 操作步骤234.3.1 创建子Class控制组和Workload控制组234.3.2 更新控制组的资源配额234.3.3 删除控制组 234.4 查看控制组的信息 openGauss…

Docker的基本操作(黑马学习笔记)

镜像操作 镜像名称 首先来看下镜像的名称组成&#xff1a; ● 镜名称一般分两部分组成&#xff1a;[repository]:[tag]。 ● 在没有指定tag时&#xff0c;默认是latest&#xff0c;代表最新版本的镜像 如图&#xff1a; 这里的mysql就是repository&#xff0c;5.7就是tag&…

第八节 龙晰Anolis 8.8 安装 DDE 桌面环境

一、前言 最小化安装的龙晰 Anolis OS 8.8 是不带图形化界面的&#xff0c;只能使用命令行&#xff0c;有些时候需要用到桌面环境&#xff0c;而DDE (Deepin Desktop Enviroment) 就是很好的桌面环境&#xff0c;它是指龙晰 Anolis 所搭载的中国自主桌面环境&#xff0c;用起来…

信息安全技术第1章——信息网络安全基本概念

课程介绍 网络信息安全是医学信息工程专业的限选课。主要围绕计算机网络安全所涉及的主要问题进行讲解&#xff0c;内容包括&#xff1a;对称密码与公钥密码的基本原理、相关算法及应用。电子邮件的安全&#xff0c;IP安全&#xff0c;Web安全&#xff0c;恶意软件及防火墙等内…

逼迫大模型消除幻觉,就像杨永信电击治疗网瘾少年

在科技高速发展的领域&#xff0c;进步往往伴随着争议。数字化时代&#xff0c;我们被海量信息所环绕&#xff0c;利用大模型来提取信息和生成答案&#xff0c;有时会遇到模型给出的所谓“幻觉&#xff08;hallucination&#xff09;”回应。这就带来了一个问题&#xff1a;是否…

2024阿里云服务器ECS--安全,稳定,购买灵活,低成本

阿里云服务器ECS英文全程Elastic Compute Service&#xff0c;云服务器ECS是一种安全可靠、弹性可伸缩的云计算服务&#xff0c;阿里云提供多种云服务器ECS实例规格&#xff0c;如经济型e实例、通用算力型u1、ECS计算型c7、通用型g7、GPU实例等&#xff0c;阿里云百科aliyunbai…

设计模式—命令模式:探索【命令模式】的奥秘与应用实践!

命令模式 命令模式是一种行为设计模式&#xff0c;它的主要目的是将请求封装成一个对象&#xff0c;从而使得请求的发送者和接收者之间进行解耦。 在命令模式中&#xff0c;命令被封装为一个对象&#xff0c;包含了需要执行的操作以及执行这些操作所需的所有参数。 命令的发送者…