算法基础(数据结构)
文章目录
单链表
826.单链表
- 用数组形式来模拟单链表,在算法题中,因为 new ListNode 操作比较慢,容易引起超时,所以用数组来代替指针比较快速
int e[N], ne[N], idx, head;
// e[] 为链表的数值数组, idx 为链表下标, ne[] 为链表当前的下一个下标, head 为链表头结点指向的下标
// 初始化操作
void init(){
head = -1, idx = 0; // 当链表到结尾是 head 指向下标为-1, 链表的第一个数值的下标为 idx=0
}
// 在链表的头部添加结点 数值为x
void addHead(int x){
e[idx] = x;
ne[idx] = head;
head = idx++;
}
// 删除第k个插入的数的后面的数 (第k个插入的数的下标为k-1)
void deleteK(int k){
ne[k] = ne[ne[k]];
}
// 在第k个插入的数后面插入一个数 x
void insertK(int k, int x){
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx++;
}
双链表
827.双链表
const int N = 100010;
int e[N], l[N], r[N], idx, k; //e[N]为节点,l[N]为左节点, r[N]为右节点
void init() //初始化
{
r[0] = 1;
l[0] = 1;
r[1] = 0;
l[1] = 0;
idx = 2;
}
void insert(int k, int x) // 在右边插入操作
{
e[idx] = x;
r[idx] = r[k];
l[idx] = k;
l[r[k]] = idx;
r[k] = idx;
idx++;
}
void remove(int k) // 删除节点
{
r[l[k]] = r[k];
l[r[k]] = l[k];
}
栈
822.模拟栈
const int N = 1e5 + 10;
int s[N]; //s数组为栈
int tt = -1; // tt为数组下标
void push(int s[], int x){ // 压入栈操作
s[++tt] = x;
}
void pop(int s[]){ // 弹出栈操作
tt--;
}
bool empty(int s[]){ // 判断栈是否为空
return tt > -1 ? false : true;
}
int query(int s[]){ // 查询栈顶元素
return s[tt];
}
3302. 表达式求值
-
两个核心关键点
-
双栈,一个操作数栈,一个运算符栈
-
运算法优先级,栈顶运算符和即将入栈的运算符的优先级的比较 -----------> 如果栈顶的运算符优先级低,新运算符直接入栈;如果栈顶的运算符优先级高或者和即将入栈的运算符等级相等,先出栈计算,新运算符再入栈
-
这个方法的时间复杂度为O(n),整个字符串只需要扫描一遍。
运算符有±/()~^&都没问题,如果共有n个运算符,会有一个nn的优先级表。
stack<int> num; stack<char> op; //优先级表 unordered_map<char, int> h{ {'+', 1}, {'-', 1}, {'*',2}, {'/', 2} }; void eval()//求值 { int a = num.top();//第二个操作数 num.pop(); int b = num.top();//第一个操作数 num.pop(); char p = op.top();//运算符 op.pop(); int r = 0;//结果 //计算结果 if (p == '+') r = b + a; if (p == '-') r = b - a; if (p == '*') r = b * a; if (p == '/') r = b / a; num.push(r);//结果入栈 } int main() { string s;//读入表达式 cin >> s; for (int i = 0; i < s.size(); i++) { if (isdigit(s[i]))//数字入栈 { int x = 0, j = i;//计算数字 while (j < s.size() && isdigit(s[j])) { x = x * 10 + s[j] - '0'; // 将s[j] 从char 变为 int j++; } num.push(x);//数字入栈 i = j - 1; } //左括号无优先级,直接入栈 else if (s[i] == '(')//左括号入栈 { op.push(s[i]); } //括号特殊,遇到左括号直接入栈,遇到右括号计算括号里面的 else if (s[i] == ')')//右括号 { while(op.top() != '(')//一直计算到左括号 eval(); op.pop();//左括号出栈 } else { while (op.size() && h[op.top()] >= h[s[i]])//待入栈运算符优先级低,则先计算 eval(); op.push(s[i]);//操作符入栈 } } while (op.size()) eval();//剩余的进行计算 cout << num.top() << endl;//输出结果 return 0; }
-
队列
829. 模拟队列
hh = tt = -1; // hh为头下标, tt 为尾下标
// 插入元素
s[++tt] = x;
// 弹出元素
s[hh++] = x;
单调栈
830. 单调栈
// 单调栈---找到左边离它最近的比他小的数
const int N = 1e5 + 10;
int s[N], tt = -1;
bool empty(int s[]){
if(tt > -1) return false;
return true;
}
/*
思路: 先判断栈是否为空
1. 如果为空:输出-1,将输入的数压入栈中;
2. 如果不为空:分两种情况
1. 如果输入的数大于栈顶元素,输出栈顶元素,并将输入的数压入栈中
2. 如果输入的数小于或者等于栈顶元素,将栈顶元素弹出直到栈为空或者栈顶元素小于输入元素为止
*/
int main()
{
int n;
cin >> n;
for (int i = 0; i < n; i ++ ){
int x;
cin >> x;
while(s[tt] >= x && !empty(s))
tt--;
if(empty(s)) cout << -1 << " ";
else cout << s[tt] << " ";
s[++tt] = x;
}
return 0;
}
单调队列
154.滑动窗口
const int N = 1e6 + 10;
int q[N], res[N]; //q[]存储输入数组,**res[]存储进入队列的数组的下标**
int main()
{
int n, k;
cin >> n >> k;
for (int i = 0; i < n; i ++ ){
cin >> q[i];
}
int hh =0, tt = -1;
for (int i = 0; i < n; i ++ ){
if(hh <= tt && i - k + 1 > res[hh]) hh++; // 当队列还有数并且队头元素下标小于i-k+1时,弹出队头元素
while(hh <= tt && q[i] < q[res[tt]]) // 当队列还有数 并且 当即将入队元素小于队尾元素时,将队尾元素删去,使得队列为单调递增队列
{
tt--;
}
res[++tt] = i; //每次都将下表为i的元素压入队列
if(i - k + 1 >= 0) cout << q[res[hh]] << " ";
}
puts("");
hh = 0, tt = -1;
for (int i = 0; i < n; i ++ ){
if(hh <= tt && i - k + 1 > res[hh]) hh++;
while(hh <= tt && q[i] > q[res[tt]]) tt--;
res[++tt] = i;
if(i - k + 1 >= 0) cout << q[res[hh]] << " ";
}
return 0;
}
KMP
831.kmp字符串
const int N = 1e5 + 10;
const int M = 1e6 + 10;
int n, m;
char s[N], p[M];
int ne[N]; // next数组会报错,将next数组变为ne数组
int main()
{
cin >> n >> p + 1 >> m >> s + 1; //从下标为1开始输入
//求子串的ne数组
for (int i = 2, j = 0; i <= n; i ++ ){ //ne[1] = 0,从下标2开始计算ne数组
while(j && p[i] != p[j + 1]) j = ne[j];
if(p[i] == p[j + 1]) j++;
ne[i] = j;
}
//匹配
for (int 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){
cout << i - j << " ";
j = ne[j];
}
}
return 0;
}
Trie
- 解决字符串出现多少次,需要先构建好trie树后在进行查询操作
- 一般两个操作:1. 插入操作 2. 查询操作
835.tried字符串统计
const int N = 1e5 + 10;
int son[N][26]; // son[a][b] 数组表示a的下一个字母
int idx, cnt[N]; // idx为新增结点下标,cnt[N]表示 输入字符串出现的次数
char str[N];
//插入操作
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]; //令p为字符串的下一个字母
}
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];
}
143.最大异或树
const int N = 1e5 + 10;
const int M = 3.2e6 + 10;
int a[N];
int son[M][2], idx = 0; //idx 是结点下标
void insert(int x){ //构建异或树
int p =0;
for (int i = 30; i >= 0; i -- ){
int u = x >> i & 1; //判断x的第i位数是0还是1
if(!son[p][u]) son[p][u] = ++idx;
p = son[p][u];
}
}
int query(int x){ //查询最大异或值
int p = 0;
int res = 0;
for (int i = 30; i >= 0; i -- ){
int u = x >> i & 1;
if(son[p][!u]){
p = son[p][!u];
res = res * 2 + 1;
}else{
p = son[p][u];
res = res * 2;
}
}
return res;
}
int main()
{
int n;
cin >> n;
for (int i = 0; i < n; i ++ ){
cin >> a[i];
insert(a[i]);
}
int res = 0;
for (int i = 0; i < n; i ++ ){
res = max(res, query(a[i]));
}
cout << res;
return 0;
}
并查集
- 主要解决集合的 合并 和 查询操作
836.合并集合
const int N = 1e5 + 10;
int p[N]; // p[]是父亲节点
int find(int x) // 并查集 (find函数查找元素所属并查集)
{
if (p[x] != x) p[x] = find(p[x]); // 递归查询x的祖先节点 --- 路径压缩
return p[x];
}
int main()
{
int n, m;
cin >> n >> m;
for (int i = 0; i < n; i ++ )
p[i] = i; //每个数的父亲节点都是自己
char op;
while (m -- ){
int a, b;
cin >> op >> a >> b;
if(op == 'M') p[find(a)] = find(b); // 合并操作:将a的祖先节点的父节点指向b的祖先节点让a,b变为一个集合
else {
if(find(a) == find(b))
cout << "Yes" << endl;
else cout << "No" << endl;
}
}
return 0;
}
837.连通快中的数量
int find(int x){
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main()
{
int n,m;
cin >> n >> m;
for (int i = 0; i < n; i ++ ){
p[i] = i;
countI[i] = 1;
}
while(m--){
string op;
int a, b;
cin >> op;
if(op == "C"){
cin >> a >> b;
if(find(a) != find(b)){
countI[find(a)] += countI[find(b)]; // 连接操作前将集合的总数的下表count更新 --- 主要操作
p[find(b)] = find(a); // 将集合根节点更新
}
}
else if(op == "Q1"){
cin >> a >> b;
if(find(a) == find(b)) cout << "Yes" << endl;
else cout << "No" << endl;
}
else{
cin >> a;
cout << countI[find(a)] << endl;
}
}
824 食物链
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 5e4 + 10;
int p[N];
int d[N]; // d[]代表节点到根节点的距离,模3等于0 为 A 类,等于1为B类,等于2为C类;B吃A,C吃B,A吃C
int find(int x) // 并查集
{
if (p[x] != x){
int t = find(p[x]); //先保存x的根节点
d[x] += d[p[x]]; // x到根节点的距离等于当前节点到px距离加上px到根节点距离
p[x] = t;
}
return p[x];
}
int main()
{
int n, k;
cin >> n >> k;
for (int i = 1; i <= n; i ++ ){
p[i] = i;
}
int res = 0;
while(k--){
int a, x, y;
cin >> a >> x >> y;
int px = find(x), py = find(y);
if(x > n || y > n) res++;
else {
if(a == 1){
if(px == py && (d[x] - d[y]) % 3) res++;
else if(px != py){
p[px] = py;
d[px] = d[y] - d[x];
}
}
else{
if(x == y) res++;
else if(px == py && (d[x] - d[y] - 1) % 3) res++;
else if(px != py){
p[px] = py;
d[px] = d[y] + 1 - d[x];
}
}
}
}
cout << res;
return 0;
}
堆排序
838 堆排序
堆排序操作:
1. 建立堆 — 从 n/2 位置开始执行down操作到1为止
void down(int u, int cnt){ //down操作--递归操作 判断左右儿子中有没有比自己更小的数然后交换之,随后从交换的位置处递归down操作
int t = u;
if(2 * u <= cnt && h[t] > h[2 * u]) t = 2 * u;
if(2 * u + 1 <= cnt && h[t] > h[2 * u + 1]) t = 2 * u + 1;
if(t != u)
{
swap(h[t], h[u]);
down(t, cnt);
}
2. 插入数 --- 在对尾处插入数,随后执行up操作,和down操作类似,看父节点是否比自己小,然后交换
3. 删除任意数 --- 令h[k] = h[size--] ,随后执行down操作,和up操作,两者只会有一个被执行
4. 修改任意一个数 -- h[k] = x,随后执行down操作,和up操作,两者只会有一个被执行
模拟堆
主要解决映射问题,建立一个ph和hp数组,ph[i]对应第i个插入堆得数现在在堆中的下标,hp[i]对应堆得下标为i得数是第几个插入堆的。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int h[N], ph[N], hp[N]; // ph映射到堆,hp堆到映射
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 p, int cnt)
{
int t = p;
if(t * 2 <= cnt && h[p] > h[t * 2]) p = t * 2;
if(t * 2 + 1 <= cnt && h[p] > h[t * 2 + 1]) p = t * 2 + 1;
if( t != p)
{
heap_swap(p, t);
down(p, cnt);
}
}
void up(int p)
{
int t = p;
if(t / 2 && h[t / 2] > h[p]) p = t / 2;
if(t != p)
{
heap_swap(t,p);
up(p);
}
}
int main()
{
int n;
cin >> n;
int cnt = 0, m = 0; // cnt为堆的总数,m为插入的数的下标
while(n--)
{
string op;
int k, x;
cin>>op;
if(op == "I")
{
scanf("%d", &x);
cnt++;
m++;
ph[m] = cnt, hp[cnt] = m;
h[cnt] = x;
up(cnt);
}
else if(op == "PM") printf("%d\n", h[1]);
else if(op == "DM")
{
heap_swap(1, cnt);
cnt--;
down(1,cnt);
}
else if(op == "D")
{
scanf("%d", &k);
k = ph[k];
heap_swap(k,cnt);
cnt--;
down(k,cnt);
up(k);
}
else
{
scanf("%d%d", &k, &x);
k = ph[k];
h[k] = x;
down(k,cnt);
up(k);
}
}
return 0;
}
哈希表
840.模拟散列表
哈希表需要开一个大小为质数且最接近数据范围大小的数组
查找质数方法
for(int i = 100000; ; i++){ //寻找100000后面的第一个质数
bool flag = true;
for(int j = 2; j * j <= i; j++){
if(i % j == 0)
{
flag = false;
break;
}
}
if(flag)
{
cout << i << endl;
break;
}
}
-
开放寻址法
-
开放寻址法一般就是要开数据范围2-3倍大小的数组
-
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 200003; const int null = 0x3f3f3f3f; //规定空指针 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() { int n, x; cin >> n; string op; memset(h, 0x3f, sizeof h); while (n -- ) { cin >> op >> x; if(op == "I") { int k = find(x); h[k] = x; } else { if(h[find(x)] == null) puts("No"); else puts("Yes"); } } return 0; }
-
-
拉链法
-
拉链法就是要构造单链表
-
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 200003; int h[N], e[N], ne[N], idx = 0; // 构造单链表 void insert(int x) { int k = (x % N + N) % N; idx++; e[idx] = x; ne[idx] = h[k]; h[k] = idx; } bool find(int x) { bool flag = false; int k = (x % N + N) % N; for(int i = h[k]; i != -1; i = ne[i]) { if(e[i] == x) { flag = true; break; } } return flag; } int main() { int n, x; cin >> n; string op; memset(h, -1, sizeof h); while (n -- ) { cin >> op >> x; if(op == "I") { insert(x); } else { if(find(x)) puts("Yes"); else puts("No"); } } return 0; }
-
841.字符串哈希
- 字符串哈希和kmp算法近似,使用kmp算法的可以使用字符串哈希
- 字符串哈希一般指前缀字符串哈希-----比较重要
- 将字符串映射成p进制的数然后modQ 映射成[0, Q-1]的值
- 注意:
- 不能映射成0
- p一般取131或者13331;Q=2^64时,可以有99.99%的把握认为没有冲突
#include<iostream>
#include<cstdio>
#include<string>
using namespace std;
typedef unsigned long long ULL;
const int N = 1e5+5,P = 131;//131 13331
ULL h[N],p[N];
// h[i]前i个字符的hash值
// 字符串变成一个p进制数字,体现了字符+顺序,需要确保不同的字符串对应不同的数字
// P = 131 或 13331 Q=2^64,在99%的情况下不会出现冲突
// 使用场景: 两个字符串的子串是否相同
ULL query(int l,int r){
return h[r] - h[l-1]*p[r-l+1];
}
int main(){
int n,m;
cin>>n>>m;
string x;
cin>>x;
//字符串从1开始编号,h[1]为前一个字符的哈希值
p[0] = 1;
h[0] = 0;
for(int i=0;i<n;i++){
p[i+1] = p[i]*P;
h[i+1] = h[i]*P +x[i]; //前缀和求整个字符串的哈希值
}
while(m--){
int l1,r1,l2,r2;
cin>>l1>>r1>>l2>>r2;
if(query(l1,r1) == query(l2,r2)) printf("Yes\n");
else printf("No\n");
}
return 0;
}