【Algorithms 4】算法(第4版)学习笔记 03 - 1.3 背包、队列和栈

文章目录

    • 前言
    • 参考目录
    • 学习笔记
      • 0:预热
      • 1:栈
      • 1.1:栈的链表实现
      • 1.1.1 代码实现
      • 1.2:栈的数组实现
      • 1.2.1:定容栈
      • 1.2.2:可调整大小数组
      • 1.2.3:代码实现
      • 1.3:链表与数组的取舍
      • 2:队列
      • 2.1:队列的链表实现
      • 2.1.1:代码实现
      • 2.2:队列的数组实现
      • 3:泛型
      • 4:迭代
      • 4.1:链表迭代器实现
      • 4.2:数组迭代器实现
      • 4.3:`Iterable` 接口与 `Iterator` 接口
      • 5:栈的应用
      • 5.1:Dijkstra 双栈算法 demo
      • 5.1.1:代码实现

前言

这一章节比较基础,但是也十分重要,栈以及队列都是 Java 开发中非常熟悉的底层结构了。虽然标题有背包(Bags),但实际上 Sedgewick 教授讲解的内容主要还是栈和队列,尤其是栈。

参考目录

  • B站 普林斯顿大学《Algorithms》视频课
    (请自行搜索。主要以该视频课顺序来进行笔记整理,课程讲述的教授本人是该书原版作者之一 Robert Sedgewick。)
  • 微信读书《算法(第4版)》
    (本文主要内容来自《1.3 背包、队列和栈》)
  • 官方网站
    (有书本配套的内容以及代码)

学习笔记

注1:下面引用内容如无注明出处,均是书中摘录。
注2:所有 demo 演示均为视频 PPT demo 截图。

0:预热

先来回顾一下最基础的内容。

(截图自视频 PPT)在这里插入图片描述

栈:后进先出 LIFO - last in first out

  • 入栈 push
  • 出栈 pop

队列:先进先出 FIFO - first in first out

  • 入队 enqueue
  • 出队 dequeue

在后续的实现方法中就是以上面的作为方法的命名。

在本章节中,无论是栈或者是队列,都使用了一个 demo 进行说明:

读取一系列字符,如果是"-",字符 出栈/出队 并打印;
否则,字符 入栈/入队。

1:栈

先来看看栈实现的大致效果:

在这里插入图片描述

下面按照不同的实现方式进行分析。

1.1:栈的链表实现

对应章节《1.3.3 链表》

在这里插入图片描述

官网有分步骤实现的图,太长了这里就不贴了,留个 传送门。

1.1.1 代码实现

edu.princeton.cs.algs4.LinkedStack

在这里插入图片描述

edu.princeton.cs.algs4.LinkedStack#push

在这里插入图片描述

edu.princeton.cs.algs4.LinkedStack#pop

在这里插入图片描述

1.2:栈的数组实现

实现方式:

  • 使用一个数组 s[] 存储栈的 N 个条目
  • push():在 s[N] 上添加一个新的条目
  • pop():从 s[N-1] 中移除一个条目

注:数组需要考虑容量大小问题,当N容量超过了数组的长度,会造成栈溢出。

1.2.1:定容栈

对应章节《1.3.2.1 定容栈》

表1.3.2 一种表示定容字符串栈的抽象数据类型
在这里插入图片描述
FixedCapacityStackOfStrings 这个类jar包没有源码)

这里补充一个比较细节的点 —— 对象游离
对应章节《1.3.2.4 对象游离》

对象游离,不再需要的对象依然持有引用,导致无法进行垃圾回收。

(截图自视频 PPT)在这里插入图片描述

1.2.2:可调整大小数组

对应章节《1.3.2.3 调整数组大小》

前面关于数组实现的说明中有提及,需要注意数组的大小问题,因此需要对数组的长度进行动态调整。

教授列举了两种方式。

第一种方式:

  • push():数组长度加1
  • pop():数组长度减1

但是这种方式可以看出来 开销非常大,插入一个值所需要的时间约为:1+2+3+……+N ~ N2/2。

第二种方式:
反复加倍。
每次达到最大长度后,数组长度增加一倍。

  • push():当数组满了,长度变为2倍
  • pop():当数组的内容只有一半时,长度减半

开销约为:N+(2+4+8+……+N) ~ 3N

第二种方式相较于第一种方式有了明显的提高,但是还有一个问题 —— 抖动

抖动什么意思?可以设想一种最坏的情况:
反复交替入栈和出栈。

(截图自视频 PPT)在这里插入图片描述

这样子的开销也是很难接受的,因而有了更进一步的优化:数组变为 1/4 长度时再减半

最终效果:

在这里插入图片描述

1.2.3:代码实现

edu.princeton.cs.algs4.ResizingArrayStack

在这里插入图片描述

edu.princeton.cs.algs4.ResizingArrayStack#push

在这里插入图片描述

edu.princeton.cs.algs4.ResizingArrayStack#pop

在这里插入图片描述

edu.princeton.cs.algs4.ResizingArrayStack#resize

在这里插入图片描述

1.3:链表与数组的取舍

有上面两种实现方式之后,需要如何进行选择,下面来分析一下。

链表实现:

  • 最坏的情况下,每次操作有固定时间。
  • 使用额外的时间和空间处理链。

可调整数组实现:

  • 每次操作固定的摊销时间。
  • 更少空间浪费。

表1.3.7 基础数据结构
在这里插入图片描述

顺便贴一下通义的总结吧:

栈(Stack)是一种后进先出(LIFO)的数据结构,常见的实现方式有两种:可调整大小的数组和链表。下面分别分析这两种实现方式的优缺点及适用场景:

  1. 可调整大小的数组(动态数组)实现栈

    • 优点
      • 操作速度快:由于数组支持随机访问,因此在栈顶进行插入(push)和删除(pop)操作的时间复杂度为O(1)。
      • 内存连续:数组元素存储在内存中是连续的,有利于CPU缓存优化,提高数据读取效率。
    • 缺点
      • 空间利用率:如果预先分配的数组空间不足,需要动态扩容,这可能涉及到复制整个现有栈到新的、更大的数组中,虽然大多数现代语言的库已经对此做了优化(如自动扩容),但在极端情况下仍可能导致性能损失。
      • 最大容量限制:尽管是可调整大小,但理论上存在一个最大容量限制,尤其是对于固定大小的内存区域。

    适用场景

    • 当栈内元素数量相对稳定或增长可预测时,动态数组实现的栈更为高效。
    • 对于大量push/pop操作且对时间效率要求较高的场景,数组实现通常优于链表。
  2. 链表实现栈

    • 优点
      • 动态扩展:链表无需预先设定大小,当需要更多空间时可以轻松地添加新节点,不会造成内存浪费。
      • 删除方便:链表中的元素删除只需改变指针指向即可,不需要移动其他元素。
    • 缺点
      • 访问速度慢:链表不支持随机访问,查找或修改特定位置的元素需要从头节点开始遍历,时间复杂度为O(n),但对于栈顶操作,仍然可以保持O(1)。
      • 额外的空间开销:每个链表节点都需要额外存储指向下一个节点的指针,增加了存储空间需求。

    适用场景

    • 当栈内元素数量增长不确定或可能大幅度波动时,链表实现的栈更能适应动态变化。
    • 对于栈的大小无法预知或者需要频繁进行插入删除操作而不在乎额外空间消耗的应用场景,链表实现栈也是一个合理选择。

总结来说,在大多数常见应用场景下,由于栈主要关注栈顶操作,数组实现因其简单高效的特性而更受欢迎。然而,如果考虑到空间使用上的灵活性或者有特殊的需求(比如栈的大小无法提前估计),链表实现则是一个更具弹性的选择。

2:队列

用教授的话说,这和栈的实现是一样的,所以很简单的带过了,简单整理一下相关的内容。

2.1:队列的链表实现

对应章节《1.3.3.9 队列的实现》

在这里插入图片描述

2.1.1:代码实现

edu.princeton.cs.algs4.LinkedQueue

在这里插入图片描述

edu.princeton.cs.algs4.LinkedQueue#enqueue

在这里插入图片描述

edu.princeton.cs.algs4.LinkedQueue#dequeue

在这里插入图片描述

2.2:队列的数组实现

edu.princeton.cs.algs4.ResizingArrayQueue

在这里插入图片描述

edu.princeton.cs.algs4.ResizingArrayQueue#enqueue

在这里插入图片描述

edu.princeton.cs.algs4.ResizingArrayQueue#dequeue

在这里插入图片描述

3:泛型

简单来说,泛型是为了满足同一实现对于不同类型的要求。

来看看代码 Javadoc 的说明:

在这里插入图片描述

值得注意的是,Java不支持泛型数组,因此需要进行类型强转,以之前的方法为例:

edu.princeton.cs.algs4.ResizingArrayStack#resize

在这里插入图片描述

教授的观点:

  • 优秀的代码应该不用强制类型转换(A good code has zero cast.
  • 要尽量避免强制类型转换是因为它确实在我们的实现中留下隐患。
    (例如:数据精度丢失、溢出、类型转换异常、类型不匹配、NPE 等)
  • 像这样简单的代码强制类型转换是讨厌的特性。
    ugly cast,an undesirable feature

4:迭代

需要实现的是,允许客户端遍历集合中的元素,但不需要让客户端知道底层实现用的是链表还是数组或其他任何实现。

下面以栈的实现为例进行说明。

4.1:链表迭代器实现

edu.princeton.cs.algs4.LinkedStack.LinkedIterator

在这里插入图片描述

4.2:数组迭代器实现

edu.princeton.cs.algs4.ResizingArrayStack.ReverseArrayIterator

在这里插入图片描述

4.3:Iterable 接口与 Iterator 接口

这两个接口还是比较重要的,所以单拎出来说明一下。

java.lang.Iterable

在这里插入图片描述

在这里插入图片描述

java.util.Iterator

在这里插入图片描述

在这里插入图片描述

仔细看的话应该不难看出来两者的区别,不过为了便于理解,我又去整理了一下 ChatGPT 和 通义 的回答:

在Java中,Iterable接口和Iterator接口都与集合的迭代(iteration)有关,但它们分别承担不同的角色。

  1. Iterable 接口:

    • Iterable接口是Java集合框架的根接口之一,它定义了一种表示可迭代对象(Iterable object)的标准方式。
    • 该接口中包含一个抽象方法 iterator(),该方法返回一个实现了Iterator接口的迭代器对象。
    • 类实现了Iterable接口的对象可以使用增强的for循环(foreach循环)进行遍历,因为foreach循环的底层实现是通过调用iterator()方法获取迭代器来实现的。
  2. Iterator 接口:

    • Iterator接口是用于遍历集合中的元素的标准方式。它定义了一组方法,允许按顺序访问集合中的元素,而不暴露集合的内部结构。
    • Iterator接口包含一些重要的方法,如hasNext()用于检查是否还有元素,next()用于获取下一个元素,remove()用于从集合中移除当前元素(可选)。

为什么在 Iterable 接口中需要定义 Iterator 接口呢?

  • 这种设计方式符合单一职责原则,即一个接口只负责一个职责。Iterable负责定义可迭代对象,而Iterator负责定义遍历这个可迭代对象的方式。这样的分离使得集合类可以选择性地提供不同类型的迭代器,而不必在可迭代接口中包含所有可能的遍历方法。

  • 这种设计也符合 迭代器模式(Iterator Pattern),其中Iterable表示聚合对象,而Iterator表示迭代器对象。这样的模式使得你可以在不暴露集合内部结构的情况下遍历集合中的元素。

总结来说,Iterable 是一个抽象的概念,表明一个类是可以被迭代的;而 Iterator 则是具体实现迭代行为的工具。这种分离的设计使得集合的遍历机制更加灵活,并且让集合的实现和遍历逻辑相互独立。

5:栈的应用

这一小节主要探讨了 Dijkstra 双栈算法的实现。

5.1:Dijkstra 双栈算法 demo

先来说说这个 Dijkstra 双栈算法,输入一组字符组成的表达式:

在这里插入图片描述

书本中给出的示意图是这样的:

在这里插入图片描述

但是说实话不太直观,直到看到视频里面教授 PPT 的动态演示,简直妙啊:

在这里插入图片描述

注:操作数栈具体出栈数需要根据运算符判断,如果是像开根(sqrt)操作,则只需要操作栈顶一位数。

5.1.1:代码实现

我找到了官方的 代码实现,不过我想直接用 main 方法测试,所以稍微改了一下,贴在下面:

import edu.princeton.cs.algs4.Stack;

/******************************************************************************
 *  Compilation:  javac Evaluate.java
 *  Execution:    java Evaluate
 *  Dependencies: Stack.java
 *
 *  Evaluates (fully parenthesized) arithmetic expressions using
 *  Dijkstra's two-stack algorithm.
 *
 *  % java Evaluate
 *  ( 1 + ( ( 2 + 3 ) * ( 4 * 5 ) ) )
 *  101.0
 *
 *  % java Evaulate
 *  ( ( 1 + sqrt ( 5 ) ) / 2.0 )
 *  1.618033988749895
 *
 *
 *  Note: the operators, operands, and parentheses must be
 *  separated by whitespace. Also, each operation must
 *  be enclosed in parentheses. For example, you must write
 *  ( 1 + ( 2 + 3 ) ) instead of ( 1 + 2 + 3 ).
 *  See EvaluateDeluxe.java for a fancier version.
 *
 *
 *  Remarkably, Dijkstra's algorithm computes the same
 *  answer if we put each operator *after* its two operands
 *  instead of *between* them.
 *
 *  % java Evaluate
 *  ( 1 ( ( 2 3 + ) ( 4 5 * ) * ) + )
 *  101.0
 *
 *  Moreover, in such expressions, all parentheses are redundant!
 *  Removing them yields an expression known as a postfix expression.
 *  1 2 3 + 4 5 * * +
 *
 *
 ******************************************************************************/

public class Evaluate {
    public static void main(String[] args) {
        String s = "( 1 + ( ( 2 + 3 ) * ( 4 * 5 ) ) )";
        System.out.println("s = " + s);
        evaluate(s.split(" "));
        String s2 = "( ( 1 + sqrt ( 5 ) ) / 2.0 )";
        System.out.println("s2 = " + s2);
        evaluate(s2.split(" "));
    }

    public static void evaluate(String[] args) {
        Stack<String> ops = new Stack<String>();
        Stack<Double> vals = new Stack<Double>();

        for (String s : args) {
            // 读取字符
            if (s.equals("(")) ;
            // 如果是运算符则压入栈中
            else if (s.equals("+")) ops.push(s);
            else if (s.equals("-")) ops.push(s);
            else if (s.equals("*")) ops.push(s);
            else if (s.equals("/")) ops.push(s);
            else if (s.equals("sqrt")) ops.push(s);
            // 如果是")",则弹出运算符和操作数,计算结果并压入栈中
            else if (s.equals(")")) {
                String op = ops.pop();
                double v = vals.pop();
                if (op.equals("+")) v = vals.pop() + v;
                else if (op.equals("-")) v = vals.pop() - v;
                else if (op.equals("*")) v = vals.pop() * v;
                else if (op.equals("/")) v = vals.pop() / v;
                else if (op.equals("sqrt")) v = Math.sqrt(v);
                vals.push(v);
            }
            // 如果字符既非运算符也不是括号,将它作为double值压入栈
            else vals.push(Double.parseDouble(s));
        }
        System.out.println(vals.pop());
    }

    // 源代码
    // public static void main(String[] args) {
    //     Stack<String> ops = new Stack<String>();
    //     Stack<Double> vals = new Stack<Double>();
    //
    //     while (!StdIn.isEmpty()) {
    //         String s = StdIn.readString();
    //         if (s.equals("(")) ;
    //         else if (s.equals("+")) ops.push(s);
    //         else if (s.equals("-")) ops.push(s);
    //         else if (s.equals("*")) ops.push(s);
    //         else if (s.equals("/")) ops.push(s);
    //         else if (s.equals("sqrt")) ops.push(s);
    //         else if (s.equals(")")) {
    //             String op = ops.pop();
    //             double v = vals.pop();
    //             if (op.equals("+")) v = vals.pop() + v;
    //             else if (op.equals("-")) v = vals.pop() - v;
    //             else if (op.equals("*")) v = vals.pop() * v;
    //             else if (op.equals("/")) v = vals.pop() / v;
    //             else if (op.equals("sqrt")) v = Math.sqrt(v);
    //             vals.push(v);
    //         } else vals.push(Double.parseDouble(s));
    //     }
    //     StdOut.println(vals.pop());
    // }

}

(完)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MySQL原理(一)架构组成之逻辑模块(1)组成

总的来说&#xff0c;MySQL可以看成是二层架构&#xff0c;第一层我们通常叫做SQL Layer&#xff0c;在MySQL数据库系统处理底层数据之前的所有工作都是在这一层完成的&#xff0c;包括权限判断&#xff0c;sql解析&#xff0c;执行计划优化&#xff0c;query cache的处理等等&…

算法——A/算法通识

目录 一、复杂度分析 A/时间复杂度 B/空间复杂度 C/分析技巧 二、枚举分析 A/枚举算法介绍 B/解空间的类型 C/循环枚举解空间 三、模拟算法 四、递归 A/递归介绍 递归的两个关键要素&#xff1a; B/递归如何实现 C/递归和循环的比较 一、复杂度分析 A/时间复杂度…

AVL树

文章目录 AVL树平衡因子 AVL树结点的定义AVL树类和函数接口AVL树插入元素最小不平衡子树旋转 AVL树的验证参考源码 AVL树是对普通二叉搜索树的一种优化。当二叉搜索树插入的元素是有序的时候或者接近有序的时候&#xff0c;二叉搜索树的性能会大大降低。二叉搜索树可能会变成一…

中二少年工具箱(PC端)简介

同学们可以私信我加入学习群&#xff01; 正文开始 简介一、功能模块1.node版本管理工具 总结 简介 中二少年开发的中二少年工具箱&#xff0c;相信博主&#xff0c;功能不孬。 辅助自己开发工作&#xff0c;帮助新人快速入门&#xff0c;提供交互式文档辅助学习……如果还不…

LDRA Testbed软件静态分析_Jenkins持续集成_(2)配置邮件自动发送静态分析结果

系列文章目录 LDRA Testbed软件静态分析_操作指南 LDRA Testbed软件静态分析_自动提取静态分析数据生成文档 LDRA Testbed软件静态分析_Jenkins持续集成_(1)自动进行静态分析的环境搭建 LDRA Testbed软件静态分析_Jenkins持续集成_(2)配置邮件自动发送静态分析结果 LDRA Testb…

arcgis自定义dem高程实现地形抬高 - 操作矢量,转tin、adf(tif),cesiumlab切高程服务

这次记录分享一下arcgis自定义高程全过程 /(ㄒoㄒ)/~~ 我的场景&#xff1a;前端实现地面抬高效果 自定义高程实现地形抬高 一、数据处理 - arcgis操作矢量1、准备工作&#xff08;可选&#xff09;2、绘制外围矢量&#xff08;可选&#xff09;3、操作矢量数据 二、创建tin - …

opencvb 十七 使用cmake配置opencv c++项目

1、cmake简介 1.1 cmake是什么 CMake是一个开源、跨平台的编译&#xff08;Build&#xff09;工具&#xff0c;是用来构建、测试和打包软件的。它能够用简单的语句来描述所有平台的编译过程。它能够输出各种各样的makefile或者project文件&#xff0c;能测试编译器所支持的C特…

记录一次k8s集群镜像恢复到harbor的过程

之前由于harbor的存储空间不够了&#xff0c;同事干掉了好多镜像&#xff0c;结果把现网生产的镜像也搞掉了。进行了找回操作&#xff0c;这里做下记录。 环境是k8s集群&#xff0c;容器引擎用的containerd。 最初发现这个问题是在增加节点的时候&#xff0c;发现有的节点主机…

【DPI(Direct Programming Interface)_2024.02.01】

DPI接口&#xff1a;实现SV与C的交互 ① DPI_svc test.sv文件&#xff1a; 从C import task/function到SV 从SV export task到C 利用DPI调用C code访问register test.c文件&#xff1a; C调用apb_write驱动 ② dpi_perl test.sv文件&#xff1a; 利用DPI调用c code间接调…

CKS1.28【1】kube-bench 修复不安全项

Context 针对 kubeadm 创建的 cluster 运行 CIS 基准测试工具时&#xff0c;发现了多个必须立即解决的问题。 Task 通过配置修复所有问题并重新启动受影响的组件以确保新的设置生效。 修复针对 API 服务器发现的所有以下违规行为&#xff1a; 1.2.7 Ensure that the --authoriz…

【华为】GRE Over IPsec 实验配置

【思科】GRE Over IPsec 实验配置 前言报文格式 实验需求配置拓扑GRE配置步骤IPsec 配置步骤R1基础配置GRE 配置IPsec 配置 ISP_R2基础配置 R3基础配置GRE 配置IPsec 配置 PCPC1PC2 抓包检查OSPF建立GRE隧道建立IPsec 隧道建立Ping 配置文档 前言 GRE over IPSec可利用GRE和IP…

echarts条形图添加滚动条

效果展示: 测试数据: taskList:[{majorDeptName:测试,finishCount:54,notFinishCount:21}, {majorDeptName:测试,finishCount:54,notFinishCount:21}, {majorDeptName:测试,finishCount:54,notFinishCount:21}, {majorDeptName:测试,finishCount:54,notFinishCount:21}, {maj…

关于JVM面试题汇总

JVM是如何运行的&#xff1f; JVM的执行流程如下&#xff1a; 程序再执行之前先要把Java代码转换成字节码&#xff08;class文件&#xff09;&#xff0c;JVM首先需要把字节码通过一定的方式类加载器&#xff08;ClassLoader&#xff09;把文件加载到内存中运行时数据区&…

Weblogic反序列化漏洞分析之CVE-2021-2394

目录 简介 前置知识 Serializable示例 Externalizable示例 联系weblogic ExternalizableLite接口 ExternalizableHelperl类 JdbcRowSetImpl类 MethodAttributeAccessor类 AbstractExtractor类 FilterExtractor类 TopNAggregator$PartialResult类 SortedBag$Wrappe…

【A题完整论文】2024美赛完整论文+代码参考(无偿分享)

A题&#xff1a;资源可用性和性别比例 一、问题分析 1.1 问题一分析 针对该问题&#xff0c;若七鳃鮼的性别比例受到外部环境因素的影响&#xff0c;那么这可能会导致种群大小和结构的变化。如果雌性在某些环境条件下更为优势&#xff0c;种群的增加可能对其他物种的竞争和资源…

【Python】一个简单的小案例:实现批量修改图片格式

1.代码 import os from tkinter import Tk, Button from PIL import Imagedef check_and_create_folders():# 获取当前目录current_directory os.getcwd()# 定义文件夹名称folders_to_check ["JPG", "PNG"]for folder_name in folders_to_check:folder_…

nvm-windows的安装和配置

下载安装nvm-setup.zip用于切换node版本&#xff0c;旧项目用的是14版本&#xff0c;vue3需要的node版本要高些,所以运行vue3项目前需要用nvm切换node的版本先。 下载安装好nvm-setup.zip后检查是否配置好如下信息&#xff1a; 之后在 PATH 变量中添加 %NVM_HOME% 和 %NVM_SYM…

2024年美赛 (B题MCM)| 潜水艇 |数学建模完整代码+建模过程全解全析

当大家面临着复杂的数学建模问题时&#xff0c;你是否曾经感到茫然无措&#xff1f;作为2022年美国大学生数学建模比赛的O奖得主&#xff0c;我为大家提供了一套优秀的解题思路&#xff0c;让你轻松应对各种难题。 让我们来看看美赛的B题&#xff01; 完整内容可以在文章末尾领…

找不到d3dcompiler_43.dll,无法继续执行代码的原因分析与解决方法

在运行某些软件或游戏时&#xff0c;可能会遇到系统提示找不到 d3dcompiler_43.dll 文件的情况。这个特定的动态链接库文件 (dll) 是 DirectX 3D 编译器组件的一部分&#xff0c;对于许多现代软件游戏的正常运行起着不可或缺的作用。它的主要功能在于将高级着色语言编写的代码转…

vite, vue3, vue-router, vuex, ES6学习日记

学习使用vitevue3的所遇问题总结&#xff08;2024年2月1日&#xff09; 组件中使用<script>标签忘记加 setup 这会导致Navbar 没有暴露出来&#xff0c;导致使用不了&#xff0c;出现以下报错 这是因为&#xff0c;如果不用setup&#xff0c;就得使用 export default…