(二)算法基础课之数据结构

算法基础-数据结构

  1. 链表

  1. 队列

  1. Trie树

  1. 并查集

  1. 哈希表

以上是算法基础的==数据结构==主要内容,本文将逐个介绍各个数据结构的定义和基础使用方法,以及附上代码实现。(基于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 双端队列,不常用  
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值