链表与邻接表: 树与图的存储
数组模拟单链表
链表中的每一个结点只有两个部分。我们可以用一个数组data来存储序列中的每一个数。那么每一个数的右边我们就需要再用一个数组right来存放序列中每一个数。
现在需要在8前面插入一个6,只需要将6直接存放在数组data的末尾data[10] = 6。接下来只需要将right[3]改为10,表示新序列中3号元素右边的元素存放在data[10]中。再将right[10]改为4,表示新序列中10号元素右边的元素存放在data[4]中。
#include<iostream>
using namespace std;
const int N = 100010;
int head, e[N], ne[N], idx;
void init()
{
head = -1;
idx = 0;
}
void add_to_head(int x)
{
e[idx] = x, ne[idx] = head, head = idx ++ ;
}
void add(int k, int x)
{
e[idx] = x, ne[idx] = ne[k], ne[k] = idx ++;
}
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];
remove(k - 1);
}
else
{
cin >> k >> x;
add(k - 1, x);
}
}
for(int i = head; i != -1; i = ne[i]) cout << e[i] << ' ';
cout << endl;
return 0;
}
10
H 9
I 1 1
D 1
D 0
H 6
I 3 6
I 4 5
I 4 5
I 3 4
D 6
// 6 4 6 5
数组模拟单链表
const int N = 100010;
int head,e[N],ne[N],idx;//所有idx表示的是节点的下标,也就是索引,然后当前idx的值表示下一个插入的节点的索引是多少,或者说当前插入新的节点用到的下标是多少,e[N]表示
void init(){
head = -1;//用-1来表示null
idx = 0;//下标也可以从1开始,无所谓
}
//头插法 ,在head后面插入一个节点值为x的节点
void add_to_head(int x){
e[idx] = x;
ne[idx] = head;
head = idx ++;
}
//在索引是k的下一个位置插入x ,如果知道第k的元素的索引 那么后面那个元素删除和插入操作复杂度都是O(1)
void add_after_k(int k,int x){
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx ++;
}
//删除在索引是k的下一个节点
void remove_after_k(int k){
ne[k] = ne[ne[k]];
}
//遍历一个单链表.固定模板
for(int i = head;i != -1;i = ne[i]) {
cout << e[i] << " ";
}
数组模拟双链表
const int N= 100010;
int m;
int e[N],l[N],r[N],idx;
//双链表的的插入应该先把新节点的左右指向先赋值好,然后其他操作都用新界点的左右指向来操作,这样就会方便
//应该这样不会考虑原来的指向指针丢失的问题
void init(){//链表为空的初始化状态,head和tail可以用0 1来代表,所以idx 应该从2开始
r[0] = 1,l[1] = 0;
idx = 2;
}
//在第k个元素的右边插入一个新元素,若想在k的左边插入元素直接调用add(l[k],x)就可以,或者重新实现一编逻辑
void addR(int k,int x){
e[idx] = x;
l[idx] = k;
r[idx] = r[k];
l[r[idx]] = idx;
r[k] = idx ++;
}
void addL(int k,int x) {
e[idx] = x;
r[idx]= k;
l[idx] = l[k];
l[k] = idx;
r[l[idx]] = idx ++;
}
//删除双链表中的第k个元素
void remove_k(int k){
r[l[k]] = r[k];
l[r[k]] = l[k];
}
void add_to_begin(int x){
e[idx] = x;
r[idx] = r[0];
l[idx] = 0;
r[0] = idx;
l[r[idx]] = idx ++;
}
void add_to_last(int x) {
e[idx] = x;
l[idx] = l[1];
r[idx] = 1;
r[l[idx]] = idx;
l[1] = idx ++;
}
数组模拟邻接表
const int N = 100010,M = 100010;//N 为最大节点数,M 为最大边数
int h[N],e[M],ne[M],w[M],idx;//
//加边操作模板
void add(int a,int b,int c){//加入一条从a----->b,而且权值为c,将其看作单链表的操作会更好理解
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
数组模拟栈
const int N = 100010;
int stk[N],tt;//tt表示栈顶元素的索引,为0的时候表示没有元素
void push(int x){
stk[++ tt] = x;
}
//delete
void pop(){
tt --;//删除元素直接指针下移就行
}
int top(){
return stk[tt];
}
//判空
bool empty(){
if(tt > 0) return false;//tt不为0就表示栈不为空
return true;
}
数组模拟队列
const int N = 100010;
int q[N];
int hh,tt = -1;//hh表示队头指针,tt表示队尾指针,最开始队列中一个元素都没有,所以队尾指针为-1.
void offer(int x){
q[ ++ tt] = x;//入队,队尾指针+1
}
void poll(){
hh ++;//出队,队头指针+1
}
bool empty(){
if(hh <= tt) return false;//判断队列是否为空
return true;
}
栈与队列
单调队列
常见模型:找出滑动窗口中的最大值/最小值
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;
}
154.滑动窗口;
单调队列核心思想抽象为三句话:
- 如果一个人年龄比你小(更晚被访问到))却比你厉害且更努力(值更大),那你永远不可能超过ta(你就可以出队了)
- 永远不要小看新人,因为一切皆有可能(所有新访问的数据都会入队)
- 再厉害的人也有退出舞台的那一天(下标超出窗口的范围)
//队列头hh在在左边,队列尾tt在右边 。队列头执行删除操作。队列尾执行添加操作。
#include<iostream>
using namespace std;
const int N = 100010;
int n, k;
int a[N], q[N];
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 ++)
{
//判断队头是否已经滑出窗口
if (hh <= tt && i +1 - q[hh] > k) hh ++;
while (hh <= tt && a[q[tt]] >= a[i]) tt --;
q[ ++ tt] = i;
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 >= k - 1) printf("%d ",a[q[hh]]);
}
puts("");
return 0;
}
输入:
8 3
1 3 -1 -3 5 3 6 7
输出:
-1 -3 -3 -3 3 3
3 3 5 5 6 7
单调栈
单调栈讲解小视频
单调栈
单调栈是一种特殊的栈,特殊之处在于栈内的元素都保持一个单调性。
性质
1.满足从栈顶到栈底的元素具有严格的单调性
2.满足栈的后进先出特性越靠近栈底的元素越早进栈
借助单调性处理问题的思想在于及时排除不可能的选项,保持策略集合的高度有效性和秩序性,从而为我们作出决策提供更多的条件和可能方法。
#include<iostream>
using namespace std;
const int N = 100010;
int n;
int stk[N], tt;
int main()
{
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;
}
return 0;
}
输入
5
3 4 2 7 5
输出
-1 3 -1 2 2
kmp
#include <string>
#include <iostream>
using namespace std;
const int N = 1000010;
string p, s;
int ne[N];
int main()
{
cin >> s >> p;
ne[0] = 0;
ne[1] = 0;
int i = 2, j = 0;
for (i = 2; i < p.size() + 1; i++)
{
while (j && p[i - 1] != p[j]) j = ne[j];
if (p[i - 1] == p[j]) j++;
ne[i] = j;
}
for (i = 0, j = 0; i < s.size(); i++)
{
while (j && s[i] != p[j]) j = ne[j];
if (s[i] == p[j]) j++;
if (j == p.size())
{
cout << i - j + 1 << " ";
return 0;
}
}
}
Trie
并查集
#include<iostream>
using namespace std;
const int N = 100010;
int n, m;
int p[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;
while (m --)
{
char op[2];
int a, b;
scanf("%s%d%d", op, &a, &b);
if (op[0] == 'M') p[find(a)] = find(b);
else
{
if (find(a) == find(b)) puts("Yes");
else puts("No");
}
}
return 0;
}
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
#include<iostream>
using namespace std;
const int N = 100010;
int n, m;
int p[N], Size[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;
Size[i] = 1;
}
while (m --)
{
char op[5];
int a, b;
scanf("%s", op);
if (op[0] == 'C')
{
scanf("%d%d",&a, &b );
if (find(a) == find(b)) continue;
Size[find(b)] += Size[find(a)];
p[find(a)] = find(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", Size[find(a)]);
}
}
return 0;
}
// 5 5
// C 1 2
// Q1 1 2
// Q2 1
// C 2 5
// Q2 5
堆
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int n, m;
int h[N], Size;
void down(int u)
{
int t = u;
if (t * 2 <= Size && h[u * 2] < h[t]) t = u * 2;
//判断左儿子是否在必须的范围内,如果是,则判断是否小于父节点的值,如果是则更新节点
if (u * 2 + 1 <= Size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
// 判断右儿子是否在必须的范围内,如果是,则判断是否小于父节点的值,如果是则更新节点
if (u != t)
{
swap(h[u], h[t]);
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]);
Size = n;
for (int i = n / 2; i; i --) down(i);
while (m --)
{
printf("%d ", h[1]);
h[1] = h[Size];
Size --;
down(1);
}
return 0;
}
分析
i为什么从n/2开始down?
首先要明确要进行down操作时必须满足左儿子和右儿子已经是个堆。
开始创建堆的时候,元素是随机插入的,所以不能从根节点开始down,而是要找到满足下面三个性质的结点:
-
左右儿子满足堆的性质。
-
下标最大(因为要往上遍历)
-
不是叶结点(叶节点一定满足堆的性质)
那这个点为什么时n/2?看图。
#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
const int N = 100010;
int n, m;
int h[N], ph[N], hp[N], Size;
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 (t * 2 <= Size && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= Size && 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"))
{
scanf("%d", &x);
Size ++;
m ++;
ph[m] = Size, hp[Size] = m;
h[Size] = x;
up(Size);
}
else if (!strcmp(op, "PM")) printf("%d\n", h[1]);
else if (!strcmp(op, "DM"))
{
heap_swap(1, Size);
Size -- ;
down(1);
}
else if (!strcmp(op, "D"))
{
scanf("%d", &k);
k = ph[k];
heap_swap(k, Size);
Size -- ;
down(k), up(k);
}
else
{
scanf("%d%d", &k, &x);
k = ph[k];
h[k] = x;
down(k), up(k);
}
}
return 0;
}
// 10
// I -10
// PM
// I -10
// D 1
// C 2 8
// I 6
// PM
// DM
// -10
// 6
Hash表
哈希表
可以存储各种类型的数据,当我们从哈希表中查找所需要的数据时,理想情况是不经过任何比较,一次存取便能得到所查记录,那就必须在记录的存储位置和它的关键字之间建立一个确定的对应关系 f,使每个关键字和结构中一个唯一的存储位置相对应。(关键字就是所要存储的数据,存储位置相当于数组的索引)
哈希函数
假如,我们所要存储的数据其关键字是一个人的身份证号(18位数字),这个时候我们该怎么计算关键字对应的索引呢?
比如一个人的身份证号是 411697199702076425,我们很难像例1那样直接让关键字与数字建立一一对应的关系,并且保证数字适合作为数组的索引。
哈希冲突
假如,我们所要存储的数据其关键字是一个人的身份证号(18位数字),这个时候我们该怎么计算关键字对应的索引呢?
比如一个人的身份证号是 411697199702076425,我们很难像例1那样直接让关键字与数字建立一一对应的关系,并且保证数字适合作为数组的索引。
在这种情况下,通过哈希函数计算出的索引,即使关键字不同,索引也会有可能相同。这就是哈希冲突
当索引相同时,我们该怎么存储数据呢?如何解决哈希冲突,是我们建哈希表的另一个关键问题。
哈希表如何实现时间和空间的平衡
空间换时间
哈希表充分体现了空间换时间这种经典的算法思想。
关键字是大整数时,比如上面我们举的身份证号例子,411697199702076425
假如我们能开辟一个 999999999999999999 大的空间,这样就能直接把身份证号作为关键字存储到数组中,这样可以用O(1)时间完成各项操作
假如我们只有 1 的空间,我们需要把所有信息存储到这个空间中(也就是所有数据都会产生哈希冲突),我们只能用O(n)时间完成各项操作
事实上,我们不可能开辟一个如此大的空间,也不可会开辟如此小的空间
无限空间时,时间为O(1) 1的空间时,时间为O(n)
而哈希表就是在二者之间产生一个平衡,即空间和时间的平衡。
// 拉链法
#include<cstring>
#include<iostream>
using namespace std;
const int N = 200003;
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;
}
int main()
{
int n;
scanf("%d", &n);
memset(h, -1, sizeof h);
while (n -- )
{
char op[2];
int x;
scanf("%s%d", op, &x);
if(*op == 'I') insert(x);
else
{
if(find(x)) puts("Yes");
else puts("No");
}
}
return 0;
}
5
I 1
I 2
I 3
Q 2
Q 5
Yes
No
//开放寻址法
#include<iostream>
#include<cstring>
using namespace std;
const int N = 200003, 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;
scanf("%d", &n);
memset(h, 0x3f, sizeof h);// h是一个int数,每一位都是0x3f,因此每一个数为0x3f3f3f3f memeset是按字节来操作的
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;
}
5
I 1
I 2
I 3
Q 2
Q 5
Yes
No
字符串前缀哈希法
#include<iostream>
using namespace std;
typedef unsigned long long ULL;
const int N = 100010, P = 131;
int n, m;
char str[N];
ULL h[N], p[N];
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
int main()
{
scanf("%d%d%s", &n, &m, str + 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("N0");
}
return 0;
}
// 8 3
// aabbaabb
// 1 3 5 7
// 1 3 6 8
// 1 2 1 2
C++ STL使用技巧
vector 的遍历
#include<vector>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int main()
{
vector<int> a;
for (int i = 0; i < 10; i ++ ) a.push_back(i);
for(int i = 0; i < a.size(); i ++ ) cout << a[i] << ' ';
cout << endl;
for (vector<int>::iterator i = a.begin(); i != a.end(); i ++) cout << *i << ' ';
cout << endl;
for (auto x : a) cout << x << ' ';
cout << endl;
return 0;
}
vector, 变长数组,倍增的思想
size() 返回元素个数
empty() 返回是否为空
clear() 清空
front()/back()
push_back()/pop_back()
begin()/end()
[]
支持比较运算,按字典序
pair<int, int>
first, 第一个元素
second, 第二个元素
支持比较运算,以first为第一关键字,以second为第二关键字(字典序)
string,字符串
size()/length() 返回字符串长度
empty()
clear()
substr(起始下标,(子串长度)) 返回子串
c_str() 返回字符串所在字符数组的起始地址
queue, 队列
size()
empty()
push() 向队尾插入一个元素
front() 返回队头元素
back() 返回队尾元素
pop() 弹出队头元素
priority_queue, 优先队列,默认是大根堆
size()
empty()
push() 插入一个元素
top() 返回堆顶元素
pop() 弹出堆顶元素
定义成小根堆的方式:priority_queue<int, vector<int>, greater<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()
++, -- 返回前驱和后继,时间复杂度 O(logn)
set/multiset
insert() 插入一个数
find() 查找一个数
count() 返回某一个数的个数
erase()
(1) 输入是一个数x,删除所有x O(k + logn)
(2) 输入一个迭代器,删除这个迭代器
lower_bound()/upper_bound()
lower_bound(x) 返回大于等于x的最小的数的迭代器
upper_bound(x) 返回大于x的最小的数的迭代器
map/multimap
insert() 插入的数是一个pair
erase() 输入的参数是pair或者迭代器
find()
[] 注意multimap不支持此操作。 时间复杂度是 O(logn)
lower_bound()/upper_bound()
unordered_set, unordered_map, unordered_multiset, unordered_multimap, 哈希表
和上面类似,增删改查的时间复杂度是 O(1)
不支持 lower_bound()/upper_bound(), 迭代器的++,--
bitset, 圧位
bitset<10000> s;
~, &, |, ^
>>, <<
==, !=
[]
count() 返回有多少个1
any() 判断是否至少有一个1
none() 判断是否全为0
set() 把所有位置成1
set(k, v) 将第k位变成v
reset() 把所有位变成0
flip() 等价于~
flip(k) 把第k位取反