三者都属于数据结构,做为专业的技术人员来讲,理解数据结构是不可或缺的一部分。在平常的面试中,可能会遇到栈、堆、队列等一系列问题。 html
在面试中,常常问遇到与之相关的一些列问题哈。java
栈 是一种遵循 后进先出(LIFO) 原则的有序集合。新添加和待删除的数据都保存在栈的同一端栈顶,另外一端就是栈底。新元素靠近栈顶,旧元素靠近栈底。 栈由编译器自动分配释放。栈使用一级缓存。调用时处于存储空间,调用完毕自动释放。git
举个栗子:乒乓球盒子/搭建积木程序员
javaScript中,数据类型分为基本数据类型和引用数据类型,基本数据类型包含:string、number、boolean、undefined、null、symbol、bigint这几种。在内存中这几种数据类型存储在栈空间,咱们按值访问。原型类型都存储在栈内存中,是大小固定而且有序的。es6
咱们知道了基本数据结构的存储以后,咱们再来看看JavaScript中如何经过栈来管理多个执行上下文。面试
JavaScript中每个可执行代码,在解释执行前,都会建立一个可执行上下文。按照可执行代码块可分为三种可执行上下文。 算法
由于JS执行中最早进入全局环境,因此处于"栈底的永远是全局环境的执行上下文"。而处于"栈顶的是当前正在执行函数的执行上下文",当函数调用完成后,它就会从栈顶被推出(理想的状况下,闭包会阻止该操做,闭包后续文章深刻详解)。编程
"全局环境只有一个,对应的全局执行上下文也只有一个,只有当页面被关闭以后它才会从执行栈中被推出,不然一直存在于栈底"浏览器
看个例子:缓存
let name = '蜗牛';
function sayName(name) {
sayNameStart(name);
}
function sayNameStart(name) {
sayNameEnd(name);
}
function sayNameEnd(name) {
console.log(name);
}
复制代码
当代码进行时声明:
咱们须要本身建立一个栈,而且这个栈包含一些方法。
function Stack() {
let items = [];
this.push = function(element) {
items.push(element);
};
this.pop = function() {
let s = items.pop();
return s;
};
this.peek = function() {
return items[items.length - 1];
};
this.isEmpty = function() {
return items.length == 0;
};
this.size = function() {
return items.length;
};
this.clear = function() {
items = [];
}
}
复制代码
可是这样的方式在建立多个实例的时候为建立多个items的副本。就不太合适了。 用ES如何6实现Stack类了。能够用WeakMap实现,并保证属性是私有的。
let Stack = (function() {
const items = new WeakMap();
class Stack {
constructor() {
items.set(this, []);
}
getItems() {
let s = items.get(this);
return s;
}
push(element) {
this.getItems().push(element);
}
pop() {
return this.getItems().pop();
}
peek() {
return this.getItems()[this.getItems.length - 1];
}
isEmpty() {
return this.getItems().length == 0;
}
size() {
return this.getItems().length;
}
clear() {
this.getItems() = [];
}
}
return Stack;
})();
复制代码
栈能够解决十进制转为二进制的问题、任意进制转换的问题、平衡园括号问题、汉罗塔问题。
// 例子十进制转二进制问题
function divideBy2(decNumber) {
var remStack = new Stack(),
rem,
binaryString = '';
while (decNumber > 0) {
rem = Math.floor(decNumber % 2);
remStack.push(rem);
decNumber = Math.floor(decNumber / 2);
}
while(!remStack.isEmpty()) {
binaryString += remStack.pop().toString();
}
return binaryString;
}
// 任意进制转换的算法
function baseConverter(decNumber, base) {
var remStack = new Stack(),
rem,
binaryString = '',
digits = '0123456789ABCDEF';
while (decNumber > 0) {
rem = Math.floor(decNumber % base);
remStack.push(rem);
decNumber = Math.floor(decNumber / base);
}
while(!remStack.isEmpty()) {
binaryString += digits[remStack.pop()].toString();
}
return binaryString;
}
复制代码
不一样浏览器对调用栈的大小是有限制,超过将出现栈溢出的问题。下面这段代码能够检验不用浏览器对调用栈的大小限制。
var i = 0;
function recursiveFn () {
i++;
recursiveFn();
}
try {
recursiveFn();
} catch (ex) {
console.log(`个人最大调用栈 i = ${i} errorMsg = ${ex}`);
}
复制代码
谷歌浏览器:
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Fibonacci(10) // 89
Fibonacci(100) // 超时
Fibonacci(500) // 超时
复制代码
上面代码是一个阶乘函数,计算n的阶乘,最多须要保存n个调用记录,复杂度 O(n) 。若是超出限制,会出现栈溢出问题。
递归很是耗费内存,由于须要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来讲,因为只存在一个调用帧,因此永远不会发生“栈溢出”错误。
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity
复制代码
因而可知,“尾调用优化”对递归操做意义重大,因此一些函数式编程语言将其写入了语言规格。ES6 亦是如此,第一次明确规定,全部 ECMAScript 的实现,都必须部署“尾调用优化”。这就是说,ES6 中只要使用尾递归,就不会发生栈溢出(或者层层递归形成的超时),相对节省内存。
列子来源:ECMAScript 6 入门
堆,通常由操做人员(程序员)分配释放,若操做人员不分配释放,将由OS回收释放。分配方式相似链表。堆存储在二级缓存中。
JavaScript 的数据类型除了原始类型,还有一类是 Object 类型,它包含:
Object 类型都存储在堆内存中,是大小不定,复杂可变的。 Object 类型数据的 指针 存储在栈内存空间, 指针实际指向的值存储在堆内存空间。
一般与垃圾回收机制有关。为了使程序运行时占用的内存最小。
当一个方法执行时,每一个方法都会创建本身的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将天然销毁了。所以,全部在方法中定义的变量都是放在栈内存中的;
当咱们在程序中建立一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(由于对象的建立成本一般较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁,即便方法结束后,这个对象还可能被另外一个引用变量所引用(方法的参数传递时很常见),则这个对象依然不会被销毁,只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。
队列遵循FIFO,先进先出原则的一组有序集合。队列在尾部添加元素,在顶部删除元素。在现实中最多见的队列就是排队。先排队的先服务。(请你们文明排队,不要插队。)
JavaScript是单线程,单线程任务被分为同步任务和异步任务。同步任务在调用栈中等待主线程依次执行,异步任务会在有告终果以后,将回调函数注册到任务队列,等待主线程空闲(调用栈为空),放入执行栈等待主线程执行。
Event loop执行以下图,任务队列只是其中的一部分。
执行栈在执行完同步任务以后,若是执行栈为空,就会去检查微任务(MicroTask)队列是否为空,若是为空的话,就会去执行宏任务队列(MacroTask)。不然就会一次性执行完全部的微任务队列。 每次一个宏任务执行完成以后,都会去检查微任务队列是否为空,若是不为空就会按照先进先出的方式执行完微任务队列。而后在执行下一个宏任务,如此循环执行。直到结束。
实现包含如下方法的Queue类
// 队列Queue类简单实现
function Queue() {
let items = [];
// 添加元素
this.enqueue = function(element) {
items.push(element);
};
// 删除元素
this.dequeue = function() {
return items.shift();
};
// 返回队列第一个元素
this.front = function() {
return items[0];
};
// 判断队列是否为空
this.isEmpty = function() {
return items.length === 0;
};
// 返回队列长度
this.size = function() {
return items.length;
};
}
复制代码
ES6语法实现Queue队列类,利用WeakMap来保存私有属性items,并用外层函数(闭包)来封装Queue类。
let Queue1 = (function() {
const items = new WeakMap();
class Queue1 {
constructor() {
items.set(this, []);
}
// 获取队列
getQueue() {
return items.get(this);
}
// 添加元素
enqueue (element) {
this.getQueue().push(element);
}
// 删除元素
dequeue() {
return this.getQueue().shift();
}
// 返回队列第一个元素
front() {
return this.getQueue()[0];
}
// 判断队列是否为空
isEmpty() {
return this.getQueue().length === 0;
}
// 返回队列长度
size() {
return this.getQueue().length;
}
}
return Queue1;
})();
复制代码
元素的添加和删除基于优先级。常见的就是机场的登机顺序。头等舱和商务舱的优先级高于经济舱。实现优先队列,设置优先级。
// 优先列队
function PriorityQueue() {
let items = [];
// 建立元素和它的优先级(priority越大优先级越低)
function QueueElement(element, priority) {
this.element = element;
this.priority = priority;
}
// 添加元素(根据优先级添加)
this.enqueue = function(element, priority) {
let queueElement = new QueueElement(element, priority);
// 标记是否添加元素的优先级的值最大
let added = false;
for (let i = 0; i < items.length; i++) {
if (queueElement.priority < items[i].priority) {
items.splice(i, 0, queueElement);
added = true;
break;
}
}
if (!added) {
items.push(queueElement);
}
};
// 删除元素
this.dequeue = function() {
return items.shift();
};
// 返回队列第一个元素
this.front = function() {
return items[0];
};
// 判断队列是否为空
this.isEmpty = function() {
return items.length === 0;
};
// 返回队列长度
this.size = function() {
return items.length
};
// 打印队列
this.print = function() {
for (let i = 0; i < items.length; i++) {
console.log(`${items[i].element} - ${items[i].priority}`);
}
};
}
复制代码
// 循环队列(击鼓传花)
function hotPotato(nameList, num) {
let queue = new Queue(); //{1} // 构造函数为4.3建立
for(let i =0; i< nameList.length; i++) {
queue.enqueue(nameList[i]); // {2}
}
let eliminted = '';
while(queue.size() > 1) {
// 把队列num以前的项按照优先级添加到队列的后面
for(let i = 0; i < num; i++) {
queue.enqueue(queue.dequeue()); // {3}
}
eliminted = queue.dequeue(); // {4}
console.log(eliminted + '在击鼓传花游戏中被淘汰');
}
return queue.dequeue(); // {5}
}
let names = ['John', 'Jack', 'Camila', 'Ingrid', 'Carl'];
let winner = hotPotato(names, 7);
console.log('获胜者是:' + winner);
复制代码
参考来源: