学习札礼——数据结构
哈希表
出了不能求循环节在都比KMP强
把-10e9~10e9的数映射为0~10e5,xmodN(要把N设置为第一个大于的质数,减少冲突)
开放寻址法
核心就是先找个一个位置,如果这个位置上有数就往看下一个位置,直到找到没得数的位置。一般把数组开成原来的2~3倍大小。
const int N=200003,null=0x3f3f3f3f;
int h[N];
int find(int x)// 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置
{
int k=(x%N+N)%N;//x%N可能是负数但绝对值一定比N小,再加N就为正了
while(h[k]!=null&&h[k]!=x)
{
k++;
if(k==N) k=0;
}
return k;
}
拉链法
在每个余数下拉一条链表
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;
}
字符串哈希
要快速判断两个字符串是否相等时用字符串哈希

注意:不能映射成0
核心思想:将字符串看成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];
}
C++ STL简介
1. #include <vector>
vector是变长数组,支持随机访问,不支持在任意位置O(1)插入。为了保证效率,元素的增删一般应该在末尾进行。支持比较运算(>、<、>=、<=、!=、==)。按字典序即先比较第一个在比较下一个以此类推
声明
#include <vector> 头文件
vector<int> a; 相当于一个长度动态变化的int数组
vector<int> b[233]; 相当于第一维长233,第二维长度动态变化的int数组
struct rec{…};
vector<rec> c; 自定义的结构体类型也可以保存在vector中
vector<int> a(30); 相当于一个长度为30的int数组
size/empty
size函数返回vector的实际长度(包含的元素个数),empty函数返回一个bool类型,表明vector是否为空。二者的时间复杂度都是O(1)。
所有的STL容器都支持这两个方法,含义也相同,之后我们就不再重复给出。
clear
clear函数把vector清空。
迭代器
迭代器就像STL容器的“指针”,可以用星号“*”操作符解除引用。
一个保存int的vector的迭代器声明方法为:
vector<int>::iterator it;
vector的迭代器是“随机访问迭代器”,可以把vector的迭代器与一个整数相加减,其行为和指针的移动类似。可以把vector的两个迭代器相减,其结果也和指针相减类似,得到两个迭代器对应下标之间的距离。
begin/end
begin函数返回指向vector中第一个元素的迭代器。例如a是一个非空的vector,则*a.begin()与a[0]的作用相同。
所有的容器都可以视作一个“前闭后开”的结构,end函数返回vector的尾部,即第n个元素再往后的“边界”。*a.end()与a[n]都是越界访问,其中n=a.size()。
下面两份代码都遍历了vector<int>a,并输出它的所有元素。
for (int I = 0; I < a.size(); I ++) cout << a[i] << endl;
for (vector<int>::iterator it = a.begin(); it != a.end(); it ++) cout << *it << endl;
迭代器:
vector<int>:: iterator it;
auto it=s2.begin();
while(it!=s2.end()) {
cout <<*it<<endl;
++it;}
范围for:
for(int x:a) cout<<x<<endl;
for(datatype rangeVariable:array)
statement;
现在来仔细看一看该格式的各个部分:
dataType:是范围变量的数据类型。它必须与数组元素的数据类型一样,或者是数组元素可以自动转换过来的类型。auto自动
rangeVariable:是范围变量的名称。该变量将在循环迭代期间接收不同数组元素的值。在第一次循环迭代期间,它接收的是第一个元素的值;在第二次循环迭代期间,它接收的是第二个元素的值;以此类推。
array:是要让该循环进行处理的数组的名称。该循环将对数组中的每个元素迭代一次。
statement:是在每次循环迭代期间要执行的语句。要在循环中执行更多的语句,则可以使用一组大括号来包围多个语句。
front/back
front()函数返回vector的第一个元素,等价于*a.begin() 和 a[0]。
Back()函数返回vector的最后一个元素,等价于*==a.end() 和 a[a.size() – 1]。
push_back() 和 pop_back()
a.push_back(x) 把元素x插入到vector a的尾部。
b.pop_back() 删除vector a的最后一个元素。
2. #include <queue>
头文件queue主要包括循环队列queue和优先队列priority_queue两个容器。
这里的int就像英语中的do代表某个动词类似,代表某个类型。优先队列就是堆。
声明
queue<int> q;
struct rec{…}; queue<rec> q; //结构体rec中必须定义小于号
priority_queue <int,vector<int>,less<int> >q;
等价于priority_queue<int> q; // 大根堆,降序队列,往外弹最大的数
priority_queue<int, vector<int>, greater<int>>q; // 小根堆,升序队列,往外弹最小的数
priority_queue<pair<int, int>>q;
循环队列 queue
push 从队尾插入只可队头
pop 从队头弹出只可队头
front 返回队头元素
back 返回队尾元素
优先队列 priority_queue
push 把元素插入堆
pop 删除堆顶元素(最大/小值)
top 查询堆顶元素(最大/小值)
3. #include <stack>
头文件stack包含栈。声明和前面的容器类似。
push 向栈顶插入
pop 弹出栈顶元素
4. #include <deque>
双端队列deque是一个支持在两端高效插入或删除元素的连续线性存储空间。它就像是vector和queue的结合。与vector相比,deque在头部增删元素仅需要O(1)的时间;与queue相比,deque像数组一样支持随机访问。
a[i] 随机访问
a.begin/end,返回deque的头/尾迭代器
front/back 队头/队尾元素
push_back 从队尾入队
push_front 从队头入队
pop_back 从队尾出队
pop_front 从队头出队
clear 清空队列
5. #include <set>
头文件set主要包括set和multiset两个容器,分别是“有序集合”和“有序多重集合”,即前者的元素不能重复,而后者可以包含若干个相等的元素。set和multiset的内部实现是一棵红黑树,它们支持的函数基本相同。
#include<unordered_set> unordered_set和unordered_multiset两个容器是无序的set,没有lower_bound/upper_bound其余一样。底层是哈希表
声明
set<int> s;
struct rec{…}; set<rec> s; // 结构体rec中必须定义小于号
struct rec
{
Int x,y;
bool operator< (const rec& t) const
{
return x<t.x;
}
};重载小于号
multiset<double> s;
size/empty/clear
与vector类似
迭代器
set和multiset的迭代器称为“双向访问迭代器”,不支持“随机访问”,支持星号(*)解除引用,仅支持”++”和“--“两个与算术相关的操作。
设it是一个迭代器,例如set<int>::iterator it;
若把it++,则it会指向“下一个”元素。这里的“下一个”元素是指在元素从小到大排序的结果中,排在it下一名的元素。同理,若把it--,则it将会指向排在“上一个”的元素。
begin/end
返回集合的首、尾迭代器,时间复杂度均为O(1)。
s.begin() 是指向集合中最小元素的迭代器。
s.end() 是指向集合中最大元素的下一个位置的迭代器。换言之,就像vector一样,是一个“前闭后开”的形式。因此--s.end()是指向集合中最大元素的迭代器。
insert
s.insert(x)把一个元素x插入到集合s中,时间复杂度为O(logn)。
在set中,若元素已存在,则不会重复插入该元素,对集合的状态无影响。
find
s.find(x) 在集合s中查找等于x的元素,并返回指向该元素的迭代器。若不存在,则返回s.end()。时间复杂度为O(logn)。
lower_bound/upper_bound
这两个函数的用法与find类似,但查找的条件略有不同,时间复杂度为 O(logn)。
s.lower_bound(x) 查找大于等于x的元素中最小的一个,并返回指向该元素的迭代器。
s.upper_bound(x) 查找大于x的元素中最小的一个,并返回指向该元素的迭代器。
erase
设it是一个迭代器,s.erase(it) 从s中删除迭代器it指向的元素,时间复杂度为O(logn)
设x是一个元素,s.erase(x) 从s中删除所有等于x的元素,时间复杂度为O(k+logn),其中k是被删除的元素个数。
count
s.count(x) 返回集合s中等于x的元素个数,时间复杂度为 O(k +logn),其中k为元素x的个数。
6. #include <map>
map容器是一个键值对key-value的映射,其内部实现是一棵以key为关键码的红黑树。map的key和value可以是任意类型,其中key必须定义小于号运算符。
#include<unordered_map> unordered_map效率高但不支持二分,底层是哈希表
声明
map<key_type, value_type> name;
例如:
map<long, long, bool> vis;
map<string, int> hash;
map<pair<int, int>, vector<int>> test;
size/empty/clear/begin/end均与set类似。
Insert/erase
与set类似,但其参数均是pair<key_type, value_type>。
find
h.find(x) 在变量名为h的map中查找key为x的二元组。
[ ]操作符
h[key] 返回key映射的value的引用,时间复杂度为O(logn)。
[ ]操作符是map最吸引人的地方。我们可以很方便地通过h[key]来得到key对应的value,还可以对h[key]进行赋值操作,改变key对应的value。
map<int,string>a;
a[2]=”as“
7. #include <utility>
pair是将2个数据组合成一组数据,当需要这样的需求时就可以使用pair,如stl中的map就是将key和value放在一起来保存。另一个应用是,当一个函数需要返回2个数据的时候,可以选择pair。 pair的实现是一个结构体,主要的两个成员变量是first second 因为是使用struct不是class,所以可以直接使用pair的成员变量。支持比较运算。先比较first在比较second
类模板: pair < T1, T2> template
参数:T1是第一个值的数据类型,T2是第二个值的数据类型。
功能:pair将一对值(T1和T2)组合成一个值,
这一对值可以具有不同的数据类型(T1和T2),
两个值可以分别用pair的两个公有函数first和second访问。
pair<T1, T2> p1; //创建一个空的pair对象(使用默认构造),它的两个元素分别是T1和T2类型,采用值初始化。
pair<T1, T2> p1(v1, v2); //创建一个pair对象,它的两个元素分别是T1和T2类型,其中first成员初始化为v1,second成员初始化为v2。
make_pair(v1, v2); // 以v1和v2的值创建一个新的pair对象,其元素类型分别是v1和v2的类型。
p1 < p2; // 两个pair对象间的小于运算,其定义遵循字典次序:如 p1.first < p2.first 或者 !(p2.first < p1.first) && (p1.second < p2.second) 则返回true。
p1 == p2; // 如果两个对象的first和second依次相等,则这两个对象相等;该运算使用元素的==操作符。
p1.first; // 返回对象p1中名为first的公有数据成员
p1.second; // 返回对象p1中名为second的公有数据成员
在创建pair对象时,必须提供两个类型名,两个对应的类型名的类型不必相同
pair<string, string> author("James","Joy"); // 创建一个author对象,两个元素类型分别为string类型,并默认初始值为James和Joy。
pair<string, int> name_age("Tom", 18);//创建一个name_age对象,两个元素类型分别为string类型和int类型,并默认初始值为Tom和18。
pair<string, int> name_age2(name_age); // 拷贝构造初始化
pair<int,string>a;
a=make_pair(5,”asd”);
cout<<a,first<<’ ‘<<a.second<<endl;
8. #include<bitset>
定义一个二进制串,可进行位运算
bitset<1000> a;// 定义长度为1000的0,1串未赋值的为0
a[0]=1
a.count()//返回1的个数
a.set(3);等价于a[3]=1;
a.reset(4);等价于a[4]=0;
size/empty有
9. #include <algorithm>
STL中与堆相关的4个函数——建立堆make_heap(),在堆中添加数据push_heap(),在堆中删除数据pop_heap()和堆排序sort_heap()
首先是make_heap();
他的函数原型是:void make_heap(first_pointer,end_pointer,compare_function);
一个参数是数组或向量的头指针,第二个向量是尾指针。第三个参数是比较函数的名字。在缺省的时候,默认是大跟堆。(下面的参数都一样就不解释了)
作用:把这一段的数组或向量做成一个堆的结构。范围是(first,last)
然后是pop_heap();
它的函数原型是:void pop_heap(first_pointer,end_pointer,compare_function);
作用:pop_heap()不是真的把最大(最小)的元素从堆中弹出来。而是重新排序堆。它
把first和last交换,然后将[first,last-1)的数据再做成一个堆。
接着是push_heap()
void pushheap(first_pointer,end_pointer,compare_function);
作用:push_heap()假设由[first,last-1)是一个有效的堆,然后,再把堆中的新元素加
进来,做成一个堆。
最后是sort_heap()
void sort_heap(first_pointer,end_pointer,compare_function);
作用是sort_heap对[first,last)中的序列进行排序。它假设这个序列是有效堆。(当然
,经过排序之后就不是一个有效堆了)
堆
优先队列priority_queue就是堆。
堆的基本结构
是一棵完全二叉树,即除了最后一层之外,其它层的节点都是满的
小根堆:根节点小于等于左右两个子节点
大根堆:根节点大于等于左右两个子节点
如何手写一个堆?下标从1开始
1.插入一个数 heap[++size]=x;up(size);
2.求集合当中的最小值 heap[1];
3.删除最小值 heap[1]=heap[size];size--;down(1); 就是让最后一个点覆盖掉第一个点,然后size--
4.删除任意一个元素 heap[k]=heap[size];size--;down(k);up(k); 可以一起写,但只会执行一个
5.修改任意一个元素 heap[k]=x;down(k);up(k);
// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
// ph[k]存储第k个插入的点在堆中的位置
// hp[k]存储堆中下标是k的点是第几个插入的
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 (u * 2 <= size && h[u * 2] < h[t]) t = u * 2;//u * 2 <= size看左儿子存在否
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] < h[u / 2])
{
heap_swap(u, u / 2);
u >>= 1;
}
}
// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);
并查集
并查集可以快速的处理如下问题:
(1) 将两个集合合并
(2) 询问两个元素是否在一个集合当中
可以近乎在O(1)的时间内完成上述操作
基本原理:每个集合用一棵树来表示。树根的编号就是整个集合的编号。每个节点储存它的父节点,p[x]表示x的父节点。
问题1:如何判断树根:if(p[x] == x)
问题2:如何求x的集合编号:while(p[x] != x) x = p[x];(常用优化方法:路径压缩)
int find(int x) //返回x的根节点(祖宗节点)+路径压缩
{ if(p[x]!=x) p[x]=fine(p[x]);
return p[x];
}
问题3:如何合并两个集合:px是x的集合编号,py是y的集合编号。p[x] = y;
(1)朴素并查集:
int p[N]; //存储每个点的祖宗节点
//返回x的根节点(祖宗节点)+路径压缩
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ ) p[i] = i;
// 合并a和b所在的两个集合:a的祖宗节点的父节点等于b的祖宗节点
p[find(a)] = find(b);
(2)维护size的并查集:
int p[N], size[N];
//p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
size[i] = 1;
}
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
size[b] += size[a];
(3)维护到祖宗节点距离的并查集:
int p[N], d[N];
//p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x)
{
int u = find(p[x]);
d[x] += d[p[x]];
p[x] = u;
}
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
d[I] = 0;
}
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量
Trie
基本作用:高效地存储和查找字符串集合的数据结构。
用Trie树存储字符串的时候,字符串一般都是全小写或者是全大写并且字母的个数不会很多即限制只有26个或52个
int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点
// cnt[]存储以每个节点结尾的单词数量
// 插入一个字符串
void insert(char *str)
{
int p = 0;
for (int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a'; //int &s=son[p][str[i]-'a'];
if (!son[p][u]) son[p][u] = ++ idx; //if(!s) s = ++ 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'; //int &s=son[p][str[i]-'a'];
if (!son[p][u]) return 0; //if(!s) return 0;
p = son[p][u];
}
return cnt[p];
}
KMP
KMP算法是一种字符串匹配算法,可以在 O(n+m) 的时间复杂度内实现两个字符串的匹配。
如果把模式串视为一把标尺,在主串上移动,那么暴力算法就是每次失配之后只右移一位;改进算法则是每次失配之后,移很多位,跳过那些不可能匹配成功的位置。
移动后旧的后缀要与新的前缀一致
求Next数组:
// s[]是模式串,p[]是模板串, n是s的长度,m是p的长度
for (int i = 2, j = 0; i <= m; i ++ )
{
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 <= n; i ++ )
{
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) j ++ ;
if (j == m)
{
j = ne[j];
// 匹配成功后的逻辑
}
}
队列和栈
先进先出叫队列,先进后出叫栈。
简单一点说就是:拉出来叫队列,吐出来叫栈
队列
// hh 表示队头,tt表示队尾
int q[N], hh = 0, tt = -1;
// 向队尾插入一个数
q[ ++ tt] = x;
// 从队头弹出一个数
hh ++ ;
// 队头的值
q[hh];
// 判断队列是否为空
if (hh <= tt)
{
}
栈
// tt表示栈顶
int stk[N], tt = 0;
// 向栈顶插入一个数
stk[ ++ tt] = x;
// 从栈顶弹出一个数
tt -- ;
// 栈顶的值
stk[tt];
// 判断栈是否为空
if (tt > 0)
{
}
单双链表
单链表
// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点 int head, e[N], ne[N], idx; // 初始化 void init() { head = -1; idx = 0; } // 在链表头插入一个数a void insert(int a) { e[idx] = a, ne[idx] = head, head = idx ++ ; } // 将头结点删除,需要保证头结点存在 void remove() { head = ne[head]; }
双链表
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;
// 初始化
void init()
{
//0是左端点,1是右端点
r[0] = 1, l[1] = 0;
idx = 2;
}
// 在节点a的右边插入一个数x
void insert(int a, int x)
{
e[idx] = x;
l[idx] = a, r[idx] = r[a];
l[r[a]] = idx, r[a] = idx ++ ;
}
// 删除节点a
void remove(int a)
{
l[r[a]] = l[a];
r[l[a]] = r[a];
}

浙公网安备 33010602011771号