用数组实现链表、栈和队列

news2024/10/6 10:28:52

目录

  • 前言
  • 一、用数组实现链表
    • 1.1 单链表
    • 1.2 双链表
  • 二、用数组实现栈
  • 三、用数组实现队列

前言

众所周知,链表可以用结构体和指针来实现,而栈和队列可以直接调用STL,那为什么还要费尽心思用数组来实现这三种数据结构呢?

首先,对于用结构体和指针实现的链表,每次创建节点都要使用 new 关键字,这一步非常慢,尤其是当链表的长度 ≥ 1 0 5 \geq10^5 105 时,用这种方法构造的链表在处理各种操作时很有可能TLE。其次,STL容器本身就要比原生数组慢,当数据量非常庞大的时候,使用数组来模拟这些数据结构是一个不错的选择。

一、用数组实现链表

1.1 单链表

通常,我们习惯用结构体和指针来实现单链表(两个成员变量+三个构造函数):

struct ListNode {
    int val;
    ListNode *next;
    ListNode() : val(0), next(nullptr) {}
    ListNode(int x) : val(x), next(nullptr) {}
    ListNode(int x, ListNode *next) : val(x), next(next) {}
};

我们把用上述方式实现的链表称为动态链表,把用数组方式实现的链表称为静态链表。具体来说

  • 使用动态链表存储数据,无需预先申请内存空间,而是在需要的时候用 mallocnew 关键字进行申请,所以链表的长度没有限制。动态链表因为是动态申请内存的,所以每个节点的物理地址不连续,需要通过指针来顺序访问;
  • 使用静态链表存储数据,需要预先申请足够大的一块内存空间,所以链表的初始长度一般是固定的。静态链表因为是用数组实现的,所以每个节点的物理地址连续。

对于动态链表,每个节点不仅存储了一个值,还存储了指向下一个节点的指针,在动态链表上进行移动也是用的指针。静态链表自然也需要具备这些特性,那如何用数组的方式进行实现呢?

不妨给链表中的节点从 0 0 0 开始编号,如果链表有 n n n 个节点,则编号依次为 0 , 1 , ⋯   , n − 1 0,1,\cdots,n-1 0,1,,n1。每个节点的编号可以视为指向该节点的「指针」,于是用指针访问节点便成了用「索引」访问节点(因为索引就是从 0 0 0 开始的)。设链表可能达到的最大长度为 N N N,因此开两个数组分别用来存储每个节点的值和指向下一个节点的指针:

int val[N], nxt[N];  // val用来存储每个节点的值,nxt用来存储指向下一个节点的指针

例如,对于下图中的链表(红色为节点编号,黑色为节点存储的值)

val 数组和 nxt 数组应当分别为(规定空指针为 − 1 -1 1

val[0] = 4, nxt[0] = 1;
val[1] = 2, nxt[1] = 2;
val[2] = 1, nxt[2] = 3;
val[3] = 3, nxt[3] = -1;

当然,仅用这两个数组来表示链表是远远不够的,我们还需要头指针 head 和一个指向待插入位置的指针 idx(因为是从小到大进行编号的,所以 idx 只会增加不会减少)

int head, idx;  // head是头指针,idx是指向待插入位置的指针,只增不减

头指针始终指向链表的头节点。初始时,链表为空,头指针为空指针,因此有 head = -1。因为链表中没有节点,故位置 0 0 0 待插入,所以 idx = 0。链表的初始化函数如下

void init() {
    head = -1, idx = 0;
}

接下来,我们研究静态链表的插入与删除操作。

操作一:在链表头部插入元素 x

因为 idx 为待插入的位置,因此执行 val[idx] = x 相当于创建了一个节点,接下来执行 nxt[idx] = head 相当于让该节点指向头节点,最后执行 head = idx 让头指针指向该节点。当然,不要忘了 idx++,因为 idx 已经被用过了(已经被编号了),因此待插入的位置变成 idx + 1

void insert(int x) {
    val[idx] = x, nxt[idx] = head, head = idx++;
}

操作二:在第 k k k 个插入的数后面插入一个数 x

⚠️ 第 k k k 个插入的数并不是指当前链表的第 k k k 个数。例如操作过程中一共插入了 n n n 个数,则按照插入的时间顺序,这 n n n 个数依次为:第 1 1 1 个插入的数,第 2 2 2 个插入的数, ⋯ \cdots ,第 n n n 个插入的数。

显然,idx 记录了插入顺序。即 idx 所指向的节点为按照时间顺序第 idx + 1 个插入的数(注意 idx 是从 0 0 0 开始的)。

void insert(int k, int x) {
    val[idx] = x, nxt[idx] = nxt[k - 1], nxt[k - 1] = idx++;
}

操作三:删除第 k k k 个插入的数后面的数, k = 0 k=0 k=0 时表示删除头节点。

void remove(int k) {
    if (k) nxt[k - 1] = nxt[nxt[k - 1]];
    else head = nxt[head];
}

⚠️ 切勿写成 nxt[k - 1] = nxt[k],因为编号只在数组中连续,但不一定在链表中连续,所以 nxt[k - 1] 不一定等于 k

操作四:输出链表。

for (int i = head; ~i; i = nxt[i]) cout << val[i] << ' ';

📝 ~i 等价于 i != -1

我们可以用面向对象的思维对上述代码进行封装,使其更加清晰易懂:

class List {
private:
    int val[N], nxt[N], head, idx;

public:
    List() : head(-1), idx(0) {}

    void insert(int x) {
        val[idx] = x, nxt[idx] = head, head = idx++;
    }

    void insert(int k, int x) {
        val[idx] = x, nxt[idx] = nxt[k - 1], nxt[k - 1] = idx++;
    }

    void remove(int k) {
        if (k) nxt[k - 1] = nxt[nxt[k - 1]];
        else head = nxt[head];
    }

    void display() {
        for (int i = head; ~i; i = nxt[i]) cout << val[i] << ' ';
    }
};

一个使用范例:

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

class List {...};  // 这里省略

int main() {
    List *ll = new List();
    ll->insert(3);
    ll->insert(4);
    ll->insert(5);
    ll->insert(6);
    ll->remove(4);  // 第4个插入的数是6,所以该操作将会删除6后面的5
    ll->display();  // 输出:6 4 3
    delete ll;
    return 0;
}

1.2 双链表

用结构体和指针实现的双链表如下(三个成员变量+三个构造函数):

struct ListNode {
    int val;
    ListNode *prev;
    ListNode *next;
    ListNode() : val(0), prev(nullptr), next(nullptr) {}
    ListNode(int x) : val(x), prev(nullptr), next(nullptr) {}
    ListNode(int x, ListNode *prev, ListNode *next) : val(x), prev(prev), next(next) {}
};

如何用数组来实现双链表呢?

回顾单链表的实现方式,我们定义了两个数组,一个用来存储节点的值,一个用来存储指向下一个节点的指针。类比到双链表,我们需要三个数组,如下

int val[N], pre[N], nxt[N];
// val用来存储节点的值
// pre用来存储指向上一个节点的指针
// nxt用来存储指向下一个节点的指针

同样地,我们还需要一个指针 idx 用来指向待插入的位置。注意到单链表是单向的,因此只需要一个头指针 head,而双链表是双向的,所以不仅需要头指针 head,还需要尾指针 tail

为简便起见,我们省略掉头指针和尾指针,定义 0 0 0 号节点和 1 1 1 号节点,这两个节点实际上是两个边界,链表的实质内容在这两个节点之间更新。链表的头节点是 nxt[0],链表的尾节点是 pre[1],整个链表(加上两个边界)形如

0 ⇄ nxt[0] ⇄ nxt[nxt[0]] ⇄ ⋯ ⇄ pre[pre[1]] ⇄ pre[1] ⇄ 1 0 \rightleftarrows\textcolor{red}{ \text{nxt[0]}\rightleftarrows \text{nxt[nxt[0]]}\rightleftarrows \cdots\rightleftarrows \text{pre[pre[1]]}\rightleftarrows \text{pre[1]} }\rightleftarrows \text{1} 0nxt[0]nxt[nxt[0]]pre[pre[1]]pre[1]1

红色部分才是链表的实质内容, 0 0 0 1 1 1 仅仅起到边界的作用。

初始时,链表为空,且形如 0 ⇄ 1 0\rightleftarrows1 01。因为 0 0 0 1 1 1 已经被用过了,所以 idx 应当从 2 2 2 开始。链表的初始化函数如下

void init() {
    nxt[0] = 1, pre[1] = 0, idx = 2;
}

接下来,我们研究双链表的插入与删除操作。

操作一:在第 k k k 个插入的数后面插入一个数 x

我们先来看如何在编号 k k k 的数后面插入一个数。

首先执行 val[idx] = x 来创建一个节点。然后执行 pre[idx] = k, nxt[idx] = nxt[k] 来建立新节点与已有节点之间的单向链接,最后执行 pre[nxt[k]] = idx, nxt[k] = idx 来建立双向链接(注意这里的顺序不能写反),当然不要忘了 idx++

void insert(int k, int x) {
    val[idx] = x, pre[idx] = k, nxt[idx] = nxt[k];
    pre[nxt[k]] = idx, nxt[k] = idx++;
}

而实际上, k k k 个插入的数的编号为 k + 1 k+1 k+1(因为 idx 是从 2 2 2 开始的,例如第 1 1 1 个插入的数的编号就是 2 2 2),因此实现操作一需要这样调用

insert(k + 1, x);

操作二:在第 k k k 个插入的数前面插入一个数 x

注意到第 k k k 个插入的数的编号为 k + 1 k+1 k+1,因此该操作相当于在 pre[k + 1] 的后面插入一个数 x,故可以直接调用操作一中的函数

insert(pre[k + 1], x);

操作三:在链表的头部插入数 x

该操作等价于在 0 0 0 的后面插入 x,直接调用即可

insert(0, x);

操作四:在链表的尾部插入数 x

该操作等价于在 1 1 1 的前面插入 x,也等价于在 pre[1] 的后面插入 x,直接调用即可

insert(pre[1], x);

操作五:删除第 k k k 个插入的数。

先来看如何删除编号为 k k k 的数。

首先执行 nxt[pre[k]] = nxt[k] k k k 前面的节点指向 k k k 后面的节点,再执行 pre[nxt[k]] = pre[k] k k k 后面的节点指向 k k k 前面的节点。

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

由于第 k k k 个插入的数的编号为 k + 1 k+1 k+1,故实现操作五需要这样调用

remove(k + 1);

操作六:输出链表。

从头节点 nxt[0] 遍历到尾节点 pre[1] 即可。

for (int i = nxt[0]; i != 1; i = nxt[i]) cout << val[i] << ' ';

⚠️ 在实现单链表的插入操作时,形参 k 的意义是「第 k k k 个插入的数」,而在实现双链表的插入操作时,为方便理解,形参 k 的意义变成了「编号为 k k k 的数」。事实上,前者可以采用后者的形参意义进行实现,后者也可以采用前者的形参意义进行实现。

二、用数组实现栈

有了用数组实现链表的经验后,用数组实现栈就变得非常简单了。

class Stack {
private:
    int stk[N], idx;  // idx始终指向栈顶元素

public:
    Stack() : idx(0) {}

    void push(int x) {
        stk[++idx] = x;
    }

    void pop() {
        idx--;
    }

    int top() {
        return stk[idx];
    }

    bool empty() {
        return idx == 0;
    }

    int size() {
        return idx;
    }
};

三、用数组实现队列

设置两个指针 qlqr,其中 ql 始终指向队头元素,qr 始终指向队尾元素。

class Queue {
private:
    int q[N], ql, qr;

public:
    Queue() : ql(1), qr(0) {}

    void push(int x) {
        q[++qr] = x;
    }

    void pop() {
        ql++;
    }

    int front() {
        return q[ql];
    }

    int back() {
        return q[qr];
    }

    bool empty() {
        return ql > qr;
    }

    int size() {
        return qr - ql + 1;
    }
};

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

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

相关文章

好的质量+数量 = 健康的创作者生态

缘起 CSDN 每天都有近万名创作者发表各种内容&#xff0c; 其中博客就有一万篇左右。 这个数量是非常可喜的&#xff0c;这也是 CSDN 的产品、研发运营小伙伴、和各位博主持续工作的结果。 衡量一个 IT 内容平台&#xff0c;除了数量之外&#xff0c;还有另外一些因素&#xf…

Linux——动态库

目录 制作并发布动态库 使用动态库 使用动态库程序运行时的错误 制作并发布动态库 静态库的代码在链接的时候会被拷贝进对应的可执行程序内部&#xff0c;动态库则不需要拷贝。 动态库在形成目标文件时&#xff0c;需要加一个选项 -fPIC&#xff1a;形成一个与位置无关的二…

Yocto常用术语

Yocto常用术语 Yocto是一套开源、专为嵌入式定制的编译系统&#xff0c;它提供了toolset和开发环境&#xff0c;开发人员可以利用Yocto定制基于Linux的系统。Yocto官网介绍了其常用术语&#xff0c;官网链接Yocto Project Terms&#xff0c;了解这些术语可以加深对Yocto的认识…

第五章 高级数据管理

在第4章&#xff0c;我们审视了R中基本的数据集处理方法&#xff0c;本章我们将关注一些高级话题。本章分为三个基本部分。在第一部分中&#xff0c;我们将快速浏览R中的多种数学、统计和字符处理函数。为了让这一部分的内容相互关联&#xff0c;我们先引入一个能够使用这些函数…

低功耗广域网LPWAN 8大关键技术对比

物联网被认为是继计算机、互联网之后&#xff0c;世界信息产业发展的第三次浪潮&#xff0c;它的出现将大大改变人们现有的生活环境和习惯。智能家居、工业数据采集等场景通常采用的是短距离通信技术&#xff0c;但对于广范围、远距离的连接&#xff0c;远距离通信技术不可或缺…

分享146个ASP源码,总有一款适合您

ASP源码 分享146个ASP源码&#xff0c;总有一款适合您 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c; 146个ASP源码下载链接&#xff1a;https://pan.baidu.com/s/1HG8AMPldOPHcEmMsGnVwMA?pwdg97k 提取码&#x…

矩阵的运算、运算规则及C语言实现

在人工智能运算和原理的过程中,我们需要了解非常多的数学知识,但是大学时候学的东西已经忘的差不多了,这里我把矩阵的一系列概念总结并复习一下,以便于大家在学习AI的时候要明白很多数学计算的物理意义,当年在学习线性代数的时候,我们不一定明白这些计算的意义,现在要和…

【图卷积网络】02-谱域图卷积介绍

注&#xff1a;本文为第2章谱域图卷积介绍视频笔记&#xff0c;仅供个人学习使用 目录1、图卷积简介1.1 图卷积网络的迅猛发展1.2 回顾&#xff0c;经典卷积神经网络已在多个领域取得成功1.3 两大类数据1.4 经典卷积神经网络的局限&#xff1a;无法处理图数据结构1.5 将卷积扩展…

代码随想录算法训练营第四十八天|● 198.打家劫舍 ● 213.打家劫舍II ● 337.打家劫舍III

动态规划 一、198.打家劫舍 题目&#xff1a; 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚上被小偷闯入&#xff0c;系…

流批一体计算引擎-7-[Flink]的DataStream连接器

参考官方手册DataStream Connectors 1 DataStream连接器概述 一、预定义的Source和Sink 一些比较基本的Source和Sink已经内置在Flink里。 1、预定义data sources支持从文件、目录、socket&#xff0c;以及collections和iterators中读取数据。 2、预定义data sinks支持把数据写…

Eclipse中的Build Path

Eclipse中的Build Path简介如果修改了Build Path中的中的JRE版本&#xff0c;记得还需要同步修改Java编译器的版本&#xff0c;如下图红框所示简介 Build Path是Java工程包含的资源属性合集&#xff0c;用来管理和配置此Java工程中【除当前工程自身代码以外的其他资源】的引用…

Vision Transformer 简单复现和解释

一些我自己不懂的过程&#xff0c;我自己在后面写了demo解释。 import torch import torch.nn as nnfrom einops import rearrange, repeat from einops.layers.torch import Rearrangedef pair(t):return t if isinstance(t, tuple) else (t, t) class PreNorm(nn.Module):…

数据库系统概念 | 第七章:使用E-R模型的数据库设计 | ER图设计| ER图转化为关系模型 | 强实体和弱实体

文章目录&#x1f4da;设计过程概览&#x1f4da;实体-联系模型&#x1f407;E-R数据模型&#x1f955;实体集&#x1f955;联系集&#x1f955;属性&#x1f407;E-R图&#x1f4da;映射基数&#x1f407;二元联系集⭐️&#x1f955;一对一&#x1f955;一对多&#x1f955;多…

二叉树的顺序结构——堆的概念实现(图文详解+完整源码 | C语言版)

目录 0.写在前面 1.什么是堆&#xff1f; 2.堆的实现 2.1 堆的结构定义 2.2 函数声明 2.3 函数实现 2.3.1 AdjustUp&#xff08;向上调整算法&#xff09; 2.3.2 AdjustDown&#xff08;向下调整算法&#xff09; 2.3.3 HeapCreate&#xff08;如何建堆&#xff09; …

更多的选择器 更多伪类选择器 颜色选中时写法 被选中的第一行文字 选中第几个元素

目录更多的选择器更多伪类选择器1. first-child2. last-child3. nth-child4. nth-of-type更多的伪元素选择器1. first-letter2. first-line3. selection更多的选择器 更多伪类选择器 1. first-child 选择第一个子元素 圈住的地方意思是&#xff1a;li 的第一个子元素设置为红…

第三篇:Haploview做单倍型教程3--结果解读

大家好&#xff0c;我是邓飞&#xff0c;这里介绍一下如何使用Haploview进行单倍型的分析。 计划分为三篇文章&#xff1a; 第一篇&#xff1a;Haploview做单倍型教程1–软件安装第二篇&#xff1a;Haploview做单倍型教程2–分析教程第三篇&#xff1a;Haploview做单倍型教程…

java中对泛型的理解

那么什么是泛型泛型&#xff1a;是一种把明确类型的工作推迟到创建对象或者调用方法的时候才去明确的特殊的类型。也就是说在泛型使用过程中&#xff0c;操作的数据类型被指定为一个参数&#xff0c;而这种参数类型可以用在类、方法和接口中&#xff0c;分别被称为泛型类、泛型…

【ROS2 入门】ROS2 创建工作空间

大家好&#xff0c;我是虎哥&#xff0c;从今天开始&#xff0c;我将花一段时间&#xff0c;开始将自己从ROS1切换到ROS2&#xff0c;在上几篇中&#xff0c;我们一起了解ROS 2中很多基础概念&#xff0c;从今天开始我们逐步就开始利用ROS2的特性进行开发编程了。 工作区&#…

【Linux】基础IO --- 系统级文件接口、文件描述符表、文件控制块、fd分配规则、重定向…

能一个人走的路别抱有任何期待&#xff0c;死不了 文章目录一、关于文件的重新认识二、语言和系统级的文件操作&#xff08;语言和系统的联系&#xff09;1.C语言文件操作接口&#xff08;语言级别&#xff09;1.1 文件的打开方式1.2 文件操作的相关函数1.3 细节问题2.系统级文…

【Go基础】加密算法和数据结构

文章目录一、加密算法1. 对称加密2. 非对称加密3. 哈希算法二、数据结构与算法1. 链表2. 栈3. 堆4. Trie树一、加密算法 1. 对称加密 加密过程的每一步都是可逆的 加密和解密用的是同一组密钥 异或是最简单的对称加密算法 // XOR 异或运算&#xff0c;要求plain和key的长度相…