【数据结构与算法】常用数据结构(一)

news2024/11/18 5:35:48

😀大家好,我是白晨,一个不是很能熬夜,但是也想日更的人✈。如果喜欢这篇文章,点个赞👍,关注一下👀白晨吧!你的支持就是我最大的动力!💪💪💪

在这里插入图片描述

文章目录

  • 📗前言
  • 📙常用数据结构(一)
    • 🍉单链表
    • 🍋双链表
    • 🥭栈
    • 🍊队列
    • 🍎堆
  • 📘后记

📗前言


大家好,我是白晨。本次为大家带来的常用数据结构的模拟实现,主要用于在算法比赛中快速实现一种常用模拟实现。那为什么不用STL呢?首先,STL为了保证其接口的通用性以及要严格符合一个数据结构的定义,在使用时可能不是非常方便;其次,模拟实现的数据结构在运行速度方面是要快于STL的容器的。

本篇文章将详细介绍单链表、双链表、栈、队列以及堆这五种常见数据结构的模拟实现,由于本次是面向新人的教程,白晨使用大量图片、动图和语言描述详细拆解一个模拟数据结构的实现。如果以前没有接触过这几类数据结构的同学可以先阅读每个标题下的文章。话不多说,我们开始吧。

img


📙常用数据结构(一)


🍉单链表


如果对于单链表还不是特别熟悉的同学可以先看白晨这篇文章——【数据结构】链表全解析。

  • 逻辑结构

img

  • 物理结构

本篇文章全部都是模拟实现。所以,这次我们选择用数组模拟单链表

  • 具体实现
  1. 初始化

v数组存放结点值,ne存放下一个结点的下标,idx为当前可使用结点的下标。头节点默认为0。

const int N = 100010;

int v[N], ne[N]; // v数组存放结点值,ne存放下一个结点的下标
int idx; // 头节点默认为0,idx为当前可使用结点的下标

// 初始化
void init()
{
    idx = 1;
}
  1. 在下标为k的结点后面插入

在下标为k的结点后面插入值为x的结点,只需要将新结点的值存入v数组中,然后将新结点的指针ne指向原来k结点的下一个结点,再将k结点的指针ne指向新结点即可。

单链表插入

如上图,每插入一个数据,v[idx]中填入一个数据,ne[idx]指向k(插入方式为头插),idx向后移动一次

image-20230421225111816

void add(int k, int x)
{
    v[idx] = x;
    ne[idx] = ne[k]; // 头插
    ne[k] = idx++;
}
  1. 删除下标为k结点后面的结点

删除下标为k结点后面的结点,只需要将k结点的指针ne指向下下个节点即可。

单链表删除

下标为2的结点本来指向下标为1的结点,删除2结点后面的结点也就是直接让ne[2]指向 ne[ne[2]] <==> ne[1] <==> 0

void remove(int k)
{
    ne[k] = ne[ne[k]];
}

🍋双链表


如果对于双链表还不是特别熟悉的同学可以先看白晨这篇文章——【数据结构】链表全解析。

  • 逻辑结构

img

  • 物理结构

同样数组模拟单链表

  • 具体实现

双链表使用没有单链表使用的多,但是双链表是基于单链表实现的,是个更深理解单链表实现的好例子。

  1. 初始化

v数组存放结点值,l存放左边结点的下标,r存放右边结点的下标,head为左端点,tail为右端点,idx为当前可使用结点的下标。相当将双链表拆成了两个单链表,l从右指向左,r从左指向右。

image-20230421230711870

const int N = 100010;

int v[N], l[N], r[N];
int head = 0, tail = 1, idx;

void init()
{
    l[0] = 1; // 0为左结点
    r[0] = 1;
    l[1] = 0; // 1为右结点
    r[1] = 0;
    idx = 2; // 从2开始插入数据
}
  1. 在下标为k的数的右边插入

在下标为k的数的右边插入值为x的数,只需要将新数的值存入v数组中,然后将新数的左指针l指向k,右指针r指向k的右边结点r[k]。接着将k的右指针r[k]指向新数,新数的右边结点r[idx]的左指针l[r[idx]]指向新数即可。

双链表插入

void insert(int k, int x)
{
    v[idx] = x;
    l[idx] = k;
    r[idx] = r[k];
    r[k] = idx;
    l[r[idx]] = idx;
    idx++;
}
  1. 删除下标为k的元素

删除下标为k的元素时,只需要将k的左边结点l[k]的右指针r[l[k]]指向k的右边结点r[k],将k的右边结点r[k]的左指针l[r[k]]指向k的左边结点l[k]即可。

双链表删除

void erase(int k)
{
    r[l[k]] = r[k];
    l[r[k]] = l[k];
}

🥭栈


如果对于栈还不是特别熟悉的同学可以先看白晨这篇文章——【数据结构】栈结构全解析。

  • 逻辑结构

febfb1db746d4ac5838975f5fb19cdb2

  • 物理结构

数组模拟栈

  • 具体实现
  1. 初始化

定义了一个数组st和一个变量backst用于存储栈中的元素,back表示栈顶元素的下标。

init函数用于初始化栈,将back设为-1表示栈为空。

8d03d00c723f4cf883d214c70925ff94

const int N = 100010;

int st[N], back;

void init()
{
    back = -1;
}
  1. 压栈

将元素x压入栈中,back加1表示栈顶指针向上移动一位,将x存入st[back]中。

bacb11c7a640438ea351f10c3136d20c

void push(int x)
{
    st[++back] = x;
}
  1. 出栈

弹出栈顶元素,back减1表示栈顶指针向下移动一位。

52aaabb935f64200921540667180a06c

void pop()
{
    --back;
}
  1. 取栈顶元素

返回栈顶元素,即st[back]

int top()
{
    return st[back];
}
  1. 判断栈是否为空

判断栈是否为空,即back是否小于0

bool empty()
{
    return back < 0;
}
  • 练习题目

image-20230409104158380

  • 题目链接:表达式求值

  • 参考解法:这里只给出使用STL中栈的解法,模拟栈解法大家可以自行实现,维护两个模拟栈即可,思路与STL栈没有任何区别。

#include <iostream>
#include <stack>
#include <unordered_map>
#include <cstdlib>
#include <string>

using namespace std;

stack<int> num;
stack<char> op;
unordered_map<char, int> dict{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}}; // 按照操作符优先级设置权值

void eval()
{
    int b = num.top(); num.pop();
    int a = num.top(); num.pop();
    char c = op.top(); op.pop();
    int x;
    
    if (c == '+') x = a + b;
    else if (c == '-') x = a - b;
    else if (c == '*') x = a * b;
    else x = a / b;
    
    num.push(x);
}

int main()
{
    string str;
    cin >> str;
    
    for (int i = 0; i < str.size(); ++i)
    {
        char e = str[i];
        if (isdigit(e))
        {
            int j = i, x = 0;
            while (isdigit(str[j])) x = x * 10 + str[j++] - '0';
            num.push(x);
            i = j - 1;
        }
        else if (e == '(') op.push(e);
        else if (e == ')')
        {
            // 将括号中的数据运算完
            while (op.top() != '(') eval();
            op.pop();
        }
        else
        {
            // 如果当前操作符优先级 小于等于 前面操作符,就要算出前面操作符所操作的值
            while (op.size() && op.top() != '(' && dict[e] <= dict[op.top()]) eval();
            op.push(e);
        }
    }
    
    // 上面过程进行完,表达式中没有括号 并且 操作符的优先级是升序排列,此时将数据按出栈顺序计算即可
    while (op.size()) eval();
    
    cout << num.top() << endl;
    return 0;
}

🍊队列


如果对于队列还不是特别熟悉的同学可以先看白晨这篇文章——【数据结构】栈与队列全解析。

d16ffe3ef85e40e8a6d1709a0509d93a

  • 物理结构

数组模拟队列

  • 具体实现
  1. 初始化

q为数组队列,front为队头下标,tail为队尾下标。

初始化队列,将队头指针front赋值为0,将队尾指针tail赋值为-1。

image-20230422105309403

const int N = 100010;

int q[N], front, tail;

void init()
{
    front = 0, tail = -1;
}
  1. 入队

将元素x插入到队列的末尾。由于数组下标从0开始,因此需要先将tail加1,然后再将x插入到q[tail]中。

队列插入

void push(int x)
{
    q[++tail] = x;
}
  1. 出队

弹出队首元素。由于数组下标从0开始,因此只需要将front加1即可。

队列删除

void pop()
{
    ++front;
}
  1. 取队头元素

返回队首元素。由于数组下标从0开始,因此只需要返回q[front]即可。

int top()
{
    return q[front];
}
  1. 判断队列是否为空

front > tail下标时,这个队列为空

bool empty()
{
    return front > tail;
}

🍎堆


如果对于堆还不是特别熟悉的同学可以先看白晨这篇文章——【数据结构】堆的全解析。

  • 逻辑结构

小根堆

a156b19ce7584d3eaf4cb617d77adf3b

大根堆

46c64053d5374b43b82ad4fa13cb820a

  • 物理结构

数组模拟堆

  • 具体实现

由于大小根堆的实现思路大致相同,这里具体讲解小根堆的实现思路,大根堆只需要将结点元素比较时的比较符号取反即可。

  1. 初始化

这个堆是用数组来实现的,下标从1开始。数组h存储了堆中的元素,Size表示堆中元素的个数。

const int N = 100010;

int h[N], Size; 
  1. 将堆中下标为x的数向下调整
  1. 保证要调整结点x的左右子树都是小堆。
  2. 比较N与孩子结点的大小关系。如果x小于等于两个孩子结点,调整结束;不然就让x与较小的孩子交换。
  3. 重复2过程,直到x调整结束或者被调整到叶子结点。

首先将cur赋值为x,然后判断2 * x2 * x + 1是否小于等于Size,如果是,则将cur赋值为其中较小的那个。如果cur不等于x,则交换h[x]h[cur],然后递归调用down(cur)

void down(int x)
{
    int cur = x;
    // 大根堆将 h[cur] > h[2 * x] 和 h[cur] > h[2 * x + 1] 改为 h[cur] < h[2 * x] 和 h[cur] < h[2 * x + 1]
    if (2 * x <= Size && h[cur] > h[2 * x]) cur = 2 * x;
    if (2 * x + 1 <= Size && h[cur] > h[2 * x + 1]) cur = 2 * x + 1;

    if (cur != x)
    {
        swap(h[x], h[cur]);
        down(cur);
    }
}
  1. 将堆中下标为x的数向上调整
  1. 保证要调整结点x的祖先是满足小堆性质的。
  2. 比较x与父结点的大小关系。如果x大于父结点,调整结束;不然就让x与父节点交换。
  3. 重复2过程,直到x调整结束或者被调整到根结点。

首先将cur赋值为x,然后判断cur / 2是否大于0h[cur / 2]是否大于h[cur],如果是,则交换h[cur / 2]h[cur],然后将cur除以2。重复这个过程直到cur / 2等于0或者h[cur / 2]不大于h[cur]

void up(int x)
{
    int cur = x;
    // 大根堆将 h[cur / 2] > h[cur] 改为 h[cur / 2] < h[cur]
    while (cur / 2 && h[cur / 2] > h[cur])
    {
        swap(h[cur / 2], h[cur]);
        cur /= 2;
    }
}
  1. 向下建堆

从第一个非叶子节点Size/2开始向下调整,直到调整到根节点。时间复杂度约为O(N)

如果有[31, 30, 29, …, 1]这一组数据建堆,建堆过程如下:

堆的建立

void build()
{
	for (int i = Size / 2; i; --i) down(i);
}
  1. x插入堆

Size++,再将x赋予h[Size],再向上调整h[Size]。时间复杂度为O(logN)

在[10, 9, 8, …, 1]所建成的堆中,插入一个0:

堆的插入1

void insert(int x)
{
    Size++;
    h[Size] = x;
    up(Size);
}
  1. 删除堆顶元素
  • 我们可以将栈顶元素与栈底元素交换,然后删除数组最后一个数据,这时候栈顶元素已经被删除了。
  • 现在根结点左右子树都是小堆,所以我们可以使用向下调整,调整根结点的位置,重新构建成堆。

首先交换h[1]h[Size],然后将Size减1,最后调用down(1)

删除堆顶元素:

堆的删除

void erase()
{
    swap(h[1], h[Size]);
    Size--;
    down(1);
}
  • 练习题目

image-20221231234709619

🍬原题链接:堆排序

🪅算法思想

  • 先建堆,再一个个删除,按照堆的性质,就能得到一串排好序的序列。

🪆代码实现

#include <iostream>

using namespace std;

const int N = 100010;

int h[N], Size; // 下标从1开始,小根堆

// 将堆中下标为x的数向下调整
void down(int x)
{
    int cur = x;
    if (2 * x <= Size && h[cur] > h[2 * x]) cur = 2 * x;
    if (2 * x + 1 <= Size && h[cur] > h[2 * x + 1]) cur = 2 * x + 1;

    if (cur != x)
    {
        swap(h[x], h[cur]);
        down(cur);
    }
}

// 将堆中下标为x的数向上调整
void up(int x)
{
    int cur = x;
    while (cur / 2 && h[cur / 2] > h[cur])
    {
        swap(h[cur / 2], h[cur]);
        cur /= 2;
    }
}

void erase()
{
    swap(h[1], h[Size]);
    Size--;
    down(1);
}

int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    // 向下建堆,时间复杂度为O(n)
    // n / 2 为最后一个有叶结点的结点
    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]);
        erase();
    }
    return 0;
}

📘后记


本篇文章的数据结构非常重要,不仅是常用数据结构(二)的引子,也是以后图论等算法的基础,最好能将其拿下。

如果讲解的有不对之处还请指正,我会尽快修改,多谢大家的包容。

如果大家喜欢这个系列,还请大家多多支持啦😋!

如果这篇文章有帮到你,还请给我一个大拇指 👍和小星星 ⭐️支持一下白晨吧!喜欢白晨【算法】系列的话,不如关注👀白晨,以便看到最新更新哟!!!

我是不太能熬夜的白晨,我们下篇文章见。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/451730.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

燃气管道定位83KHZ地下电子标识器探测仪ED-8000操作说明1

1、功能简要说明 ED-8000地下电子标识器探测仪是华翔天诚推出的一款可支持模拟电子标识器&#xff08;无 ID&#xff09;探测和数字 ID 电子标识器 探测两种工作模式&#xff0c;在模拟电子标识器&#xff08;无 ID&#xff09;探测模式下&#xff0c;可探测 所有按标准频率生…

Unity-ML-Agents安装

目录 1.下载ML-Agents 1.1 前往官网 1.2 选择版本 1.3 下载文件 2.下载Anaconda 3.虚拟环境 3.1 构建虚拟环境 3.2 创建项目&#xff0c;导入package.json 3.2.1 创建项目&#xff0c;导入package.json 3.2.2 导入成功 3.2.3 将模板项目拖入unity项目中 3.3 开始训练 …

低代码感觉很能打——可视化搭建系统,把格局做大

有人说「可视化搭建系统」说到底只是重复造轮子产生的玩具&#xff1b; 有人说「可视化搭建系统」本质是组件枚举&#xff0c;毫无意义。 片面的认知必有其产生的道理&#xff0c;但我们不妨从更高的角度出发&#xff0c;并真切落地实践&#xff0c;也许你会发现&#xff1a;我…

Java面试题总结 | Java面试题总结5- 数据结构模块(持续更新)

数据结构 文章目录 数据结构顺序表和链表的区别HashMap 和 Hashtable 的区别Java中用过哪些集合&#xff0c;说说底层实现&#xff0c;使用过哪些安全的集合类Java中线程安全的基本数据结构有哪些ArrayList、Vector和LinkedList有什么共同点与区别&#xff1f;ArrayList和Linke…

怎样正确做web应用的压力测试?

web应用&#xff0c;通俗来讲就是一个网站&#xff0c;主要依托于浏览器实现其功能。 提到压力测试&#xff0c;我们想到的是服务端压力测试&#xff0c;其实这是片面的&#xff0c;完整的压力测试包含服务端压力测试和前端压力测试。 下文将从以下几部分内容展开&#xff1a…

源码简读 - AlphaFold2的2.3.2版本源码解析 (1)

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://blog.csdn.net/caroline_wendy/article/details/130323566 时间&#xff1a;2023.4.22 官网&#xff1a;https://github.com/deepmind/alphafold AlphaFold2是一种基于深度学习的方法…

torch中fft和ifft新旧版本使用

pytorch旧版本&#xff08;1.7之前&#xff09;中有一个函数torch.rfft()&#xff0c;但是新版本&#xff08;1.8、1.9&#xff09;中被移除了&#xff0c;添加了torch.fft.rfft()&#xff0c;但它并不是旧版的替代品。 torch.fft label_fft1 torch.rfft(label_img4, signal…

25岁走出外包后,感到迷茫了.....

我认识一个老哥&#xff0c;他前段时间从外包出来了&#xff0c;他在外包干了3年左右的点工&#xff0c;可能也是自身的原因&#xff0c;也没有想到提升自己的技术水平&#xff0c;后面觉得快废了&#xff0c;待着没意思就出来了&#xff0c;出来后他自己更迷茫了&#xff0c;本…

Linux安装Jenkins搭配Gitee自动化部署Springboot项目

目录 前言一、环境准备二、全局工具配置jdk、maven、git三、配置Gitee四、新建任务-部署Springboot项目 前言 Jenkins是一款流行的开源持续集成&#xff08;CI&#xff09;和持续交付&#xff08;CD&#xff09;工具。它可以帮助开发人员自动构建、测试和部署软件应用程序&…

广州蓝景分享—快速了解Typescript 5.0 中重要的新功能

作为一种在开发人员中越来越受欢迎的编程语言&#xff0c;TypeScript 不断发展&#xff0c;带来了大量的改进和新功能。在本文中&#xff0c;我们将深入研究 TypeScript 的最新迭代版本 5.0&#xff0c;并探索其最值得注意的更新。 1.装饰器 TypeScript 5.0 引入了改进的装饰…

二、SQLServer 的适配记录

SQLServer 适配记录 说明:由于 SQLSERVER 数据库本身和MYSQL数据库有一定的语法,创表结构,物理模式等差别,在适配过程中,可能会出现各种错误情况,可参考本次适配记录。 当前环境: 适配项目:JDK11,SpringBoot服务。 适配数据库:SELECT @@VERSION,得 Microsoft SQL …

ProtocolBuffer入门和使用

<<<<<<< HEAD 基础 入门 优势 protocol buffer主要用于结构化数据串行化的灵活、高效、自动的方法&#xff08;简单来说就是结构化数据的可串行化传输&#xff0c;类似JSON、XML等&#xff09;。 比XML解析更快&#xff1a;解析的层数更少&#xff0c;…

【技术发烧】MySqlServer,MySQL WorkBench安装详细教程

目录 一.下载安装MySQLSever 1.下载 2.安装 1.解压 2.编写配置文件 二.初始化数据库 1.以管理员身份打开命令提示符 2.初始化数据库 3.安装mysql服务并启动 4.连接MySQL 5. 修改密码 三.MySQL WorkBench下载 一.下载安装MySQLSever 1.下载 下载路径&#xff1a;https:/…

java导入导出excel数据图片合成工具

目录 java导出和导入excel数据java读取excel数据java数据导出成excel表格 java服务端图片合成的工具 java导出和导入excel数据 可以使用hutool的ExcelUtil工具。 在项目中加入以下依赖&#xff1a; <dependency><groupId>cn.hutool</groupId><artifactI…

【计算机基础】绝对路径和相对路径

目录 一.绝对路径 二.相对路径 例如 三.举例 一.绝对路径 绝对路径是指从根目录开始的完整路径&#xff0c;包括所有父目录的路径&#xff0c;直到目标文件或者目录 所在的位置。 全文件名全路径文件名绝对路经完整的路径 例如&#xff0c;在windows系统中&#xff0c;绝…

《Linux基础》09. Shell 编程

Shell 编程 1&#xff1a;Shell 简介2&#xff1a;Shell 脚本2.1&#xff1a;规则与语法2.2&#xff1a;执行方式2.3&#xff1a;第一个 Shell 脚本 3&#xff1a;变量3.1&#xff1a;系统变量3.2&#xff1a;用户自定义变量3.2.1&#xff1a;规则3.2.2&#xff1a;基本语法3.2…

Python自动发送消息小脚本,可用于各种聊天框~

作者主页&#xff1a;爱笑的男孩。的博客_CSDN博客-深度学习,YOLO,活动领域博主爱笑的男孩。擅长深度学习,YOLO,活动,等方面的知识,爱笑的男孩。关注算法,python,计算机视觉,图像处理,深度学习,pytorch,神经网络,opencv领域.https://blog.csdn.net/Code_and516?typecollect 个…

安装k3s

k3s官方文档 architecture quick start 概述&#xff1a;k3s一个轻量级的kubernetes,因资源消耗知识kubernetes的一半&#xff0c;故取名k3s k3s的node分为 server node 和agent node: server node: 可以运行kubectl等命令&#xff0c;且包含 agent node的功能。agent node:…

【升级】专为小白设计的TypeScript入门课无密拟把疏狂图一醉

TypeScript&#xff1a;JavaScript的超集&#xff0c;提高代码可靠性和可维护性 【升级】专为小白设计的TypeScript入门课 download&#xff1a;https://www.666xit.com/3817/ 随着现代Web应用程序的复杂性增加&#xff0c;使用JavaScript编写大型项目变得越来越困难。TypeS…

Centos 安装MySQL

CentOS 安装 MySQL 1. 安装 VMware 以及 CentOS2. 安装 docker2.1 卸载&#xff08;可选&#xff09;2.2 安装 Docker2.3 启动 Docker2.4.配置镜像加速2.5 设置 Docker 开机自启 3. 安装 MySQL3.1 从docker镜像仓库中拉取mysql镜像3.2 创建实例&#xff0c;并启动3.3.查看docke…