分类 其它 下的文章

相信不少人和我一样,是从“
汉语新解
”这段爆火提示词中知道李继刚这位“神人”的。直到看到11月4日公众号
“数字生命卡兹克”
对继刚做了专访文章
《专访"Prompt之神"李继刚 - 我想用20年时间,给世界留一句话》
,让我初步了解到继刚其实是一位有理想、爱读书、善思考的人。这激发了我的强烈好奇心和沟通欲,于是11月13日我专程前往北京,约继刚进行了一次深度面谈。别具一格的是,作为赠送给“粉丝”的签名纪念,也是他刚使用Claude生成的一句话:
“写越简,用越广;删越多,存越精”

还是熟悉的地方(望京·聚宝源),还是熟悉的话题(提示工程),期间继刚思绪翻飞、滔滔不绝,完全不像一个“i人”,从这也能看出他对这个领域的专注与热爱,以至于饭后大家都意犹未尽,对我个人来说也是受益良多,故记述此文,以享诸位。

1. 提示词的本质

饭桌上火锅的温度刚上来,隐约中冒着热气,二人略作寒暄,我们便直入主题了。

继刚首先问:“你觉得提示词的本质是什么?”,并追加道:“我花了半年多的时间,终于把这个问题想明白了。”

“这个问题我尝试想过,但确实未做过深度思考,可能短时间也很难有确定结论。”,我略显尴尬并如实回复说。

“无妨,你放开想、敞开说,即便不对也没有关系,这个思考过程也很重要。”,继刚安慰我道,希望我不要有额外的压力。

我略作沉吟,细想了一会说道:“可能我现在还没法用合适的语言组织起来,但是可以用一些词描述我的理解,如自然形式的编程语言、大模型的解压密码、大模型推理方向的向导诸如此类。”

看我已绞尽脑汁,继刚决定不再“折磨”我了,回应我说:“你说的这些主要在描述提示词是什么,还是停留在表象,并非提示词的本质。就像射向标靶的无数根长矛,每根矛都指向靶心,但矛本身并不是靶心。”

“在我看来,提示词的本质,是表达。”
,继刚直接说出了他的结论。

提示词的本质

相信大多未对提示词本质做过相关思考的人,可能和我是相似的反应,面对这个结论仍是一脸懵逼,得其形,而不知其义。有一定的哲学基础的人,很清楚这就是继刚所理解的提示词的“道”,或者说是他认为的提示词的“第一性原理”。但是知“道”,并不意味着可以践“道”。

正如《道德经》所述:

有物混成,先天地生。……独立而不改,周行而不殆,……吾不知其名,强字之曰道,强为之名曰大。……

道就摆在那里,一直存在,但想做到知行合一并不容易。你可能理解“表达”是什么含义,但不一定能理解“提示词所表达的表达”是什么含义。道过于抽象,需要利用“实现”去对其进一步剖析,继刚给出了表达的第一步解析。

表达的含义解析

简而言之,本意是人的脑海中的想法,表示想做什么。文意是本意的符号化(提示词),表示想法如何描述。解意是让大模型理解人的想法,表示想法如何解读。通过这三个阶段的拆解,可以细致地还原提示词从人到大模型的过程,也就是提示词的本质。

提示词的目的是把人脑海中的想法(本意),精准无误地提供给大模型去理解(解意)。优秀的提示工程师善于通过控制提示词(文意),缓解本意与解意之间的差距。而首当其冲的,就是如何精准地描绘本意,告诉大模型,你到底想做什么?然后才是优化文意,尽量把本意无损的传递给大模型,也就是优化提示词。最后才是大模型,虽然不能通过提示词提升大模型的理解能力,但可以选择理解能力更好的大模型。

2. 如何清晰表达

继刚花了一年的时间去琢磨如何清晰地描述脑海中的想法,也就是如何描绘本意。

精准表达的方法

首先是经验,经验是想法的具象化,这是人理解想法的基础,没有体感经验的想法是空中楼阁。其次是词汇,词汇是经验在语言上的映射,是想法的符号化形式。最后是知识,知识是对词汇含义的详细解读和描述,是想法符号化为精确词汇的基础。有了以上的基础,才可以准确地表达脑海中的想法,实现清晰表达。

3. 怎样提升效果

分析如何提升大模型的问答效果,继刚给出了这样的思考。

大模型效果提升的方法

  • 首先要认清的是,大模型是放大器,不是许愿器。种什么因,结什么果,提示词的的输入直接影响大模型的输出,这里对应解意的部分。

  • 其次,要定义清楚要解决的问题或任务是什么,描述清楚本意。这里继刚给了一个非常形象的描述:“人要比AI凶”。说白了就是不要惧怕AI,要有信心操纵好AI,有种“战略上要藐视敌人”的意思。

  • 最后才是提示词,通过文本把想法表达出来,对应的就是文意。提示词要足够精准,有助于大模型在Embedding语义空间的准确定位。提示词要足够简练,有助于大模型Attention机制实现重点意义的关联。

4. 提示工程方法论

以上,是继刚总结的提示工程的“道”,接下来描述提示工程的“术”,即如何写好提示词。

4.1 乔哈里视窗

提示词到底怎么写?乔哈里视窗本是关于沟通技巧的理论,继刚巧妙地将其迁移到人与大模型沟通的场景下,描绘了提示工程的基本沟通框架,有点“见人说人话,见鬼说鬼话”的莫明体感。

乔哈里视窗

静态来看,视窗中的一、四象限所描述的是大多数人比较熟悉的方式,第二象限对提问的能力有比较高的要求,需要人不断地学习(可参考:
问题之锤
),第三象限需要人和大模型协同探索未知的领域和边界。

动态来看,未来大模型知道的会越来越多(X轴不断下移),那么对人的每个个体来说,如何丰富自身的认知边界就显得非常重要了(控制Y轴)。

最后,针对不同的象限,可以灵活地调节提示词的描述方式,从小到大,从简到繁地优化提示词的整体状态(Debug提示词),这就是提示工程的基本逻辑。

4.2 极致压缩

继刚对自己的提示词风格做过总结,去年他致力于提示词的清晰表达,而今年则专注于提示词的压缩表达。

提示词怎么简化?对于大模型来说,最容易理解的符号是向量,既而是token、单词、句子等,而人则反之。显然,单词是人和大模型沟通中最高效的形式。而作为将函数作为一等公民的LISP语言(首个函数式语言),代码形式与数据形式完全相同,这种高度的简洁性设计极度适合充当单词之间的“粘合剂”,构建最极致压缩的提示词表示,简直是天作之合!

提示词的极致压缩

虽然这是继刚最初的个人猜想,但经过无数的实验验证,大模型(尤其是Claude)具备理解这种提示词形式的能力,真正做到了《庄子》中所说的“得其意,忘其言”,妙到毫颠!

荃者所以在鱼,
得鱼而忘荃
;蹄者所以在兔,
得兔而忘蹄
;言者所以在意,
得意而忘言

4.3 点亮星星

那么,如何找到最合适的单词呢?既然大模型具备理解单词的能力,那么选用哪个单词就是很关键的问题。如前面拆解本意时提到的“词汇”与“知识”概念,尽量选用词汇的定义而非词汇的描述(可参考:
定义之矛
),让提示词中的单词“直击本质”。

这件事情说起来容易,做起来一点也不简单……

《这就是ChatGPT》插图:Word向量空间

继刚常用“点亮星星”的比喻(可参考:
类比之弓
)来描述自己寻找本质词汇的过程。

想象我在一间没有灯光的屋子里(向量空间),周围都是黯淡无光的星星(单词向量),我可以喊星星的名字去点亮星星,当我按照顺序点亮星星时,它们之间的连线构成了一个星象图,大模型可以理解这个星象图的模式含义并做泛化输出,当我写提示词时,我的脑海其实在放烟花。

类比之弓:Embedding

4.5 Read in. Prompt out.

最后,怎么写好提示词呢?大家应该可以看到,丰富的知识积累、深度的词汇理解、成熟的工程素养,对于写好提示词都至关重要,这里没有捷径,套用《卖油翁》中的话,可以表述为:“无他,惟读书尔”。多读、多思、多写,每个人才能悟出自己的提示词之“道”。

5. 提示词工程师

再回头看提示词工程师这个角色,他是一个交叉领域的岗位。借用继刚的原话:“提示词工程师,既要有提示词的写作能力,又要有工程师的素养,谜底就在谜面上。”

提示词工程师的画像

热爱协作的技术人,或者喜欢技术的创作者,将是提示词工程师的最佳人才画像。王小波必然是创作者的典型,而技术人中,有一类角色也将十分契合,他们叫“开源布道师”……

6. 尾记

13日和继刚聊完后,脑子一直处于发热的状态,14-15这两天又赶上全球机器学习大会,开启了“疯狂社交”模式,根本无暇整理思路。比较巧合的是,16日PEC 2024(提示工程峰会)继刚又给了《提示词的道与术》的演讲,主题与我们面谈的内容基本一致,所以文章我也直接引用了他演讲PPT中的内容作为辅助素材。

建了一个小群,大家一块聊聊提示词技术,感兴趣的同学可以进群保持关注。致未来优秀的提示词工程师们,一起加油!!!

窗外灯光点点,总算对这部分的心得做完了细致整理,还被家里人偷拍深夜码字的状态……

另外,大家也可以直接关注云中江树的“结构词AI”公众号,找到
“Prompt设计的艺术与构建AI原生产品”
分论坛的直播回顾视频直接观看(第47min开始)。

7. 参考资料

  1. 专访"Prompt之神"李继刚 - 我想用20年时间,给世界留一句话:
    https://mp.weixin.qq.com/s/JT2oOG2SYw2pDYEHlEmcyQ
  2. Claude Prompt: 汉语新解:
    https://mp.weixin.qq.com/s/7CYRPFQxi37ONTlX0hfzRQ
  3. Claude Prompt:问题之锤:
    https://mp.weixin.qq.com/s/KlkomVKEYKjVAb6NEXcjSg
  4. Claude Prompt:定义之矛:
    https://mp.weixin.qq.com/s/eNcqU-_-8SMpVBXAcgeQRQ
  5. Claude Prompt:类比之弓:
    https://mp.weixin.qq.com/s/p1viD22cPtD3iLzOIb_FMg
  6. 关于说话的一切:
    https://weread.qq.com/book-detail?type=1&senderVid=4000012&v=10132d20813ab77a6g012034
  7. 为什么伟大不能被计划:
    https://weread.qq.com/book-detail?type=1&senderVid=101531&v=0bf32020813ab7e6bg016510
  8. 深度学习的数学:
    https://weread.qq.com/web/bookDetail/01d327c071a122c701d71f3
  9. 拐点:
    https://weread.qq.com/web/bookDetail/08732220811e7ef55g012f82
  10. GPT图解大模型是怎样构建的:
    https://weread.qq.com/web/bookDetail/e0d32f10811e7ee55g010619
  11. 这就是ChatGPT:
    https://weread.qq.com/web/bookDetail/74332a90813ab86c4g019d98

工作电脑为一体机。所有的USB接口都在屏幕的后面。插拔U盘极不方便。于是搜索USB小物件,看能不能通过小物件将USB的接口延长到屏幕前

寻觅一番,找到一个中意的小物件——
ELEKS MAKER 极客桌面控制器

如上图所示,小物件有着朋克风,充满现代感。带有3个USB2.0接口,日常工作使用足够。另外还有三个小按键,以及一个大旋钮。大旋钮可以旋转,也可以当按钮用。

按照使用说明,大旋钮的旋转是控制系统的音量,大旋钮的按键是切换系统是否静音。

也可以通过控制程序,来自定义大旋钮按键的功能。

于是,有了个想法,
将大旋钮的的按键,自定义为锁屏
。当我离开工位,按一下大旋钮,就将工作电脑锁屏了。

于是,打开控制程序,如下图

可以看出,控制程序分别对
三个按钮

一个大旋钮
的功能进行了设定的功能

但是
!对“按下按钮”的这个动作,只能指定多媒体键,也就是在若干个指定的多媒体键中间选择一个。并不能选择其他的快捷键。比如,锁屏的快捷键是Win+L,却是没法设定。

于是,在网上搜索一番。找到了两篇有用的文章

【转载】修改Windows下键盘按键对应功能的一些方案

Win10 64位电脑如何以桌面快捷方式创建一个一键锁屏程序?

第一篇文章,讲解了,如何修改
多媒体键
对应的功能

如下面所示,通过注册表,将
计算器多媒体键
的功能改为指向记事本(notepad.exe)

Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AppKey\18]
"ShellExecute"="notepad.exe"

其中,ShellExecute字段指向其他程序。

也可以用Association字段指向扩展名,调用该扩展名对应的程序。

例如:

"Association"="mailto",表示调用默认的邮件程序

"Association"=".doc",表示调用默认的doc的程序,一般是Word

第二篇文章,讲解了,如何通过快捷方式,设定锁屏

在快捷方式下,通过

rundll32.exe user32.dll,LockWorkStation

调用系统的锁频程序

于是,灵光一闪,将上面两篇文章的内容合二为一

1、将按下按钮的多媒体快捷键改为“计算器”

2、编写注册表,将计算器多媒体键的对应的内容改为指向锁屏。

Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AppKey\18]
"ShellExecute"="rundll32.exe user32.dll,LockWorkStation"

将注册表导入到系统内之后

此时,按小物件的大按钮,电脑立刻锁屏!

大家好,我是汤师爷~

今天聊聊
商品概念模型设计。

优秀的商品概念模型应具备充分的灵活性和抽象性,以适应不同行业的需求变化,并在系统升级或业务调整时,能最小化重构的工作量。

商品模型是商品管理系统的核心,整体来看,可以划分为三个关键部分:

  • 基础资料:用于定义和管理商品的基本要素,是商品的“元数据”。包括但不限于品牌、类目、属性库、单位等关键信息。通过标准化这些基础资料,可以确保整个系统中商品信息的一致性和标准化。
  • 商品主档信息:这部分是商品的核心描述,包含关联的基础资料信息,以及商品的一些描述信息,例如名称、副标题、图片、编码、条码、商品描述、销售配置、供应链配置、财务配置等。
  • 渠道差异化信息:在全渠道零售环境中,这部分非常重要,它可以针对不同渠道提供个性化配置,例如渠道特定的价格、销售配置等。

基础资料

接下来,我们将详细介绍商品系统的基础资料。它包括商品类目、属性库和多单位等重要组成部分,这些元素共同构建了商品的基本框架。

1、商品类目

商品类目是一种系统化的分类方法,用于组织和管理各种商品。它为商品提供了一个层次化的结构,使商品能够被有效地归类、检索和管理。

商品类目通常采用树状结构,从顶层的大类逐步细分到具体的小类。每个层级都代表了商品的不同特征或属性。合理的类目管理能显著提升商品管理效率:

  • 对于消费者
    :类目导航优化了商品发现的路径,让用户能快速找到目标商品。
  • 对于商家
    :类目为商品提供了标准化的组织方式,帮助运营人员快速筛选、定位商品,支持库存管理和销售分析等操作。

商品类目通常分为前台类目和后台类目,以满足消费者和商家的不同需求。

前台类目面向消费者,优化了商品浏览和搜索体验。其核心特征是灵活性,根据用户购物习惯、促销活动和市场趋势动态调整。如图所示。

通过这样的调整,前台类目能够快速响应市场需求,提升用户购物体验和转化率。

后台类目主要服务于商家,为商品管理和数据分析提供稳定的框架。与前台类目的灵活性不同,后台类目结构相对固定,变更频率低。例如,按类目统计某一季度的商品销售额;通过后台类目筛选滞销商品,制定清库存策略。

2、属性库

属性库通过集中化管理,为商品搜索、分类和分析提供了坚实的基础,同时简化了复杂商品属性的维护流程。

商品属性,又称为产品属性、商品参数,是产品本身固有的特征。

不同行业的商品,差异非常大,有很多行业差异化属性。根据使用目的、用途不同,演化出各式各样的属性,有的用于展示,有的用于分析,有的用于经营管控。下面根据商品属性不同的分类法,逐一展开描述:

  • 描述属性:包括商品名称、商品描述、规格、型号、产地、等级、生产厂商、商品图片等。这些属性主要用于向消费者展示商品的基本信息。
  • 统计属性:品牌、类目、系列、款式、适用人群、适用年龄等。这类属性为商品数据统计和分析提供依据,例如统计某品牌的月销售额。
  • 考核属性:一般用于组织业绩考核。例如,基于品牌、分类或系列统计的销售额,用于评估部门或员工的业绩。
  • 物流属性:长、宽、高、净重、毛重、重量单位等。这些属性影响配送成本和仓储规划。例如,大件商品需要特殊的仓储和配送方案。
  • 管控属性:是否季节商品、是否有保险、是否支持配送、是否支持打折、是否保质期管控、是否串码管理等。这类属性为运营管控提供支持,例如控制保质期商品的销售策略。
  • 销售渠道属性:针对不同销售渠道的特殊属性。例如,美团、饿了么平台上商品的最小购买数量或平台分类。
  • 规格属性:该属性是组成SKU的特殊属性,直接影响 SKU 的生成,例如衣服的颜色、尺寸等。这类属性不仅影响消费者购买决策,也直接关系到商家的库存管理。

为了避免属性重复创建,同时提高管理效率和数据一致性,通常会建立一个统一的属性库。

属性库的结构由三部分组成:属性组、属性项、属性值。

  • 属性组:
    顶层分类,用于按属性的共性特征管理属性。例如,手机的属性组可包括“外观属性”(颜色、材质)和“性能属性”(处理器、运行内存)。
  • 属性项:
    具体的属性名,用于定义商品的某个特征,例如颜色、尺码、口味等。
  • 属性值:
    属性的具体内容,例如“颜色”的值为红色、绿色、蓝色等。

3、多单位

在零售场景中,不同消费者对商品的计量需求千差万别。例如,消费者希望按瓶购买饮料,而企业客户则希望按整箱下单。多单位功能为这一需求提供了灵活解决方案。

这种灵活性体现在多个方面:

  • 销售多样性:例如,一种饮料可以按瓶购买,也可以按整箱(含多瓶)购买。
  • 库存精确管理:商家可以同时跟踪单品和批量单位的库存,提高库存管理的精确度。
  • 定价策略优化:可以为不同单位设置不同价格,如单瓶价格和整箱优惠价,刺激消费。
  • 物流效率提升:支持按不同单位发货,优化仓储和配送流程。

在实现层面,多单位功能需要以下概念模型支持:

  • 单位:是商品的计量标准,如件、盒、瓶、公斤等。多单位功能则允许一个SKU支持多种计量方式。
  • 单位转换关系:为每种商品定义单位间的转换关系(如1箱=12瓶),并在库存管理、物流发货中支持动态换算。

多单位功能不仅满足了不同消费者的购买需求,还为商家提供了更灵活的经营策略,同时简化了库存管理流程,提高了整体运营效率。

商品主档信息

在介绍完商品系统的基础资料后,我们将深入探讨商品主档信息。

1、商品

商品指商家在零售环境中提供的具体产品或服务,旨在满足消费者的多样化需求。例如,在服装行业,一件商品可能有多种颜色和尺码的规格供消费者选择;而在生鲜行业,商品可能按照重量或数量进行销售。

商品的多样性不仅体现在种类上,还体现在规格和属性特征上,这些共同构成了商品在零售系统中的完整定义。

2、SKU

SKU(Stock Keeping Unit)是库存量单位,也称为最小库存单元,是库存管理的基本单位。

SKU 明确定义了具体商品的规格属性值。例如,"iPhone 16"这款商品的关键规格包括颜色(黑色、红色、银色、金色)和容量(128G、256G、512G),可以组合出 4×3=12 个 SKU。

之所以称为"最小库存单元",是因为 SKU 是库存管理中的实际管理对象。每个 SKU 都有明确的规格、价格、库存和条形码,是不可再细分的管理单位。无论是商品的采购、入库、销售、出库还是库存盘点,系统跟踪的对象都是 SKU。

3、商品类型

在新零售业务中,商品种类繁多,为了更高效地管理商品数据,需要将商品进行类型划分。商品类型不仅影响库存管理和交易方式,也直接决定消费者的购买体验。

  • 实物商品:以有形实体存在,不能通过网络来传递,必须依赖传统的物流运输系统来传递。例如,鸡蛋、大米、手机等。
  • 服务商品:能够实现交易的无形商品,无需物流参与,就能完成交易,例如,话费充值、游戏点券、线上课程等。
  • 组合商品:组合商品是由多个单独售卖的商品组成的捆绑销售商品,例如:下午茶套餐(包含咖啡、蛋糕、小食)、七夕美妆组合(包含口红、香水、护肤品)等。
  • 多规格商品:多规格商品是由多个 SKU 组成的商品集合,消费者只能选择其中一个 SKU。例如,以iphone16为例,关键规格有颜色(黑色、红色、银色、金色)、容量(128G、256G、512G),消费者选中了”黑色128G的iphone16“进行下单。

4、商品状态

商品状态是商品生命周期管理的核心,贯穿商品从创建到退市的全过程。在新零售系统中,商品状态用于标识商品在业务流程中的具体阶段,不同的状态对应不同的管理和运营策略。

商品的生命周期状态包括建档、新品、正常、预淘汰、淘汰、清理、待归档等。

  • 建档:
    商品信息初次录入系统,完成基础数据的创建,包括名称、类目、品牌、规格和价格等。这一阶段的商品尚未对外展示,仅供内部审核和信息完善,确保商品上线前具备完整的基础信息。
  • 新品:
    商品审核通过并正式上线,进入市场的初期阶段。此时通常会通过新品标签、首页推荐等方式进行重点推广,并配合首发折扣或赠品活动,吸引消费者关注。
  • 正常:
    商品进入稳定销售阶段,成为商家常规运营的一部分。此状态下的商品以正常定价销售,同时可能参与常规促销活动(如满减、限时折扣),以维持销量。
  • 预淘汰:
    商品销量下降或市场需求减弱,逐步退出核心销售渠道。在此阶段,商家减少库存补货,并通过特定促销活动(如清仓折扣)加速库存清理。
  • 淘汰:
    商品已停止销售,但库存尚未完全清理完毕。此状态的商品通常从前台下架,仅通过特定渠道(如线下门店或促销专区)进行有限销售,直至库存耗尽。
  • 清理:
    商品进入最终清仓阶段,彻底准备退市。商家集中处理剩余库存,通过降价促销、库存转移或销毁计划等手段清空库存,确保资源优化配置。
  • 待归档:
    商品生命周期结束,进入历史归档阶段。在此状态下,商品不可编辑或销售,但其数据被保留,用于查询和分析历史销售记录,为业务决策提供数据支持。

渠道差异化信息

在全渠道零售环境中,不同销售渠道的用户特征和需求各异。商家需要通过渠道差异化策略,灵活调整商品展示、价格体系及运营方式,才能更高效地满足消费者需求。

1、渠道级商品与 SKU 管理

为适应多渠道需求,新零售体系采用多层次的商品管理结构:

  • 商品库
    :作为企业商品信息的主数据,集中存储和管理所有商品数据,确保信息的一致性。
  • 渠道级商品
    :针对不同渠道(如微信商城、美团外卖、饿了么外卖、抖音、小红书等渠道)设置商品差异化信息。

2、多维度商品价格策略

渠道差异化管理的核心在于灵活的定价策略,通过多维度的价格体系满足不同场景需求:

  • 指导价
    :厂商建议的零售价格,为商家提供定价参考。
  • 渠道价格
    :根据渠道特性制定差异化定价,如外卖平台因配送成本较高,售价通常高于线下门店。
  • 日历价格
    :针对不同时间段制定动态定价策略,例如早餐时段的特价优惠。
  • 成本价
    :精确到 SKU 的单品成本,作为统计利润的基础依据。

3、渠道差异化的其他关键信息

其他的一些渠道相关的差异化信息:

  • 渠道销售状态
    :控制商品的上下架状态。例如,某商品可能仅在特定平台渠道展示,而其他平台隐藏。
  • 配送方式
    :支持快递、同城配送、自提等多种配送方式。例如,社区团购用户更倾向于自提,而电商用户更依赖快递配送。
  • 其他销售设置
    :设置购买数量上限、销售时段等规则。例如,通过限定销售时段实现高峰期促销,或设定单次购买数量上限避免库存被抢购。

本文已收录于,我的技术网站:
tangshiye.cn
里面有,算法Leetcode详解,面试八股文、BAT面试真题、简历模版、架构设计,等经验分享。

JavaScript 中栈的运用

在 JavaScript 中,栈(Stack)是一种非常有用的数据结构,它遵循后进先出(Last In First Out,LIFO)的原则。在本文中,我们将深入探讨栈的概念以及在 JavaScript 中的实际运用。

一、栈的概念

栈是一种线性数据结构,它只能在一端进行插入(称为入栈或压栈,push)和删除(称为出栈或弹栈,pop)操作。想象一下一摞盘子,你只能从最上面拿盘子(出栈)或者把盘子放在最上面(入栈)。

栈通常具有以下几个基本操作:

  1. push(element)
    :将一个元素压入栈顶。
  2. pop()
    :弹出栈顶元素并返回它。
  3. peek()
    :查看栈顶元素,但不弹出它。
  4. isEmpty()
    :判断栈是否为空。

二、在 JavaScript 中实现栈

以下是用 JavaScript 实现一个简单栈的代码:

class Stack {
    constructor() {
        this.items = [];
    }

    push(element) {
        this.items.push(element);
    }

    pop() {
        if (this.isEmpty()) {
            return "Underflow";
        }
        return this.items.pop();
    }

    peek() {
        if (this.isEmpty()) {
            return null;
        }
        return this.items[this.items.length - 1];
    }

    isEmpty() {
        return this.items.length === 0;
    }

    size() {
        return this.items.length;
    }
}

三、栈的实际运用

(一)表达式求值

  1. 中缀表达式转后缀表达式
    在计算机科学中,将中缀表达式转换为后缀表达式是栈的一个重要应用。中缀表达式是我们通常使用的算术表达式形式,如
    (2 + 3) * 4
    。后缀表达式则是将运算符放在操作数之后,例如
    2 3 + 4 *

算法步骤如下:

  • 初始化一个空栈用于存储运算符。
  • 从左到右遍历中缀表达式。
  • 如果遇到操作数,直接输出。
  • 如果遇到左括号,将其压入栈。
  • 如果遇到右括号,弹出栈中的运算符并输出,直到遇到左括号,然后丢弃左括号。
  • 如果遇到运算符,根据其优先级进行处理。如果栈顶运算符的优先级高于或等于当前运算符,则弹出栈顶运算符并输出;否则,将当前运算符压入栈。
  • 遍历结束后,将栈中的剩余运算符依次弹出并输出。

以下是用 JavaScript 实现中缀表达式转后缀表达式的代码:

function infixToPostfix(expression) {
    const stack = new Stack();
    let postfix = "";
    const precedence = {
        '+': 1,
        '-': 1,
        '*': 2,
        '/': 2
    };

    for (let char of expression) {
        if (/[0-9]/.test(char)) {
            postfix += char;
        } else if (char === '(') {
            stack.push(char);
        } else if (char === ')') {
            while (!stack.isEmpty() && stack.peek()!== '(') {
                postfix += stack.pop();
            }
            stack.pop(); // 弹出左括号
        } else {
            while (!stack.isEmpty() && precedence[stack.peek()] >= precedence[char]) {
                postfix += stack.pop();
            }
            stack.push(char);
        }
    }

    while (!stack.isEmpty()) {
        postfix += stack.pop();
    }

    return postfix;
}
  1. 后缀表达式求值
    一旦将中缀表达式转换为后缀表达式,就可以很容易地对后缀表达式进行求值。

算法步骤如下:

  • 从左到右遍历后缀表达式。
  • 如果遇到操作数,将其压入栈。
  • 如果遇到运算符,弹出栈中的两个操作数,进行相应的运算,然后将结果压回栈。
  • 遍历结束后,栈中唯一的元素就是表达式的结果。

以下是用 JavaScript 实现后缀表达式求值的代码:

function evaluatePostfix(postfix) {
    const stack = new Stack();
    for (let char of postfix) {
        if (/[0-9]/.test(char)) {
            stack.push(parseInt(char));
        } else {
            const operand2 = stack.pop();
            const operand1 = stack.pop();
            switch (char) {
                case '+':
                    stack.push(operand1 + operand2);
                    break;
                case '-':
                    stack.push(operand1 - operand2);
                    break;
                case '*':
                    stack.push(operand1 * operand2);
                    break;
                case '/':
                    stack.push(operand1 / operand2);
                    break;
            }
        }
    }
    return stack.pop();
}

(二)函数调用栈

在 JavaScript 中,当一个函数调用另一个函数时,会在内存中创建一个称为调用栈(Call Stack)的结构。调用栈是一种栈数据结构,它用于跟踪函数的调用顺序。

例如:

function functionA() {
    console.log("Inside functionA");
    functionB();
}

function functionB() {
    console.log("Inside functionB");
}

functionA();


functionA
被调用时,它的执行上下文被压入调用栈。当
functionA
调用
functionB
时,
functionB
的执行上下文也被压入调用栈。当
functionB
执行完毕后,它的执行上下文从调用栈中弹出。然后,
functionA
继续执行,直到它也执行完毕,其执行上下文也从调用栈中弹出。

这种机制确保了函数的正确执行顺序和变量的作用域管理。

(三)深度优先搜索(DFS)

深度优先搜索是一种图遍历算法,它可以使用栈来实现。

以下是用 JavaScript 实现深度优先搜索的代码:

class Graph {
    constructor() {
        this.adjacencyList = {};
    }

    addVertex(vertex) {
        if (!this.adjacencyList[vertex]) {
            this.adjacencyList[vertex] = [];
        }
    }

    addEdge(vertex1, vertex2) {
        this.adjacencyList[vertex1].push(vertex2);
        this.adjacencyList[vertex2].push(vertex1);
    }

    dfs(startVertex) {
        const stack = new Stack();
        const visited = {};
        stack.push(startVertex);
        visited[startVertex] = true;

        while (!stack.isEmpty()) {
            const currentVertex = stack.pop();
            console.log(currentVertex);

            for (let neighbor of this.adjacencyList[currentVertex]) {
                if (!visited[neighbor]) {
                    stack.push(neighbor);
                    visited[neighbor] = true;
                }
            }
        }
    }
}

可以使用以下方式调用:

const graph = new Graph();
graph.addVertex('A');
graph.addVertex('B');
graph.addVertex('C');
graph.addVertex('D');
graph.addVertex('E');

graph.addEdge('A', 'B');
graph.addEdge('A', 'C');
graph.addEdge('B', 'D');
graph.addEdge('C', 'E');

graph.dfs('A');

在这个例子中,深度优先搜索从给定的起始顶点开始,使用栈来存储待访问的顶点。每次从栈中弹出一个顶点,访问它,并将其未访问过的邻居顶点压入栈。

(四)括号匹配

检查一个字符串中的括号是否匹配是栈的另一个常见应用。

算法步骤如下:

  • 初始化一个空栈。
  • 遍历字符串中的每个字符。
  • 如果遇到左括号,将其压入栈。
  • 如果遇到右括号,检查栈是否为空。如果为空,说明右括号没有匹配的左括号,返回 false。如果栈不为空,弹出栈顶元素,检查弹出的左括号是否与当前右括号匹配。如果不匹配,返回 false。
  • 遍历结束后,如果栈为空,说明所有括号都匹配,返回 true;否则,返回 false。

以下是用 JavaScript 实现括号匹配的代码:

function isBalanced(str) {
    const stack = new Stack();
    for (let char of str) {
        if (char === '(' || char === '[' || char === '{') {
            stack.push(char);
        } else if (char === ')' || char === ']' || char === '}') {
            if (stack.isEmpty()) {
                return false;
            }
            const top = stack.pop();
            if ((char === ')' && top!== '(') || (char === ']' && top!== '[') || (char === '}' && top!== '{')) {
                return false;
            }
        }
    }
    return stack.isEmpty();
}

四、总结

栈是一种强大的数据结构,在 JavaScript 中有许多实际应用。从表达式求值到函数调用栈,从图的遍历到括号匹配,栈都发挥了重要作用。理解栈的概念和操作,以及如何在 JavaScript 中实现和应用栈,对于编写高效的代码和解决各种编程问题非常有帮助。

前言

深入理解相机视口,摸索相机视口旋转功能,背景透明或者不透明。
本篇,实现了一个左下角旋转HUD且背景透明的相机视口。


Demo

请添加图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


HUD相机的坐标

抬头HUD就是通过投影矩阵来实现,具体可参看《OSG开发笔记(二十):OSG使用HUD显示文字》

  • Hud要单独创建一个新相机
  • 注意关闭光照,不受光照影响,所以内容以同一亮度显示
  • 关闭深度测试
  • 渲染顺序设置为POST,否则可能会被场景中的其他图形所覆盖。
  • 设置参考贴为绝对型:setReferenceFrame(osg::Transform:ABSOLUTE_RF)
  • 使其不受父节点变换的影响:setMatrix(osg::Matrix::identity())
  • 投影矩阵通常会设置为屏幕尺寸大小


相机(Camera)

相机(osg::Camera)和视口(Viewport)是两个核心概念,对于理解OSG中的三维场景渲染至关重要。
相机在OSG中用于模拟真实世界中的摄影机,它负责捕捉和渲染三维场景。相机类(osg::Camera)继承自osg::Transform和osg::CullSetting类,用来管理OSG中的模型——视图矩阵。相机的管理主要是通过各种变换实现的,这些变换包括:

  • 视点变换:设置视点的方向和位置。默认情况下,视点定位为坐标原点,指向Y正方向。可以通过调整视点的位置和参考点的位置来改变相机的观察方向和角度。
  • 投影变换:由于显示器只能用二维图像显示三维物体,因此要靠投影来降低维数。投影变换的目的是定义一个视景体,使视景体外多余的部分被裁减掉,最终进入图像的只是视景体内的有关部分。OSG支持两种投影方式:透视投影(Perspective Projection)和正视投影(Orthographic Projection)。透视投影能够模拟人眼的视觉效果,使远处的物体看起来更小,而正视投影则保持物体的大小不变,不受距离影响。
  • 视口变换:将视景体内投影的物体显示在二维的视口平面上。即将经过几何变换、投影变换和裁剪变换后的物体显示于屏幕窗口内指定的区域内,这个区域通常为矩形,称为视口。


视口(ViewPort)

具体来说,视口变换涉及以下几个参数:

  • 屏幕左下角的坐标:定义了视口在屏幕上的左下角位置。
  • 屏幕宽度和高度:定义了视口的宽度和高度,即相机捕捉的场景在屏幕上显示的区域大小。
    在OSG中,可以通过调用相机的setViewport方法来设置视口。例如:
pCamera->setViewport(new osg::Viewport(0, 0, width, height));

这行代码创建了一个新的视口,并将其设置为相机的当前视口。其中,0和0是屏幕左下角的坐标,width和height是视口的宽度和高度。


相机与视口的关系

相机和视口在OSG中紧密相连,共同决定了三维场景的渲染效果。相机负责捕捉和渲染场景,而视口则定义了相机捕捉的场景在屏幕上的显示位置和大小。通过调整相机的各种变换和设置视口的大小和位置,可以实现丰富的三维视觉效果和交互体验。


设置相机观察函数

void setViewMatrixAsLookAt(const osg::Vec3d& eye, const osg::Vec3d& center, const osg::Vec3d& up);
  • eye:表示相机的位置。这是一个三维向量,指定了相机在世界坐标系中的位置。
  • center:表示相机观察的中心点。这也是一个三维向量,指定了相机应该对准的物体或场景的中心位置。
  • up:表示哪个方向是正方向。这同样是一个三维向量,通常用于指定相机的上方方向(例如,通常设置为 (0,0,1) 表示Y轴正方向为上方)。
    设置相机位置和方向:通过指定 eye、center 和 up 三个参数,你可以精确地控制相机的位置和姿态。eye 和 center 之间的向量表示相机的观察方向,而 up 向量则用于确定相机的上方方向。
    关闭漫游器:在使用 setViewMatrixAsLookAt 函数之前,通常需要关闭相机的漫游器(Camera Manipulator)。这是因为漫游器会自动更新相机的观察矩阵,从而覆盖你通过 setViewMatrixAsLookAt 设置的参数。可以通过调用 viewer->setCameraManipulator(NULL) 来关闭漫游器。
    坐标系:OSG 使用右手坐标系,其中 X 轴向右,Y 轴向上,Z 轴向前。因此,在设置 eye、center 和 up 参数时,需要确保它们符合右手坐标系的规则。
    视图矩阵:setViewMatrixAsLookAt 函数实际上是通过设置相机的视图矩阵来实现相机位置和姿态的调整。视图矩阵是一个 4x4 的矩阵,用于将相机坐标系中的点转换到世界坐标系中。
    setViewMatrixAsLookAt 是一个强大的函数,它允许你以直观的方式设置相机的位置和姿态。通过合理地使用这个函数,你可以创建出各种复杂而逼真的三维场景和视觉效果。


Demo关键源码


创建Hud相机

    // 步骤一:创建HUD摄像机
// osg::ref_ptr<osg::Camera> pCamera = new osg::Camera;
osg::ref_ptr<HudRotateCamera> pCamera = new HudRotateCamera;
pCamera->setMasterCamera(_pViewer->getCamera());
// 步骤二:设置投影矩阵
// pCamera->setProjectionMatrix(osg::Matrix::ortho2D(0, 1280, 0, 800));
// 步骤三:设置视图矩阵,同时确保不被场景中其他图形位置变换影响, 使用绝对帧引用
pCamera->setReferenceFrame(osg::Transform::ABSOLUTE_RF);
pCamera->setViewMatrix(osg::Matrix::identity());
// 步骤四:清除深度缓存
pCamera->setClearMask(GL_DEPTH_BUFFER_BIT);
// 步骤五:设置POST渲染顺序(最后渲染)
// pCamera->setRenderOrder(osg::Camera::PRE_RENDER); // 渲染不显示
// pCamera->setRenderOrder(osg::Camera::NESTED_RENDER);
pCamera->setRenderOrder(osg::Camera::POST_RENDER);
// 步骤六:设置为不接收事件,始终得不到焦点
pCamera->setAllowEventFocus(false);

// osg::ref_ptr<osg::Geode> pGeode = new osg::Geode();
// pGeode = new osg::Geode();
osg::ref_ptr<osg::StateSet> pStateSet = pGeode->getOrCreateStateSet();
// 步骤七:关闭光照
pStateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
// 步骤九:关闭深度测试
pStateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF);

// pGeode->addDrawable(pGeometry.get());

pCamera->addChild(pGeode.get());

pGroup->addChild(pCamera.get());


HudRotateCamera.h

#ifndef HUDROTATECAMERA_H
#define HUDROTATECAMERA_H

#include "osg/Camera"
#include "osg/CopyOp"

class HudRotateCamera : public osg::Camera
{
public:
HudRotateCamera();

HudRotateCamera(const HudRotateCamera& copy, const osg::CopyOp &copyOp = osg::CopyOp::SHALLOW_COPY);

META_Node(osg, HudRotateCamera);

public:
void setMasterCamera(Camera* camera);

public:
virtual void traverse(osg::NodeVisitor& nodeVisitor);

protected:
virtual ~HudRotateCamera();

protected:
osg::observer_ptr<Camera> _pMasterCamera; // 新增了相机,主要是用来获取举证的
};

#endif // HUDROTATECAMERA_H


HudRotateCamera.cpp

#include "HudRotateCamera.h"

HudRotateCamera::HudRotateCamera(): Camera()
{

}

HudRotateCamera::HudRotateCamera(const HudRotateCamera & copy, const osg::CopyOp & copyOp)
: Camera(copy, copyOp),
_pMasterCamera(copy._pMasterCamera)
{

}

HudRotateCamera::~HudRotateCamera()
{

}

void HudRotateCamera::setMasterCamera(osg::Camera *camera)
{
_pMasterCamera = camera;
}

void HudRotateCamera::traverse(osg::NodeVisitor &nodeVisitor)
{
double fovy, aspectRatio, vNear, vFar;
_pMasterCamera->getProjectionMatrixAsPerspective(fovy, aspectRatio, vNear, vFar);

// 设置投影矩阵,使缩放不起效果, 改为正投影,正投影不会随相机的拉近拉远而放大、缩小,这样就没有缩放效果,
// 放大缩小是根据左右,上下距离,越大就物体越小,越小就物体越大
this->setProjectionMatrixAsOrtho(-50 * aspectRatio,
50 * aspectRatio,
-50,
50,
vNear,
vFar);
// 让坐标轴模型位于窗体左下角
osg::Vec3 vec3(-40, -40, 0);
if (_pMasterCamera.valid())
{
// 改变视图矩阵, 让移动位置固定
osg::Matrix matrix = _pMasterCamera->getViewMatrix();

// 让移动固定, 即始终位于窗体右下角,否则鼠标左键按住模型可以拖动或按空格键时模型会动
matrix.setTrans(vec3);
this->setViewMatrix(matrix);
}
osg::Camera::traverse(nodeVisitor);
}


工程模板v1.35.0

在这里插入图片描述


入坑


入坑一:没有按照预期的方式全屏显示在正中间


问题

想一直显示在中间,且能旋转,移动中心,但是实际效果如下,方格100x100,间距1.0,测试:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


尝试

将面方格缩小为10x10,线放小,测试:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

缩小正交投影:
在这里插入图片描述
在这里插入图片描述

可能跟相机查看位置有关,新增相机位置和方向等信息:
在这里插入图片描述

没什么影响:
在这里插入图片描述

这里可能理解有问题,我们需要区域投影到视口,那么一个是投影的区域三维区域的大小,一个是投影到桌面2D他的大小,这里其实类似于HUD,通过HUD的方式,添加了几行代码设置投影矩阵:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

经过测试,可以跳过相机调整视口、中心改变后也会移动,所以他一种在其位置区域。
然后回到前面,发现也可以,再次摸索,发现如下特点:
在这里插入图片描述

所以此时,纵横都是10,所以窗口大小要是符合1:1(10:10=1:1)的比例,改为400,400测试,还是一样:
在这里插入图片描述

但是调整为800x800就好了:
在这里插入图片描述

放最大也不会截取少了:
在这里插入图片描述

所以这个有点搞不明白了,总之是解决了,且投影矩阵和正交矩阵都可以解决,测试投影矩阵和正交举证都收视口大小影响,但是影响具体不知,就好像800x800是最小一样(其他的没测了,只测了400x400、600x600不行,看比例800x800是最小正好满窗口了)。
在这里插入图片描述

又怀疑投过去的区域小了,将区域放大,其位置反倒缩小,所以跟理解不一样:

  • 一种是理解直接投射过去,投射过去区域变大所以变大(不是的);
  • 一种是投射过去区域不变,那么区域变大视图区域可见空间范围变大(实际是这样,但是视口对屏幕的大小未变);
    综合以上,又测试了加大视口,也正常,所以怀疑有可能是qt和osg结合的时候这个地方设置了一个最小值,而可能吧,欢迎探讨,这里深究暂时也没结果,且解决了,所以不继续深究了。


解决

修改相机视口大小为最小800x800。
在这里插入图片描述


后续补充

后续查看做的这个qtosg兼容类,做的时候,自己设置的800x800,就是这个原因了:
在这里插入图片描述


入坑二:相机视口区域不透明


问题

当作最前面的文本hud,是可以透明,但是这里进行调整之后,无法透明。
在这里插入图片描述


尝试

修改了语句,可以透明了部分,但是没了。
在这里插入图片描述

在这里插入图片描述


解决

相机是一个投影矩阵,没有透明,但是文字hud为什么透明呢?。


入坑三:内置几何体关闭光照后纯白色


问题

关闭光照后,几何体白色
在这里插入图片描述


原理

光照关闭要设置颜色,不想设置颜色,就单独给体开放关照。


解决

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


入坑四:文本看不到


问题

文本看不到,旋转后发现是太大了。
在这里插入图片描述

位置较大。


尝试

测试下是把整个坐标区域的展示范围扩大,那么实际看起来就是缩小。


解决

在这里插入图片描述

在这里插入图片描述


入坑五:hud旋转中心不对


问题

Hud旋转中心不对
在这里插入图片描述

这时旋转中心还不对,可能需要调整旋转中心
在这里插入图片描述


原理

开始去修改矩阵,发现都不对,其实中间点一直是0,0,0,其就是中心,那么我们设置文本的显示点不是从0,0开始即可。
下面将四边形的角点改为0,0,0来标识,然后修改文本的中心点:
在这里插入图片描述


解决

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


入坑六:hud旋转反向了


问题

旋转是反的,然后光照也反了,变换矩阵有问题
在这里插入图片描述


原理

数据几何变换算来算去很费劲,直接测试的结论:
在这里插入图片描述

代码写反了,让上下反向了,应该是-50~50


解决

在这里插入图片描述

在这里插入图片描述

还剩下光照问题,这个不好咋弄了,反正是关闭光照,或者是自己手动添加光源,用系统的可能有点问题。
在这里插入图片描述

在这里插入图片描述

这个暂时没解决,实际使用就是用一个,可以从长度单独给这个相机设置一个光源,这里因为需求本身不需要投影,不做测试了。