基础数据结构

链表

数组模拟法

单链表

ITCX7121寒假第二周-A单链表
在这里插入图片描述

模板题分析

  1. 单链表需要用到的数组:head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
  2. 初始化
void init(){
    head = -1;
    idx = 0;
}
  1. 将x点插到下表是k点后面:
void insert(int k,int x){
    e[idx] = x;
	ne[idx] = ne[k];
	ne[k] = idx++;
}
  1. 将头结点删除,需要保证头结点存在
void remove(){
    head = ne[head];
}

模板题代码

//P30单链表

//链表
/*
struct Node{
	int val;
	Node *next;
};
*/
//竞赛中最常用的是邻接表
//邻接表用的最多的是 存储图 和 树

//双链表用来优化某些问题

#include <iostream>
using namespace std;
const int N = 100010;
//head表示头结点的下标
//e[i]表示节点i的值
//ne[i]表示节点i的next指针是多少,或者说下一个节点i的地址是什么
//idx存储当前已经用到了哪个点
int head, e[N], ne[N], idx;

//初始化
void init() {
	head = -1;
	idx = 0;
}

//将x插入到头结点:
//1.将x的next指针指向head节点指向的下一个位置
//2.将head指针删掉,然后指向x节点
void add_to_head(int x) {
	e[idx] = x; //把x这个值先存下来
	ne[idx] = head;
	head = idx;
	idx++;
}

//将x点插到下表是k点后面
void add(int k, int x) {
	e[idx] = x;
	ne[idx] = ne[k];
	ne[k] = idx;
	idx++;
}

//单链表的删除操作
//将下表是k的点后面的点删掉
void remove(int k) {
	ne[k] = ne[ne[k]];
}

int main() {
	int m;
	cin >> m;

	init();//初始化

	while (m--) {
		int k, x;
		char op;
		cin >> op;
		if (op == 'H') {
			cin >> x;
			add_to_head(x);
		} else if (op == 'D') {
			cin >> k;
			if (!k)
				head = ne[head];
			else
				remove(k - 1);//前几次错因:没加else
		} else {
			cin >> k >> x;
			add(k - 1, x);
		}
	}
	for (int i = head; i != -1; i = ne[i])
		cout << e[i] << ' ';
	cout << endl;
	return 0;
}

双链表

ITCX7121寒假第二周-B双链表
在这里插入图片描述

模板题分析

  1. 双链表需要用到的数组:e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
  2. 双链表初始化:
void init() {
	//0表示左端点,1表示右端点
	r[0] = 1;
	l[0] = 0;
	idx = 2;//因为0和1已经被占用了
}
  1. 在第k点的右边插入x:
void add_to_right(int k, int x) {
	e[idx] = x;//将x存入当前下标的数值数组中
	r[idx] = r[k];//当前下标的后继为k的后继
	l[idx] = k;//当前下标的前驱为k
	l[r[k]] = idx;//k的后继节点的前驱为idx
	r[k] = idx++;//k的后继节点为idx,idx++
}

在第k点的左边插入x,等价于在l[k]的右边插入x,也就是add_to_right(l[k],x)

  1. 删除第k个点:
void remove(int k) {
	r[l[k]] = r[k];//k的前驱的后继为k的后继
	l[r[k]] = l[k];//k的后继的前驱为k的前驱
}

模板题代码

//P31双链表

#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010;
//e用来存储数值,
//l[]用来存储前驱(左边数的下标),
//r[]用来存储后继(右边数的下标)
//idx用来记录当前下标
int e[N], l[N], r[N], idx;

//初始化
void init() {
	//0表示左端点,1表示右端点
	r[0] = 1;
	l[0] = 0;
	idx = 2;//因为0和1已经被占用了
}

//在第k点的右边插入x
void add_to_right(int k, int x) {
	e[idx] = x;//将x存入当前下标的数值数组中
	r[idx] = r[k];//当前下标的后继为k的后继
	l[idx] = k;//当前下标的前驱为k
	l[r[k]] = idx;//k的后继节点的前驱为idx
	r[k] = idx++;//k的后继节点为idx,idx++
}

//在k点的左边插入x,等价于在l[k]的右边插入x,也就是add_to_right(l[k],x);
void add_to_left(int k, int x) {
	add_to_right(l[k], x);
}

//删除第k个点
void remove(int k) {
	r[l[k]] = r[k];//k的前驱的后继为k的后继
	l[r[k]] = l[k];//k的后继的前驱为k的前驱
}

//在链表的最左端插入数x
void add_to_head(int x) {
	add_to_right(0, x);//在0后面插入x
}

//在链表的最右端插入数x
void add_to_tail(int x) {
	add_to_right(l[1], x);
}

int main() {
	int m;
	init();
	cin >> m;
	int k, x;
	while (m--) {
		string op;
		cin >> op;
		if (op == "L") {
			cin >> x;
			add_to_head(x);
		} else if (op == "R") {
			cin >> x;
			add_to_tail(x);
		} else if (op == "D") {
			cin >> k;
			remove(k + 1);
		} else if (op == "IL") {
			cin >> k >> x;
			add_to_left(k + 1, x);
		} else  {
			cin >> k >> x;
			add_to_right(k + 1, x);
		}
	}
	for (int i = r[0]; i != 1; i = r[i]) {
		cout << e[i] << ' ';
	}
	cout << endl;

	return 0;
}

模拟栈

内容

  1. tt表示栈顶int stk[N], tt = 0;
  2. 向栈顶插入一个数stk[ ++ tt] = x;
  3. 从栈顶弹出一个数tt -- ;
  4. 栈顶的值stk[tt];
  5. 判断栈是否为空:如果 tt > 0,则表示不为空

模板题分析

ITCX7121寒假第二周-C模拟栈
在这里插入图片描述

模板题代码

#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010;
int stk[N], tt;

//向栈顶插入一个数x
void push(int x) {
	stk[++tt] = x;
}

//从栈顶弹出一个数x
void pop() {
	tt--;
}

//判断栈是否为空
void empty() {
	if (tt > 0)
		cout << "NO" << endl;
	else
		cout << "YES" << endl;
}

//查询栈顶元素
void query() {
	cout << stk[tt] << endl;
}
int main() {
	int m;
	cin >> m;
	string op;
	while (m--) {
		cin >> op;
		if (op == "push") {
			int x;
			cin >> x;
			push(x);
		} else if (op == "pop") {
			pop();
		} else if (op == "empty") {
			empty();
		} else if (op == "query") {
			query();
		}
	}
	return 0;
}

单调栈

常见模型:找出每个数左边离它最近的比它大/小的数

int tt = 0;
for (int i = 1; i <= n; i ++ )
{
    while (tt && check(stk[tt], i)) tt -- ;
    stk[ ++ tt] = i;
}

模板题分析

ITCX7121寒假第二周-G单调栈
在这里插入图片描述

  1. 当栈里不空,并且栈里元素大于当前这个数时tt--
  2. 如果栈里元素不空,输出当前这个数左边第一个比它小的数;如果栈里元素是空的,输出-1
  3. 最后把x插到栈里面去stk[++tt] = x

模板题代码

//P34单调栈

#include <iostream>
using namespace std;
const int N = 100010;
int n;
int stk[N], tt;

int main() {
	cin.tie(0);
	ios::sync_with_stdio(false);
	cin >> n;
	for (int i = 0; i < n; i++) {
		int x;
		cin >> x;
		while (tt && stk[tt] >= x)//当栈里不空,并且栈里元素大于当前这个数
			tt--;
		if (tt)//如果栈里元素不空
			cout << stk[tt] << ' ';//输出当前这个数左边第一个比它小的数
		else//如果栈里元素是空的
			cout << -1 << ' ';
		stk[++tt] = x;//最后把x插到栈里面去
	}
	return 0;
}

队列

模拟队列

内容

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

  1. 向队尾插入一个数
    q[ ++ tt] = x;
  2. 从队头弹出一个数
    hh ++ ;
  3. 队头的值
    q[hh];
  4. 判断队列是否为空,如果 hh <= tt,则表示不为空

ITCX7121寒假第二周-F模拟队列
在这里插入图片描述

模板题分析

在队尾插入元素,在队头弹出元素
int q[N],hh,tt = -1;
hh表示对头,tt表示队尾

  1. 插入q[++tt] = x;
  2. 弹出hh++;
  3. 判断队列是否为空if(hh<tt)not empty; else empty;
  4. 取出队头元素q[hh]

模板题代码

#include <iostream>
using namespace std;
const int N = 100010;
int q[N], tt = 0, hh = 1;//别忘了初始化

//1.向队尾插入一个数x
void push(int x) {
	q[++tt] = x;
}

//2.从队头弹出一个数
void pop() {
	hh++;
}

//3.判断队列是否为空
void empty() {
	if (hh <= tt)
		cout << "NO" << endl;
	else
		cout << "YES" << endl;
}

//4.查询队头元素
void query() {
	cout << q[hh] << endl;
}

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

单调队列

ITCX7121寒假第二周-H滑动窗口
在这里插入图片描述

模板题分析

常见模型:找出滑动窗口中的最大值/最小值

int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
    while (hh <= tt && check_out(q[hh])) hh ++ ;  // 判断队头是否滑出窗口
    while (hh <= tt && check(q[tt], i)) tt -- ;
    q[ ++ tt] = i;
}

模板题代码

//P35滑动窗口

#include <iostream>
using namespace std;
const int N = 1000010;
//a是原数组,q是单调队列
int a[N], q[N];
int n, k;

int main() {
	scanf("%d%d", &n, &k);
	for (int i = 0; i < n; i++)
		scanf("%d", &a[i]);

	//最小值
	int hh = 0, tt = -1;
	for (int i = 0; i < n; i++) {
		//判断队头是否已经滑出窗口
		//用下标来判断,队列q里存的是下标
		//当前终点是i,起点是i-k+1
		if (hh <= tt && i - k + 1 > q[hh])
			hh++;//说明hh已经出了窗口了
		while (hh <= tt && a[q[tt]] >= a[i])
			tt--;
		//把当前这个数插到队列里去
		q[++tt] = i;
		//特判
		if (i >= k - 1)
			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 >= k - 1)
			printf("%d ", a[q[hh]]);
	}
	puts("");

	return 0;
}

KMP

ITCX7121寒假第二周-I KMP
在这里插入图片描述

模板题分析

next[i]表示以i为终点的后缀和从1开始的前缀相等,并且后缀的长度最长
i是遍历s的所有字母,想要和s数组匹配的是p[j+1]

模板题代码

#include <iostream>
using namespace std;
const int N = 100010, M = 1000010;
int n, m;
char p[N], s[M];
int ne[N];

int main() {
	cin >> n >> p + 1 >> m >> s + 1;//下标都从1开始

	//求next的过程
	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;
	}

	//kmp匹配过程
	//i是遍历s的所有字母
	//想要和s数组匹配的是p[j+1]
	for (int i = 1, j = 0; i <= m; i++) {//
		while (j && s[i] != p[j + 1])//当j没有退回起点,s和p数组不能匹配了
			j = ne[j];
		if (s[i] == p[j + 1])//如果s与j已经匹配了
			j++;//j就可以移到下一个位置
		if (j == n) {//说明匹配成功
			printf("%d ", i - n);
			j = ne[j];
		}
	}
	return 0;
}

Trie树

能快速查找一组字符串
ITCX7121寒假第二周-J Trie字符串统计
在这里插入图片描述

模板题分析

字符串插入:

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];
}

模板题代码

//P27Tire字符串统计

#include <iostream>
using namespace std;
const int N = 100010;
int son[N][26], cnt[N], idx;//下标是0的点,既是根节点,又是空节点
//son[N][26]存的是每个节点的所有儿子是什么
//idx是当前所在的节点
char str[N];

//插入操作
void insert(char str[]) {
	int p  = 0;//从根节点开始
	for (int i = 0; str[i]; i++) {//字符串末尾是0
		int u = str[i] - 'a';//把小写字母映射成0~25
		if (!son[p][u])//如果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];//返回以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;
}

并查集

简介

一. 并查集:

  1. 将两个集合合并
  2. 询问两个元素是否在一个集合当中

二. 基本原理:
每个集合用一颗树来表示,树根的编号就是整个集合的编号,每个节点存储它的父节点,p[x]表示x的父节点

三. 问题
4. 如何判断树根:if(p[x]==x)
5. 如何求x的集合编号:while(p[x]!=x)x = p[x]
6. 如何合并两个集合:p[x]是x的集合编号,p[y]是y的集合编号,p[x] = y

优化:压缩路径

模板题1

ITCX7121寒假第二周-L合并集合
在这里插入图片描述

分析

优化:压缩路径:

int find(int x) { //返回x的祖宗节点+压缩路径
	if (p[x] != x)//如果此时这个点x不是根节点
		p[x] = find(p[x]);//就让x的父节点等于x的祖宗节点
	return p[x];
}
  1. 合并操作:让a的祖宗节点的父亲等于b的祖宗节点p[find(a)] = find(b)
  2. 询问操作:判断find(a) == find(b)就行了

代码

#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int p[N];//p表示每个元素它的父节点是谁

int find(int x) { //返回x的祖宗节点+压缩路径
	if (p[x] != x)//如果此时这个点x不是根节点
		p[x] = find(p[x]);//就让x的父节点等于x的祖宗节点
	return p[x];
}

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; 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);//a的祖宗节点的父亲等于b的祖宗节点
		else {//询问a,b是否在同一个集合里
			if (find(a) == find(b))//如果a的祖宗节点等于b的祖宗节点
				puts("Yes");//那么输出Yes
			else
				puts("No");//否则输出No
		}
	}
	return 0;
}

模板题2

ITCX7121寒假第二周-M连通块中点的数量
在这里插入图片描述

分析

size[]数组:表示每一个集合的大小,每一个集合里元素的个数,就是存储根节点所拥有的子节点数

  1. 合并a和b所在的两个集合:
    size[find(b)] += size[find(a)];将节点数加到新根节点数上
    p[find(a)] = find(b);a的根节点的根节点等于b的根节点
  2. 询问点a和点b是否在同一个连通块中:判断find(a) == find(b)
  3. 询问点a所在连通块中点的数量:sizee[find(a)]

代码

#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
//p存储每个点
//size表示每一个集合的大小,每一个集合里元素的个数,就是存储根节点所拥有的子节点数
//只保证根节点的size有意义
int p[N], sizee[N];

int find(int x) {//返回祖宗节点+路径压缩
	if (p[x] != x)
		p[x] = find(p[x]);
	return p[x];
}

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++) {
		p[i] = i;
		sizee[i] = 1;//最开始的时候每一个集合里只有自己一个点
	}
	while (m--) {
		char op[3];
		int a, b;
		scanf("%s", op);
		if (op[0] == 'C') {//连边,就是合并
			scanf("%d%d", &a, &b);
			if (find(a) == find(b))//如果a和b已经在同一个集合里了,就continue跳出
				continue;
			sizee[find(b)] += sizee[find(a)];//将节点数加到新根节点数上
			p[find(a)] = find(b);//a的根节点的根节点等于b的根节点
		} else if (op[1] == '1') {
			scanf("%d%d", &a, &b);
			if (find(a) == find(b))//判断是或在同一个集合里
				puts("Yes");
			else
				puts("No");
		} else {
			scanf("%d", &a);
			printf("%d\n", sizee[find(a)]);
		}
	}
	return 0;
}

堆排序

ITCX7121寒假第二周-O堆排序
在这里插入图片描述

模板题分析

关键写down函数

void down(int u) {
	int t = u;//t存的是三个点中的最小的那个节点编号
	if (u * 2 <= sizee && h[u * 2] < h[t])//如果左儿子存在,并且左儿子小于父亲h[t]
		t = u * 2;//就让t = 左儿子
	if (u * 2 + 1 <= sizee && h[u * 2 + 1] < h[t])//如果右儿子存在,并且右儿子小于父亲h[t]
		t = u * 2 + 1;//就让t = 右儿子
	if (u != t) {//如果根节点u不是三个点中的最小值t,
		swap(h[t], h[u]);//就交换他们
		down(t);
	}
}

模板题代码

//P26堆排序
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, m;
int h[N], sizee;//h是heap堆

void down(int u) {
	int t = u;//t存的是三个点中的最小的那个节点编号
	if (u * 2 <= sizee && h[u * 2] < h[t])//如果左儿子存在,并且左儿子小于父亲h[t]
		t = u * 2;//就让t = 左儿子
	if (u * 2 + 1 <= sizee && h[u * 2 + 1] < h[t])//如果右儿子存在,并且右儿子小于父亲h[t]
		t = u * 2 + 1;//就让t = 右儿子
	if (u != t) {//如果根节点u不是三个点中的最小值t,
		swap(h[t], h[u]);//就交换他们
		down(t);
	}
}

//void up(int u) {
//	while (u / 2 && h[u / 2] > h[u]) {
//		swap(h[u / 2], h[u]);
//		u /= 2;
//	}
//}

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++)
		scanf("%d", &h[i]);
	sizee = n;
	for (int i = n / 2; i; i--)
		down(i);
	while (m--) {
		printf("%d ", h[1]);//每次输出堆顶,也就是最小值
		h[1] = h[sizee];//在删除最小值
		sizee--;
		down(1);
	}
	return 0;
}

模拟堆

ITCX7121寒假第二周-P模拟堆
在这里插入图片描述

模板题分析

手写一个堆:

  1. 插入一个数:heap[++size] = x;up(size);
  2. 求集合当中的最小值:heap[1];
  3. 删除最小值:heap[1] = heap[size];size–;down(1);
  4. 删除任意一个元素:heap[k] = heap[size];size–;down(k);up(k);
  5. 修改任意一个元素:heap[k] = x;down(k);up(k);

模板题代码

//注意:要将size写成sizee
#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;
const int N = 100010;
//p是下标(pointer),h是堆(heap)
//ph就是从下标映射到堆里面去
//ph[k]存的是第k个插入的数的下标是什么,也就是在堆里面的下标是什么
//hp是从堆里面映射到下标
//hp是堆里面的某一个点是第几个插入的点
int h[N], ph[N], hp[N], sizee;

//在堆里面交换两个元素
void heap_swap(int a, int b) {
	swap(ph[hp[a]], ph[hp[b]]);
	swap(hp[a], hp[b]);
	swap(h[a], h[b]);
}

void down(int u) {
	int t = u;
	if (u * 2 <= sizee && h[u * 2] < h[t])
		t =  u * 2;
	if (u * 2 + 1 <= sizee && h[u * 2 + 1] < h[t])
		t = u * 2 + 1;
	if (u != t) {
		heap_swap(u, t);
		down(t);
	}
}

void up(int u) {
	while (u / 2 && h[u / 2] > h[u]) {
		heap_swap(u / 2, u);
		u /= 2;
	}
}

int main() {
	int n, m = 0;
	scanf("%d", &n);
	while (n--) {
		char op[10];
		int k, x;
		scanf("%s", op);
		if (!strcmp(op, "I")) {//1.插入一个数x
			scanf("%d", &x);
			sizee++;//堆里面多一个元素
			m++;//当前是第几个插入的数
			//第m个插入的数一开始在堆里面size这个位置,就是最后一个位置
			//堆里面size这个数一开始对应的是m
			ph[m] = sizee, hp[sizee] = m;
			h[sizee] = x;
			up(sizee);//生成新堆
		} else if (!strcmp(op, "PM"))//2.输出当前集合中的最小值
			printf("%d\n", h[1]);//就是堆顶
		else if (!strcmp(op, "DM")) {//3.删除当前集合中的最小值
			heap_swap(1, sizee);//就是把最后一个元素放到第一个元素的位置上去,堆交换
			sizee--;//删掉最后一个元素
			down(1);//生成新堆
		} else if (!strcmp(op, "D")) {//4.删除第k个插入的数
			scanf("%d", &k);
			k = ph[k];//让k找到它在堆里面的位置
			heap_swap(k, sizee);//删除就是把最后一个元素与k进行堆交换
			sizee--;//删掉最后一个元素
			down(k), up(k);//生成新堆
		} else {//5.修改第k个插入的数,将其变为x
			scanf("%d%d", &k, &x);
			k = ph[k];//先找到第k个插入的数是啥
			h[k] = x;//将堆中第k个插入的数修改为x
			down(k), up(k);//生成新堆
		}
	}
	return 0;
}

补充函数知识点

  1. strcmp函数:
    int strcmp(const char*str1,const char *str2);
    比较两个字符串的大小
    int strcmp(str1,str2);
    str1>str2时,返回大于0的数字
    str1=str2时,返回0
    str1<str2时,返回小于0的数字
  2. strcat函数:
    char *strcat(char*destination,const char *source);
    追加拷贝,追加到目标空间后面,目标空间必须足够大,能容下源字符串的内容
    通俗来讲就是字符串拼接,将后面那个字符串拼接到前面那个字符串后面
  3. strcpy函数:
    char*strcpy(char*destination,const char*source)
    strcpy是覆盖拷贝,将全覆盖拷贝到destination,会把’\0’也拷过去,且destination的空间必须>=source的空间
  4. strlen函数
    strlen函数返回的是在字符串中’\0’前面出现的字符的个数
    int strlen(const char*str)

哈希

哈希表

ITCX7121寒假第二周-R模拟散列表
在这里插入图片描述

拉链法

分析
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 find(int x)
    {
        int k = (x % N + N) % N;
        for (int i = h[k]; i != -1; i = ne[i])
            if (e[i] == x)
                return true;

        return false;
    }
完整代码
//哈希表
//拉链法
#include <iostream>
#include <cstring>
using namespace std;
/*
做哈希的时候,数组长度或者说mod的数N一般要取成质数,
并且这个质数要尽可能离2的整次幂远,这样取冲突的概率最小
先算一下大于100000的第一个质数是什么,就把N取成刚算的数
#include <iostream>
using namespace std;
int main() {
	for (int i = 100000;; i++) {
		bool flag = true;
		for (int j = 2; j * j <= i; j++) {
			if (i % j == 0) {
				flag = false;
				break;
			}
			if (flag) {
				cout << i << endl;
				break;
			}
		}
	}
	return 0;
}//结果:i= 100003;
*/
const int N = 100003;
//h数组开个槽,就是创建一个链表h[k]
int h[N], e[N], ne[N], idx;

void insert(int x) {//把x插到h[k]上
//x可能是负数,在C++中负数模N还是负数,x%N+N就一定是一个正数,所以x就要模N加N再模N
	int k = (x % N + N) % N;
	e[idx] = x;
	ne[idx] = h[k];
	h[k] = idx++;
}

bool find(int x) {//在k这个链表h[k]中找一下有没有x
	int k = (x % N + N) % N;
	for (int i = h[k]; i != -1; i = ne[i])
		if (e[i] == x)//e用来存数值
			return true;
	return false;
}

int main() {
	int n;
	scanf("%d", &n);
	memset(h, -1, sizeof h);//把槽h[N]清空,空指针用-1来表示
	while (n--) {
		char op[2];
		int x;
		scanf("%s%d", op, &x);//尽量用%s读字符
		if (op[0] == 'I')//todo
			insert(x);
		else {
			if (find(x))
				puts("Yes");
			else
				puts("No");
		}
	}
	return 0;
}

开放寻址法

分析
 int h[N];

    // 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置
    int find(int x)
    {
        int t = (x % N + N) % N;
        while (h[t] != null && h[t] != x)
        {
            t ++ ;
            if (t == N) t = 0;
        }
        return t;
    }
完整代码
//哈希表
//开放寻址法
#include <iostream>
#include <cstring>
using namespace std;
const int N = 200003, null = 0x3f3f3f3f;
//0x3f3f3f3f是个大于1e9的数,一定不在x的范围内
int h[N];

//核心操作find
//如果x在哈希表中存在,那么返回x在哈希表中的位置,
//如果x在哈希表中不存在,那么返回x应该在哈希表中存储的位置
int find(int x) {
	int k = (x % N + N) % N;

//当前位置不等于空,也就是坑位上有人,并且这个坑位上的人不等于x,那就应该去看下一个坑位
	while (h[k] != null && h[k] != x) {
		k++;
		if (k == N)//说明已经看完了最后一个坑位
			k = 0;//就循环看第一个坑位
	}
	return k;
}

int main() {
	int n;
	scanf("%d", &n);
//按字节来memset,h是int型数组,有4个字节,每一个字节都是0x3f,每个数就是4个0x3f,
//因此初始化成0x3f
	memset(h, 0x3f, sizeof h);//数组初始化,把h数组初始化成0x3f
	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;
}

字符串哈希

ITCX7121寒假第二周-S字符串哈希
在这里插入图片描述

模板题分析

快速判断两个字符串是否相等,可以用字符串前缀哈希这个做法

核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低
小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果

typedef unsigned long long ULL;
ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64

// 初始化
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
    h[i] = h[i - 1] * P + str[i];
    p[i] = p[i - 1] * P;
}

// 计算子串 str[l ~ r] 的哈希值
ULL get(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];
}

模板题代码

//P28字符串哈希
#include <iostream>
using namespace std;
typedef unsigned long long ULL;
const int  N = 100010, P = 131;
//P一般取131或13331,代表P进制
int  n, m;
char str[N];
ULL h[N], p[N];
//h数组表示某一个值前缀哈希值
//p数组用来表示乘了p的多少次方

ULL get(int l, int r) { //计算l~r区间的哈希值的公式
	return h[r] - h[l - 1] * p[r - l + 1];
}

int main() {
	scanf("%d%d%s", &n, &m, str + 1);
	p[0] = 1;//表示p的0次方等于1
	for (int i = 1; i <= n; i++) {
		p[i] =  p[i - 1] * P;
		h[i] = h[i - 1] * P + str[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;
}

kmp做法可以用来求一个字符串的循环节,字符串前缀哈希不可以
其他都可以用字符串前缀哈希

STL简介

vector

    变长数组(数组长度可以动态变化) 倍增思想
    size()返回元素个数
    empty()返回是否为空
    clear()清空
    front()返回vector第一个数/back()返回vector最后一个数
    push_back向vector最后插入一个数/pop_back把vector最后一个数删掉
    begin()是vector的第0个数/end()是vector的最后一个数的后面那个数 迭代器
    vector支持随机寻址
    vector的三种遍历方式:
    for(int i = 0;i<a.size();i++)cout<<a[i]<<' ';
    for(vector<int>::iterator i = a.begin();i!a.end();i++)cout<<*i<<' ';
    //iterator<int>iterator可以直接写成auto,a.begin()就是a[0],a.end()就是a[size()]
    for(auto x : a)cout<<x<<' ';
    vector 支持比较运算,按字典序

pair<int,int>

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

string 字符串

    size()/length()返回字符串长度
    empty()
    clear()
    '+'号做字符串拼接
    substr()返回某一个子串
    c_str()返回string对应数组的头指针

queue 队列

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

priority_queue 优先队列 (堆)

   默认是大根堆
    push()往堆里插入一个元素
    top()返回堆顶元素
    pop()弹出堆顶元素
    定义成小根堆的方式:
        priority_queue<int,vector<int>,greate<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() 支持++ --操作 返回前驱和后继
    set/multiset:set里没有重复元素,multiset里可以有重复元素
        insert() 插入一个数
        find()查找一个数
        count()返回某一个数的个数
        erase():
            1.输入是一个数x,删除所有x   时间复杂度是o(k+logn) k是x的个数
            2.输入是一个迭代器,删除这个迭代器
        lower_bound()/upper_bound():
            lower_bound(x) 返回大于等于x的最小的数的迭代器
            upper_bound(x) 返回大于x的最小的数的迭代器
    map/multimap://存的是一个映射
        insert()插入的数是一个pair
        erase()输入的参数是pair或者迭代器
        find()
        []可以像数组一样用,时间复杂度是o(logn)

unordered_set,unordered_map,unordered_multiset,unordered_multimap

基于哈希表实现,无序
    和上面类似,增删改查的时间复杂度是o(1)
    不支持lower_bound()/upper_bound()和迭代器的++ --

bitset 压位

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

PS:

   any()返回有多少个1
    count()判断是否至少有一个1
    none()判断是否全为0
    set()把所有位置变成1
    set(k,v)将第k位变成v
    reset()把所有位变成0
    flip()把所有位取反,等价于~
    flip(k)把第k位取反

系统为某一程序分配空间时,所需时间,与空间大小无关,与申请次数有关

  • 17
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值