acwing基础课第二章 数据结构

单链表/数组模拟

#include<iostream>
using namespace std;

const int N = 100010;

int e[N], ne[N]; // e[i]表示第i数的值,ne[i]表示第i个数后面的数
int head, idx; // head表示链表头,idx表示当前用到了哪个节点 

void init()
{
	head = -1;
	idx = 0;	
} 
// 将x插入到头节点后面 
void add2head(int x)
{
	e[idx] = x;
	ne[idx] = head; // idx节点指向head指向的那个节点
	head = idx; // head指向idx节点 
	idx++; 
}
// 在第k个插入的数后面插入x 
// k表示的是第k个插入的数,不是当前链表中第k个数 
void add(int k, int x)
{
	e[idx] = x;
	// 下面两句顺序不能变 
	ne[idx] = ne[k];
	ne[k] = idx;
	
	idx++; // 当前节点+1 
}
// 删除第k个插入的数后面的数 
void remove(int k)
{
	ne[k] = ne[ne[k]];
}


int main()
{
	
	init();
	
	int m;
	cin >> m;
	
	
	while(m --) 
	{
		char op;
		int x, k;
		cin >> op;
		switch(op)
		{
			case'H':
				cin >> x;
				add2head(x);
				break;
			case'D':
				cin >> k;
				if(!k) head = ne[head];
				else   remove(k - 1);
				break;
			case'I':
				cin >> k >> x;
				add(k - 1, x);
				break;
		}
	}
	for(int i = head; i != -1; i = ne[i]) cout << e[i] << " ";
	cout << endl;
	
	return 0; 
}

数组模拟 栈

#include<iostream>
using namespace std;

const int N = 1000010;

int stk[N], tt = 0; // stk是栈,tt是栈顶
// 插入
stk[++tt] = x;
// 删除栈顶
tt--;
// 判断是否为空
if(tt > 0)  not empty;
else	    empty;
// 取栈顶
stk[kk]; // 



// *************** //
//  ......hh......tt....

// 队列
int q[N], hh, tt = -1; // q是队列,hh是头,tt是尾
// 队尾插入
q[++t] = x;
// 删除队首
hh++;  
// 判断队列是否为空
if(hh > tt) empty;
else		not empty;   
// 取队首
x = q[hh];

单调栈

例子: 找到一个数左边第一个比该数小的数

输入样例
5
3 4 2 7 5

输出样例
-1 3 -1 2 2

思路

  1. 用一个栈保存 a[i] 左边的数
  2. 如果i < j并且 a[i] > a[j],那么在后面的过程中a[i] 在后面永远不会被用到,所以栈中保存的是一个单调递增的序列;所以对于每个a[i],如果栈不为空(tt>=0)并且栈顶元素大于a[i] (stk[tt] >= a[i])那么就将栈顶元素删除,直到栈为空或者栈顶元素小于a[i],输出栈顶元素或者-1
  3. 然后将a[i]存入栈中

注意:使用scanf() 最快

代码

#include<iostream>
using namespace std;
const int N = 1000010;
int stk[N], tt = 0; // stk是栈, tt是栈顶
int a[N];
int n;
int main()
{
	scanf("%d ", &n);
	for(int i = 0; i < n; i++) scanf("%d", &a[i]);
	
	for(int i = 0; i < n; i++) {
		while(tt && stk[tt] >= a[i]) tt--; // 如果栈不为空,并且栈顶大于a[i]
										  // 就把栈顶剔除
		if(tt) printf("%d ", stk[tt]);    // 栈顶不为空的话,说明栈顶就是a[i]左边第一个比a[i]小的数 
		else   printf("-1 "); 
		
		// 把a[i]加入到stk中
		stk[++tt] = a[i];  
	}
	return 0;
 } 

单调队列

给定一个大小为 n≤106n≤106 的数组。

有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。

你只能在窗口中看到 k 个数字。

每次滑动窗口向右移动一个位置。

以下是一个例子:

该数组为 [1 3 -1 -3 5 3 6 7],k 为 33。

窗口位置 最小值 最大值
[1 3 -1] -3 5 3 6 7 -1 3
1 [3 -1 -3] 5 3 6 7 -3 3
1 3 [-1 -3 5] 3 6 7 -3 5
1 3 -1 [-3 5 3] 6 7 -3 5
1 3 -1 -3 [5 3 6] 7 3 6
1 3 -1 -3 5 [3 6 7] 3 7

你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。

输入格式

输入包含两行。

第一行包含两个整数 nn 和 kk,分别代表数组长度和滑动窗口的长度。

第二行有 nn 个整数,代表数组的具体数值。

同行数据之间用空格隔开。

输出格式

输出包含两个。

第一行输出,从左至右,每个位置滑动窗口中的最小值。

第二行输出,从左至右,每个位置滑动窗口中的最大值。

输入样例:

8 3
1 3 -1 -3 5 3 6 7

输出样例:

-1 -3 -3 -3 3 3
3 3 5 5 6 7

思路

首先对于求最小值,和上面单调栈的思路相近,使用一个队列来存放a[i]左边的数的下标(为什么存放的是下标不是数呢,待会下面会讲)

考虑这个队列中的元素和a[i]的大小关系,如果这个队列中有比a[i]大的数,只要a[i]在,那么窗口内比a[i]大的数就不可能被用到,所以我们维护一个单调队列:

  1. 首先保证这个队列处于窗口内,
  2. 然后保证队首的元素是当前窗口最小的,队列内的元素单调递增,
  3. 那么只需要取队首就是窗口内最小元素了。

为什么队列里面存的是下标而不是数呢,因为我们要保证队列中的元素一直处于窗口内,那么就需要知道每个数的下标,根据下标判断队首的元素是不是超过了窗口,所以我们在队列里面存对应数字的下标是最好的方法。

代码

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

const int N = 1000010;

int q[N], hh = 0, tt = -1; // hh是头,tt是尾部 
int a[N];
int n, k;
int main()
{
	scanf("%d%d", &n, &k);
	for(int i = 0; i < n; i++) scanf("%d", &a[i]);
	
	// 先输出最小数 
	hh = 0, tt = -1;
	for(int i = 0; i < n; i++)
	{
		// 如果队列头部超过了k的大小,前面的出队 
        // 因为这里每次窗口只右移动一个,所以每次只需要一个if判断一次head是不是越过了窗口即可
		if(hh <= tt && i - k + 1 > q[hh]) hh++;
		
		// 如果队尾大于a[i],那么需要将队尾移除 
		while(hh <= tt && a[q[tt]] >= a[i]) tt--;
		
		// 把a[i]的下标i加入到队列
		q[++tt] = i;
		
		// 当有k个数之后再输出 
		if(i+1>=k)
		{
			printf("%d ",a[q[hh]]);
		 } 
		  
	}
	puts("");
	
	//输出最大值
	// 重置队列
	hh = 0, tt = -1;
	for(int i = 0; i < n; i++)
	{
		if(hh <= tt && i - k + 1 > q[hh]) hh++;
		
		while(hh <= tt && a[q[tt]] <= a[i]) tt--;
		
		q[++tt] = i;
		
		if(i+1 >= k)
		{
			printf("%d ",a[q[hh]]);
		}
	 } 
	return  0;
 } 

trie树

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

“I x”向集合中插入一个字符串x;
“Q x”询问一个字符串在集合中出现了多少次。
共有N个操作,输入的字符串总长度不超过 105105,字符串仅包含小写英文字母。

输入格式

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

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

输出格式

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

每个结果占一行。

数据范围

1≤N≤2∗1041≤N≤2∗104

输入样例:

5
I abc
Q abc
Q ab
I ab
Q ab
输出样例:

1
0
1

//trie树算法
#include<iostream>
#include<cstdio>
#include<cstring>
const int N = 1000010;

using namespace std;
int son[N][26], cnt[N], idx;
// son[][]存储树中每个节点的子节点,
// cnt[i]表示该单词i的出现次数
// idx表现当前用到了哪个节点,idx单调递增

void insert(char * str)
{
	int p = 0; // parent
	for(int i = 0; str[i]; i++)
	{
		int u = str[i] - 'a';
		if(!son[p][u]) son[p][u] = ++idx; // idx = 0 是根节点root 
		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);
	char op[2];
	char str[N];
	while(n--)
	{
		scanf("%s%s",op,str);
		if(op[0] == 'I') insert(str);
		else 			 printf("%d\n", query(str));
	}	
	return 0;
} 

并查集

模拟散列表

拉链法

// 拉链法存/查 hash表
// 取模的最好使用质数

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;

const int N = 100003; // 大于100000的最小质数

int h[N], e[N], ne[N], idx; 
// h[]表示散列表
// e[] ne[] idx是单链表里面的

void insert(int x)
{
	int k = (x%N + N)%N; // 简单散列函数,这么做是为了防止负数
	e[idx] = x;
	ne[idx] = h[k]; // 头插法
	h[k] = idx++; 
}

bool find(int x)
{
	int k = (x%N + N)%N;
	// 这里的-1,是因为初始化的时候将h[k]都初始化成了-1 
	for(int i = h[k]; i != -1; i = ne[i])
	{
		if(e[i] == x) return true; 
	}
		
	return false;
	
} 


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

image-20220222201829774

开放寻址法

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N = 200003; // 通常开2~3倍 
const int null = 0x3f3f3f3f; // h初始化成它
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()
{
 	memset(h, 0x3f, sizeof(h)); // memset按照字节初始化,h是int类型,所以初始化完之后就是0x3f3f3f3f 
   
}

字符串hash

image-20220222204146192

实质就是将一个字符串映射成整数

方便判断两个字符串是否相等

img

给定一个长度为n的字符串,再给定m个询问,每个询问包含四个整数 l1,r1,l2,r2 ,请你判断[ l1,r1 ]和[ l2,r2 ]这两个区间所包含的字符串子串是否完全相同。

字符串中只包含大小写英文字母和数字。

输入格式

第一行包含整数n和m,表示字符串长度和询问次数。

第二行包含一个长度为n的字符串,字符串中只包含大小写英文字母和数字。

接下来m行,每行包含四个整数 l1,r1,l2,r2 ,表示一次询问所涉及的两个区间。

注意,字符串的位置从1开始编号。

输出格式

对于每个询问输出一个结果,如果两个字符串子串完全相同则输出“Yes”,否则输出“No”。

每个结果占一行。

数据范围

1 ≤ n, m ≤ 105
输入样例:
8 3
aabbaabb
1 3 5 7
1 3 6 8
1 2 1 2

输出样例:

Yes
No
Yes


思路
依据经验,我们使得数值尽量重复时需要限定:
1.p最好是131或者13331
2.对数值%(226),我们刚好采取unsigned long long 溢出时的状态也是一致的
这样几乎没有可能重复从而冲突

将字符串的前缀转换为数来存值
由于每位的权值是不一样的 所以每个前缀值都对应着唯一的一种字符串
所以相减后的值也应该是唯一的 从而利用相减后的值可以判断字符串的区间段是否相等
————————————————
版权声明:本文为CSDN博主「ken的信徒」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_43910320/article/details/107718725


代码:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;

// 小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果
typedef unsigned long long ULL;
const int N = 100010;
char str[N];
ULL h[N], p[N]; // h表示一个字符串的前缀和 
				// p表示各个位的权重 
int P = 131; // 131 或者 13331

ULL get(int l, int r)
{
	return h[r] - h[l-1]*p[r-l+1];	
} 

int main()
{
	int n, m;
	scanf("%d%d", &n, &m);
	
	scanf("%s",str+1);// 因为前面可能空格? 
	for(int i = 1; i < n; i++ )
	{
		p[0] = 1;
		for(int i = 1; i <= n; i++)
		{
			p[i] = p[i-1] * P;  // h[i]表示各位的权重 
			h[i] = h[i-1]*P + str[i]; // h[i]表示各位的前缀和 
		}
	}
	while(m--)
	{
		int l1, r1, l2, r2;
		scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
		
		if(get(l1, r1) == get(l2, r2)) puts("Yes");
		else						   puts("No");
	 } 
	 
	 return 0;
}
posted @ 2022-02-22 21:23  VanHope  阅读(37)  评论(0编辑  收藏  举报