面试第三篇
数据结构
常见数据结构
八种常见数据结构:数组、栈、队列、链表、图、树、前缀树、哈希表
数组与链表
数组(Array)是最简单,也是最常用的数据结构。栈和队列都是由数组衍生出来的。
数组根据维度区分分为一维数组和多维数组
链表(Linked List)也是线性结构,它与数组看起来非常像,但是它们的内存分配方式、内部结构和插入删除操作方式都不一样。
链表是一系列节点组成的链,每一个节点保存了数据以及指向下一个节点的指针。链表头指针指向第一个节点,如果链表为空,则头指针为空或者为null。
链表可以用来实现文件系统、哈希表和邻接表。
链表分为单向链表和双向链表。双链表每个节点除了保存数据及指向下一个节点的指针,还保存了前一个节点的指针。
还有一类链表是环形链表,链表的尾部节点指向链表中某个节点
栈与队列
栈中的元素采用LIFO (Last In First Out),即后进先出。
队列(Queue)与栈类似,都是采用线性结构存储数据。它们的区别在于,栈采用LIFO方式,而队列采用先进先出,即FIFO(First in First Out)。
图
图(graph)由多个节点(vertex)构成,节点之间可以互相连接组成一个网络。(x, y)表示一条边(edge),它表示节点x与y相连。边可能会有权值(weight/cost)。
树
树(Tree)是一个分层的数据结构,由节点和连接节点的边组成。树是一种特殊的图,它与图最大的区别是没有循环。
树被广泛应用在人工智能和一些复杂算法中,用来提供高效的存储结构。
树的分类
N叉树、平衡树、二叉树、二叉查找树、平衡二叉树、红黑树、2-3树
二叉树又分为满二叉树、完全二叉树、平衡二叉树
二叉树和二叉查找树是最常用的树。
霍夫曼树、B树、B+树、B*树
B树
B+树
特点:
- 关键字数和子树相同。在 B+ 树中,节点的关键字代表子树的最大值,因此关键字数等于子树数。
- 非叶子节点仅用作索引,它的关键字和子节点有重复元素。除叶子节点外的所有节点的关键字,都在它的下一级子树中同样存在,最后所有数据都存储在叶子节点中。
- 叶子节点用指针连在一起。叶子节点包含了全部的数据,并且按顺序排列,B+ 树使用一个链表将它们排列起来,这样在查询时效率更快。
遍历方法
前缀树
前缀树(Prefix Trees或者Trie)与树类似,用于处理字符串相关的问题时非常高效。它可以实现快速检索,常用于字典中的单词查询,搜索引擎的自动补全甚至IP路由。
哈希(Hash)将某个对象变换为唯一标识符,该标识符通常用一个短的随机字母和数字组成的字符串来代表。哈希可以用来实现各种数据结构,其中最常用的就是哈希表(hash table)与hashmap。
哈希表通常由数组实现,哈希表的性能取决于三个指标:
哈希函数、哈希表的大小、哈希冲突处理方式
数组和链表的区别
线性表包含顺序线性表和链表。顺序表是用数组实现的,链表是用指针实现的,在内存中不连续。
数组是顺序存储结构,数组的内存要求连续的区域,因此要求连续的内存空间,查找速度快,可以随机访问和修改,插入删除效率低,,大小固定,不能动态扩展
链表是链式存储结构,链表的内存可以存在任何地方,不要求连续,插入删除速度快,不能随机查找,必须从第一个开始遍历,效率低,且不允许随意存取,大小没有固定,扩展灵活
堆和栈(数据结构与内存)
数据结构中的堆和栈
堆是常用的树形结构,是一种特殊的完全二叉树。只有所有节点的值总是不大于或者不小于其父节点的值的完全二叉树被称为堆。根节点最大的称为大顶堆,根节点最小的称为小顶堆,
堆用数组存储,
内存中的堆和栈
一个c++程序内存分配
全局区(static):存放全局变量和静态变量。初始化的在一块,未初始化的在一块
栈区(stack):存放局部变量、函数参数值等,由编译器自动分配释放,操作方式类似于数据结构中的栈
堆区(heap):由程序员分配释放,若程序员不释放程序结束时由系统回收。分配方式类似于数据结构中的链表,与栈不同
文字常量区:常量和字符串放在这里,程序结束后由系统释放。
程序代码区:存放函数体,为二进制代码
栈由系统分配释放,速度快,空间小,windows下不超过2m,堆由程序员自己生成,速度慢,空间大且灵活,
0.1+0.2 != 0.3
0.1
和0.2
的二进制都是以1100无限循环的小数,
0.1的二进制
1 | 0.0001100110011001100110011001100110011001100110011001101 |
0.2的二进制
1 | 0.001100110011001100110011001100110011001100110011001101 |
理论上讲相加应该得到
1 | 0.0100110011001100110011001100110011001100110011001100111 |
实际上JavaScript相加得到
1 | 0.0100110011001100110011001100110011001100110011001101 |
JavaScript
中使用的是64位双精度浮点数编码,所以它的符号位
占1
位,指数位占11
位,尾数位占52
位。
尾数位
就是存储科学计数法后的有效数字;
由于限制,有效数字第53
位及以后的数字是不能存储的,它遵循,如果是1
就向前一位进1
,如果是0
就舍弃的原则。
0.1的二进制科学计数法第53位是1,0.2
有着同样的问题,其实正是由于这样的存储,在这里有了精度丢失,导致了0.1+0.2!=0.3
。
BigInt
是第七种原始类型。
BigInt
是一个任意精度的整数。这意味着变量现在可以计算9007199254740991
即最大安全整数以上的数字。
1 | const b = 1n; // 追加 n 以创建 BigInt |
解决方法
1.把计算数字 提升 10 的N次方 倍 再 除以 10的N次方。N>1.
1 | (0.1*1000+0.2*1000)/1000==0.3 |
2.使用Number.EPSILON方法
1 | Number.EPSILON=(function(){ //解决兼容性问题 |
排序算法
冒泡排序、快速排序、选择排序、插入排序、堆排序
冒泡排序: 最小的元素会慢慢浮到队列的顶端
从左到右逐一比较相邻的元素,如果不符合顺序交换位置,逐渐把大的放在最后,小的放在前面
选择排序
遍历,每次找到最小的放在最前面,依次
插入排序:
从第一个依次每次向后取一个,逐个检查插入准确位置
快速排序:
以左边第一个为基准,将大于的放在右边,小于的放在左边,依次排列
https://blog.csdn.net/java_green_hand0909/article/details/84970372
前端
js
JavaScript为什么是单线程
1.消耗资源和成本:
如果 Javascript 被设计为多线程的程序,那么操作 DOM 必然会涉及到资源的竞争,那么这么语言必然会被实现的非常臃肿,那么在 Client 端中跑这么一门语言的程序,资源消耗和性能都将是不乐观的,同时在 Client 实现多线程还真没有那么刚性的需求;但是如果设计成单线程,并辅以完善的异步队列来实现,那么运行成本就会比多线程的设计要小很多了。
2.操作逻辑
作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
js与ts的区别
ts比js更严谨,提供类型检查,是js的超集,ts执行时编译为js
ts报错也可以执行,可以编译成js
具体:
ts给js中添加了类、模块,可以把声明、数据、函数和类封装在模块中,类提供了接口
在编码期间可以检查错误,更高效
js设计模式
策略模式
定义一系列算法,把它们一个个封装起来,并使它们可以相互替换
意义:
利用组合、委托等技术和思想,有效避免很多if条件语句
提供了开放封闭原则,使代码更容易理解
代码复用
单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点。实现的方法为先判断实例是否 存在,如果存在则直接返回,
代理模式
为对象提供一个代用品或者占位符,以便控制它的访问
意义:
单一职责,只有代理对象才能引起该对象的变化
用代理来过滤掉一些不合规的访问,减少该对象的访问,可以提高性能。
观察者模式
定义对象间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知
意义:
支持简单的广播通信,自动通知所有订阅过的对象
装饰者模式
在不改变对象自身的基础上,在程序运行期间给对象动态地添加方法。
意义:原有方法维持不变,在原有方法上再挂载其他方法满足现有需求
拓展对象功能
什么是DOM,怎么操作DOM,交换Dom
放进数组,交换数组元素
1 | <div id="divs"> |
new实例的过程、手写new的过程
版本1:
1.以构造器的prototype属性为原型,创建新对象;
2.通过 this 将属性和方法添加至这个对象2
3.最后返回 this 指向的新对象,也就是实例
版本2:
1.创建一个空对象,作为将要返回的对象实例,
2.将这个空对象的原型指向构造函数的prototype属性
3.将这个空对象赋值给函数内部的this关键字
4.开始执行构造函数内部代码
1 | //接受一个函数 |
this、手写bind、call、apply
this 永远指向最后调用它的那个对象
改变this指向的方法:
- 使用 ES6 的箭头函数
- 在函数内部使用
_this = this
- 使用
apply
、call
、bind
- new 实例化一个对象
apply 和 call 的区别是 call 方法接受的是若干个参数列表,而 apply 接收的是一个包含多个参数的数组,bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。
手写bind、call、apply
bind
1 | //调用原生案例 |
call
1 | //调用原生案例 |
apply
1 | //调用原生案例 |
手写深拷贝与浅拷贝
区别:
浅拷贝是指拷贝内存地址,公用同一个堆内存,两个变量相互受影响,深拷贝是指,开辟一块内存空间,保存相同的值。互不受影响。
浅拷贝实现
三种方式
- ES6:object.assign()
- 展开运算符…
- 自己封装函数实现
深拷贝实现
三种方式
- JSON.parse() (但是如果里面有 function 和 undefined 不可用)
- lodash
- 自己封装
手写深拷贝
1 | function deepCopy(obj){ |
JSON.parse(JSON.stringfy(X)) 的缺点:
在序列化JavaScript对象时,所有函数和原型成员会被有意忽略。
如:如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式,而不是对象的形式
如果obj里有RegExp(正则表达式的缩写)、Error对象,则序列化的结果将只得到空对象;
如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失;
如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null
JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor;
通俗点说,JSON.parse(JSON.stringfy(X)),其中X只能是Number, String, Boolean, Array, 扁平对象,即那些能够被 JSON 直接表示的数据结构。
手写typeof与instanceof
typeof
一般被用于判断一个变量的类型,我们可以利用 typeof
来判断number
, string
, object
, boolean
, function
, undefined
, symbol
这七种类型,typeof可以判断出基本数据类型(当然除了null的数据类型为object的bug),还可以正确判断出某个对象是否为function,其余的Date,Array等无法判断
手写typeof
1 | //使用Object.prototype.toString实现 |
instanceof
主要的作用就是判断一个实例是否属于某种类型,instanceof
也可以判断一个实例是否是其父类型或者祖先类型的实例。
instanceof相反,可以准确判断出复杂数据类型,但是无法判断简单数据类型.
手写instanceof
1 | function instanceOf(left,right) { |
手写ajax
ajax请求是通过js的xmlHttpRequest对象与后端服务器交换数据
1 | var xhr = new XMLHttpRequest(); |
状态码说明:
0-未初始化,还未调用send方法 1-载入,已调用send方法发送请求,等待响应 2-载入完成,完全接收到响应内容 3-交互,正在解析响应内容 4-完成解析,可以进行调用
手写jsonp
jsonp原理:因为jsonp发送的并不是ajax请求,其实是动态创建script标签 script标签是没有同源限制的,把script标签的src指向请求的服务端地址。
1 | function jsonp (url,data={},callback='callback') { |
防抖与节流
防抖
防抖的含义:当一次事件发生后,事件处理器要等一定阈值的时间,如果这段时间过去后 再也没有 事件发生,就处理最后一次发生的事件。假设还差 0.01
秒就到达指定时间,这时又来了一个事件,那么之前的等待作废,需要重新再等待指定时间。
1 | // 防抖动函数 |
节流
可以将一个函数的调用频率限制在一定阈值内,例如 1s,那么 1s 内这个函数一定不会被调用两次
手写节流
1 | function throttle(fn, wait) { |
函数防抖的使用场景:
- 手机号、邮箱输入检测
- 窗口大小resize
函数节流使用场景:
滚动加载,加载更多或者滚到底部监听
搜索框搜索联想功能
高频点击提交、重复提交
手写js数组push、splice、map、foreach方法
push方法
1 | Array.prototype.push = function () { |
splice方法
1 | Array.prototype._splice = function (index, howmany = 0) { |
map方法(reduce实现)
1 | if (!Array.prototype.mapUsingReduce) { |
手写数组flat方法
数组拍平方法 Array.prototype.flat()
也叫数组扁平化、数组拉平、数组降维。
1 | Array.prototype.flat = function () { |
二维数组降维
1 | var flattened = [[0,1],[2,3],[4,5],[6,7]].reduce( |
ES6、ES5数组去重
利用ES6:
set类型是不重复的空数组,将array转成set再转成array就行。
1 | function unique (arr) { |
双循环
1 | function unique(arr){ |
建立一个空数组,利用indexof判断空数组中是否有元素,没有则逐项添加,最后返回新数组
1 | function unique(arr) { |
利用array.reduce去重
1 | let myArray = ['a','a','b','b','c','c','e','d','d','d','d','d'] |
`
1 | let arr = [1,1,2,3,4,2,3,5,5,5] |
函数柯里化
把接收多个参数的函数转换为接收单一参数的函数
1 | //普通函数 |
函数科里化实现
1 | function curry(fn) { |
偏函数
偏函数是将n个参数的函数转换为固定x个参数的函数,剩余n-x个参数将在下次调用全部传入.
偏函数和函数科里化有点像,所以根据函数科里化的实现可以写出偏函数的实现
1 | function partial(fn, ...args) { |
遍历深层次对象
给定一个对象,用给定str去遍历
1 | const obj = { |
数字千分位加逗号
最简单:js内置的toLocaleString函数可以实现这个功能
其他:转换成字符数组,再循环遍历
1 | function numFormat(num){ |
正则表达式
1 | function numFormat(num){ |
箭头函数不能作为构造函数的原因
构造函数是通过new 关键字来生成对象实例,生成对象实例的过程也是通过构造函数给实例绑定this的过程,而箭头函数没有自己的this,因此不能使用箭头函数作为构造函数,也不能通过new操作符来调用箭头函数
函数调用
函数调用有四种方式:
- 作为一个函数调用
- 函数作为方法调用
- 使用构造函数调用函数
- 作为函数方法调用函数(call、apply)
ES6语法转ES5语法的实现
ES6 转 ES5 目前行业标配是用 Babel
,转换思路为:
1.把代码字符串解析成AST(Abstract Syntax Tree,抽象语法树)
2.AST就是一个json,按照一定规则把这个json里ES6部分的东西转换成ES5的
3.再把修改后的AST转换成代码
举例
1 | function hello(a, b, c){ |
有了这个json后,就可以对代码进行操作了。在没有树的情况下,如果要对文件里的某一个语句进行替换的话,一般就是全局
搜索然后replace,这样有可能影响到别的代码,但是有了树后,就可以对这个json进行操作,精确地去修改某个对象的属性,也就不会影响到别的代码了。所以babel转换ES6的核心,就是在ast中按照一定的规则取修改json里的属性和方法,然后再把tree转换成代码
let、const、var区别
主要是let和const、let和var区别
let只在声明的代码块里有效,let不允许重复声明,let声明的全局变量不会作为window对象的一个属性
只要使用let
声明了一个变量,那这个变量就“绑定”到了这个作用域(全局/局部/块级),该变量就不再受外层作用域的影响。如果区块中存在let
和const
命令,这个区块对这些命令声明的变量从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。这在语法上称为暂时性死区。
var允许重复定义变量,使用var
声明的全局变量,会被JS自动添加在全局对象window
上,作为该对象的一个属性。
用var
声明的变量,会在其作用域中发生变量提升
的过程。变量会被提升到作用域顶部,JS默认给变量一个undefined
值。在使用var
声明一个变量前访问它,得到的值永远是undefined
。
const声明值时必须初始化值且不可修改,为常量,const不允许重复声明
实例
1 | //题1 |
Jquery事件
onclick:鼠标点击某个对象
ondblclick:双击事件
onchange:文本对象变化
onselect 文本被选中。
onfocus:获得焦点
onblur:失去焦点
1.onMousedown:某个鼠标按键被按下
2.onmouseup 鼠标按键被松开。
3.onmousemove 鼠标被移动。
4.onmouseover 鼠标移到某元素之上。
5.onmouseout 鼠标从某元素移开。
onload:是某个页面的css js html 文档结构和图像被完全加载
键盘事件:
1.onkeydown 某个键盘按键被按下。
2.onkeyup 某个键盘按键被松开。
3.onkeypress 某个键盘按键被按下并松开。
onsubmit 确认按钮被点击。
onreset 重置按钮被点击。
定时器原理
js实现红绿灯
使用setTimeout、Promise、async await 三种方式实现红绿灯代码,红灯2秒,黄灯1秒,绿灯3秒,循环改变颜色。改变颜色的方法,就简单写成打印出颜色。
1 | function changeColor(color) { |
Async await实现
1 | function sleep(duration) { |
使用Promise,把下一次的颜色改变写在then里面,最后同样使用递归完成循环。
1 | function sleep(duration){ |
连线
通过canvas划线
https://www.jianshu.com/p/bd55a625b839
js判断数组方法比较
typeof无法直接返回数组,
tostring方法
使用array对象的isarray方法
浏览器兼容问题
笔试资源
js笔试:Https://www.huaweicloud.com/articles/2774625590336c3fe79e6468b956c6a3.html