算法基础-数据结构
以上是算法基础的==数据结构==主要内容,本文将逐个介绍各个数据结构的定义和基础使用方法,以及附上代码实现。(基于cpp数组的数据结构实现)
###1.链表
####1)单链表
通过数组模拟的单链表,按照尾加法的方式插入元素。
首先需要定义头结点head, 结点关键词数组e[N], 存储下一结点关键词的ne[N], 以及地址分配器idx.
下面完成单链表的各种操作
void init()//链表创建操作
{
head = -1;//头结点指向-1,目的是为了知道链表的尾部在哪里, 头指针指向空
idx = 0;//可以看作链表各个元素的内存分配器,或者是链表各个单元的下标, idx = 0为第0个操作数
}
void add_head(int x)//尾加法,在头结点后插入
{
e[idx] = x;//为加入的节点分配位置idx,关键词为x
ne[idx] = head;//将加入的节点指向头指针指向的节点
head = idx++;//头指针指向idx,idx自增
}
void add(int x, int k)//在第k个插入的数后面新增一个关键词为x的节点
{
e[idx] = x;//新创建一个关键词为x的节点
ne[idx] = ne[k];//将新增结点指向
ne[k] = idx++;
}
void remove(int k)//移除第k个插入的节点的下一个结点
{
//idx = k;
ne[k] = ne[ne[k]];
}
void print()//遍历输出链表
{
for (int i = head; i != -1; i++)
cout << e[i] << " ";
}
2)双链表
双链表核心思想和单链表基本相同,都是添加新节点.要注意的是要注意左右节点.
void init()
{
idx = 2;//需要用两个点进行初始化
r[0] = 1;
l[1] = 0;
}
void add(int k, int x)//在第k个插入的点右侧插入关键词为x的点
{
e[idx] = x;
l[idx] = k;
r[idx] = r[k];
l[r[k]] = idx;
r[k] = idx++;
}
void remove(int k)//删除第k个插入的点
{
r[l[k]] = r[k];
l[r[k]] = l[k];
}
2.栈
1)栈
栈就是一种先进后出的数据结构,可以理解为一个桶,后放进去的东西先拿出来
//基于数组模拟的栈
int stk[N], tt = 0;//stk为栈数组, tt为栈顶
//入栈
stk[++tt] = x;
//出栈
tt--;
//栈顶
stk[tt];
//栈的大小
tt;
//判空
if(!tt) cout << "empty";
//STL中栈
#include <stack>
stack<int> stk;
int x;
//入栈
stk.push(x);
//出栈
stk.pop();
//栈顶元素
stk.top();
//栈的大小
stk.size();
//栈判空
stk.empty();
2)单调栈
单调栈就是栈中的元素满足单调性,可以利用单调栈完成特定类型的题目,比如:
给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1
(详见acwing830)
int stk[N], tt = 0;
for (int i = 1; i <= n; i++)
{
while (tt && stk[tt] >= x) tt--;//不断判断要入栈的数字是大于栈顶
stk[++tt] = x;//加入该数字,此时栈一直满足条件
}
3.队列
1)普通队列
队列就是一种先进先出的数据结构,可以看为排队,先到先出.从队尾加入新元素,在队头离开.
//数组模拟队列
int q[N], hh = 0, tt = -1;//hh为队首, tt为队尾
//入队
q[++tt] = x;
//出队
hh++;
//队头的值和队尾的值
q[hh], q[tt];
//判空
if (hh > tt) cout << "empty";
//STL中队列
#include <queue>
queue<int> q;
//入队
q.push(x);
//出队
q.pop();
//队首元素
q.front();
//队列长度
q.size();
//队列判空
q.empty();
2)循环队列
循环队列就是当元素到达队列的尾部之后时,自动变为第一位.
int q[N], hh = 0, tt = 0;
//入队
q[tt++] = x;
if (tt == N) tt = 0;//循环思想
//出队
hh++;
if (hh == N) hh = 0;
//队首
q[hh];
//判空
if (hh == tt) cout << "empty";
3)单调队列
单调队列是具有单调性的队列,用于处理滑动窗口类的题目
如acwing154.滑动窗口
int a[N], q[N], hh = 0, tt = -1;//q中存a数组的下标
int n, k;//n为总长度,k为窗口长度(队列长度)
for (int i = 1; i <= n; i++)
{
if (hh <= tt && i - k + 1 > q[hh]) hh++;//判断队头是否划出窗口
while (hh <= tt && a[i] >= a[q[tt]]) tt--;//不断循环到找到能使队列继续单调的元素
q[++tt] = a[i];
if (i >= k - 1) cout << a[q[hh]] << " ";//此时a[q[hh]]为窗口中最小的元素
}
4.Trie树
Trie树:高效存储和查找字符串的集合
是一种存储字符串的数据结构,可以利用Trie树来存储多个字符串,也可以查询某个字符串的次数.
一般只在有26个字母组成的字符串题目中,可能用到Trie树.
int son[N][26], cnt[N];//son数组用来存储某个节点的子节点, cnt数组用来存储以某个节点结尾的字符串数量
int idx = 0;//0号点既是根节点也是空节点
//插入字符串操作
void insert(char* str)
{
int p = 0;//从根节点开始
for (int i = 0; str[i]; i++)//遍历要插入的字符串
{
int u = str[i] - 'a';//u为当前字母的编号
if (!son[p][u])//如果当前节点的子节点为空
son[p][u] = ++idx;//为其分配地址
p = son[p][u];//继续遍历
}
//遍历完成之后
cnt[p]++;//此时p为插入字符串的尾结点
}
//查询字符串
void 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];//返回数量
}
###5.并查集
并查集是一个高效完成集合操作的数据结构,操作的时间复杂度近乎O(1);
并查集有两个强大的功能:合并两个集合,询问两个元素是否属于一个集合
如何判断两个元素是否在同一个集合中呢,只需要查看两个元素的祖宗节点是不是相同,如果相同,那么自然就是在一个集合。
int p[N];//存储祖宗节点
//初始化,每个元素都是各自为一个集合
for (int i = 1; i <= n; i++)
p[i] = i;
//并查集的核心:查询元素x的祖宗节点,并且进行路径优化
int find1(int x)//返回x的祖宗节点,路径压缩
{
if (p[x] != x) {
t = find(p[x]);
p[x] = t;
}
return p[x];
}
int find2(int x)//每个节点指向祖父
{
while (p[x] != x)//x不为当前集合的祖宗节点
{
int t = p[x];//先将x的父亲结点存下来
p[x] = p[p[x]];//将x的父亲结点指向下一个父亲结点
x = t;//这时候将x变为原来的父亲结点
}
return x;
}
int find3(int x)//路径减半,每隔一个结点指向祖父
{
while (p[x] != x)
{
p[x] = p[p[x]];
x = p[x];
}
return x;
}
//合并集合的操作
//将编号为a和编号为b的集合合并,如果已经在一个集合,则忽略
if (find(a) != find(b))
p[find(a)] = find(b);
//查询集合
if (find(a) == find(b))
cout << "Yes";
else cout << "No";
6.堆
####1)手写堆
小根堆是一种堆顶一直为最小值的一种数据结构.堆有俩种最基本的操作up(x), down(x),将元素x上移或下移.
首先堆这种数据结构,是一种完全二叉树(除了最后一层),每个结点的左儿子为2x,右儿子为2x+1
在小根堆中,父亲节点一定大于左右儿子。
手写堆有如下5个常用操作:
插入 heap[++size] = x; up(size)
求最小值 heap[1]
删除最小值 heap[1] = heap[size]; size--; down(1);
删除编号为k的值 heap[k] = heap[size]; size--; up(k); down(k);
修改编号为k的值 heap[k] = x; up(k); down(k);
下面为操作up和down的代码
//首先实现基本操作up和down
void down(int u)
{
int t = u;//t为三个节点中最小的结点编号
if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2;
if (u * 2 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if (t != u)
{
swap(h[t], h[u]);
//此时编号为u的点为三个点中值最小的点
//但是t点相对于其子节点就不一定了
down(t);
}
}
void up(int u)
{
int t = u;
while (u / 2 && h[u / 2] > h[t])
{
swap(h[u], h[u / 2]);
u = u >> 1;
}
}
下面为交换结点和建堆的代码
int h[N], ph[N], hp[N];
//h[N]存储堆中的值
//ph[N]为从第i个插入的点到堆中位置的映射
//hp[N]为堆中位置到第i个插入的点的映射
void heap_swap(int a, int b)
{
swap(ph[hp[a]], ph[hp[b]]);
swap(hp[a], hp[b]);
swap(h[a], h[b]);
}
//建堆的方法
for (int i = n / 2; i; i--) down(i);//从有子节点的节点开始,不断向上
//时间复杂度为O(1)
2)STL堆
#include <queue>
//大根堆
priority_queue<int> q;
//小根堆
priority_queue<int, vector<int>, greater<int>> q;
//操作类似于queue:q.size(), q.empty(), q.push(), q.top(), q.pop();
7.哈希表
首先是哈希表(Hash Table)的定义:是一种根据关键词值(key)而进行直接访问的数据结构。映射函数为散列函数,存储记录的数组称为散列表。
N最好为一个质数,而且离2^n^越远越好。
1)普通哈希表
#####a)开放寻址法
int h[N];//N需要开到2-3倍
const int N = 0x3f3f3f3f;
int find(int x)//x为关键词值
{
int t = ((x % N) + N) % N;
while (h[t] != null && h[t] != x)//这个坑位有人,而且不是要找的那个人
{
t ++;
if (t == N) t = 0;
}
return t;
}//如果x存在于散列表中,返回下标t;否则返回插入之后的下标t
b)拉链法(邻接表法)
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++;
}
//查询x是否存在
bool find(int x)
{
int t = ((x % N) + N) % N;
for (int i = h[t]; i != -1; i = ne[i])
{
if (e[i] == x)//找到了
return true;
}
return false;
}
8.STL
以下总结一些常用的STL
//首先是vector,变长数组,利用倍增的思想
#include <vector>
vector<int> a;
a.size();
a.empty();
a.clear();
a.front();
a.back();
a.push_back();
a.pop_back();
a.begin();
a.end();
支持a[i]
支持比较运算,按字典序
//pair
pair<int, int> q;
q.first(), q.second();
支持比较运算,先比较first
//string
#include <cstring>
string str;
str.length(), str.size();
str.empty();
str.clear();
str.substr(起始下标,(子串长度));//返回子串
c_str();//返回字符串所在字符数组的起始地址
//queue
//stack
//priority_queue
//均见上
//deque 双端队列,不常用