常用数据结构
链表
为什么要用数组来模拟链表?
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字符串统计
【题目描述】
【思路】
使用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. 滑动窗口
【题目描述】
【思路】
先朴素做法:用一个队列维护该窗口的所有值,计算该窗口的极值。 时间复杂度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]]+" ");
}
}
}
相似题:最大滑动窗口
并查集
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");
}
}
}
}
}