栈、堆、队列 深入理解

栈、堆、队列都属于常见数据结构。

一、栈

1、简介

栈 是一种遵循 后进先出(LIFO) 原则的有序集合。

按照常识理解就是有序的挤公交,最后上车的人会在门口,然后门口的人会最先下车

 js中没有栈的数据类型,但我们可以通过Array来模拟一个

const stack = [];

stack.push(1); // 入栈
stack.push(2); // 入栈

const item1 = stack.pop();  //出栈的元素

2、存储栈

3、执行栈(函数调用栈)

我们知道了基本数据结构的存储之后,我们再来看看JavaScript中如何通过栈来管理多个执行上下文

  • 程序执行进入一个执行环境时,它的执行上下文就会被创建,并被推入执行栈中(入栈)。
  • 程序执行完成时,它的执行上下文就会被销毁,并从栈顶被推出(出栈),控制权交由下一个执行上下文。

JavaScript中每一个可执行代码,在解释执行前,都会创建一个可执行上下文。按照可执行代码块可分为三种可执行上下文。

  • 全局可执行上下文:每一个程序都有一个全局可执行代码,并且只有一个。任何不在函数内部的代码都在全局执行上下文。
  • 函数可执行上下文:每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都被调用时都会创建它自己的执行上下文。
  • Eval可执行上下文:Eval也有自己执行上下文。

因为JS执行中最先进入全局环境,所以处于"栈底的永远是全局执行上下文"。而处于"栈顶的是当前正在执行函数的执行上下文",当函数调用完成后,它就会从栈顶被推出(理想的情况下。闭包会阻止该操作)。

"全局环境只有一个,对应的全局执行上下文也只有一个,只有当页面被关闭之后它才会从执行栈中被推出,否则一直存在于栈底"

看个例子:

    let name = '蜗牛';
    function sayName(name) {
        sayNameStart(name);
    }
    function sayNameStart(name) {
        sayNameEnd(name);
    }
    function sayNameEnd(name) {
        console.log(name);
    }

当代码进行时声明:

执行sayName函数时,会把直接函数压如执行栈,并且会创建执行上下文,执行完毕编译器会自动释放:

4、栈的执行状态

假设用ESP指针来保存当前的执行状态,在系统栈中会产生如下的过程:

  1. 调用func, 将 func 函数的上下文压栈,ESP指向栈顶
  2. 执行func,又调用f函数,将 f 函数的上下文压栈,ESP 指针上移
  3. 执行完 f 函数,将ESP 下移,f函数对应的栈顶空间被回收
  4. 执行完 func,ESP 下移,func对应的空间被回收

图示如下:

5、栈溢出问题

1.5.1 栈大小限制

不同浏览器对调用栈的大小是有限制,超过将出现栈溢出的问题。下面这段代码可以检验不同浏览器对调用栈的大小限制。

var i = 0;
function recursiveFn () {
    i++;
    recursiveFn();
}
try {
    recursiveFn();
} catch (ex) {
    console.log(`我的最大调用栈 i = ${i} errorMsg = ${ex}`);
}

1.5.2 递归调用的栈溢出问题

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) 。如果超出限制,会出现栈溢出问题。

1.5.3 尾递归调用优化

递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(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 中只要使用尾递归,就不会发生栈溢出(或者层层递归造成的超时),相对节省内存。

二、堆

1、简介

堆,一般由操作人员(程序员)分配释放,若操作人员不分配释放,将由OS(操作系统)回收释放。分配方式类似链表。堆存储在二级缓存中。

三、队列

1、简介

队列是和栈相反,遵循 先进先出(FIFO)原则的一组有序集合。

按照常识理解就是银行排号办理业务, 先去领号排队的人, 先办理业务。

同样 js中没有栈的数据类型,但我们可以通过 Array来模拟一个

const queue = [];

// 入队
queue.push(1);
queue.push(2);

// 出队
const first = queue.shift();
const end = queue.shift();

3.2 任务队列

JavaScript是单线程,单线程任务被分为同步任务和异步任务。同步任务在调用栈中等待主线程依次执行,异步任务会在有了结果之后,将回调函数注册到任务队列,等待主线程空闲(调用栈为空),放入执行栈等待主线程执行。

Event loop执行如下图,任务队列只是其中的一部分。

执行栈在执行完同步任务之后,如果执行栈为空,就会去检查微任务(MicroTask)队列是否为空,如果为空的话,就会去执行宏任务队列(MacroTask)。否则就会一次性执行完所有的微任务队列。 每次一个宏任务执行完成之后,都会去检查微任务队列是否为空,如果不为空就会按照先进先出的方式执行完微任务队列。然后在执行下一个宏任务,如此循环执行。直到结束。

2、优先队列

元素的添加和删除基于优先级。常见的就是机场的登机顺序。头等舱和商务舱的优先级高于经济舱。实现优先队列,设置优先级。

// 优先列队
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}`);
        }
    };
}

3、循环队列(击鼓传花)

// 循环队列(击鼓传花)
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);

实现一个模拟击鼓传花的游戏:

  1. 利用队列类,创建一个队列。
  2. 把当前玩击鼓传花游戏的所有人都放进队列。
  3. 给定一个数字,迭代队列,从队列的开头移除一项,添加到队列的尾部(如游戏就是:你把花传给旁边的人,你就可以安全了)。
  4. 一旦迭代次数到达,那么这时拿着花的这个人就会被淘汰。
  5. 最后剩下一个人,这个人就是胜利者。


 

  • 7
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值