基础算法模板 (十) —— 常用数据结构

链表

为什么要用数组来模拟链表?

new对象操作本身是很慢的。在笔试题中,节点个数一般是10万甚至是100万级别。如果每次都使用new 一个对象,在申请过程中就有可能会出现超时。上述在使用时才去申请空间称为动态链表。
笔试中常用的是是采用数组来模拟,在全局位置一次申请足够多的空间。

如何表示

使用数组e和数组ne来分别存储节点的属性:值和next域*(存储下一个节点的位置/编号)。然后通过下标将这两个数组关联起来,也就是相同下标表示同一个节点。这种存储方法也叫链式前向星

示例如下:
在这里插入图片描述

插入

头插法

在这里插入图片描述
操作如下
在这里插入图片描述

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

插入到第k个输入的数后

在这里插入图片描述

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

删除指定下标后的元素

  /**
     删除第 k 个插入的数后面的数,即要删除第k-1个插入的数
     第k-1个插入的数编号为k - 2,也就是要删除编号为k - 1后面的数
     */
    //将下标是k的点后面的点删除
    public static void remove(int k){
        ne[k] = ne[ ne[k] ];
    }

完整代码

【题目描述】
在这里插入图片描述在这里插入图片描述

【思路】

import java.util.Scanner;
import java.util.Arrays;
public class Main{
    
    static int N = 100010;
    
    static int e[] = new int[N]; // e[i]表示编号为i的节点的值
    static int ne[] = new int[N]; // ne[i]表示编号为i节点的next值(下一个节点的位置)
    static int idx; //idx存储当前已经用到了哪个节点了(已经分配了N个节点的空间)
    static int head;
    public static void init(){
        head = -1; //head存储头结点的编号
        idx = 0;
    }
    
    public static void add_to_head(int x){
        
        e[idx] = x;
        ne[idx] = head;
        head = idx;
        idx ++;
    }
    public static void add(int k, int x){
        e[idx] = x;
        ne[idx] = ne[k];
        ne[k] = idx;
        idx ++;
        
    }
    /**
     删除第 k 个插入的数后面的数,即要删除第k-1个插入的数
     第k-1个插入的数编号为k - 2,也就是要删除编号为k - 1后面的数
     */
    //将下标是k的点后面的点删除
    public static void remove(int k){
        ne[k] = ne[ ne[k] ];
    }
    
    public static void main(String args[]){
        Scanner reader = new Scanner(System.in);
        int m = Integer.parseInt(reader.nextLine()); //要将整行读入 消除换行符对下个读入的影响
        init();
        while( m -- >  0){
            String s[] = reader.nextLine().split(" ");
            if( s[0].equals("H")){
                add_to_head(Integer.parseInt(s[1]) );
            }else if( s[0].equals("D") ){
                int k = Integer.parseInt(s[1]);
                //删除头结点时要特判
                if( k == 0 ) head =  ne[head];
                else remove( k - 1);
            }else{
                add(Integer.parseInt(s[1]) - 1, Integer.parseInt(s[2]));
            }
           
        }
          for(int i = head; i != - 1; i = ne[i])
            System.out.print(e[i]+" ");
        System.out.println();
      
    }
}

【题目描述】
Acwing 828. 模拟栈
模板



int stk[] = new int[N];
//tt表示栈顶
int tt = 0;

//向栈顶插入一个数
stk[ ++ tt] = x

//从栈顶弹出一个数
tt --

//栈顶的值
stk[tt]

//判断栈是否为空
if( tt > 0) not empty
else empty

import java.io.*;

public class Main{
    
    static int N = 100010;
    static int q[] = new int[N];
    static int tt;
    

    static void push(int x){
        q[ ++ tt] = x;
    }
    
    static int pop(){
        return q[ tt --];
    }
    
    static boolean isEmpty(){
        return tt <= 0;
    }
    
    static int query(){
        return q[ tt ];
    }
    
    public static void main(String args[])throws Exception{
        BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
        
        int m = Integer.parseInt( bf.readLine() );
        
        for(int i = 0; i < m;  i++){
            String s [] = bf.readLine().split(" ");
            if( s[0].equals("push") ){
                push( Integer.parseInt(s[1]) );
            }else if( s[0].equals("query") ){
                System.out.println(query());
            }else if( s[0].equals("pop") ){
                pop();
            }else{
                if( isEmpty() ) System.out.println("YES");
                else System.out.println("NO");
            }
        }
    }
}

队列

在两端操作,队首(hh)弹出数据,队尾(tt)插入数据。
模板


int q[] =  new int[N];
//hh表示队首  tt表示队尾 
int hh = 0, tt = -1;

//往队尾插入元素
q[ ++ tt] = x

//从队头弹出一个数
hh ++;

//队首值
q[hh]  

//队列是否为空
if(   hh <= tt)  not empty
else empty

import java.io.*;
public class Main{
    
    static int N = 100010;
    static int hh = 0, tt = -1;
    
    static int q[] = new int[N];
    static void push(int x){
        q[ ++ tt] = x;
    }
    
    static int query(){
        return q[hh];
    }
    
    static boolean isEmpty(){
        return hh <= tt;
    }
    
    
    static void pop(){
        hh ++;
    }
    public static void main(String args[]) throws Exception{
        BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
        int m =  Integer.parseInt(bf.readLine() );
        while( m -- > 0){
            
            String s[] = bf.readLine().split(" ");
            if( s[0].equals("push") ){
                push( Integer.parseInt(s[1]) );
            }else if( s[0].equals("query") ){
                System.out.println(query());
            }else if( s[0].equals("pop") ){
                pop();
            }else{
                if( isEmpty() ) System.out.println("NO");
                else System.out.println("YES");
            }
            
        }
    }
}

Trie

Trie 是一种用来高效地存储和查找字符串(通常还有01串、数字、全是大(小)写字符串)集合的数据结构。

存储形式:
在这里插入图片描述

Acwing 835. Trie字符串统计

【题目描述】

在这里插入图片描述

835. Trie字符串统计

【思路】
使用trie数据结构存储并查询

import java.util.Scanner;
public class Main{
    
    static int M = 100010; //总结点数
    static int son[][] = new int [M][26]; // 总共有M个节点, 每个节点最多有26的子节点
    static int idx;  //每个节点的编号
    static int cnt[] = new int[M]; //cnt[i]表示以编号为i节点结束的字符串的个数
    
    public static void insert(char c[]){
        int p = 0; // 根节点 ; p == 0 可能是根节点也可能是空节点
        for(int i = 0; i < c.length; i ++){//依次取出字符数组c的每个字符
            int u = c[i] - 'a'; // 子节点
            //子节点不存在则 new一个新的节点 
            if( son[p][u] == 0 ) son[p][u] = ++idx; 
            //然后p走到子节点位置
            p = son[p][u];
        }
        cnt[p] ++;
    }
    //在trie字符串集合中查找目标字符串的个数
    public static int query(char target[]){
        int p = 0;
        for(int i = 0; i < target.length; i ++){
            int u =  target[i] - 'a';
            if( son[p][u] == 0 ) return 0;
            p = son[p][u];
        }
        return cnt[p]; //返回以编号p结尾的字符串个数
    }
    public static void main(String agrs[]){
        Scanner reader = new Scanner(System.in);
        int N =  Integer.parseInt(reader.nextLine());
        while( N -- > 0){
            String s[] = reader.nextLine().split(" ");
            char c[] = s[1].toCharArray();
            if( s[0].equals("I") ) insert(c);
            else System.out.println(query(c));
        }
    }
}

单调队列

AcWing 154. 滑动窗口

【题目描述】

在这里插入图片描述在这里插入图片描述

AcWing 154. 滑动窗口

【思路】
先朴素做法:用一个队列维护该窗口的所有值,计算该窗口的极值。 时间复杂度O(n*k)
在朴素做法的基础上优化:
实际上队列没必要维护窗口的所有值。对于求每个窗口的最小值而言: 队列中靠前的数,如果比靠后的数大那么,肯定是不会用作答案输出的。因为靠后(数组中序号更大的数)的数值小且更靠后被滑出。那么可以剔除原先队列中的所有逆序对。剔除后就是单调上升的队列。那么最小值就是队头元素。

以最小值为例:我们从左到右扫描整个序列,用一个队列来维护最近 k 个元素。如果用暴力来做,就是每次都遍历一遍队列中的所有元素,找出最小值即可,但这样时间复杂度就变成 O(nk) 了;
然后我们可以发现一个性质:如果队列中存在两个元素,满足 a[i] >= a[j] 且 i < j,那么无论在什么时候我们都不会取 a[i] 作为最小值了,所以可以直接将 a[i] 删掉;此时队列中剩下的元素严格单调递增,所以队头就是整个队列中的最小值,可以用 O(1)的时间找到;
为了维护队列的这个性质,我们在往队尾插入元素之前,先将队尾大于等于当前数的元素全部弹出即可;这样所有数均只进队一次,出队一次,所以时间复杂度是 O(n) 的。

注意 这里队列存放元素的下标

import java.io.*;
public class Main{
    static int N = 1000010;
    static int q[] = new int[N]; // 单调队列
    static int a[] = new int[N]; 
    public static void main(String args[]) throws Exception{
        BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
        String s[] = bf.readLine().split(" ");
        String data[] =bf.readLine().split(" ");
        int n = Integer.parseInt(s[0]), k = Integer.parseInt(s[1]);
        for(int i = 0; i < n; i ++ ) a[i] = Integer.parseInt(data[i]);
        
        //指向队头、队尾指针
        int hh = 0, tt = -1;
        
        for(int i = 0; i < n; i ++){
            //判断队头是否滑出窗口: 注意 q存储的是元素的下标
            if( hh <= tt && i - k + 1 > q[hh]) hh ++;
            //如果当前元素a[i]不大于队尾的元素时:注意 等于的时候也要更新(因为更靠后)
            while(hh<=tt && a[i] <= a[ q[tt] ] ) tt --;
            q[++ tt] = i;
            if(i >= k - 1) System.out.print(a[q[hh]]+" ");
        }      
        System.out.println();
        hh = 0;
        tt = -1;
        for(int i = 0; i < n; i++){
            
            if( hh <= tt && i - k + 1 > q[hh]) hh ++;
            while( hh <= tt && a[i] >= a[ q[tt]] ) tt --;
            q[ ++ tt ] = i;
            if(i >= k - 1) System.out.print(a[q[hh]]+" ");
        }
        
    }
}

相似题:最大滑动窗口

并查集

在这里插入图片描述
在这里插入图片描述Acwing 836. 合并集合

import java.util.Scanner;
class Main{
    static int N = 100010;
    static int p[] = new int [N]; // 存储父节点编号
    // 寻找祖宗节点 + 路径压缩
    /*
        在寻找过程中:所有同在一个集合的节点指向同一个根节点
     */
    public static int find(int x){
        if(p[x] != x) { // 不是根节点
            p[x] = find(p[x]); // Attention:去找x的父节点的父节点
        }
        return p[x];
    }
    public static void main(String args[]){
        Scanner reader = new Scanner(System.in);
        String str[] = reader.nextLine().split("\\s+");
        // reader.nextInt()遇空格结束,不能吸收上次输入末尾的回车符
      
        int m = Integer.parseInt(str[0]), n = Integer.parseInt(str[1]);   
        // 初始化时,所有节点指向自己
        for(int i = 1; i <= m; i ++) {
            p[i] = i;
        }
        while(n -- > 0){
            // 从第一个字符开始读取,不忽略空格;读取包括单词之间的空格所有符号。
            // 遇回车符号结束
            String s[] = reader.nextLine().split("\\s+"); 
            int a = Integer.parseInt(s[1]), b =Integer.parseInt(s[2]);
            if(s[0].equals("M")){ // 连接操作,a的父节点指向b集合的根节点
                p[find(a)] = find(b);
            }else{
                if(find(a) == find(b)){
                    System.out.println("Yes");
                }else{
                    System.out.println("No");
                }
            }
        }
        
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值