FANnn https://ilnafz.cn/ zh-CN FANnn Fri, 05 May 2023 19:49:00 +0800 Fri, 05 May 2023 19:49:00 +0800 五月,你好 https://ilnafz.cn/index.php/archives/43/ https://ilnafz.cn/index.php/archives/43/ Fri, 05 May 2023 19:49:00 +0800 快两个月没有写博文了,不得不说时间过得很快。

五一假期没有回家,想着好好清净几天,就呆在了宿舍。期间想着学一些新的知识,可怎么也提不起兴趣,于是就休息了,也是对前两个月忙碌生活的一个收尾。

开学后的我,大多数时间在面向简历学习,期间也会去探索一些比较感兴趣的事情,下面就简单的回顾下近期。

学习计划

面试前准备

[scode type="blue"]用一个词来形容三月的话,用忙碌更加合适不过了,三月份需要将所有的知识进行梳理,并去形成自己的体系是一个较大的任务。[/scode]

[scode type="green"]

三月计划如下

1.算法与数据结构:自三月起再次补充了算法,也是有了一个系统的思路,针对常见类型的算法现在也是能很快的想出解题思路

2.JS:两周的时间将JS的从上到下的梳理为xmind脑图的形式,以便后面回顾。

3.vue:对于一些vue原理与常见面试题进行了阐述与产出。

4.计算机网络+浏览器原理:面试前会浏览一遍,以防忘记

5.项目:将自己项目模块中的重难点,用xmind脑图的形式进行描述记录,也是回了后期方便回顾。
[/scode]

当时两周时间做出的脑图笔记

因为四月份就要投递简历的原因,三月份时间安排上紧凑了些,期间也是封闭了一段时间全心全意总结,现在回想起来,安排的不是很妥当,导致后面多出了几天的空档期。

一个月眨眼就过去了,觉得不可思议,对于三月份的学习成果并没有以文章的形式产出,大多是以xmind的形式保存在电脑了,本想着五一期间总结出来,后来也是打消了这个念头。

投递简历

计划是四月初开始投递简历的,不过还有一部分内容有些空缺,继续复习了一周,接下来就是投递简历了,在boss上我投递了近百家公司,虽然专业不对口,但因为有一份实习经历的原因,面试的机会还蛮多的。

面试大多为三面,也有四面,整体难度还好,集中在一周内面了五家公司,只有一家公司挂掉了,其他都顺利通过了,实习薪资还可以,后来也是没有决定去,想着后续在面面。

在后来的我在图书馆泡了一段时间,期间会刷刷算法,也会看一些自己感兴趣的书籍。

四月的最后一周

最后一周整体上放慢了脚步,把之前被打乱的计划重新实行,不像之前那么紧凑了,期间有小伙伴进行校招面试的话,面试前会去学习区帮助串一些知识点,并跟着复盘一些面试中的问题,在这个过程中经常对之前总结的知识进行阐述与分享,使得我对知识的理解的更加的深入透彻。

大家这一周的面试都比较多,算下来大概有个10多场吧,这段时间也是经常在学习区与图书馆之间来回跑,但最后结果都是好的。

落笔至此,心里不由得泛起一阵感慨,感慨大家为即将结束的大学生活,画上了完美的句号,也感慨自己一个人在未来继续去书写后续的篇章。

一个小项目

根据老师的需求去调研了webGL与webGPU,简单的看了看文档,知道最后大概要实现的是什么,不过并没有写一个demo去测试下,在后来花了3~4对要实现的产品进行了原型图实还原。

在后面就是代码逻辑实现了,不过距离上次写项目已经有半年了,还是有一点生疏的,决定先实现第一版。

四月的最后一周

室友的毕业论文差不多都定稿,这期间晚上偶尔会跟一起去开黑,仿佛一下子回到了高一放学后跟小伙伴们五黑的时光,感觉无比放松。

近期生活

三点一线的生活按部就班(宿舍—学习区—图书馆),大多的精力都投入到了面试准备上,平时经常讨论的也是这些内容,只能从日常习惯上的改变上去聊一下。

就寝时间

因为宿舍是毕业生宿舍,自己平时回宿舍也比较晚,就寝时间大概时间是1点左右,加上早上需要早起的原因,每天的睡眠时间差不多是6个多小时,中午不午休的话,整个人的精神状态就会下降很多i。

晚睡久了慢慢就变为一种习惯,反馈给我身体的情况也是比较直观,比如免疫力与代谢的下降和精力不够充沛。

新手机

年前就一直想换一个新手机,年后看了很多手机测评,对比了小米13与水果14后,对于两款手机在本子上写了密密麻麻一页的参数对比 ,在价位差的不是很多的前提下,最后选择了水果14。

不过在期待了很久的新手机到的那一刻,也不在像当初期待时的那么欢喜了,简单的录了一个测评后就没有在花心思去研究它,不过后面用起来整体还是不错的。

日常锻炼

长时间的久坐与用眼,不得不需要每天花一些时间在锻炼上,加上之前也一直有着减重10斤的一个小目标一直督促着,我更是动力满满。晚十点后,每天在操场跳三组绳,偶尔会快跑一公里。期间有尝试引体向上,不过好像拉不上去,最高纪录做了五个😁,不过俯卧撑还是能做几组的。

这样的计划坚持了一个多月,期间因为面试也是断更了一周,断更后有陆陆续续的跟室友一起锻炼,但不像以前每天坚持了,五一期间也是很少去锻炼。

五月计划

学习上会一边刷算法一边补充React技术栈,也是希望未来任职公司使用的技术栈为React,期间也有兴趣去学习一些原型图设计的软件。学习新知识的同时会投递一些暑假实习,不过在投递前还需要再整改一版简历,感觉之前的简历有些有些不如意的地方。

在生活上需要在作息与日常锻炼上进行合理规划 ,去实现一个月内完成减重10斤的小目标吧!平时没事的时候多看些课外书,去做些自己感兴趣且有意义的事情,最后就是存一些钱,在六月去游玩,地点待定。

文章的结尾还有两个的任务待完成

  • 基于原型图的功能实现(之前是还原了原型图,后续需要简单的进行开发)
  • 数学建模比赛(上次的比赛因为时间安排的原因没做准备,就决定参加这个比赛)

因为好久没有写文章记录的原因,近期的学习笔记及面试问题都没有进行一个凝练与复盘,后续也是会陆陆续续梳理出来并发布,周期大概是应该是一周。

]]>
2 https://ilnafz.cn/index.php/archives/43/#comments https://ilnafz.cn/index.php/feed/
回顾2022,展望2023 https://ilnafz.cn/index.php/archives/35/ https://ilnafz.cn/index.php/archives/35/ Mon, 30 Jan 2023 08:39:00 +0800 2022由于手术休学一年的缘故,距离开学还有很长的空档期,开启了漫长的成长之路。

考虑到篇幅较长,该篇以学习,生活,未来规划三大块讲述。

学习

2022年,学习上紧跟自己的计划循序渐进,对原有的学习方法作出改变,确定了未来的工作方向,对之前学过的知识作出了相应的实践,并对其做出了总结沉淀,收获颇多

时间线

[collapse status="ture" title="校外"]

  • 2021年3月~8月份 软筑科技实习
  • 2021年月~6月份 el-admin框架
  • 2022年3月~4月开发工作流项目,主要负责表单生成器的调研与通讯录模块的开发
  • 2022年5月中旬 回顾webpack、es6
  • 2022年5月中旬~6月 相继学习vue3、ts、vite、gin-vue-admin框架
  • 2022年7月~8月开发政府项目——新乡市网格化管理系统,主要负责重点人员,风险预警板块以及首页地图的开发
  • 2022年8月中旬,进行年终总结报告
  • 2022年8月~9月 学习react16、react-Hooks、redux-thunk、react-router

[/collapse]

[[collapse status="ture" title="校内"]

  • 2022年9月~11月 计算机专业课与算法的学习
  • 2022年11月~12月 学习Node+React18,并使用Node+express+React18搭建个人博客

[/collapse]
[collapse status="ture" title="寒假"]

  • 寒假期间面经准备(HTML CSS 浏览器原理 JS 计算机网络)

[/collapse]

知识产出

平台总数相较于上年多产出
简书36篇

8篇

CSDN28篇

20篇

个人博客15篇

4篇

算法110道

110道

github5个

3个

收获与不足
[collapse status="false" title="学习成长"]

  1. 问题解决能力提高:开发中能够快速定位bug并解决
  2. 知识的学习由浅入深:从学会一个知识到学好一个知识,体现在多次项目开发中对综合知识的熟练使用
  3. 项目开发能力提高:多次的项目锻炼,秉承承诺即交付的原则,一步步的扎实前端技术栈
  4. 新知识学习能力提高:项目中使用到的新技术,能够快速学习并且上手使用,体现在网格化管理系统项目

[/collapse]

[collapse status="false" title="学习中的不足与改进"]

  1. 算法能力薄弱:进行系统性,有针对性的刷题,做到每周一小测,每月一大测。
  2. 总结输出不足:定期总结知识并精炼重难点内容,并组织在团队内分享。
  3. 知识深度:对学过的知识定期挖掘,并总结输出。

[/collapse]

生活

在住宿上,学长工作后住的房子还有些时间,我便住了进去,期间作息依旧,早上7.30出发,晚上10.30回家,住宿的地方距离公司还蛮近的,偶尔会回家做些饭。

学习之余也会找比较开阔的地方进行锻炼(由于是术后康复初期,仅仅做一些简单的锻炼),期间也有考虑过办个健身卡进行科学的康复锻炼,后续也是打消了这个念头。
[scode type="green"]工作流开发

在3月份接到了开发工作流的任务,分配的任务量还是蛮大的,不过由于项目周期比较长的原因,任务能后有序完成,期间还是非常感谢项目负责人的帮助,收获颇多。[/scode]

周末回家的路上买了一小只兔子,恰巧家里阳台上有一个比较大的笼子,想试着养一养,后来赠与了学长,回想起来这个兔子我还没有起名字😂。

后续搬到了产业园,在产业园待的前一个月中,查漏补缺了很多知识,深入的学习了前端的一些知识。

[scode type="green"]网格化管理系统开发

7月份加入新乡市·新乡市网格化管理系统的开发工作中,由于开发周期短,中间陆续的加入一些新的需求,所以大部分时间都在开发,没有时间去拓扑新的知识,有时会对接到很晚才离开,项目交付后,维护着部分模块,期间过生日的那天没什么心思,简单的吃了一碗面就回去休息了。[/scode]

项目结束后查漏补缺了一些知识,并在时间允许的条件下学习了React技术栈,并进行科学的锻炼。

年度工作总结报告

当时花了几天的时间准备了PPT,内容充分后就没有再次修改自己的PPT,当时觉得自己准备可以了,模拟演讲时被指出很多问题,修正后还是存在着部分问题的。到了正式汇报时,由于ppt篇幅较长,内容冗余,造成了不必要的麻烦,加上有些紧张,使得很多想说的内容没有展现出来😀,汇报结束后,评委们也是给出了很多宝贵的建议,使我受益匪浅,且期待并珍惜下次的汇报💪。

青岛旅行

暑假10多天假期,与小伙伴商量后,决定出省旅行。

[album]

[/album]

[vplayer url="https://fan.ilnafz.cn/%E8%A7%86%E9%A2%91%E6%96%87%E4%BB%B6/VID_20220817_180912.mp4" /]

洛阳旅行

刚回到家没来的及写青岛游玩的记录,就被同学叫去爬山了,两天前刚爬完崂山,不是很累,决定浅浅的爬一下老君山。

[album]

[/album]

开学

下图是当时做出的下半学期规划

因为疫情的原因,很长一段时间,教室与实验室不对外开放,在寝室学习了一段时间,整体效率不是很高,紧赶慢赶的完成了那段时间的计划。

JS回顾

封在寝室的这些日子,没有学习新知识,就拉着王帅花了一部分时间把之前学习的JS复习了一遍,外加一些常见习题及题解,下面是当时的笔记总结。

[post cid="25" cover="https://fan.ilnafz.cn/%E7%9F%A5%E8%AF%86%E8%83%8C%E6%99%AF%E5%9B%BE/JS.awebp"/]

个人博客搭建

暑假时学习了React16,并花了一周写了一个练习项目。由于上手的时间比较短,且已经两个月没看了,对React的知识已经遗忘的差不多了 ,且React主流已经是18版本了,多了一些新特性需要去了解,自然需要一个demo实践下React18了,加上近期博主有计划性的学习下Node,并构思一个基于Node+React前后端分离的项目,于是产生了搭建个人博客的想法。

当时开发个人博客时长一个月了,所有功能基本上完善,开发期间也会受到大大小小的事情影响,但总算是完成了自己的个人博客项目,收获颇多,也是对自己近期的学习进行一个总结性的产出。

[post cid="8" cover="https://fan.ilnafz.cn/%E7%9F%A5%E8%AF%86%E8%83%8C%E6%99%AF%E5%9B%BE/wallhaven-6doz9w.jpg"/]

退组

寒假初,退出三月软件实验室

点滴回顾(2020~2022)

加入三月→网页二班班长→寒假学习→负责大年初二活动策划→利剑班班长→校内学习→见习→暑假学习→二纵队副队长兼一组组长→寒假线上学习负责人→化学管理系统开发→软筑见习→工作流开发→网格化管理系统开发→主持会议→年度工作总结→青岛之旅→校内学习→同期学习负责人→退出三月

大二回忆

[album]

打字比赛

分享会

写对联

春晚花絮

双湖讨论

高品生日

操场团建

年底团建

利剑合照

[/album]

工位变化

[album]

泰投产业园

学校实验室

大数据产业园

大数据产业园

网格化管理系统开发

寒假学习

[/album]

还有各种团体聚餐这里就不再展示了😂

寒假

假期中,留在新乡跟小伙伴们一起合租了一个房子一起学习,一个半月的集中学习使得这段日子格外充实。

制定计划

寒假大量时间都在准备面经,下面是我个人的完成情况

[goal title="前端面经"]
[item progress="100%"] HTML+CSS(2天) [/item]
[item progress="100%"] 浏览器原理 (3天)[/item]
[item progress="60%"] JavaScript(1周) [/item]
[item progress="100%"] 计算机网络(两周) [/item]
[item progress="50%"] 算法与数据结构 [/item]
[item progress="100%"] 简历完善 [/item]
[item check="false"] vue原理(3周)[/item]
[item check="true"] 维护个人博客[/item]
[/goal]

学习花絮

记唯一次夜跑

[album]

新的学习场地

冬至包饺子

日常讨论

[/album]

除了上述这些,博主寒假闲暇之余会坚持运动跟研究美食😋。

宝泉游玩

腊月24,回家前一天,决定跟着好友去宝泉游玩一天后回家。

[album]

[/album]

未来规划

  • 坚持学习与反思
  • 考一些必要的证书
  • 暑假前找一份好的实习
  • 坚持运动
  • 一次旅行
  • 自己专业不挂科
]]>
5 https://ilnafz.cn/index.php/archives/35/#comments https://ilnafz.cn/index.php/feed/
个人博客搭建历程 https://ilnafz.cn/index.php/archives/8/ https://ilnafz.cn/index.php/archives/8/ Tue, 27 Dec 2022 21:09:00 +0800 搭建博客初衷

[scode type="blue"]主要原因[/scode]

博主近期在学习React ,暑假时学习了React16,并花了一周写了一个练习项目。由于上手的时间比较短,且已经两个月没看了,对React的知识已经遗忘的差不多了 ,且React主流已经是18版本了,多了一些新特性需要去了解,自然需要一个demo实践下React18了 ::aru:shy2:: 。
加上近期博主有计划性的学习下Node,并构思一个基于Node+React前后端分离的项目,于是产生了搭建个人博客的想法。

[scode type="blue"]其他原因[/scode]

之前博主的文章,生活感悟记录在简书平台,学习笔记记录在CSDN,两类的文章分别在不同的平台记录,自然需要一个专用的网站能够同时记录生活感悟与学习笔记。

搭建前准备

技术选型

使用的技术比较明确,前端使用React18+antd+router_v6+redux+Ts,于是博主花了一周的时间复习了下React,并实践了React18的一些新特性。服务端则是使用Node+express+mysql,在学校花了一周多的时间看了看文档并敲了些demo,达到能够为个人博客提供一些基本的接口,自然是没问题了。

服务器+域名购买

博主这里使用的是阿里云2核4G轻量应用服务器,由于域名需要十多天的审核备案,开发前需提前进行备案。

项目部署

前期使用的是XShell,用起来比较麻烦,后面使用的是宝塔

整体开发思路

数据库简单设计—>服务端基本接口(1周)—>前台(1周)—>后台(1周)—>部署测试(1周),也就是差不多一个月的时间搞定。

博客v1.0上线

数据库

作为第一版博客,数据库建表就本着简单的原则建了三张表分别是用户表,分类表,文章表。
开始本地数据库使用的是SQlite,后使用mysql

服务端

用了近两周的时间学习了Node+Express,并写了大大小小15个接口(写接口用时一周左右,上传与条件查询的接口相对麻烦些),本地测试没问题后开始着手于前台开发

完成接口如下

[scode type="green"]

  1. 登录接口
  2. 分类增删改查
  3. 博客表增删改查
  4. 博客表查询分页接口
  5. 上传接口
  6. 加入token验证接口

[/scode]

前端(前台+后台)

开始觉得整个项目开发会在服务端上投入大部分的时间,不过最后写下来后,发现花在前端上的比重较重一些,一个前台,一个后台(熟悉使用React开发后,大部分时间花在构思布局跟调一些基本样式上面)这里回顾下前台的开发流程:

  1. 基于React+ts+antd+Eslint初始化项目,代码格式化仅在编辑器进行了配置,并保存了常用的代码片段便于后续使用
  2. 在前台页面上的多端适配上,使用了Ant Design自带的layout组件+媒体查询的方式进行布局
  3. 比较React18与React16,使用的路由跟Redux的一些常用API都有了些变化,但整体变化不大,在路由上进行了简单设计,因为使用到了Redux,并对其进行封装,开始进行了一个简单的模块化封装,自己的小项目够用就可以,使用后总是感觉,模块分的并不是很清晰,后续重新梳理了下codewhy老师的Redux封装,并使用。
  4. Axios封装就自由方便些,因为前后端都是自己写的,对于HTTP返回的状态码可自定义返回的内容,对于一些请求拦截在前端上作出简单的处理提示。

在熟悉React开发流程后,很快便把前台与后台的页面写完了,并在线上服务器测试了所有接口

前台完成

[scode type="green"]

  1. 登录页
  2. 展示页面(部分样式未调整)
  3. 详情页

[/scode]

后台完成

[scode type="green"]

  1. 文章管理
  2. 分类管理
  3. 添加修改文章页面
  4. 用户管理页面

[/scode]

[scode type="yellow"]使用React18开发还是踩了蛮多坑的,这里简单的记录了一下当时开发中的踩坑记录。[/scode]

(3条消息) React18开发中问题记录(持续更新)_一缕阳光@的博客-CSDN博客_routeobject ts

项目展示

后台展示

image-20221224101930823

前台展示(PC+移动,1.0版本仅后台对接口测试后展示,前台接口测试未展示)

image-20221224102134372

image-20221224102146345

[scode type="green"]代码已经上传至github[/scode]

sunshine-fan/personal-blog: 个人博客项目,前端:React18+antd+router_v6+redux,服务端Node+express+mysql (github.com)

博客迁移至typecho

博主开发个人博客已经一个月了,所有功能基本上完善,开发期间也会受到大大小小的事情影响,但总算是完成了自己的个人博客项目,收获颇多,也是对自己近期的学习进行一个总结性的产出。

临近假期博主因为要处理一些事情的原因,投入时间颇多,并计划假期中跟小伙伴们一起准备面试的内容,所以个人博客项目在后期美化与维护上就不在投入时间了,也就停掉了当下的任务。

在后来决定使用博客框架对自己的本博文进行托管,于是使用了现在的typecho,后续博主会将博文陆续迁移至typecho中,且博客不会停止更新。

]]>
1 https://ilnafz.cn/index.php/archives/8/#comments https://ilnafz.cn/index.php/feed/
React18开发中问题记录(持续更新) https://ilnafz.cn/index.php/archives/22/ https://ilnafz.cn/index.php/archives/22/ Mon, 02 Jan 2023 12:37:00 +0800 本文用于记录笔者在使用React18开发时踩得一些坑及一些常用的知识。

1.动态className与普通className 写在同一个标签内

<Timeline className={`${!type ? "isshow" : ""}${"Timeline"}`}>

2.react-router v6配置时类型错误

根据react v16 的文档去配置项目中的路由时发现

在这里插入图片描述

找了很久没到到答案(对于ts的问题还是有点蒙的),然后在github找了个 关于 ts+react+router v6的项目结构看了看,原来是

[scode type="yellow"]路由配置文件后缀名应为 jsx或者tsx,因为使用到了tsx语法,并且引入react[/scode]
于是我将router.ts改为router.tsx,并引入了react后错误就消失了😂。

3.类型“RouteObject[]”的参数不能赋给类型

报错如下

在这里插入图片描述

interface RouteObject {
  caseSensitive?: boolean
  children?: RouteObject[]
  element?: React.ReactNode
  index?: boolean
  path?: string
  auth?: boolean
}
 const element = useRoutes(routes)

多写了一个 index 属性(删除就不报错了)

interface RouteObject {
  caseSensitive?: boolean
  children?: RouteObject[]
  element?: React.ReactNode
  path?: string
  auth?: boolean
}

4.React18懒加载组件导致的路由跳转错误

react18中提供了lazy函数,用于懒加载,即当用到的时候才会开始加载,但是路由跳转是立刻完成的,所以就不免出现需要渲染的组件和没有加载完毕,本来也没多大问题,也就是会白屏用户体验不好,这个问题在以前的版本是不会报错的,但是本开发环境中却直接报错了,查了很多资料,终于找到了比较好的解决方案,使用react18提供的新组件Suspense解决了,顺手还能实现个加载显示加载动画的功能。

报错图片

在这里插入图片描述

该问题出现的环境

react 18.0.0
react-router-dom 6.3.0

copy 解决的代码

// index.js
import React, { lazy, Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import {Spinner} from 'react-bootstrap'
import {
  BrowserRouter,
  Routes,
  Route,
} from "react-router-dom";


const root = ReactDOM.createRoot(document.getElementById('root'));

const App = lazy(() => import('./views/App'));
const Login = lazy(() => import('./views/Login'));
const Register = lazy(() => import('./views/Register'));
const NotFound = lazy(() => import('./views/NotFound'));

root.render(
  <BrowserRouter>
    <Routes>
        <Route path="/" element={<App />}>

        </Route>
        <Route path="/login" element={
          <Suspense fallback={<Spinner animation="border" variant="primary" />}>
            <Login />
          </Suspense>
        } />
        <Route path="/register" element={
          <Suspense fallback={<Spinner animation="border" variant="primary" />}>
            <Register />
          </Suspense>
        } />
        <Route path="*" element={
          <Suspense fallback={<Spinner animation="border" variant="primary" />}>
            <NotFound />
          </Suspense>
        } />
    </Routes>
  </BrowserRouter>
);

个人解决代码如下
引入

import React, { memo, Suspense } from "react"

包裹

<Suspense fallback={<div>Loading...</div>}>{element}</Suspense>
import React, { memo, Suspense } from "react"
import "./App.less"
import { useRoutes, Link } from "react-router-dom"
import router from "./router/router"

const App: React.FC = function () {
  const element = useRoutes(router)
  return (
    <>
      <Link to={"/home"}>home</Link>
      <hr></hr>
      <Link to={"/login"}>login</Link>
      //{element}
      <Suspense fallback={<div>Loading...</div>}>{element}</Suspense>
    </>
  )
}
export default memo(App)

解决问题文章链接:http://t.csdn.cn/PeUx3

5.类型“AxiosResponse<any, any>”上不存在属性“rows”。

ts(2339)

问题起源

在这里插入图片描述

问题解决

request.ts文件中写入类型

declare module "axios" {
    interface AxiosResponse<T = any> {
      rows: any[];
    }
    export function create(config?: AxiosRequestConfig): AxiosInstance;
}

6.react-redux/immutable中getIn(['props1','props2'])无法使用

解决

yarn redux-immutable

该插件返回一个combineReducers函数,把redux的combineReducers函数替换掉即可;
如下:

import { combineReducers } from 'redux-immutable';
import { reducer as user } from '../pages/user/store';

export default combineReducers({
  user: user
});

\###后续补充中~

]]>
0 https://ilnafz.cn/index.php/archives/22/#comments https://ilnafz.cn/index.php/feed/
typescript常见数据结构与算法 https://ilnafz.cn/index.php/archives/40/ https://ilnafz.cn/index.php/archives/40/ Sat, 04 Mar 2023 11:24:00 +0800 吸取了之前刷题的教训,近期系统的学习了下数据结构,并理解性的对常用的数据结构进行封装,颇有收获。
[scode type="blue"]期间所写的代码已上传至github:sunshine-fan/ts-data-structure-arithmetic: 封装一些常用的数据结构以及一些算法题解 (github.com),后续对一些算法题的题解也会陆陆续续上传至该仓库。[/scode]

线性结构

image-20230304150922862

1.数组

数组(Array)结构是一种重要的数据结构
  1. 原生数据结构
  2. 可以借助数组结构来实现其他的数据结构,比如栈(Stack)、队列(Queue)、堆(Heap)
通常数组的内存是连续的,所以数组在知道下标值的情况下,访问效率是比较高的
数组的缺点:
  1. 数组的创建通常需要申请一段连续的内存空间(一整块的内存),并且大小是固定的(大多数编程语言数组都是固定的),所以当当前数组不能满足容量需求时,需要扩容。 (一般情况下是申请一个更大的数组,比如2倍。 然后将原数组中的元素复制过去)
  2. 而且在数组开头或者中间位置插入数据的成本很高,需要大量元素位移
  3. 尽管javascript的Array底层可以帮我们做这些事,但背后的原理依然是这样。

数组的各种用法(MDN): https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array

2.栈结构

栈(stack),它是一种受限的线性结构,后进先出(LIFO)
实现栈结构有两种比较常见的方式
  1. 基于数组实现
  2. 基于链表实现

基于数组实现栈结构

class stack<T>{
  private arr:T[] = [] 
  //压入栈
  push(ele:T){
    this.arr.push(ele) 
  }
  //栈顶元素弹出栈
  pop():T|undefined{
    return this.arr.pop()
  }
  //查看栈顶元素
  peek():T{
    return this.arr[this.arr.length-1]
  }
  //栈是否为空
  isEmpty():boolean{
    return this.arr.length === 0
  }
  //栈长度
  size():number{
    return this.arr.length
  }
}
export default stack; 

测试(代码已上传至github)

  1. 十进制转二进制
  2. 有效的括号

2.队列结构

队列(Queue),它是一种受限的线性表,先进先出(FIFO First In First Out)
  1. 受限之处在于他只允许在队列前端进行删除操作
  2. 而在队列的后端进行插入操作

image-20230304153724645

队列跟栈一样,有两种实现方案
  1. 基于数组实现
  2. 基于链表实现

基于数组实现队列Queue

// 定义队列方法接口
interface IQueue<T>{
  //入队
  enqueue(ele:T):void;
  //出队
  dequeue():T  | undefined;
  // 查看队头元素
  peek():T|undefined;
  //队列是否为空
  isEmpty():boolean;
  //队列长度
  size():number;
}
// 队列结构实现
class ArrayQueue<T> implements IQueue<T>{
  private arr:T[]=[];
  enqueue(ele: T): void {
    this.arr.push(ele);
  }
  dequeue(): T | undefined {
    return this.arr.shift();
  }
  peek(): T | undefined {
     return this.arr[0];
  }
  isEmpty(): boolean {
    return this.arr.length===0;
  }
  size(): number {
     return this.arr.length;
  }

}
export default ArrayQueue
测试(代码已上传至github)

击鼓传花

约瑟夫环

3.链表结构

链表和数组一样,可以用于存储一系列的元素,但是链表和数组的实现机制完全不同。
链表的优势
  1. 相对于数组,内存空间不是连续的,可以充分利用计算机的内存,实现灵活的内存动态管理
  2. 链表不必在创建时就确定大小,并且大小可以无限的延伸下去。
  3. 链表在插入和删除数据时,时间复杂度可以达到O(1),相对数组效率高很多
相对于数组链表的一些缺点
  1. 链表访问任何一个位置的元素时,都需要从头开始访问。(无法跳过第一个元素访问任何一个元素)。
  2. 无法通过下标直接访问元素,需要从头一个个访问,直到找到对应的元素

链表结构的封装

class Node<T>{
  value:T
  next:Node<T> | null = null
  constructor(value:T){
    this.value = value
  }
}
//创建LinkedList的类
class LinkedList<T>{
  private head:Node<T> | null = null
  private size:number = 0
  //根据元素位置获取当前元素节点
  private getNode(position:number):Node<T> | null{
    let index = 0
    let current = this.head
    while(index++<position && current){
      current = current.next
    }
    return current
  }
    
  get length(){
    return this.size
  }
  
  //链表添加操作
  append(value:T){
    const node = new Node(value)
    const head = this.head
    if(!head){
      this.head = node
      this.size++
      return this.head
    }else{
      let current:Node<T> | null = head
      while(current.next){
        current = current!.next
      }
      current.next = node
      this.size++
      return current
    }
  }
    
  //插入操作 引出了Node节点的位置属性
  insert(value:T,position:number):boolean{
    //判断越界问题
    if(position<0 || position>this.size) return false;
    const newNode = new Node(value)
    //判断当前插入是否是第一个节点
    let current = this.head
    if(position===0){
      newNode.next = current
      //head也是一个节点
      this.head = newNode
    }else{
      let previous = this.getNode(position-1)
      //记录前一个节点
      newNode.next = previous!.next
      previous!.next = newNode
    }
    this.size++
    //默认返回
    return true;
  }
    
  //删除链表中指定位置元素
  removeAt(position:number):boolean{
    if(position<0||position>this.size) return false
    let current = this.head;
    if(position===0){
      current = null
    }else{
     current = this.getNode(position-1)
     current!.next = current?.next?.next??null
    }
    this.size--;
    return true;
  }
    
  //过去对应位置的元素
  get(position:number):T|null{
    if(position<0||position>this.size) return null
    let current = this.head
    let index = 0
    while(index++<position&&current){
      current= current.next
    }
    return current?.value??null
  }
    
  //遍历链表
  traverse(){
    if(!this.head) return null
    let current:Node<T> | null = this.head
    let str =""
    while(current){
      str+=current.value+"-->"
      current = current.next
    }
    return str+"null"
  }
    
  //修改某个位置的元素
  update(element:T,position:number){
    if(position<0||position>this.size) return
    //获取节点
    const current = this.getNode(position)
    if(current){
      current.value = element
    }
  }
}

const list =new LinkedList()
list.append(1)
list.append(2)
list.append(3)
list.insert(9,2)
list.insert(8,4)
console.log(list.traverse());
console.log(list.removeAt(2)); //删除操作
console.log(list.traverse());
console.log(list.get(3)); //8 
export {Node,LinkedList}

数组与链表的复杂度对比

image-20230304161459741

数组是一种连续的存储结构,通过下标可以直接访问数组中的任意元素。
  1. 时间复杂度:对于数组,随机访问时间复杂度为O(1),插入和删除操作时间复杂度为O(n)。
  2. 空间复杂度:数组需要连续的存储空间,空间复杂度为O(n)。

链表是一种链式存储结构,通过指针链接起来的节点组成,访问链表中元素需要从头结点开始遍历

  1. 时间复杂度:对于链表,随机访问时间复杂度为O(n),插入和删除操作时间复杂度为O(1)。
  2. 空间复杂度:链表需要为每个节点分配存储空间,空间复杂度为O(n)。

在实际开发中,选择使用数组还是链表需要根据具体应用场景来决定。

  1. 如果数据量不大,且需要频繁随机访问元素,使用数组可能会更好。
  2. 如果数据量大,或者需要频繁插入和删除元素,使用链表可能会更好

删除Node节点(给定一个节点)

class listNode{
  val:number
  next:listNode|null
  constructor(val?:number,next?:listNode|null){
    this.val = val===undefined?0:val
    this.next = next===undefined?null:next
  }
}
// 将后一个节点的值赋予,当前要删除的节点,并将当前删除的节点的指向后一个节点,
// 做到替换删除
function deleteNode(node:listNode|null):void{
  node!.val =  node!.next!.val
  node!.next = node!.next!.next
}
let node = new listNode(1)
let current = node
for(var i =3;i<5;i++){
  current.next = new listNode(i)
  current = current.next
}
console.log(node);
deleteNode(node.next)
console.log(node);

export {listNode}

反转链表

listNode.ts

class listNode{
  val:number
  next:listNode|null
  constructor(val?:number,next?:listNode|null){
    this.val = val===undefined?0:val
    this.next = next===undefined?null:next
  }
}
export {listNode}
栈结构实现
import {listNode} from './listNode'

function reverseList(head:listNode | null): listNode|null {
  if(!head || head.next===null) return head
  //栈结构 加大了空间复杂度
  let stack:listNode[] = []
  while(head){
    stack.push(head)
    head = head.next  
  }
  let newHead:listNode = stack.pop()!
  let newHeadCurrent= newHead
  while(stack.length>0){
    newHeadCurrent.next = stack.pop()!
    newHeadCurrent = newHeadCurrent.next
  }
  newHeadCurrent.next = null 
  return newHead;
}
 
//todo 创建链表结构
let node = new listNode(1)
let current = node
for(var i =3;i<5;i++){
  current.next = new listNode(i)
  current = current.next
}
console.log("反转前"+node);
console.log("反转后"+reverseList(node));
循环实现
import {listNode} from './listNode'

function reverseList(head:listNode | null): listNode|null {
  if(!head || head.next===null) return head
  let newHead:listNode|null = null
  let current:listNode | null;
  while(head){
    current= head.next
    head.next = newHead
    newHead = head
    head = current
  }
  return newHead;
}

//todo 创建链表结构
let node = new listNode(1)
let current = node
for(var i =3;i<5;i++){
  current.next = new listNode(i)
  current = current.next
}
console.log("反转前");
console.log(node);
console.log("反转后");
console.log(reverseList(node));

递归实现

import {listNode} from './listNode'

function reverseList(head:listNode | null): listNode|null {
  if(!head || head.next===null) return head
  let newHead = reverseList(head.next)
  head.next.next = head
  head.next = null
  return newHead
} 

//todo 创建链表结构
let node = new listNode(1)
let current = node
for(var i =3;i<5;i++){
  current.next = new listNode(i)
  current = current.next
}
console.log("反转前");
console.log(node);
console.log("反转后");
console.log(reverseList(node));

4.哈希表(HashTable)

哈希表到底是什么呢

它的结构就是数组,但是它神奇的地方在于对数组下标值的一种变换,这种变换我们可以使用哈希函数,通过哈希函数可以获取到HashCode。

image-20230304162109978

哈希表的一些概念(由于hashtable的概念较多,本文不在罗列了,详情可以看下述文章)

javascript hash是什么-js教程-PHP中文网

哈希表通常是基于数组进行实现的,但是相对于数组,它也很多的优势
  1. 它可以提供非常快速的插入-删除-查找操作;
  2. 无论多少数据,插入和删除值都接近常量的时间:即O(1)的时间复杂度。实际上,只需要几个机器指令即可完成;
  3. 哈希表的速度比树还要快,基本可以瞬间查找到想要的元素;
  4. 哈希表相对于树来说编码要容易很多;
哈希表相对于数组的一些不足
  1. 哈希表中的数据是没有顺序的,所以不能以一种固定的方式(比如从小到大)来遍历其中的元素(没有特殊处理情况下)。
  2. 通常情况下,哈希表中的key是不允许重复的,不能放置相同的key,用于保存不同的元素。
  3. 空间利用率不高,底层使用的是数组,并且某些单元是没有被利用的
  4. 不能快速的找出哈希表中的最大值或者最小值这些特殊的值

创建哈希表结构

class HashTable<T=any>{
  storage:[string,T][][] = []
  //定义数组的长度
  private limit:number =7
  //记录已经存在元素的个数
  private count:number=0
  //hash函数
  private hashFunc(key:string,max:number):number{
    let hashCode = 0
    const length =key.length
    for(let i=0;i<length;i++){
      // 霍纳法则计算hashCode
      hashCode = 31*hashCode+key.charCodeAt(i)
    }
    //求出索引值
    const index= hashCode % max
    return index    
  }
  //扩容的质数
  private isPrime(num:number){
    const temp = Math.floor(Math.sqrt(num))
    for(let i = 2; i <= temp; i++){
      if(num % i){
        return false
      }
    }
    return true
  }
  //获取一个质数为容量
  private getPrime(num:number){
    let prime = num
    while(!this.isPrime){
      prime++
    }
    return prime
  }
  //插入元素
  put(key:string,value:T){
    let index = this.hashFunc(key,this.limit)
    //取出索引位置对应位置的数组(桶)
    let bucket = this.storage[index]
    //3.判断桶内是否存在值
    if(!bucket){  
      bucket = []
      this.storage[index] =bucket
    }
    //4.确定已经有一个数组了,但是数组中是否已经存在key(判断是否存在)
    let isUpdate = false
    for(let i=0;i<bucket.length;i++){
      const tuple = bucket[i]
      const tupleKey = tuple[0]
      if(tupleKey===key){
        //更新的操作 
        tuple[1] = value
        isUpdate=true
      }
    }
    // 如果桶中没有该元素,就直接加入桶中
    if(!isUpdate){
      bucket.push([key,value])
      this.count++
      //判断数组是否扩容
      if(this.count>this.limit*0.75){
        const primeNum = this.getPrime(Math.floor(this.limit*2))
        this.resize(primeNum)
      }
    }
  }
  //获取元素
  get(key:string){
    //生成对应的key值
    const index = this.hashFunc(key,this.limit)
    //根据索引获取桐bucket
    const bucket = this.storage[index]
    if(!bucket) return null
    //3.遍历桶中的数据
    for(let i =0;i<bucket.length;i++){
      const tuple = bucket[i]
      if(tuple[0] === key){
        return tuple[1]
      }
    }
    return null
  }
  //删除数据
  delete(key:string){
    //生成对应的key值
    const index = this.hashFunc(key,this.limit)
    //根据索引获取桐bucket
    const bucket = this.storage[index]
    if(!bucket) return null
    //3.遍历桶中的数据
    for(let i =0;i<bucket.length;i++){
      const tuple = bucket[i]
      if(tuple[0] === key){
        bucket.splice(i,1)
        this.count--
         //判断数组是否扩容
        if(this.count>8 && this.count<this.limit*0.25){
          const primeNum = this.getPrime(Math.floor(this.limit/2))
          this.resize(primeNum)
        }
        return tuple[1]
      }
    }
    return null
  }
  //扩容
  resize(newlimit:number){
    //1.保存旧数组中的内容
    const oldStorage = this.storage
    //2.重置属性
    this.limit = newlimit
    this.count = 0
    this.storage =[]
    //3.遍历原来数组中的所有数据
    oldStorage.forEach(bucket=>{
      //如果没有数据直接返回
      if(!bucket)return
      //3.1bucket中存在数据,那么将所有数据重置hash
      for(let i=0;i<bucket.length;i++) {
        const tuple = bucket[i]
        this.put(tuple[0],tuple[1]) 
      }
    })
  }
}
let hsTable = new HashTable()
hsTable.put("aaa",100)
hsTable.put("asd",200)
hsTable.put("bns",300)
hsTable.put("abc",300)
console.log(hsTable.delete("abc"));
console.log(hsTable.get("bns"));
console.log(hsTable.storage);

5.树

树的术语

  1. 节点的度(Degree):节点的子树个数。
  2. 树的度 (Degree) :树的所有节点中最大的度数。
  3. 叶节点(Leaf):度为0的节点。(也称为叶子节点)
  4. 父节点(Parent):有子树的节点是其子树的根节点的父节点
  5. 子节点(Child):若A节点是B节点的父节点,则称B节点是A节点的子节点;子节点也称孩子节点。
  6. 兄弟节点(Sibling):具有同一父节点的各节点彼此是兄弟节点。
  7. 路径和路径长度:从节点n1到nk的路径为一个节点序列n1 ,n2,… ,nk

    • ni是 n(i+1)的父节点
    • 路径所包含 边 的个数为路径的长度。
  8. 节点的层次(Level):规定根节点在1层,其它任一节点的层数是其父节点的层数加1。
  9. 树的深度(Depth):对于任意节点n, n的深度为从根到n的唯一路径长,根的深度为0。
  10. 树的高度(Height):对于任意节点n,n的高度为从n到一片树叶的最长路径长,所有树叶的高度为

image-20230304164021951

二叉树的概念

如果树中每个节点最多只能有两个子节点,这样的树就成为"二叉树",几乎所有的树都可以表示为二叉树的形式(兄弟表示法)

二叉树的定义
  1. 二叉树可以为空,也就是没有节点。
  2. 若不为空,则它是由根节点 和 称为其 左子树TL和 右子树TR 的两个不相交的二叉树组成

二叉树的五种形态

image-20230304164306931

二叉树的特性

  1. 一颗二叉树第 i 层的最大节点数为:2^(i-1),i >= 1;
  2. 深度为k的二叉树有最大节点总数为: 2^k - 1,k >= 1;
  3. 对任何非空二叉树 T,若n0表示叶节点的个数、n2是度为2的非叶节点个数,那么两者满足关系n0 = n2 + 1。

完美二叉树

完美二叉树(Perfect Binary Tree) ,也称为满二叉树(Full Binary Tree)
  1. 在二叉树中,除了最下一层的叶节点外,每层节点都有2个子节点,就构成了满二叉树。

image-20230304165041053

完全二叉树

完全二叉树(Complete Binary Tree)

  1. 除二叉树最后一层外,其他各层的节点数都达到最大个数。
  2. 且最后一层从左向右的叶节点连续存在,只缺右侧若干节点。
  3. 完美二叉树是特殊的完全二叉树。

下面不是完全二叉树,因为D节点还没有右节点,但是E节点就有了左右节点。

image-20230304165140228

二叉树的存储

二叉树的存储常见的方式是数组和链表。
数组存储

image-20230304165323706

链表存储

二叉树最常见的方式还是使用链表存储

  1. 每个节点封装成一个Node,Node中包含存储的数据,左节点的引用,右节点的引用。

image-20230304165444269

二叉搜索树

二叉搜索树(BST,Binary Search Tree),也称二叉排序树或二叉查找树
二叉搜索树是一颗二叉树,可以为空;
如果不为空,满足以下性质:
  1. 非空左子树的所有键值小于其根节点的键值。
  2. 非空右子树的所有键值大于其根节点的键值。
  3. 左、右子树本身也都是二叉搜索树。
二叉搜索树的特点
  1. 二叉搜索树的特点就是相对较小的值总是保存在左节点上,相对较大的值总是保存在右节点上。
  2. 那么利用这个特点,我们可以做什么事情呢?
  3. 查找效率非常高,这也是二叉搜索树中,搜索的来源

二叉搜索树的封装

class Node<T>{
  value:T;
  constructor(value:T){
    this.value = value;
  }
}
export default Node
//引入打印树结构的工具
import { btPrint } from 'hy-algokit'
import Node from '../types/Node'
class TreeNode <T> extends Node<T>{
  left:TreeNode <T> | null = null;
  right:TreeNode <T> | null = null;
  //当前节点的父节点
  parent:TreeNode<T>|null =null;
  //判断当前节点是父节点的左右子节点
  get isLeft():boolean{
    return !!(this.parent && this.parent.left === this)
  }
  get isRight():boolean{
    return !!(this.parent && this.parent.right === this)
  }
}
//二叉搜素树封装
class BSTree<T>{
  private root:TreeNode<T> | null = null;
  //设置单层递归逻辑
  private insert(newNode:TreeNode<T>,oldNode:TreeNode<T>){
    //二叉搜素树中插入的节点值大于或者小于当前节点时
    if(newNode.value>oldNode.value){
      if(oldNode.right!==null){
        this.insert(newNode,oldNode.right)
      }else{
        oldNode.right = newNode
      }
    }else{
      if(oldNode.left!==null){
        this.insert(newNode,oldNode.left)
      }else{
        oldNode.left = newNode
      }
    }
  }
  //设置单层查找 递归查找某个值是否存在
  private searchNode(value:T):TreeNode<T>|null{
    // 1.搜索:当前是否有这个value
    let current = this.root
    let parent:TreeNode<T>|null  = null 
    while(current){
      //1.如果找到我们的current,直接返回即可
      if(current.value === value){
        return current
      }
      // 2.继续向下寻找
      parent = current
      if(value>current.value){
        current = current.right
      }else{
        current = current.left
      }
      //如果current有值,那么current保存自己的父节点
      if(current!==null)current.parent = parent
    }   
    return null
  }
  //先序遍历
  preOrderTraverse(){
    this.preOrderTraverseNode(this.root)
  }
  //中序遍历
  inOrderTraverse(){
    this.inOrderTraverseNode(this.root)
  }
  //后序遍历
  NextOrderTraverse(){
    this.NextOrderTraverseNode(this.root)
  }
  //todo 单层遍历逻辑{前 中 后 层序}
  //先序遍历
  private preOrderTraverseNode(node:TreeNode<T>|null){
    if(node===null) return
    console.log(node.value);
    this.preOrderTraverseNode(node.left)
    this.preOrderTraverseNode(node.right)
  }
  //中序遍历
  private inOrderTraverseNode(node:TreeNode<T>|null){
    if(node===null) return
    this.inOrderTraverseNode(node.left)
    console.log(node.value);
    this.inOrderTraverseNode(node.right)
  }
  //后序遍历
  private NextOrderTraverseNode(node:TreeNode<T>|null){
    if(node===null) return
    this.NextOrderTraverseNode(node.left)
    this.NextOrderTraverseNode(node.right)
    console.log(node.value);
  }
  //todo非递归{前 中 后 遍历}
  //先序遍历  
  //思路:将左节点先放入栈中 然后从栈中取出并将其右节点放入栈中 
  //判断其是否具有右节点
  preOrderTraversalNoRecursion(){
    let stack:TreeNode<T>[] = []
    let current = this.root
    while(current!==null||stack.length!==0){
      while(current!==null){
        console.log(current.value);
        stack.push(current)
        current = current.left
      }
      current = stack.pop()!
      current = current.right
    }
  }
  //中序遍历
  inOrderTraversalNoRecursion(){
    let stack:TreeNode<T>[] = []
    let current = this.root
    while(current!==null||stack.length!==0){
      while(current!==null){
        stack.push(current)
        current = current.left
      }
      current = stack.pop()!
      console.log(current.value);
      current = current.right
    }
  }
  //后续遍历 : 后续遍历的非递归比较麻烦一些
  NextOrderOrderTraversalNoRecursion(){
    let stack:TreeNode<T>[] = []
    let current:TreeNode<T>|null = this.root
    let lastVisitedNode:TreeNode<T>|null = null
    while(current!==null || stack.length!==0){
      while(current !== null){
        stack.push(current)
        current = current.left
      }
      current  = stack[stack.length-1]
      if(current.right === null && current.right === lastVisitedNode){
        console.log(current.value);
        lastVisitedNode = current
        stack.pop()
        current = null
      }else{
        current = current.right
      }
    }
  }
  //层序遍历  队列结构时间
  levelOederTraverse(){
    //如果没有根节点,那么不需要遍历
    if(!this.root) return
    //创建队列结构
    const queue:TreeNode<T>[] = []
    queue.push(this.root)
    while(queue.length>0){
      const current = queue.shift()!
      console.log(current.value);
      if(current.left!==null)queue.push(current.left)
      if(current.right!==null)queue.push(current.right)
    }
  }
  //获取最大值  获取最小值雷同
  getMaxValue(){
    if(!this.root) return null
    let current:TreeNode<T>|null = this.root 
    while(current){
      if(current.right===null) return current.value
      current = current.right  
    }
  }
  getMinValue(){
    let current:TreeNode<T>|null = this.root 
    while(current&&current.left!==null){
      current = current.left
    }
    //curent为null的时候返回null
    return current?.value??null
  }
  print(){
    btPrint(this.root)
  }
  //插入节点
  insertNode(value:T){
   let newNode = new TreeNode(value)
   if(this.root!== null){
      this.insert(newNode,this.root)
   }else{
      this.root = newNode
   }
  }
  //循环的方式查找某个节点是否存在
  search(val:T):boolean{
    return !!this.searchNode(val)
  }
  //实现current有两个节点的删除操作--->后继节点
  private getSuccessor(delNode:TreeNode<T>):TreeNode<T>{
    //1.获取右子树
    let current = delNode.right
    //2.寻找后继节点
    let successor:TreeNode<T>|null = null
    while(current){
      successor = current;
      current = current.left 
      //todo 解析:(对树结构的疑惑点)只有删除的当前节点被赋予parent值(原因是调用了查询方法) 
      //todo 其余的节点未被赋予,需要我们手动加入,不然 successor!.parent!.left = successor!.right 
      //todo 此代码不成立(successor!.parent为空) 
      if(current){
        current.parent = successor
      }
    }
    console.log("删除节点",delNode.value,"后继节点",successor?.value);
    //3.将删除节点的left(因为是后继节点),赋值给后继节点的left
    successor!.left = delNode.left 
    //拿到了后继节点
    if(successor!==delNode.right){
      successor!.parent!.left = successor!.right
      successor!.right = delNode.right
    }
    return successor!
    
  }
  //实现删除操作
  remove(value:T):boolean{
    let current =  this.searchNode(value)
    //1.未搜索到
    if(!current) return false
    //获取到 1.当前节点 2.父节点  3.属于父节点的左右节点
    // console.log("当前节点:"+current.value+" 父节点:"+current.parent?.value);
    //2.如果删除的叶子节点
    if(current.left===null&&current.right===null){
      if(current.parent===null)return false
      if(current.isLeft){
        current.parent.left = null
      }else{
        current.parent.right = null
      }
    }
    //3.当前节点只有一个左子节点
    else if(current.right===null){
      //根节点==>左子节点
      if(current===this.root){
         this.root =  current.left 
      }else if(current.isLeft){
        //在其父节点的左侧
        current.parent!.left = current.left
      }else{
        //在其父节点的右侧
        current.parent!.right = current.left
      }
    }
    //4.当前节点只有一个右子节点
    else if(current.left===null){
      //根节点==>左子节点
      if(current===this.root){
         this.root =  current.right 
      }else if(current.isLeft){
        //在其父节点的左侧
        current.parent!.left = current.right
      }else{
        //在其父节点的右侧
        current.parent!.right = current.right
      }
    }
    //5.有两个子节点,代码较长已经抽取
    //比 current小一点点的节点,称为current节点的前驱节点
    //大一点点的称为后继节点
    else{
      const successor = this.getSuccessor(current)
      if(current===this.root){
        this.root = successor
      }else if(current.isLeft){
        current.parent!.left = successor
      }else{
        current.parent!.right = successor
      }
    }
    
    return true
  }
}
let bst = new BSTree<number>()
bst.insertNode(11)
bst.insertNode(7)
bst.insertNode(15)
bst.insertNode(5)
bst.insertNode(3)
bst.insertNode(9) 
bst.insertNode(8)
bst.insertNode(10)
bst.insertNode(13)
bst.insertNode(12)
bst.insertNode(14)
bst.insertNode(20)
bst.insertNode(18)
bst.insertNode(25)
bst.insertNode(6)
// bst.insertNode(11) // 相同的节点根据个人去处理?
bst.print()
bst.remove(11)
bst.print()
bst.remove(15)
bst.print()
bst.remove(9)
bst.print()
// bst.remove(13)
bst.remove(7)
bst.print()

[scode type="yellow"]提示:二叉搜索树的删除操作考虑的情况较多,要注意三种不同的情况[/scode]

6.图结构

什么是图?

在计算机程序设计中,图结构 也是一种非常常见的数据结构。
  1. 但是,图论其实是一个非常大的话题
  2. 我们通过本章的学习来认识一下关于图的一些内容 - 图的抽象数据类型 – 一些算法实现。
什么是图?
  1. 图结构是一种与树结构有些相似的数据结构。
  2. 图论是数学的一个分支,并且,在数学的概念上,树是图的一种。
  3. 它以图为研究对象,研究 顶点 和 边 组成的图形的数学理论和方法。
  4. 主要研究的目的是事物之间的关系,顶点代表事物,边代表两个事物间的关系
我们知道树可以用来模拟很多现实的数据结构
  1. 比如: 家谱/公司组织架构等等
  2. 那么图长什么样子?
  3. 或者什么样的数据使用图来模拟更合适呢

图的术语

image-20230304171050284

顶点:
  1. 顶点刚才我们已经介绍过了,表示图中的一个节点。
  2. 比如地铁站中某个站/多个村庄中的某个村庄/互联网中的某台主机/人际关系中的人。
边:
  1. 边刚才我们也介绍过了,表示顶点和顶点之间的连线。
  2. 比如地铁站中两个站点之间的直接连线,就是一个边。
  3. 注意: 这里的边不要叫做路径,路径有其他的概念,待会儿我们会介绍到。
  4. 之前的图中: 0 - 1有一条边,1 - 2有一条边,0 - 2没有边。
相邻顶点:
  1. 由一条边连接在一起的顶点称为相邻顶点。
  2. 比如0 - 1是相邻的,0 - 3是相邻的。 0 - 2是不相邻的。

image-20230304171234030

image-20230304171320560

图的表示

怎么在程序中表示图呢?

  1. 我们知道一个图包含很多顶点,另外包含顶点和顶点之间的连线(边)
  2. 这两个都是非常重要的图信息,因此都需要在程序中体现出来
顶点的表示相对简单,我们先讨论顶点的表示。
  1. 上面的顶点,我们抽象成了1 2 3 4,也可以抽象成A B C D。
  2. 在后面的案例中,我们使用A B C D。
  3. 那么这些A B C D我们可以使用一个数组来存储起来(存储所有的顶点)
  4. 当然,A,B,C,D也可以表示其他含义的数据(比如村庄的名字).
那么边怎么表示呢?
  1. 因为边是两个顶点之间的关系,所以表示起来会稍微麻烦一些。
  2. 下面,我们具体讨论一下变常见的表示方式。

邻接矩阵

一种比较常见的表示图的方式: 邻接矩阵。
  1. 邻接矩阵让每个节点和一个整数项关联,该整数作为数组的下标值。
  2. 我们用一个二维数组来表示顶点之间的连接。
  3. 二维数组0 -> A -> C
画图演示:

image-20230304171609877

图片解析:
  1. 在二维数组中,0表示没有连线,1表示有连线。
  2. 通过二维数组,我们可以很快的找到一个顶点和哪些顶点有连线。(比如A顶点,只需要遍历第一行即可)
  3. 另外,A - A,B - B(也就是顶点到自己的连线),通常使用0表示。
邻接矩阵的问题:
  1. 邻接矩阵还有一个比较严重的问题,就是如果图是一个稀疏图
  2. 那么矩阵中将存在大量的0,这意味着我们浪费了计算机存储空间来表示根本不存在的边

邻接表

另外一种常用的表示图的方式: 邻接表。
  1. 邻接表由图中每个顶点以及和顶点相邻的顶点列表组成。
  2. 这个列表有很多种方式来存储: 数组/链表/字典(哈希表)都可以。
画图演示

image-20230304171737676

图片解析:
  1. 其实图片比较容易理解。
  2. 比如我们要表示和A顶点有关联的顶点(边),A和B/C/D有边,
  3. 那么我们可以通过A找到对应的数组/链表/字典,再取出其中的内容就可以啦。
邻接表的问题:
  1. 邻接表计算"出度"是比较简单的(出度: 指向别人的数量,入度: 指向自己的数量)
  2. 邻接表如果需要计算有向图的"入度",那么是一件非常麻烦的事情。
  3. 它必须构造一个“逆邻接表”,才能有效的计算“入度”。但是开发中“入度”相对用的比较少c

创建图类

class Graph<T>{
  //顶点
  private verteces:T[] = []
  //边,邻接表
  private adjList:Map<T,T[]> = new Map()
  /**添加顶点跟边的方法*/
  addVertex(vertex:T){
    //保存顶点
    this.verteces.push(vertex)
    //创建邻接表
    this.adjList.set(vertex,[])
  }

   /**添加边*/
  addEdge(v1:T,v2:T){
    this.adjList.get(v1)?.push(v2)
    this.adjList.get(v2)?.push(v1)
  }

  /**遍历*/
  traverse(){
    this.verteces.forEach((vertex)=>{
      const edges = this.adjList.get(vertex)
      console.log(`${vertex} -> ${edges?.join(" ")}`);
    }
  )}
  /**BFS*/ 
  bfs(){
    if(this.verteces[0]===0) return
    let queue:T[] = []
    queue.push(this.verteces[0])
    //访问过后的顶点插入set中
    let visited = new Set()
    visited.add(this.verteces[0])
    while(queue.length){
      //拿到队列头部元素
      const vertex = queue.shift()! 
      this.adjList.get(vertex)?.forEach(item=>{
        if(!visited.has(item)){
          visited.add(item)
          queue.push(item)
        }
      })
    }
    return visited
  }
  /**DFS*/  
  //无论是从哪个方向开始深层次挖掘最终结果是一样的
  dfs(){
    if(this.verteces[0]===0) return
    //模拟栈结构 循环
    let stack:T[] = []
    stack.push(this.verteces[0])
    //创建set结构
    let visited = new Set<T>()
    visited.add(this.verteces[0])

    while(stack.length){
      const vertex = stack.pop()!
      this.adjList.get(vertex)!.forEach(ele=>{
        if(!visited.has(ele)){
          stack.push(ele)
          visited.add(ele)
        }
      })
    }
    return visited
  }
}
const graph = new Graph()
graph.addVertex("A")
graph.addVertex("B")
graph.addVertex("C")
graph.addVertex("D")
graph.addVertex("E")
graph.addVertex("F")
graph.addVertex("G")
graph.addVertex("H")

graph.addEdge("A","B")
graph.addEdge("B","C")
graph.addEdge("C","D")
graph.addEdge("D","E")
graph.addEdge("E","F")
graph.addEdge("F","G")
// graph.traverse()
//todo 广度队列结构一层一层 深度while循环,类递归 
console.log(graph.bfs());
console.log(graph.dfs());
export {}
]]>
1 https://ilnafz.cn/index.php/archives/40/#comments https://ilnafz.cn/index.php/feed/
学校一角 https://ilnafz.cn/index.php/archives/33/ https://ilnafz.cn/index.php/archives/33/ Wed, 11 Jan 2023 15:28:00 +0800 111

]]>
0 https://ilnafz.cn/index.php/archives/33/#comments https://ilnafz.cn/index.php/feed/
TCP三次握手与四次挥手面试题 https://ilnafz.cn/index.php/archives/20/ https://ilnafz.cn/index.php/archives/20/ Sat, 07 Jan 2023 10:18:00 +0800 本文是博主看完小林coding-tcp篇幅后留下的一个简短笔记记录。

大纲

请输入图片描述

TCP基本认识

TCP 头

序列号: 在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。

确认应答号: 指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。

控制位:

  • ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1
  • RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
  • SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
  • FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。

为什么需要 TCP 协议? TCP 工作在哪一层?

IP 层是不可靠的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。
如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。
因为 TCP 是一个工作在传输层可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。

什么是 TCP ?

TCP 是面向连接的、可靠的、基于字节流的传输层通信协议(数据传输服务)。

  • 面向连接:一定是「一对一」才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;
  • 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。
  • 字节流:用户消息通过 TCP 协议传输时,消息可能会被操作系统「分组」成多个的 TCP 报文,如果接收方的程序如果不知道「消息的边界」,是无法读出一个有效的用户消息的。并且 TCP 报文是「有序的」,当「前一个」TCP 报文没有收到的时候,即使它先收到了后面的 TCP 报文,那么也不能扔给应用层去处理,同时对「重复」的 TCP 报文会自动丢弃。

什么是 TCP 连接?

简单来说就是,用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。
所以我们可以知道,建立一个 TCP 连接是需要客户端与服务端端达成上述三个信息的共识。

  • Socket:由 IP 地址和端口号组成
  • 序列号:用来解决乱序问题等
  • 窗口大小:用来做流量控制

如何唯一确定一个 TCP 连接呢?

TCP 四元组可以唯一的确定一个连接,四元组包括源地址、源端口,目标地址、目标端口。

有一个 IP 的服务端监听了一个端口,它的 TCP 的最大连接数是多少?

服务端通常固定在某个本地端口上监听,等待客户端的连接请求。
因此,客户端 IP 和 端口是可变的,其理论值计算公式如下:

img

对 IPv4,客户端的 IP 数最多为 232 次方,客户端的端口数最多为 216 次方,也就是服务端单机最大 TCP 连接数,约为 248 次方。

UDP 和 TCP 有什么区别呢?分别的应用场景是?

UDP 不提供复杂的控制机制,利用 IP 提供面向「无连接」的通信服务。
UDP 协议真的非常简,头部只有 8 个字节( 64 位),UDP 的头部格式如下:

  • 目标和源端口:主要是告诉 UDP 协议应该把报文发给哪个进程。(分别占用16位)
  • 包长度:该字段保存了 UDP 首部的长度跟数据的长度之和。(16位)
  • 校验和:校验和是为了提供可靠的 UDP 首部和数据而设计,防止收到在网络传输中受损的 UDP包。(16位)

区别

  1. 连接
  • TCP 是面向连接的传输层协议,传输数据前先要建立连接。
  • UDP 是不需要连接,即刻传输数据。
  1. 服务对象
  • TCP 是一对一的两点服务,即一条连接只有两个端点。
  • UDP 支持一对一、一对多、多对多的交互通信
  1. 可靠性
  • TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。
  • UDP 是尽最大努力交付,不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现一个可靠的传输协议,比如 QUIC 协议。
  1. 拥塞控制、流量控制
  • TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
  • UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。
  1. 首部开销
  • TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。
  • UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
  1. 传输方式
  • TCP 是流式传输,没有边界,但保证顺序和可靠(需要应用层自己去定义消息边界)。
  • UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。
  1. 分片不同
  • TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。
  • UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层。

[scode type="blue" size=""]为什么 UDP 头部没有「首部长度」字段,而 TCP 头部有「首部长度」字段呢?[/scode]

原因是 TCP 有可变长的「选项」字段,而 UDP 头部长度则是不会变化的,无需多一个字段去记录 UDP 的首部长度。

TCP 和 UDP 可以使用同一个端口吗?

答案:可以的
在传输层中,需要通过端口进行寻址,来识别同一计算机中同时通信的不同应用程序。
所以,传输层的「端口号」的作用,是为了区分同一个主机上不同应用程序的数据包。
传输层有两个传输协议分别是 TCP 和 UDP,在内核中是两个完全独立的软件模块。
当主机收到数据包后,可以在 IP 包头的「协议号」字段知道该数据包是 TCP/UDP,所以可以根据这个信息确定送给哪个模块(TCP/UDP)处理,送给 TCP/UDP 模块的报文根据「端口号」确定送给哪个应用程序处理。

tcp和udp模块

因此, TCP/UDP 各自的端口号也相互独立,如 TCP 有一个 80 号端口,UDP 也可以有一个 80 号端口,二者并不冲突。

TCP 连接建立

TCP 三次握手过程是怎样的?

TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手来进行的。三次握手的过程如下图:

TCP三次握手.drawio

  • 一开始,客户端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端口,处于 LISTEN 状态
  • 客户端会随机初始化序号(client_isn),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN 标志位置为 1 ,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。
  • 服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1, 接着把 SYNACK 标志位置为 1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。
  • 服务端收到客户端的应答报文后,也进入 ESTABLISHED 状态

从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的,这也是面试常问的题。
一旦完成三次握手,双方都处于 ESTABLISHED 状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。

如何在 Linux 系统中查看 TCP 状态?

TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看。

TCP 连接状态查看

为什么是三次握手?不是两次、四次?

相信大家比较常回答的是:“因为三次握手才能保证双方具有接收和发送的能力。”
这回答是没问题,但这回答是片面的,并没有说出主要的原因。

在前面我们知道了什么是 TCP 连接
用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。

所以,重要的是为什么三次握手才可以初始化Socket、序列号和窗口大小并建立 TCP 连接。

原因一:三次握手才可以阻止重复历史连接的初始化(主要原因),旧的重复连接初始化造成混乱

三次握手避免历史连接

客户端连续发送多次 SYN (都是同一个四元组)建立连接的报文,在网络拥堵情况下:

  • 一个「旧 SYN 报文」比「最新的 SYN 」 报文早到达了服务端,那么此时服务端就会回一个 SYN + ACK 报文给客户端,此报文中的确认号是 91(90+1)。
  • 客户端收到后,发现自己期望收到的确认号应该是 100+1,而不是 90 + 1,于是就会回 RST 报文。
  • 服务端收到 RST 报文后,就会释放连接。
  • 后续最新的 SYN 抵达了服务端后,客户端与服务端就可以正常的完成三次握手了。

上述中的「旧 SYN 报文」称为历史连接,TCP 使用三次握手建立连接的最主要原因就是防止「历史连接」初始化了连接
如果是两次握手连接,就无法阻止历史连接,那为什么 TCP 两次握手为什么无法阻止历史连接呢?
我先直接说结论,主要是因为在两次握手的情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费

原因二:三次握手才可以同步双方的初始序列号

TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:

  • 接收方可以去除重复的数据;
  • 接收方可以根据数据包的序列号按序接收;
  • 可以标识发送出去的数据包中, 哪些是已经被对方收到的(通过 ACK 报文中的序列号知道);

可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。

四次握手与三次握手

四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了「三次握手」。
而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。

原因三:三次握手才可以避免资源浪费

如果只有「两次握手」,当客户端发生的 SYN 报文在网络中阻塞,客户端没有接收到 ACK 报文,就会重新发送 SYN由于没有第三次握手,服务端不清楚客户端是否收到了自己回复的 ACK 报文,所以服务端每收到一个 SYN 就只能先主动建立一个连接,这会造成什么情况呢?
如果客户端发送的 SYN 报文在网络中阻塞了,重复发送多次 SYN 报文,那么服务端在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。

两次握手会造成资源浪费

即两次握手会造成消息滞留情况下,服务端重复接受无用的连接请求 SYN 报文,而造成重复分配资源。

小结

TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。
不使用「两次握手」和「四次握手」的原因:

  • 「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
  • 「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。

为什么每次建立 TCP 连接时,初始化的序列号都要求不一样呢?

主要原因有两个方面:

  • 为了防止历史报文被下一个相同四元组的连接接收(主要方面);
  • 为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收;

接下来,详细说说第一点。
假设每次建立连接,客户端和服务端的初始化序列号都是从 0 开始:
isn相同

过程如下:

  • 客户端和服务端建立一个 TCP 连接,在客户端发送数据包被网络阻塞了,然后超时重传了这个数据包,而此时服务端设备断电重启了,之前与客户端建立的连接就消失了,于是在收到客户端的数据包的时候就会发送 RST 报文。
  • 紧接着,客户端又与服务端建立了与上一个连接相同四元组的连接;
  • 在新连接建立完成后,上一个连接中被网络阻塞的数据包正好抵达了服务端,刚好该数据包的序列号正好是在服务端的接收窗口内,所以该数据包会被服务端正常接收,就会造成数据错乱。

可以看到,如果每次建立连接,客户端和服务端的初始化序列号都是一样的话,很容易出现历史报文被下一个相同四元组的连接接收的问题

如果每次建立连接客户端和服务端的初始化序列号都「不一样」,就有大概率因为历史报文的序列号「不在」对方接收窗口,从而很大程度上避免了历史报文,比如下图:

isn不相同

相反,如果每次建立连接客户端和服务端的初始化序列号都「一样」,就有大概率遇到历史报文的序列号刚「好在」对方的接收窗口内,从而导致历史报文被新连接成功接收。

所以,每次初始化序列号不一样很大程度上能够避免历史报文被下一个相同四元组的连接接收,注意是很大程度上,并不是完全避免了(因为序列号会有回绕的问题,所以需要用时间戳的机制来判断历史报文)

初始序列号 ISN 是如何随机产生的?

起始 ISN 是基于时钟的,每 4 微秒 + 1,转一圈要 4.55 个小时。

RFC793 提到初始化序列号 ISN 随机生成算法:ISN = M + F(localhost, localport, remotehost, remoteport)。

  • M 是一个计时器,这个计时器每隔 4 微秒加 1。
  • F 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。

可以看到,随机数是会基于时钟计时器递增的,基本不可能会随机成一样的初始化序列号。

既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?

我们先来认识下 MTU 和 MSS

MTU 与 MSS

  • MTU:一个网络包的最大长度,以太网中一般为 1500 字节;
  • MSS:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度;

如果在 TCP 的整个报文(头部 + 数据)交给 IP 层进行分片,会有什么异常呢?

当 IP 层有一个超过 MTU 大小的数据(TCP 头部 + TCP 数据)要发送,那么 IP 层就要进行分片,把数据分片成若干片,保证每一个分片都小于 MTU。把一份 IP 数据报进行分片以后,由目标主机的 IP 层来进行重新组装后,再交给上一层 TCP 传输层。

这看起来井然有序,但这存在隐患的,那么当如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传

因为 IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。

当某一个 IP 分片丢失后,接收方的 IP 层就无法组装成一个完整的 TCP 报文(头部 + 数据),也就无法将数据报文送到 TCP 层,所以接收方不会响应 ACK 给发送方,因为发送方迟迟收不到 ACK 确认报文,所以会触发超时重传,就会重发「整个 TCP 报文(头部 + 数据)」。

因此,可以得知由 IP 层进行分片传输,是非常没有效率的。

所以,为了达到最佳的传输效能 TCP 协议在建立连接的时候通常要协商双方的 MSS 值,当 TCP 层发现数据超过 MSS 时,则就先会进行分片,当然由它形成的 IP 包的长度也就不会大于 MTU ,自然也就不用 IP 分片了。

握手阶段协商 MSS

经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率。

第一次握手丢失了,会发生什么?

客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发「超时重传」机制,重传 SYN 报文,而且重传的 SYN 报文的序列号都是一样的,重传次数由tcp_syn_retries控制默认一般为5。

不同版本的操作系统可能超时时间不同,有的 1 秒的,也有 3 秒的,这个超时时间是写死在内核里的,如果想要更改则需要重新编译内核,比较麻烦。

当客户端在 1 秒后没收到服务端的 SYN-ACK 报文后,客户端就会重发 SYN 报文,那到底重发几次呢?

在 Linux 里,客户端的 SYN 报文最大重传次数由 tcp_syn_retries内核参数控制,这个参数是可以自定义的,默认值一般是 5。

# cat /proc/sys/net/ipv4/tcp_syn_retries
5

通常,第一次超时重传是在 1 秒后,第二次超时重传是在 2 秒,第三次超时重传是在 4 秒后,第四次超时重传是在 8 秒后,第五次是在超时重传 16 秒后。没错,每次超时的时间是上一次的 2 倍

当第tcp_syn_retries次超时重传后,会继续等待 xx 秒,如果服务端仍然没有回应 ACK,客户端就不再发送 SYN 包,然后客户端断开 TCP 连接。

第二次握手丢失了,会发生什么?

客户端就觉得可能自己的 SYN 报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传 SYN 报文

然后,因为第二次握手中包含服务端的 SYN 报文,所以当客户端收到后,需要给服务端发送 ACK 确认报文(第三次握手),服务端才会认为该 SYN 报文被客户端收到了。

那么,如果第二次握手丢失了,服务端就收不到第三次握手,于是服务端这边会触发超时重传机制,重传 SYN-ACK 报文

因此,当第二次握手丢失了,客户端和服务端都会重传:

  • 客户端会重传 SYN 报文,也就是第一次握手,最大重传次数由 tcp_syn_retries内核参数决定;
  • 服务端会重传 SYN-ACK 报文,也就是第二次握手,最大重传次数由 tcp_synack_retries 内核参数决定。

具体过程:

  • 当服务端超时重传 2 次 SYN-ACK 报文后,由于 tcp_synack_retries 为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接。

什么是 SYN 攻击?如何避免 SYN 攻击?

我们都知道 TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。

SYN 攻击

先跟大家说一下,什么是 TCP 半连接和全连接队列。

在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:

  • 半连接队列,也称 SYN 队列;
  • 全连接队列,也称 accept 队列;

我们先来看下 Linux 内核的 SYN 队列(半连接队列)与 Accpet 队列(全连接队列)是如何工作的?

正常流程

正常流程:

  • 当服务端接收到客户端的 SYN 报文时,会创建一个半连接的对象,然后将其加入到内核的「 SYN 队列」;
  • 接着发送 SYN + ACK 给客户端,等待客户端回应 ACK 报文;
  • 服务端接收到 ACK 报文后,从「 SYN 队列」取出一个半连接对象,然后创建一个新的连接对象放入到「 Accept 队列」;
  • 应用通过调用 accpet() socket 接口,从「 Accept 队列」取出连接对象。

不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,默认情况都会丢弃报文。

SYN 攻击方式最直接的表现就会把 TCP 半连接队列打满,这样当 TCP 半连接队列满了,后续再在收到 SYN 报文就会丢弃,导致客户端无法和服务端建立连接。

避免 SYN 攻击方式,可以有以下四种方法:

方法一:调大 netdev_max_backlog;

当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值如下参数,默认值是 1000,我们要适当调大该参数的值,比如设置为 10000:

方法二:增大 TCP 半连接队列;
方法三:开启 tcp_syncookies;

开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接,相当于绕过了 SYN 半连接来建立连接。

具体过程:

  • 当 「 SYN 队列」满之后,后续服务端收到 SYN 包,不会丢弃,而是根据算法,计算出一个 cookie 值;
  • 将 cookie 值放到第二次握手报文的「序列号」里,然后服务端回第二次握手给客户端;
  • 服务端接收到客户端的应答报文时,服务端会检查这个 ACK 包的合法性。如果合法,将该连接对象放入到「 Accept 队列」。
  • 最后应用程序通过调用 accpet() 接口,从「 Accept 队列」取出的连接。

可以看到,当开启了 tcp_syncookies 了,即使受到 SYN 攻击而导致 SYN 队列满时,也能保证正常的连接成功建立。

方法四:减少 SYN+ACK 重传次数

当服务端受到 SYN 攻击时,就会有大量处于 SYN_REVC 状态的 TCP 连接,处于这个状态的 TCP 会重传 SYN+ACK ,当重传超过次数达到上限后,就会断开连接。

那么针对 SYN 攻击的场景,我们可以减少 SYN-ACK 的重传次数,以加快处于 SYN_REVC 状态的 TCP 连接断开。

SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定(默认值是 5 次),比如将 tcp_synack_retries 减少到 2 次:

TCP 连接断开

TCP 四次挥手过程是怎样的?

天下没有不散的宴席,对于 TCP 连接也是这样, TCP 断开连接是通过四次挥手方式。

双方都可以主动断开连接,断开连接后主机中的「资源」将被释放,四次挥手的过程如下图:

客户端主动关闭连接 —— TCP 四次挥手

  • 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。
  • 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSE_WAIT 状态。
  • 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
  • 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
  • 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态
  • 服务端收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭。
  • 客户端在经过 2MSL 一段时间后,自动进入 CLOSE 状态,至此客户端也完成连接的关闭。

你可以看到,每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手

这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。

为什么挥手需要四次?

再来回顾下四次挥手双方发 FIN 包的过程,就能理解为什么需要四次了。

  • 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
  • 服务端收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。

从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACKFIN 一般都会分开发送,因此是需要四次挥手。

第一次挥手丢失了,会发生什么?

客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文,重发次数由 tcp_orphan_retries 参数控制。

当客户端重传 FIN 报文的次数超过 tcp_orphan_retries 后,就不再发送 FIN 报文,则会在等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到第二次挥手,那么直接进入到 close 断开连接状态。

第二次挥手丢失了,会发生什么?

ACK 报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。

具体过程:

  • 当客户端超时重传 2 次 FIN 报文后,由于 tcp_orphan_retries 为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次挥手(ACK 报文),那么客户端就会断开连接。

如果主动关闭方使用 shutdown 函数关闭连接,指定了只关闭发送方向,而接收方向并没有关闭,那么意味着主动关闭方还是可以接收数据的。

此时,如果主动关闭方一直没收到第三次挥手,那么主动关闭方的连接将会一直处于 FIN_WAIT2 状态

第二次挥手丢失

第三次挥手丢失了,会发生什么?

当服务端(被动关闭方)收到客户端(主动关闭方)的 FIN 报文后,内核会自动回复 ACK,同时连接处于 CLOSE_WAIT 状态,顾名思义,它表示等待应用进程调用 close 函数关闭连接。

此时,内核是没有权利替代进程关闭连接,必须由进程主动调用 close 函数来触发服务端发送 FIN 报文。

服务端处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会发出 FIN 报文,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。

如果迟迟收不到这个 ACK,服务端就会重发 FIN 报文,重发次数仍然由 tcp_orphan_retries 参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的。

第四次挥手丢失了,会发生什么?

当客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT 状态。

在 Linux 系统,TIME_WAIT 状态会持续 2MSL 后才会进入关闭状态。

然后,服务端(被动关闭方)没有收到 ACK 报文前,还是处于 LAST_ACK 状态。

如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries 参数控制。

为什么 TIME_WAIT 等待的时间是 2MSL?

MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。

MSL 与 TTL 的区别: MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。

TTL 的值一般是 64,Linux 将 MSL 设置为 30 秒,意味着 Linux 认为数据报文经过 64 个路由器的时间不会超过 30 秒,如果超过了,就认为报文已经消失在网络中了

TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间

比如,如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 FIN 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。

可以看到 2MSL时长 这其实是相当于至少允许报文丢失一次。比如,若 ACK 在一个 MSL 内丢失,这样被动方重发的 FIN 会在第 2 个 MSL 内到达,TIME_WAIT 状态的连接可以应对。

为什么不是 4 或者 8 MSL 的时长呢?你可以想象一个丢包率达到百分之一的糟糕网络,连续两次丢包的概率只有万分之一,这个概率实在是太小了,忽略它比解决它更具性价比。

2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时

在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒

其定义在 Linux 内核代码里的名称为 TCP_TIMEWAIT_LEN:

#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT 
                                    state, about 60 seconds  */

如果要修改 TIME_WAIT 的时间长度,只能修改 Linux 内核代码里 TCP_TIMEWAIT_LEN 的值,并重新编译 Linux 内核。

为什么需要 TIME_WAIT 状态?

主动发起关闭连接的一方,才会有 TIME-WAIT 状态。

原因一:防止历史连接中的数据,被后面相同四元组的连接错误的接收

原因二:保证「被动关闭连接」的一方,能被正确的关闭

TIME_WAIT 过多有什么危害?

过多的 TIME-WAIT 状态主要的危害有两种:

  • 第一是占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等;
  • 第二是占用端口资源,端口资源也是有限的,一般可以开启的端口为 32768~61000,也可以通过 net.ipv4.ip_local_port_range参数指定范围。

客户端和服务端 TIME_WAIT 过多,造成的影响是不同的。

如果客户端(主动发起关闭连接方)的 TIME_WAIT 状态过多,占满了所有端口资源,那么就无法对「目的 IP+ 目的 PORT」都一样的服务端发起连接了,但是被使用的端口,还是可以继续对另外一个服务端发起连接的。具体可以看我这篇文章:客户端的端口可以重复使用吗?(opens new window)

因此,客户端(发起连接方)都是和「目的 IP+ 目的 PORT 」都一样的服务端建立连接的话,当客户端的 TIME_WAIT 状态连接过多的话,就会受端口资源限制,如果占满了所有端口资源,那么就无法再跟「目的 IP+ 目的 PORT」都一样的服务端建立连接了。

不过,即使是在这种场景下,只要连接的是不同的服务端,端口是可以重复使用的,所以客户端还是可以向其他服务端发起连接的,这是因为内核在定位一个连接的时候,是通过四元组(源IP、源端口、目的IP、目的端口)信息来定位的,并不会因为客户端的端口一样,而导致连接冲突。

如果服务端(主动发起关闭连接方)的 TIME_WAIT 状态过多,并不会导致端口资源受限,因为服务端只监听一个端口,而且由于一个四元组唯一确定一个 TCP 连接,因此理论上服务端可以建立很多连接,但是 TCP 连接过多,会占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等。

如何优化 TIME_WAIT?

这里给出优化 TIME-WAIT 的几个方式,都是有利有弊:

  • 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项;
  • net.ipv4.tcp_max_tw_buckets
  • 程序中使用 SO_LINGER ,应用强制使用 RST 关闭。

服务器出现大量 TIME_WAIT 状态的原因有哪些?

首先要知道 TIME_WAIT 状态是主动关闭连接方才会出现的状态,所以如果服务器出现大量的 TIME_WAIT 状态的 TCP 连接,就是说明服务器主动断开了很多 TCP 连接。

问题来了,什么场景下服务端会主动断开连接呢?

  • 第一个场景:HTTP 没有使用长连接
  • 第二个场景:HTTP 长连接超时
  • 第三个场景:HTTP 长连接的请求数量达到上限

服务器出现大量 CLOSE_WAIT 状态的原因有哪些?

CLOSE_WAIT 状态是「被动关闭方」才会有的状态,而且如果「被动关闭方」没有调用 close 函数关闭连接,那么就无法发出 FIN 报文,从而无法使得 CLOSE_WAIT 状态的连接转变为 LAST_ACK 状态。

所以,当服务端出现大量 CLOSE_WAIT 状态的连接的时候,说明服务端的程序没有调用 close 函数关闭连接

那什么情况会导致服务端的程序没有调用 close 函数关闭连接?这时候通常需要排查代码。

我们先来分析一个普通的 TCP 服务端的流程:

  1. 创建服务端 socket,bind 绑定端口、listen 监听端口
  2. 将服务端 socket 注册到 epoll
  3. epoll_wait 等待连接到来,连接到来时,调用 accpet 获取已连接的 socket
  4. 将已连接的 socket 注册到 epoll
  5. epoll_wait 等待事件发生
  6. 对方连接关闭时,我方调用 close

可能导致服务端没有调用 close 函数的原因,如下。

第一个原因:第 2 步没有做,没有将服务端 socket 注册到 epoll,这样有新连接到来时,服务端没办法感知这个事件,也就无法获取到已连接的 socket,那服务端自然就没机会对 socket 调用 close 函数了。

不过这种原因发生的概率比较小,这种属于明显的代码逻辑 bug,在前期 read view 阶段就能发现的了。

第二个原因: 第 3 步没有做,有新连接到来时没有调用 accpet 获取该连接的 socket,导致当有大量的客户端主动断开了连接,而服务端没机会对这些 socket 调用 close 函数,从而导致服务端出现大量 CLOSE_WAIT 状态的连接。

发生这种情况可能是因为服务端在执行 accpet 函数之前,代码卡在某一个逻辑或者提前抛出了异常。

第三个原因:第 4 步没有做,通过 accpet 获取已连接的 socket 后,没有将其注册到 epoll,导致后续收到 FIN 报文的时候,服务端没办法感知这个事件,那服务端就没机会调用 close 函数了。

发生这种情况可能是因为服务端在将已连接的 socket 注册到 epoll 之前,代码卡在某一个逻辑或者提前抛出了异常。之前看到过别人解决 close_wait 问题的实践文章,感兴趣的可以看看:一次 Netty 代码不健壮导致的大量 CLOSE_WAIT 连接原因分析(opens new window)

第四个原因:第 6 步没有做,当发现客户端关闭连接后,服务端没有执行 close 函数,可能是因为代码漏处理,或者是在执行 close 函数之前,代码卡在某一个逻辑,比如发生死锁等等。

可以发现,当服务端出现大量 CLOSE_WAIT 状态的连接的时候,通常都是代码的问题,这时候我们需要针对具体的代码一步一步的进行排查和定位,主要分析的方向就是服务端为什么没有调用 close

如果已经建立了连接,但是客户端突然出现故障了怎么办?

TCP 有一个机制是保活机制。这个机制的原理是这样的:

定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。

如果已经建立了连接,但是服务端的进程崩溃会发生什么?

TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP 四次挥手的过程。

我自己做了个实验,使用 kill -9 来模拟进程崩溃的情况,发现在 kill 掉进程后,服务端会发送 FIN 报文,与客户端进行四次挥手

套接字概念

Socket是一套用于不同主机间 通信的API,他工作在我们的TCP ip 协议栈之上,他的应用无处不在

比如你的浏览器 手机应用用于服务器管理的SSH客户端全都是基于socket实现的

要通过socket与不同主机建立通信,我们只需要指定主机的IP地址和一个端口号

image-20230105151712711

通过socket 我们可以建立一条用于不同主机 不同应用之间的虚拟数据通道,并且他是点对点(应用对应用的)的

我们经常用到的Socket 有两种类型 TCP与UDP

补充:我们可以使用文件描述符引用套接字。Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是文件主要应用于本地持久化数据的读写,而套接字多应用于网络进程间数据的传递。

]]>
0 https://ilnafz.cn/index.php/archives/20/#comments https://ilnafz.cn/index.php/feed/
JavaScript基础面试题(含手写与输出题)理解 https://ilnafz.cn/index.php/archives/25/ https://ilnafz.cn/index.php/archives/25/ Fri, 06 Jan 2023 11:08:00 +0800 JavaScript基础面试题理解

封在寝室的这些日子,新的知识不是很能学进去,就拉着王帅花了一部分时间把之前学习的JS复习了一遍,外加一些常见习题及题解。

1.延迟加载 JS的方式有哪些

1.async
html与js 是一起加载的,js谁先加载完毕 先执行谁
2.defer
html加载完毕后 js将进行同步加载

2.数据类型和考题

基本数据类型
string number undefined symbol null bigint boolean
对象数据类型
object
Array function
考题

console.log( true + 1 ); 2
console.log( 'name'+ true ); 'nametrue'
console.log( undefined + 1 ); undefined
console.log( typeof undefined ); undefined

console.log( typeof(number)); undefined
console.log( typeof(Number)); function
console.log( typeof(NaN)); number
console.log( typeof(null) ); object(通常我们给变量赋空值时 var a = null)

3.null与undefined 的区别

var a; 与 var a = null 的区别
1
null与undefined都表示为空 ,主要区别在于 undefined 表示尚未初始化的变量的值 (注:声明与定义不是一回事,这里指的是仅声明),而 null 表示该变量有意缺少对象指向(声明了,定义为null,有意初始化该变量值)

undefined
已声明 未定义
隐藏式(未定义) 空值

null
值声明且定义,但它并未指向任何内存中的对象(注:null是object类型)
声明式 空值

4.== 与 === 有什么不同

console.log(null === undefined)
console.log(typeof (null))
console.log(typeof (undefined))
1
2
3
== 值的校验
=== 类型与值的校验

5.JS宏任务与微任务

宏任务与微任务的定义
js是单线程语言,执行流程分为同步与异步
同步代码执行完毕后,执行异步代码,异步分宏微任务

执行顺序
js代码执行流程:同步执行完==》事件循环
同步的任务都执行完了,才会执行事件循环的内容
进入事件循环:请求、定时器、事件…
事件循环中包含:【微任务、宏任务】
要执行宏任务的前提是清空了所有的微任务
流程:同步==》事件循环【微任务和宏任务】》微任务》宏任务=》微任务…
宏任务:

微任务:

习题:

console.log('start');

function foo () {
console.log(' ');
}
foo()
setTimeout(function () {
console.log('setTimeout');
}, 1000)

new Promise(resolve => {
console.log('promise'); //注意 这里是同步任务
resolve()
})
.then(function () {

console.log('promise1');

})
.then(function () {

console.log('promise2');

})

console.log('end');
答案: start foo promise end promise1 promise2 setTimeout
1
宏任务与微任务

6.JS作用域考题

1.只有函数拥有块级作用域
2.作用域链:内部可以访问外部的变量,但是外部不能访问内部的变量。
注意:如果内部有,优先查找到内部,如果内部没有就查找外部的。

  1. 注意声明变量是用var还是没有写(window)
  2. 注意:js有变量提升的机制【变量悬挂声明】
  3. 优先级:声明变量 > 声明普通函数 > 参数 > 变量提升
    1
    2
    3
    4
    5
    6
    习题一

function c(){

var b = 1;
function a(){
    console.log( b );
    var b = 2;
    console.log( b );
}
a();
console.log( b );

}
c();
1
2
3
4
5
6
7
8
9
10
11
习题二

var name = 'a';
(function(){

if( typeof name == 'undefined' ){
    var name = 'b';
    console.log('111'+name);
}else{
    console.log('222'+name);
}

})()
1
2
3
4
5
6
7
8
9
习题三:

function fun( a ){

var a = 10;
function a(){}
console.log( a );

}
fun( 100 );
1
2
3
4
5
6

7.JS对象考题

1.对象是通过new操作符构建出来的,所以对象之间不想等(除了引用外);
2.对象注意:引用类型(共同一个地址);
3.对象的key都是字符串类型

var name = {
a: 1,
b: "11"
};
for (key in name) {
console.log(typeof (key));
}
1
2
3
4
5
6
7
对象如何找属性|方法;
查找规则:先在对象本身找 ===> 构造函数中找 ===> 对象原型中找 ===> 构造函数原型中找 ===> 对象上一层原型查找
习题1:
[1,2,3] === [1,2,3] //false
1
习题二

var obj1 = {

a:'hellow'

}
var obj2 = obj1;
obj2.a = 'world';
console.log(obj1); //{a:world}
(function(){

console.log(a);     //undefined
var a = 1;

})();
1
2
3
4
5
6
7
8
9
10
习题三:

var a = {}
var b = {

key:'a'

}
var c = {

key:'c'

}

a[b] = '123';
a[c] = '456';

console.log( a[b] ); // 456 (如果不理解,请遍历打印a的key)

8.JS作用域+this指向+原型考题

习题1:

function Foo(){

getName = function(){console.log(1)} //注意是全局的window.
return this;

}

Foo.getName = function(){console.log(2)}
Foo.prototype.getName = function(){console.log(3)}
var getName = function(){console.log(4)}
function getName(){

console.log(5)

}

Foo.getName(); //2
getName(); //4
Foo().getName(); //1
getName(); //1
new Foo().getName();//3

习题二:

var o = {

a:10,
b:{
    a:2,
    fn:function(){
        console.log( this.a ); // 2
        console.log( this );   //代表b对象
    }
}

}
o.b.fn();

习题三:

window.name = 'ByteDance';
function A(){

this.name = 123;

}
A.prototype.getA = function(){

console.log( this );
return this.name + 1;

}
let a = new A();
let funcA = a.getA;
funcA(); //this代表window

习题四

var length = 10;
function fn(){

return this.length + 1;

}
var obj = {

length:5,
test1:function(){
    return fn();
}

}
obj.test2 = fn;
console.log( obj.test1() ); //1
console.log( fn()===obj.test2() ); //false
console.log( obj.test1() == obj.test2() ); //false

迷惑文章(闭包与原型链):
https://blog.csdn.net/m0_65912225/article/details/124120364

9.JS判断变量是不是数组的多个方法

方法一:Array的isArray方法
var arr = [1,2,3];
console.log( Array.isArray( arr ) );
1
2
方法二 数组对象的构造函一定是Array
var a = {a:1,b:2}
console.log(a.constructor);
var b = [1,2]
console.log(b.constructor === Array);
1
2
3
4
方法三 instanceof (可写可不写)
运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

var arr = [1,2,3];
console.log( arr instanceof Array );
1
2
方法四 原型prototype 使该对象拥有object的toString方法
var arr = [1,2,3];
console.log( Object.prototype.toString.call(arr).indexOf('Array') > -1 );
1
2
方法五 isPrototypeOf()

   var arr = [1,2,3];
console.log(  Array.prototype.isPrototypeOf(arr) )
//下述代码是上述代码的解释
var arr  = [1,2]
console.log(Array.prototype);
console.log(arr.__proto__);

1
2
3
4
5
6
7

10.slice是干嘛的 splice是否会改变原数组

slice(start,end)
slice是来截取的
参数可以写slice(3)、slice(1,3)、slice(-3)
返回的是一个新的数组

splice 功能有:插入、删除、替换
返回:删除的元素
该方法会改变原数组

11.JS数组去重

方式一:new set(arr)

var arr1 = [5,1,2,3,2,4,1];
console.log(...new Set(arr1));
1
2
方式二:indexOf

var arr2 = [1,2,3,2,4,1];
function unique( arr ){

var brr = [];
for( var i=0;i<arr.length;i++){
    if(  brr.indexOf(arr[i]) == -1 ){
        brr.push( arr[i] );
    }
}
return brr;

}
console.log( unique(arr2) );
1
2
3
4
5
6
7
8
9
10
11
方式三:sort

var arr3 = [1,2,3,2,4,1];
function unique( arr ){

arr = arr.sort();
var brr = [];
for(var i=0;i<arr.length;i++){
    if( arr[i] !== arr[i-1]){
        brr.push( arr[i] );
    }
}
return brr;

}
console.log( unique(arr3) );

方式四:splice

var arr = [1, 1, 8, 8, 12, 12, 15, 15, 16, 16];

function unlink(arr) {

for (var i = 0; i < arr.length; i++) {    // 首次遍历数组
    for (var j = i + 1; j < arr.length; j++) {   // 再次遍历数组
        if (arr[i] == arr[j]) {          // 判断连个值是否相等
            arr.splice(j, 1);           // 相等删除后者
            j--;
        }
    }
}
return arr

}
console.log(unlink(arr));
1
2
3
4
5
6
7
8
9
10
11
12
13
14

12.找出多维数组最大值

function fnArr(arr){

var newArr = [];
arr.forEach((item,index)=>{
    newArr.push( Math.max(...item)  )
})
return newArr;

}
console.log(fnArr([

[4,5,1,3],
[13,27,18,26],
[32,35,37,39],
[1000,1001,857,1]

]));

13.给字符串新增方法实现功能

给字符串对象定义一个addPrefix函数,当传入一个字符串str时,它会返回新的带有指定前缀的字符串,例如:
console.log( ‘world’.addPrefix(‘hello’) ) 控制台会输出helloworld

解答:
String.prototype.addPrefix = function(str){

return str  + this;

}
console.log( 'world'.addPrefix('hello') )

14.找出字符串出现最多次的数字var str = ‘aaabbbbbccddddddddddx’;

var obj = {};
for(var i=0;i<str.length;i++){

var char = str.charAt(i);
if( obj[char] ){
    obj[char]++;
}else{
    obj[char] = 1;
}

}
console.log( obj );
//统计出来最大值
var max = 0;
for( var key in obj ){

if( max < obj[key] ){
    max = obj[key];
}

}
//拿最大值去对比
for( var key in obj ){

if( obj[key] == max ){
    console.log('最多的字符是'+key);
    console.log('出现的次数是'+max);
}

}

15.new 操作符具体做了什么

  1. 创建了一个空的对象
  2. 将空对象的原型,指向于构造函数的原型
  3. 将空对象作为构造函数的上下文(改变this指向)
  4. 对构造函数有返回值的处理判断
    1
    2
    3
    4
    function Fun( age,name ){
    this.age = age;
    this.name = name;
    }
    function create( fn , ...args ){
    //1. 创建了一个空的对象
    var obj = {}; //var obj = Object.create({})
    //2. 将空对象的原型,指向于构造函数的原型
    Object.setPrototypeOf(obj,fn.prototype);
    //3. 将空对象作为构造函数的上下文(改变this指向)
    var result = fn.apply(obj,args);
    //4. 对构造函数有返回值的处理判断
    return result instanceof Object ? result : obj;
    }
    console.log( create(Fun,18,'张三') )

16.闭包

什么是闭包
概念·:一个函数(函数内的函数)和他的周围状态(函数内函数的环境)的引用捆绑在一起的组合(晦涩)
理解:使外部能够访问函数内的自由变量

两个闭包的体现
案列一:将函数作为返回值

function fun(){

  var count = 1;
    return function (){//匿名函数
      console.log("count" + count);  //匿名函数内并没有定义count,所以会去上级作用域内查找,此时所要查找的是匿名函数定义                                 位置作用域,还是匿名函数执行位置外作用域??
    }
} 
var fun2 = fun(); //将返回值保存至fun2
var count = 3 ;
fun2 ();  // 1 由此可得知,count这个自由变量会向上一层 去定义时函数作用域内查找此变量的值,而不是执行函数的作用域内查找。

1
2
3
4
5
6
7
8
9
解析:
案列二:函数作为参数

function fun(fun2){

  var count = 1;
  fun2();  
} 
var count = 3 ;
function fun2(){        //当执行fun2这个函数时 count 是自由变量
console.log("count" + count);  //也是输出定义是作用域内变量
}
fun(fun2);

1
2
3
4
5
6
7
8
9
习题
var n = 1;

function fun1() {
  test = 10;
  var n = 999;
  nAdd = function () {
    n += 1;
    console.log(n);
  }

  function fun2() {
    console.log(n);
  }
  return fun2;
}
var result = fun1();
result();    //999
console.log(test);   //10
console.log(n);   //1
nAdd();   //1000
result();//1000

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
实际应用场景
检测一个接口的调用次数
使用闭包

function fun(){

  var count = 0;
  return function func(){
    count++;
    // 接口的操作
    console.log(count);//调用接口的次数
  }
}
var fun2  = fun()
fun2()

17.原型链

原型链可以解决什么问题?
对象共享属性和共享方法

谁有原型
函数拥有:prototype
对象拥有:proto

对象查找属性或者方法的顺序
先在对象本身查找 --> 构造函数中查找 --> 对象的原型 --> 构造函数的原型中 --> 当前原型的原型中查找

原型链
是什么?:就是把原型串联起来
原型链的顶端是null
var a = {}
console.log(a.prototype); //显示原型
console.log(a.__proto__); //underfined
function Person(name) {

this.name = name

}
console.log(Person.__proto__) //隐式原型
console.log(Person.prototype) //显示原型
1
2
3
4
5
6
7
8

18.JS继承有哪些方式

方式一:

class Parent{

constructor(){
    this.age = 18;
}

}

class Child extends Parent{

constructor(){
    super();
    this.name = '张三';
}

}
let o1 = new Child();
console.log( o1,o1.name,o1.age );
1
2
3
4
5
6
7
8
9
10
11
12
13
14
方式二:原型链继承

function Parent(){

this.age = 20;

}
function Child(){

this.name = '张三'

}
Child.prototype = new Parent();
let o2 = new Child();
console.log( o2,o2.name,o2.age );
1
2
3
4
5
6
7
8
9
方式三:借用构造函数继承

function Parent(){

this.age = 22;

}
function Child(){

this.name = '张三'
Parent.call(this);

}
let o3 = new Child();
console.log( o3,o3.name,o3.age );
1
2
3
4
5
6
7
8
9

19.说一下call 与 apply bind的 区别

共同点 功能一致
可以改变this指向

区别
call apply可以立即执行。bind不会立即执行,因为bind返回的是一个函数需要调用
参数不同:apply第二个参数是数组。call和bind有多个参数需要挨个写。
场景

  1. 用apply的情况
    var arr1 = [1,2,4,5,7,3,321];
    console.log( Math.max.apply(null,arr1) )
  2. 用bind的情况
    var btn = document.getElementById('btn');
    var h1s = document.getElementById('h1s');
    btn.onclick = function(){
    console.log( this.id );
    }.bind(h1s)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    20.sort背后原理是什么
    V8引擎sort 函数只给出了两种排序 InsertionSort 和 QuickSort,数量小于10的数组使用InsertionSort,比10大的数组则使用 QuickSort。
    之前的版本是:插入排序和快排,现在是冒泡

原理实现链接:https://github.com/v8/v8/blob/ad82a40509c5b5b4680d4299c8f08d6c6d31af3c/src/js/array.js
710行代码开始
1
2
21.深拷贝与浅拷贝
共同点
复制

浅拷贝
只复制引用,而未复制真正的值。

var arr1 = ['a','b','c','d'];
var arr2 = arr1;

var obj1 = {a:1,b:2}
var obj2 = Object.assign(obj1);
1
2
3
4
5
深拷贝
概念复制真正的值
var obj3 = {

a:1,
b:2

}
var obj4 = JSON.parse(JSON.stringify( obj3 ));
1
2
3
4
5
递归方法

function copyObj( obj ){

if(  Array.isArray(obj)  ){
    var newObj = [];
}else{
    var newObj = {};
}
for( var key in obj ){
    if( typeof obj[key] == 'object' ){
        newObj[key] = copyObj(obj[key]);
    }else{
        newObj[key] = obj[key];
    }
}
return newObj;

}
console.log( copyObj(obj5) );

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

22.localStorage、sessionStorage、cookie的区别

共同点:在客户端存放数据
不同点:

存放数据的有效期 sessionStorage:进当前浏览器窗口关闭之前有效,关闭后就消失。 loaclStorage:始终有效,窗口或浏览器关闭也一直保存,所以叫做持久化存储
localStorage、sessionStorage不可以设置过期时间
cookie 有过期时间,可以设置过期(把时间调整到之前的时间,就过期了)
存储大小的限制
cookie存储量不能超过4k
localStorage、sessionStorage不能超过5M
**根据不同的浏览器存储的大小是不同的。

23.var let const的区别

共同点:var const let 都是可以声明变量的
不同点:
区别一:
var 具有变量提升机制
let 和 const 没有变量提升机制

区别二:
var 可以多次声明
let 和 const 不可以多次声明同一个变量

区别三:
var let 声名变量的
const 声明常量
var 和 let 声明的变量可以再次赋值,但是const不可以再次赋值了。

区别四
var 声明的变量没有自身作用域
let与const声明的变量有自身的作用域

24.作用域考题

考题一:let和const没有变量提升性
console.log( str );//undefined
var str = '你好';

console.log( num );//报错
let num = 10;
1
2
3
4
5
考题二:
function demo(){

var n = 2;
if( true ){
    var n = 1;
}
console.log( n );//1

}
demo();

function demo(){

let n = 2;
if( true ){
    let n = 1;
}
console.log( n );//2

}
demo();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
考题三:可以修改
const obj = {

a:1

}
obj.a = 11111;
console.log( obj )

const arr = ['a','b','c'];
arr[0]= 'aaaaa';
console.log( arr );
1
2
3
4
5
6
7
8
9

25.将下列对象进行合并

方式一:Object.assign
const a = {a:3,b:4},
const b = {b:2,c:4}
let obj = Object.assigon(a,b);
console.log(obj)
1
2
3
4
方式二:扩展运算符
let obj1 = {...a,...b}
console.log(obj1)
1
2
方式三:自己封装的方法
function extend( target, source ){

for(var key in source){
    target[key] = source[key];
}
return target;

}
console.log( extend(a,b) );

26.箭头函数和普通函数区别

1、外形不同:箭头函数使用箭头定义,普通函数中没有。
2、 箭头函数全都是匿名函数:普通函数可以有匿名函数,也可以有具名函数
3、箭头函数不能用于构造函数:普通函数可以用于构造函数,以此创建对象实例。
4、箭头函数中 this 的指向不同:在普通函数中,this 总是指向调用它的对象,如果用作构造函数,它指向创建的对象实例。
5、箭头函数不具有 arguments 对象:每一个普通函数调用后都具有一个
arguments 对象,用来存储实际传递的参数。但是箭头函数并没有此对象。
6、其他区别:箭头函数不具有 prototype 原型对象。箭头函数不具有 super。
7、箭头函数不能new(不能当作构造函数)
1
2
3
4
5
6
7
8

27.Promise有几种状态

有三种状态:
pending(进行中)
fulfilled(已成功)
rejected(已失败)
1
2
3
4

28.find与 fitter的区别

区别一:返回的内容不同

filter 返回是新数组
find   返回具体的内容

区别二:

find :匹配到第一个即返回
filter : 返回整体(没一个匹配到的都返回)

1
2
3
4
5
6

29.some与 every 的区别

some ==》 如果有一项匹配则返回true
every ==》 全部匹配才会返回true

]]>
0 https://ilnafz.cn/index.php/archives/25/#comments https://ilnafz.cn/index.php/feed/
maxfo —— 回顾一下大二学习java时写的rgb游戏 https://ilnafz.cn/index.php/archives/18/ https://ilnafz.cn/index.php/archives/18/ Tue, 27 Dec 2022 23:21:00 +0800 [scode type="blue"]大二暑假期间看完java紫皮书上的知识点后,陆续模拟了贪吃蛇,和飞机大战小游戏后,开始了自己的java小游戏制作。[/scode]

video

视频
[vplayer url="http://fan.ilnafz.cn/%E5%B0%8F%E6%B8%B8%E6%88%8F/maxfo.mp4" pic=" http://fan.ilnafz.cn/%E5%B0%8F%E6%B8%B8%E6%88%8F/05.jpg" /]

工具

编辑:idea
绘图:PS RGB-Maker

主要逻辑

通过所有管卡,收集五把通关钥匙(!不解救公主系列)

整体思路

1.构建窗体
2.开启线程(刷新窗体,双缓冲解决闪烁,后续会引进帧的概念)
2.角色生成(让角色动起来)
3.碰撞检测(与碰撞物做测试)
4.发射子弹(子弹的创建与销毁)
5.敌人出场 (敌人的创建与销毁)
6.绘制地图 地图读取(动态添加障碍物)
7.地图卷动(地图上可见物品都要卷起来)
8.丰富功能:技能(技能特效,特效类对象的创建与销毁),装备(装备拾取,装备装备),道具(消耗品,拾取与使用),商店(购买物品)
9.丰富逻辑:怪物生成条件,进入下一关卡条件,胜利条件
10.游戏优化:(多对象会造成游戏卡顿)

所用知识

  1. 窗体类
  2. 线程
  3. 面向对象
  4. 对象的创建与销毁
  5. 碰撞检测
  6. 地图卷动
  7. 数组
  8. 集合
  9. 键盘类
  10. 其他

游戏目录结构

在这里插入图片描述

背景地图绘制

底板层(草地+墙体)

RGB-maker 工具完成(不需要地图的跳过)

在这里插入图片描述

修饰层(花草+树+标记)

使用ps对底板进行装饰

在这里插入图片描述

修饰层(树)

ps绘制出单一树的图层

在这里插入图片描述

最终效果

注意人物位置

在这里插入图片描述

障碍物生成

map.txt 文件(1为碰撞物,0为可移动区域)

在这里插入图片描述

文件读写(如下图所示,红色方块为碰撞物)

//调用地图读取
{
       try {
           createSquare();
       } catch (Exception e) {
           e.printStackTrace();
       }
 }
 public void readGetMap() throws Exception {
        FileInputStream fis = new FileInputStream("src/map.txt");
        InputStreamReader isr = new InputStreamReader(fis);
        BufferedReader br = new BufferedReader(isr);

        for (String value = br.readLine(); value != null; value = br.readLine()) {
            lineData.add(value);
        }
        // 关闭输入流
        br.close();
        //  确定行高
        int row = lineData.size();
        int cloum = 0;
        // 读取一行 确定列数
        for (int i = 0; i < 1; i++) {
            String str = lineData.get(i);
            //用 , 分割 str 字符串,并且将分割后的字符串数组赋值给 buff。
            String[] values = str.split(",");
            cloum = values.length;
        }
        //行列清楚表明
        originData = new int[row][cloum];
        //将读到的字符转换为整数,并赋值给二维数组map
        for (int i = 0; i < lineData.size(); i++) {
            String str = lineData.get(i);
            String[] values = str.split(",");
            for (int j = 0; j < values.length; j++) {
                originData[i][j] = Integer.parseInt(values[j]);
            }
        }
    }
    //地图矩形绘制
    public void createSquare() throws Exception {
        // 读取地图
        readGetMap();
        for (int i = 0; i < originData.length; i++) {
            for (int j = 0; j < originData[0].length; j++) {
                if (originData[i][j] == 1) {
                    wallList.add(new Square(x + j * 32, y + 32 * i, 32, 32, rectangle));
                }
            }
        }
    }

地图卷动与碰撞检测

地图卷动

卷动对象:背景图,墙体,道具(商店,技能栏,工具栏,掉落物品) 血条 敌人 子弹 子弹爆炸特效
卷动逻辑:以角色为参照物,所有物品参照角色位置进行卷动,下图是禁止标记卷动失败的案例(卷动方向错误导致禁止对象位置出现了偏差)

碰撞检测

1.静态物体碰撞检测
静态碰撞检测处理移动中角色与各个静态道具的碰撞检测(如上图红色为墙体,角色与墙体之间的碰撞检测)
2.动态物体碰撞检测
主要解决移动中角色与移动中怪物的碰撞检测如何处理(怪物与角色相向而行的情况)

详细内容查看代码

对象的生成与销毁

GameObject类(核心)
游戏中所有的物体的父类(如下图)

package com.fan.games;

import java.awt.*;
import java.awt.Image;

/**
 * 游戏物体
 */
public class GameObject {
    // 坐标
    int x, y;
    //宽跟高
    int width, height;
    // 图像
    Image img;
    //游戏物体默认方向
    public Direction direction;
    // 矩形
    Rectangle rectangle;

    /**
     * 构造方法
     */
    public GameObject() {
    }

    public GameObject(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public GameObject(int x, int y, Direction direction) {
        this.x = x;
        this.y = y;
        this.direction = direction;
    }

    public GameObject(int x, int y, Image img) {
        this.x = x;
        this.y = y;
        this.img = img;
    }
    //墙体
    public GameObject(int x, int y, int width, int height, Rectangle rectangle) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        this.rectangle = rectangle;
    }
    //掉落道具
    public GameObject(int x, int y, int width, int height,double hp,double mp, Image img,Rectangle rectangle) {
        this.x = x;
        this.y = y;
        this.img=img;
        this.width = width;
        this.height = height;
        this.rectangle = rectangle;
    }
    public GameObject(int x, int y, int width, int height, Rectangle rectangle, Direction direction) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        this.rectangle = rectangle;
        this.direction = direction;
    }

    //拥有默认方向的构造函数
    public GameObject(int x, int y, int width, int height, Image img, Rectangle rectangle,Direction direction) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        this.img = img;
        this.rectangle = rectangle;
        this.direction = direction;
}

    //物体矩形获取
    public Rectangle getRect() {
         return new Rectangle( x,  y, width, height);
    }
    //修改这个矩形物体
    public void setRect(int x , int y,int width , int height) {
        this.rectangle = new Rectangle( x, y, width, height);
    }
    /**
     * get/set
     * @return
     */
    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return width;
    }

    public Image getImg() {
        return img;
    }

    public Rectangle getRectangle() {
        return rectangle;
    }

    public void setX(int x) {
        this.x = x;
    }

    public void setY(int y) {
        this.y = y;
    }

    public void setWidth(){

    }
    public void setHeight(){

    }
    public void setImg(Image img) {
        this.img = img;
    }

    public void setRectangle(Rectangle rectangle) {
        this.rectangle = rectangle;
    }

    /**
     * 绘制
     *
     * @param g
     */
    public void drawSelf(Graphics g) {
        g.drawImage(img, x, y, null);
    }

}

主要对象的创建与销毁逻辑(核心)
静态资源:开始创建,结束销毁
动态资源
1.角色:开始时生成,血量为零时销毁,并游戏结束。
2.敌人 :开始时生成第一波敌人,通关关卡后,下一个管卡的怪物会被创建,以此类推。怪物血量为零时销毁
3.消耗品:开始时创建与野怪死亡时创建,使用后销毁。
4.子弹:
角色:创建{角色按下攻击键创建} 销毁{1.达到一定距离 2.碰撞敌人 3.碰撞墙体}
敌人:创建{根据敌人状态随机创建} 销毁{1.碰撞墙体 2.碰撞角色}
5.打击效果:碰撞时创建,短时间自动销毁
6.持续伤害效果:碰撞时创建,受击者死亡销毁,短时间内自动销毁。

游戏一览

开始界面

在这里插入图片描述

关卡2

在这里插入图片描述

死亡

在这里插入图片描述

关于游戏

游戏名称:maxfo(我个人的游戏id名称)
制作周期:40天(基础知识18天,制作22天)
背景音乐:Hawaiian Sun(light version)
备选音乐:ひやむぎ、そーめん、时々うどん
绘图工具:PS RGB-maker
剪辑工具:剪映
代码已经上传至github(sunshine-fan/game-maxfo: 大二暑假学习java时开发的一款小游戏 (github.com)加粗文字

]]>
0 https://ilnafz.cn/index.php/archives/18/#comments https://ilnafz.cn/index.php/feed/
测试样式 https://ilnafz.cn/index.php/archives/13/ https://ilnafz.cn/index.php/archives/13/ Tue, 27 Dec 2022 20:38:00 +0800 !> 1

i> 2

@> 3

x> 4

√> 5

]]>
0 https://ilnafz.cn/index.php/archives/13/#comments https://ilnafz.cn/index.php/feed/
Typora+PicGo+七牛云图床实现图片上传存储 https://ilnafz.cn/index.php/archives/10/ https://ilnafz.cn/index.php/archives/10/ Mon, 26 Dec 2022 09:44:00 +0800 概述:

近期在对一些博文进行迁移时,发现有大量的图片需要存储至服务器。大量的图片资源在前台同时展示时,无疑是对服务器增加了的压力,需要一个专门储存图片的服务器存储图片,加之平时笔记会第一时间写入typora,后期修改后上传文章至后台。
typora文章中的图片存储路径为本地,使得图片无法正常迁移(补充:在typora上传图片后,默认为本地的一个地址,当我们拷贝整篇幅文章时并发表时,图片的访问路径因为是本地,在浏览器上无法正常显示,我们需要在typora中上传图片默认上传至服务器,并通过服务器地址的形式访问图片),下面我们具体讲解如何配置。

图床是什么?

了解七牛云图床之前,我们需要先了解一下图床是什么,图床就是把图片上传到一个专门储存图片的服务器,可以用外链网址直接访问到,neng'go有效减少我们自己站点服务器的资源,市面上图床比较多,免费和收费的都有,这里我选择使用的是七牛云图床。

为什么使用七牛云图床?

文章中的图片是存储在自己的服务器上的并进行访问的,当大量的图片资源需要在前台展示时间无疑增加了服务器的压力。

图床使用

  1. 账号注册:七牛云 | 一站式场景化智能视频云 (qiniu.com)
  2. 创建存储服务:你注册完一个七牛云的账号,登录到控制台,然后进入对象存储子菜单空间管理并创建存储服务(新建空间),本存储空间是用于站点图床,所以选择访问控制类型为公开空间,如下所示,我这里选择的是公开,区域随意。

image-20221226090629254

  1. 存储服务基本信息:进入该存储服务,可以看到下图

image-20221226090102338

CDN测试域名:七牛提供的测试域名,只能使用30天。 后面需要添加已备案的域名。

CDN加速域名:这里需要添加自己的域名(lf.ilnafz.cn)并作出配置。

  1. 配置存储服务

存储服务创建完成后,需要配置一个融合CDN域名,融合CDN域名简单来说就是指资源对象的外链域名,七牛云提供了融合CDN的测试域名,官方提示为:七牛融合 CDN 测试域名(使用CDN测试域名后的融合域名http://rnc8vzscy.bkt.clouddn.com/%E5%B0%8F%E6%B8%B8%E6%88%8F/01.jpg),每个域名每日限总流量 10GB,每个测试域名自创建起 30 个自然日后系统会自动回收,仅供测试使用并且不支持 Https 访问。因此需要我们自己配置一个CDN加速域名,以本站点为例,控制台中点击创建域名,域名类型选择普通域名,其他配置默认或根据需求优化配置即可。这里域名我填的是我已经备案的域名的二级子域名lf.ilnafz.cn

  1. 配置域名解析

配置完成后跳转到域名管理界面,显示了一个cname记录,这就是配置好七牛云存储提供的的cdn加速域名,我的域名服务器是阿里云,所以要在阿里云域名解析那配置一条记录,这里配置的lf.ilnafz.cn域名和对应的cname记录需要
[scode type="blue"]配置域名的 CNAME:(https://developer.qiniu.com/fusion/kb/1322/how-to-configure-cname-domain-name)[/scode]

PicGo

PicGo是什么?

PicGo是一个用于快速上传图片并获取图片 URL 链接的工具,Typora笔记软件内置了此工具

img

PicGo软件下载

下载地址:Releases · Molunerfinn/PicGo (github.com)

fc7ac43903af5b2005644c1e9bc730b8

Typora+PicGo+七牛云

[scode type="green"]利用编辑器Typora和图床工具PicGo可实现Markdown文件使用七牛云图床图片,具体步骤如下[/scode]

1. 在PicGo中配置七牛云

image-20221226092351803

在这里AccessKeySecretKey是访问和操作对象存储的密钥对,这个在七牛云个人中心里

关于存储区域:存储区域_产品简介_对象存储 - 七牛开发者中心 (qiniu.com),这里我的存储区域为华东浙江2,所以填写cn-east-2

2. 在Typora 配置PicGo

打开Typora 软件文件菜单中的偏好设置,点击图像选项,如下是我个人的配置

image-20221226093024605

验证图片上传选项(如下是我点击验证图片上传选项后的提示框,上传图片后右下角落会出现提示信息)

image-20221226093121272

最后结果

当我们将图片拉入typora中后发现图片的路径变为了七牛云图床的融合图片路径,我们可通过复制此链接在浏览器访问该图片,同样也可以看到右下角出现了上传成功的提示框

image-20221226093345578

]]>
0 https://ilnafz.cn/index.php/archives/10/#comments https://ilnafz.cn/index.php/feed/
vue3+vite 在html文件中添加环境变量 https://ilnafz.cn/index.php/archives/21/ https://ilnafz.cn/index.php/archives/21/ Thu, 14 Jul 2022 10:13:00 +0800 vue3+vite 在html文件中添加环境变量

这里我项目中定义的变量 以 VITE开头
根目录.env.development文件下加入

VITE_MAP_SERVER = http://xxx.xxx.xxx.xxx:xxxx/_AMapService

根目录创建static.env.config.js文件
使用import.meta.env 赋值给window上

window._AMapSecurityConfig = {
 serviceHost: import.meta.env.VITE_MAP_SERVER,
};

最重要的一步在 vite.config.ts中进行配置

import { ConfigEnv, loadEnv } from 'vite';
import { createHtmlPlugin } from 'vite-plugin-html';
//import.meta.env  
export default ({ mode }: ConfigEnv) => {
  //环境变量
  const env = loadEnv(mode, process.cwd());
  return {
    plugins: [
      legacyPlugin({
        targets: [
          'Android > 39',
          'Chrome >= 60',
          'Safari >= 10.1',
          'iOS >= 10.3',
          'Firefox >= 54',
          'Edge >= 15',
        ],
      }),
      vue(),
      createHtmlPlugin({
        inject: {
          data: {
            ...env,
            injectScript: `<script type="module" src="./static.env.config.js"></script>`,
          },
        },
      }),
    ],
   
  };
};

然后将 插值到index.html根目录中

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <meta
      content="xxx"
      name="keywords" />
    <link rel="icon" href="group.ico"/>
    <title></title>
  </head>

  <body>
    <div id="app"></div>
    <%- injectScript %>
    <script type="module" src="./src/main.ts"></script>
  </body>
</html>

打开浏览器控制台可以看到生成的 script标签

请添加图片描述

打印window对象可以看到_AMapSecurityConfig属性

]]>
0 https://ilnafz.cn/index.php/archives/21/#comments https://ilnafz.cn/index.php/feed/
在vue3项目中引入ts过程记录 https://ilnafz.cn/index.php/archives/23/ https://ilnafz.cn/index.php/archives/23/ Sun, 03 Jul 2022 23:48:00 +0800 1.基于脚手架创建vue3项目

创建项目

vue create vue3-ts

选择自定义预设,ts设置未选中状态

在这里插入图片描述

选择yarn与npm启动项目(根据个人,在这里我选择yarn)

2.在页面中(HomeView.vue)引入ts

问题一:

在这里插入图片描述

问题二:

在这里插入图片描述

问题一解决

在script 标签中引入ts后,会产生JSX语法错误,这时我们需要引入ts(全局引用与局部引用)
第一步:
全局或者局部 引用Ts并通过tsc -v 查看版本号 ts安装成功,这里我直接在项目中 yarn add typescript

npm install -g typescript
yarn add -g typescript(需要添加环境变量后 tsc-v生效)

在这里插入图片描述

注意:使用yarn全局引入后 需要配置环境变量后 tsc-v才生效

第二步
命令生成ts配置文件tsconfig.json

tsc --init  //生成 tsconfig.json

用以下内容填充该文件,如有重复,删除此文件内的重复项

{
    "compilerOptions": {
        "outDir": "./built/",
        "sourceMap": true,
        "strict": true,
        "noImplicitReturns": true,
        "module": "es2015",
        "moduleResolution": "node",
        "target": "es5"
    },
    "include": [
        "./src/**/*"
    ]
}

在 tsconfig.json 文件中设置 “jsx”:“preserve”,设置后发现还是报错,(这里设置无效,如果设置有效下一步可以略过,继续百度)
第三步:在jsconfig.json 文件下添加与 jsx(jsx 属性允许我们在项目中使用 .tsx 文件)

在这里插入图片描述

解决问题二 (找不到模块“XXX.vue”或其相应的类型声明)

在根目录中创建 shims.d.ts文件

declare module '*.vue' {
    import { ComponentOptions } from 'vue'
    const componentOptions: ComponentOptions
    export default componentOptions
}
declare module '*'

我们也可以修改tsconfig.json 中替换 declare module ‘*’ 如下所示

"noImplicitAny": false,
"allowJs": true,

尝试打开项目会发现如下报错

在这里插入图片描述

解决方案
安装我们的依赖 确保安装了 TypeScript、Webpack、Vue 和必要的加载程序。

安装@vue/cli-plugin-typescript

@vue/cli-plugin-typescript内部预置了ts-loader的配置,无需单独配置(这一步是不是覆盖掉)

yarn add  @vue/cli-plugin-typescript --save-dev

安装完毕后 尝试打开项目

在这里插入图片描述

我们将 main.js 改为 main.ts {构建成功}

在这里插入图片描述

我们尝试注释掉 shims.d.ts 中的 declare module ‘*’ 或者 替代他的 tsconfig文件中的 “noImplicitAny”: false, “allowJs”: true,

再次编译项目会发现引入的router与store 没有相应的内置类型文件,当然main.js不会出现此问题(个人理解)

在这里插入图片描述

修改回来 tsconfig.json shims.d.ts我们进行下一步

3.配置vue3+ts项目

我们可以看到在模块文件中产生分号报错,但是他并不会影响项目启动,我们对typescript-eslint进行配置

在这里插入图片描述

yarn add  @typescript-eslint/eslint-plugin @typescript-eslint/parser --save-dev
yarn add  @vue/eslint-config-typescript --save-dev

修改.eslintrc.js

module.exports = {
    extends: [
        'plugin:vue/vue3-essential',
        'eslint:recommended',
        '@vue/typescript/recommended'
    ],
    plugins: [
        'vue',
        '@typescript-eslint'
    ],
    rules: {
        '@typescript-eslint/no-unused-vars': 'error',
        // 允许非空断言
        '@typescript-eslint/no-non-null-assertion': 'off',
        // 允许自定义模块和命名空间
        '@typescript-eslint/no-namespace': 'off',
        // 允许对this设置alias
        '@typescript-eslint/no-this-alias': 'off',
        // 允许使用any类型
        '@typescript-eslint/no-explicit-any': ['off'],
        ......
    }
}

错误解决

在这里插入图片描述

项目中会出现的其他错误

在这里插入图片描述

解决:在.eslintrc.js文件中添加以下代码

在这里插入图片描述

4.其他配置

vueconfig.js配置 增加extension,引入 ts/tsx 文件时不必加后缀

module.exports = {
    chainWebpack: config => {
          config.resolve.extensions
            .add('ts')
            .add('tsx');
    }
}

在vscode中command+,打开settiings.json可配置保存时自动eslint格式化

在这里插入图片描述

5.在HomeView.vue 使用Ts语法

<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png" />
    <HelloWorld msg="Welcome to Your Vue.js App" />
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import HelloWorld from "@/components/HelloWorld.vue";
interface Todo {
  id: number;
  title: string;
  isCompleted: boolean;
}
const aa = ref<Todo[]>([])
console.log(aa);
const bb:boolean = false
console.log(bb);
</script>

到这里就引入成功了

]]>
0 https://ilnafz.cn/index.php/archives/23/#comments https://ilnafz.cn/index.php/feed/
JAVA实现10种排序 https://ilnafz.cn/index.php/archives/26/ https://ilnafz.cn/index.php/archives/26/ Tue, 24 May 2022 16:42:00 +0800 动图来源:

visualgo :一款可视化学习算法的工具

参考链接
参考链接

1.冒泡排序

在这里插入图片描述

package 排序;
import java.util.Arrays;

public class 冒泡排序 {
    public static void main(String[] args) {
        int [] arr = new int[]{7,8,9,2,1,5,9,4,4,5,6};
        bubbleSort(arr);
        System.out.println(Arrays.toString(arr));
    }
    public static void bubbleSort(int[] arr){
        for (int i = 0 ; i<arr.length-1;i++){
            for (int j= 0;j<arr.length-1-i;j++){
                if (arr[j]>arr[j+1]){
                    int temp = arr[j];
                    arr[j]= arr[j+1];
                    arr[j+1] = temp;
                }
            }
        }
    }
}

2.选择排序

在这里插入图片描述

package 排序;
import java.util.Arrays;

public class 选择排序 {
    public static void main(String[] args) {
        int [] arr = new int[]{7,8,9,2,1,5,9,4,4,5,6};
        for (int i=0;i<arr.length-1;i++){
            int index = i;//需要比较的数 arr[index]
            System.out.println(i);
            for (int j=i+1;j<arr.length;j++){
                if (arr[j]<arr[index]){
                    index= j;//保存最小元素的小标,成为带比较的数字
                }
            }
            //找到最小值,将其放在本轮比较的开头,进行下一遍循环。
            int temp = arr[index];
            arr[index]=arr[i];
            arr[i]  = temp;
        }
        System.out.println(Arrays.toString(arr));
    }
}

3.插入排序

在这里插入图片描述

package 排序;

import java.util.Arrays;

public class 插入排序 {
    public static void main(String[] args) {
        int [] arr = new int []{3,44,38,5,47,15,36,26,27,2,46,4,19,50,48};
        for (int i = 0;i<arr.length;i++){
            int Instertvalue  = arr[i];
            int Instertindex = i-1;
            while(Instertindex>=0&&Instertvalue<arr[Instertindex]){
                arr[Instertindex+1]=arr[Instertindex];
                Instertindex--;
//                System.out.println(Instertindex);
            }
            arr[Instertindex+1]=Instertvalue;
        };
        System.out.println(Arrays.toString(arr));
    }
}

4.希尔排序

在这里插入图片描述

package 排序;
import java.util.Arrays;

public class 希尔排序
{
    public static void main(String[] args) {
        int[] arr = new int[]{3,5,2,7,8,1,2,0,4,7,4,3,8};
        shellSort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void shellSort(int[] arr) {
        //遍历所有的步长,决定循环轮次
        for (int d=arr.length/2;d>0;d/=2){
            //遍历所有元素
            for (int i = d; i<arr.length;i++){
                //遍历本组中的所有元素
                for (int j=i-d;j>=0;j-=d){
                    //如果当前元素大于加上步长后的那个元素
                    if (arr[j]>arr[j+d]){
                        int temp = arr[j];
                        arr[j] = arr[j+d];
                        arr[j+d] = temp;
                    }
                }
            }
        }
    }
}

5.快速排序

参考博客

package 排序;

import java.util.Arrays;

public class 快速排序 {
    public static void main(String[] args) {
        int[] arr = new int[]{4,7,6,7,8};
        System.out.println(Arrays.toString(arr));
        quickSort(arr,0,arr.length-1);
        System.out.println("改变后"+Arrays.toString(arr));
    }
    /**
     *
     * @param arr    排序数组
     * @param start  开始位置
     * @param end    结束位置
     */
   public static void quickSort(int[] arr,int start,int end){
        //当开始位置>结束位置时间 进行递归(没有条件会一直执行,并且报错)
       if (start<end){
           //把数组中的 第0个数字座位基准数字
           int stard = arr[start];
           //记录需要排序的下标
           int low = start;
           int high= end;

           //循环找 比标准数大的数 和 比标准数小的数
           while(low<high){
               //右边的数字比标准数大
               while(low<high&&stard<=arr[high]){
                   high--;
               }
               //使用右边的数字 替换 左边的数字
               arr[low] = arr[high];

               //如果左边的数字比标准书小
               while(low<high&&arr[low]<=stard){
                   low++;
               }
               arr[high] = arr[low];
           }
           arr[low] = stard;
           //处理所有小的数字
           //System.out.println(low);
           quickSort(arr,start,low);
          //quickSort(arr,start,low);
           //处理所有大的数字
           quickSort(arr,low+1,end);
       }
   }
}

6.归并排序

归并排序(英语:Merge sort,或Mergesort),是创建在归并操作上的一种有效的排序算法,其时间复杂度为O(N*logN)。

概念:

是利用递归与分治的技术将数据序列划分为越来越小的半子表,再对半子表排序,最后再用递归方法将排好序的半子表合并成越来越大的有序序列。

核心思想

将两个有序的数列合并成一个大的有序的序列。通过递归,层层合并,即为归并。**

在这里插入图片描述

过程展示:

在这里插入图片描述

package 排序;

import java.util.Arrays;

public class 归并排序 {
    public static void main(String[] args) {
        int[] arr = new int[]{1,3,5,2,4,6,8,10,9,7};
        System.out.println(Arrays.toString(arr));
        mergeSort(arr,0,arr.length-1);
        System.out.println(Arrays.toString(arr));
    }

    /**
     *
     * @param arr
     * @param start   开始下标
     * @param end     结束下标
     */
    public static void mergeSort(int[] arr, int start,int end) {
            //分治结束条件
        if (start>=end){
            return;
        }

        //两位数的中位数
            //通过位运算不会造成溢出
            int mid = start+((end-start)>>1);
            //处理左边
            mergeSort(arr,start,mid);
            //处理右边
            mergeSort(arr,mid+1,end);
            //归并
            merge(arr,start,mid,end);
    }

    public static void merge(int[] arr,int start,int mid,int end){
        //用于存储归并后的临时数组
        int [] temp = new int[end-start+1];
        //记录第一个数组中需要遍历的下标
        int i = start;
        //记录第二个数组中 需要遍历的下标
        int j = mid+1;
        //记录临时数组中存放的下标
        int index = 0;
        //遍历两个数组,取出小的数字放入临时数组中去
        while (i<=mid&&j<=end){
            if (arr[i]<=arr[j]){
                //把小的数据放入临时数组中去
                temp[index] = arr[i];
                //临时数组下标加1
                i++;
                index++;
            }else{
                temp[index] = arr[j];
                j++;
                index++;
            }
        }

        //处理多余的数据
        while (j<=end){
            temp[index] = arr[j];
            j++;
            index++;
        }
        while (i<=mid){
            temp[index] = arr[i];
            i++;
            index++;
        }
        //把临时数组中的数据重新存入原数组
        for (int k =0;k<temp.length;k++){
            //不同数组的不同开始位置  左侧数组开始位置:0  右侧数组开始位置
            arr[k+start] = temp[k];
        }
    }
}

7.堆排序

在这里插入图片描述

package 排序;

import java.util.Arrays;

public class 堆排序 {
    public static void main(String[] args) {
        int[] arr = new int[]{9,6,8,7,0,1,10,4,2,4,5,7};
        heapSort(arr);
        System.out.println(Arrays.toString(arr));
    }
    public static void heapSort(int[]arr){
        //开始位置是最后一个非叶子节点,即最后一个节点的父节点
        int start  = (arr.length-1)/2;
        //预设为大顶堆
        for (int i=start;i>=0;i--){
            maxHeap(arr,arr.length,i);
        }
        //先把数组中的第0个和堆中的最后一个数交换位置,再把前面的处理为大顶堆
        for (int i=arr.length-1;i>0;i--){
            int temp = arr[0];
            arr[0] = arr[i];
            arr[i] = temp;
            maxHeap(arr,i,0);
        }
    }
    /**
     *
     * @param arr
     * @param size  调整次数
     * @param index 子节点位置
     */
    public static void maxHeap(int [] arr,int size, int index) {
        //左子节点
        int leftNode = 2*index+1;
        //右子节点
        int rightNode = 2*index+2;
        //记录当前节点,对比
        int max = index;

        //与两个子节点相比,找出最大节点
        if (leftNode<size&&arr[leftNode]>arr[max]){
            max = leftNode;
        }
        if (rightNode<size&&arr[rightNode]>arr[max]){
            max = rightNode;
        }
        //交换位置
        if (max!=index){
            int temp = arr[index];
            arr[index] = arr[max];
            arr[max] = temp;
            //交换位置以后,可能会破坏之前排好的堆,之前排好的堆需要在重新调整:
            maxHeap(arr,size,max);
        }
    }
}

8.计数排序

排序思路

  1. 假设有N个数的数组Array,取值范围在(0~9)之间,所以初始化一个数组countArray大小为10(即最大值+1),然后全部初始化为0
  2. 遍历Array数组填充countArray数组。就是把Array数组的值作为countArray数组的下标然后+1
  3. 遍历countArray数组,把值按照顺序填回Array数组,最终输出结果即可

在这里插入图片描述

package 排序;
import java.util.Arrays;

public class 计数排序 {
    public static void main(String[] args) {
        int[] array = new int[]{0, 2, 4, 4, 1, 1, 6, 5, 8, 9, 9, 6};
        int[] sortedArray = countSort(array);
        System.out.println(Arrays.toString(sortedArray));
    }
    
    private static int[] countSort(int[] array) {
        //求Array 数组的最大值
        int max  = 0;
        for (int i = 0; i<array.length;i++){
            if (array[i]>max){
                max = array[i];
            }
        }
        //创建一个长度为max +1 的数组
        int [] countArray = new int[max+1];
        //计数排序
        for (int i = 0 ; i<array.length;i++){
            countArray[array[i]]++;
        }
        int index = 0;
        int[] sortedArray = new int [array.length];
        for (int i = 0 ; i<countArray.length;i++){
            for (int j=0;j<countArray[i];j++){
                sortedArray[index++] = i;
            }
        }
        return sortedArray;
    }

}

9.桶排序

在这里插入图片描述

import java.util.Arrays;

public class bucketSortTest {
    public static void bucketSort(int[] num) {

        // 遍历原始数组,找到数组中的最大值
        int max = num[0];
        for (int i = 0; i < num.length; i++) {
            if (num[i] > max) {
                max = num[i];
            }
        }

        // 创建一个下标为原始数组中最大值的桶数组,该桶数组的下标代表元素,该数组下标所对应的值代表这个值出现的次数
        
        int[] bucketArray = new int[max + 1];

        // 再次遍历原始数组,得到原数组中存在的各个元素,以及出现的次数
        for (int i = 0; i < num.length; i++) {
            bucketArray[num[i]]++;
        }

        // 遍历桶数组,外层循环从桶的第一位开始(即下表为零);内层循环遍历桶数组中下标为i的值出现的次数
        int index = 0;
        for (int i = 0; i < bucketArray.length; i++) {
            for (int j = 0; j < bucketArray[i]; j++) {
                num[index++] = i;
            }
        }
    }

    public static void main(String[] args) {
            int[] num=new int[] { 2,5,6,8,5,2,9,6};
            bucketSort(num);
            System.out.println(Arrays.toString(num));
              
    }
}

10.基数排序

在这里插入图片描述

package 排序;

import java.util.Arrays;

public class 基数排序 {
    public static void main(String[] args) {
        int [] arr = new int[]{23,6,189,45,9,287,56,1,798,34,65,652,5};
        radixSort(arr);
        System.out.println(Arrays.toString(arr));
    }
    public static void radixSort(int[] arr) {
        //存储数组中最大数字
        int max = Integer.MIN_VALUE;
        for (int i=0;i<arr.length;i++){
            if (arr[i]>max){
                max=arr[i];
            }
        }
        System.out.println(max);
        //计算最大数字是几位数
        int maxlength = (max+"").length();
        //用于临时存储数据的数组(最大长度为arr.lenth,一般情况下放不满)
        int[][]temp = new int[10][arr.length];
        
        //用于记录在相应的数组中存放的数字的数量
        int []count = new int[10];

        //根据最大长度决定比较的次数
        for (int i = 0,n=1;i<maxlength;i++,n*=10){
            //把每一个数字分别计算余数
            for (int j=0;j<arr.length;j++){
                //计算余数
                int ys = arr[j]/n%10;
                temp[ys][count[ys]] = arr[j];
                count[ys]++;
            }
            //记录去的元素要放的位置
            int index= 0;
            //把数字都取出来
            for (int k=0;k<count.length;k++){
                if (count[k]!=0){
                  //循环取出元素
                    for (int l = 0;l<count[k];l++){
                        //取出元素
                        arr[index] = temp[k][l];
                        //记录下一个位置
                        index++;
                    }
                    //把数量滞空
                    count[k]=0;
                }
            }
        }
    }
}
]]>
0 https://ilnafz.cn/index.php/archives/26/#comments https://ilnafz.cn/index.php/feed/