JS之数据结构与算法-栈和队列

243 阅读10分钟

1.栈

栈是一个线性结构,在计算机中是一种相当常见的数据结构。

1.1 栈与数组对比

我们知道数组是一种线性结构,并且可以在数组的任意位置插入和删除数据。但是有时候,我们为了实现某些功能,必须对这种任意性加以限制。而栈和队列就是比较常见的受限线性结构,我们先从栈开始介绍。 栈的特点:先进先出(LIFO)

  • 其限制是仅允许在表的一端进行插入和删除运算,这一端被称为栈顶,相对地,把另一端称为栈底
  • LIFO表示就是后进入的元素,第一个弹出栈空间,类似自动餐托盘,最后放上的托盘,往往先把它拿出去使用。
  • 向一个栈插入新元素又称进栈、入栈或者压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;
  • 从一个栈删除元素又称作出栈或者退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

1.2 栈的实现

1.2.1 栈的常见操作

  • push(element):添加一个新元素到栈顶位置;
  • pop():移除栈顶的元素,同时返回被移除的元素;
  • peek():返回栈顶的元素,不对栈做任何修改(该方法不会移除栈顶的元素,仅仅返回它);
  • isEmpty():如果栈里没有任何元素就返回true,否则返回false;
  • size():返回栈里的元素个数。这个方法和数组的length属性类似;
  • toString():将栈结构的内容以字符串的形式返回。

1.2.2 栈的封装

//封装栈类
function Stack() {
    //栈中的属性
    this.items = [];

    //栈的相关操作
    //1.push(): 将元素压入栈
    //方法一(不推荐):给当前兑现添加的方法,其他对象不能使用
    /* this.push = () => {

    } */

    //方法二:给Stack的原型上添加方法,能够给多个对象使用
    Stack.prototype.push = function(element) {
        this.items.push(element);
    }

    //2.pop(): 从栈中取出元素
    Stack.prototype.pop = function() {
        return this.items.pop(); //返回最后一个元素,也就是栈顶元素
    }

    //3.peek(): 查看栈顶元素
    Stack.prototype.peek() = function() {
        return this.items[this.items.length - 1];
    }

    //4. isEmpty(): 判断栈是否为空
    Stack.prototype.isEmpty() = function() {
        return this.items.length === 0;
    }

    //5.size(): 获取栈中元素的个数
    Stack.prototype.size() = function() {
        return this.items.length;
    }

    //6.toString(): 以字符串的形式输出栈的元素
    Stack.prototype.toString() = function() {
        //希望输出的形式:20 10 12 8 7
        var retString = '';
        for(var i = 0;i<this.items.length;i++) {
            retString += items[i] + ' ';
        }
        return retString;
    }
}

1.2.3 栈的操作的使用

let s = new Stack();
// 入栈
s.push(12);
s.push(23);
s.push(34);
s.push(45);

console.log(s);
// [12,23,34,45]

//取出栈顶元素
console.log(s.pop());   //45
console.log(s);         //[12,23,34]

//查看栈顶元素
console.log(s.peek()); //34
// 判断栈是否为空
console.log(s.isEmpty()); //false
//查看栈中元素的个数
console.log(s.size());  //3
// 以字符串的形式输出栈内的元素
console.log(s.toString());  //'12,23,34'

1.2.4 栈的应用 —— 十进制转二进制

利用栈结构的特点封装十进至转换为二进至的函数:

100 --> 1100100 100/2 ==> 余数 0 50/2 ==> 余数 0 25/2 ==> 余数 1 12/2 ==> 余数 0 6/2 ==> 余数 0 3/2 ==> 余数 1 1/2 ==> 余数 1

代码实现:

// 函数:将十进制转成二进制
function dec2bin(decNumber) {
    //1.定义一个栈,保存余数
    var stack = new Stack();

    //2.循环操作
    while(decNumber) {
        //2.1 获取余数,并且放入到栈中
        stack.push(decNumber % 2);

        //2.2 获取整除后的结果,用于下一次循环
        decNumber = Math.floor(decNumber / 2);  //floor向下取整
    }

    //3.从栈中取出0和1
    var bindaryString = '';
    while(!stack.isEmpty()) {
        bindaryString += stack.pop();
    }
    return bindaryString;
}

//测试
console.log(dec2bin(100));  //1100100
console.log(dec2bin(1000));  //1111101000

1.2.5 刷题练习

1.2.5.1 有效的括号

题目来源:leetcode20题

实现代码:

var isValid = function(s) {
    let map = {
        '(': 1,
        ')': -1,
        '[': 2,
        ']': -2,
        '{': 3,
        '}': -3
    }
   let stack = [];
   for(let i=0;i<s.length;i++) {
       if(map[ s[i] ] > 0) {
        stack.push(s[i]);
       }else{
           let last = stack.pop();
           if(map[last] != -(map[ s[i] ])){
               return false;
           }
       }
   }
   if(stack.length > 0) {
        return false;
   }
   return true;
};
1.2.5.2 包含迷min函数的栈

题目来源:剑指offer

思路:

  • 定义两个栈,一个用来存储数据,一个用来存最小的元素。
  • 首先把数据进入数据栈
  • 每次进栈的时候,都将进栈的数据和最小值栈的顶元素进行比较,如果比栈顶值小就把该数加入最小值栈
  • 如果比最小值栈的栈顶元素大,那就把最小值栈的栈顶元素再次(复制)插入最小值栈中。
  • 在出栈的时候,需要删除最小值栈的栈顶元素,即最小值栈和数据站都应该出栈。
  • 这样最小值栈的栈顶元素就永远都是当前栈的最小值

以数据[3,4,2,7,9,0]为例,让这组数字依次如栈,则栈和其对应的最小值栈如下:

//首先定义两个栈
var dataStack = [];
var minStack = [];
//进栈函数
function push(node) {
    // 首选不管怎么,数据栈都会进栈
    dataStack.push(node);
    // 如果node比最小值栈的栈顶元素小那就把node进入最小值栈,否则就把最小值栈再次(复制)再次入最小值栈
    if (minStack.length === 0 || node < min()) {
        minStack.push(node);
    }else {
        minStack(min());
    }
}
//出栈函数
// 出栈的时候数据栈出栈,最小值栈删掉栈顶元素,
function pop() {
    minStack.pop();
    return dataStack.pop();
}
// 获取最小值栈的栈顶元素
function min() {
    var length = minStack.length;
    return minStack[length - 1]&&length > 0;  //栈不空才能返回
}
1.2.5.3 栈的引入和弹出,序列

[题目] 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。 假设压入栈的所有数字均不相等。 例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。 (注意:这两个序列的长度是相等的)

题目来源:牛客网-剑指offer

思路:

  • 首先判断给出的这两个栈序列是否存在并且不为空
  • 然后借助一个工作站,来存放压入栈的弹出过程
  • 遍历压入栈,然后依次存入工作站中
  • 如果工作栈的栈顶元素和弹出栈的栈顶元素相同,工作站就出栈,并且弹出栈的索引往后移
  • 如果不同就继续将压入栈元素压入工作栈继续,相当于入栈
  • 最后如果工作站为空就说明第二个序列是第一个序列的弹出顺序
//传入两个栈序列,一个是压入栈,一个是弹出栈
function IsPopOrder(pushV,popV) {
    //首先判断这两个栈是否存在并且不为空
    if(pushV || popV || pushV.length === 0 || popV.length === 0 ) {
        return;
    } 
    var workStack = [];  //定义一个工作栈
    var outIndex = 0;  //在弹出栈中移动的索引
    for(var i=0;i<pushV.length;i++) {
        // 从栈底开始把压入栈的元素放入工作栈中
        workStack.push(pushV[i])
        //工作栈栈顶元素和弹出栈索引位置相同的话,工作站出栈,并且索引后进一位
        while(workStack.length && workStack[workStack.length - 1] === popV[outIndex]) {
            workStack.pop();
            outIndex ++;
        }
    }
    // 如果工作栈最后为空,说明弹出栈就是压入栈的出栈序列
    return workStack.length === 0;
}

2. 队列

队列是一种首先的线性表,先进先出(FIFO)

  • 受限之处在于它只允许在标的前端进行删除操作
  • 而在表的后端进行插入操作

2.1 队列的现实应用

  • 打印队列:计算机打印多个文件的时候,需要排队打印;
  • 线层队列:在开发中,为了让人物可以并行处理,通常会开启多个线程,当开启多线程时,当新开启的线程所需的资源不足时就先放入线程队列,等待CPU处理。

2.2 队列的实现

队列的实现和栈一样,有两种方案:

  • 基于数组实现
  • 基于链表实现

2.2.1 队列的常见操作:

  • enqueue(element):向队列尾部添加一个(或多个)新的项;
  • dequeue():移除队列的第一(即排在队列最前面的)项,并返回被移除的元素;
  • front():返回队列中的第一个元素——最先被添加,也将是最先被移除的元素。队列不做任何变动(不移除元素,只返回元素信息与Stack类的peek方法非常类似);
  • isEmpty():如果队列中不包含任何元素,返回true,否则返回false;
  • size():返回队列包含的元素个数,与数组的length属性类似;
  • toString():将队列中的内容,转成字符串形式;

2.2.2 队列的封装

function Queue() {
    //属性
    this.items = [];

    //方法
    // 1.enqueue():将元素加入到队列中
    Queue.prototype.enqueue = element => {
        this.items.push(element);
    }
    //2.dequeue():从队列中删除前端元素
    Queue.prototype.dequeue = () => {
        return this.items.shift();  //删除第一个元素
    }
    //3.fromt():查看前端元素
    Queue.prototype.front = () => {
        return this.items[0];
    }
    //4.isEmpty:查看队列是否为空
    Queue.prototype.isEmpty = () => {
        return this.items.lenght == 0;
    }
    //5.size():查看队列中元素的个数
    Queue.prototype.size = () => {
        return this.items.lenght;
    }
    //6.toString():将队列中元素以字符串形式输出
    Queue.prototype.toString = () => {
        let retString = '';
        for(let i=0;i>this.items.lenght;i++) {
            retString += this.items[i] + ' ';
        }
        return retString;
    }
}

2.2.3 队列的操作的使用

// 使用测试
let queue = new Queue();

//添加元素  入队
queue.enqueue('abc');
queue.enqueue('nba');
queue.enqueue('mba');
queue.enqueue('cba');
console.log(queue);  //['abc','nba','mba','cba']

//出队。删除元素
queue.dequeue();
console.log(queue);  //['abc','nba','mba']

//查看队列头部元素
console.log(queue.front()); //'nba'
//查看队列是否为空
console.log(queue.isEmpty());  //false

//查看队列中的元素个数
console.log(queue.size());  //3

//将队列中元素以字符串形式输出
console.log(queue.toString());  //nba mba cba 

2.3 队列的应用 -- 面试题

**击鼓传花: **

游戏规则:几个朋友一起玩一个游戏,围成一圈,开始数数,数到某个数字的的人自动退出淘汰,最后剩下的这个人会获得胜利,请问最后剩下的是原来哪个位置上的人。 程序规则:击鼓传花,使用队列实现,在队列中传入一组数据和设定的数字num,循环遍历数组内元素,遍历到的元素为指定数字num时将该元素删除,直至数组剩下一个元素。

function passGame(nameList,num) {
    //1.创建一个队列
    let queue = new Queue();

    //2.将所有人添加
    for(let item of nameList) {
        queue.enqueue(item);
    }
    //3.开始数数
    
    // 一直循环数到队列只剩下一个人
    while(queue.size() > 1) {
        //不是num的时候,重新添加到队列的末尾
        //是num的时候,就把这个元素从队列中删除
        for(let j=1;j<num;j++) {
            //都不是num
            queue.enqueue(queue.dequeue());
        }
        queue.dequeue();
    }
    console.log("队列的长度:" + queue.size());
    var endName = queue.front();
    console.log("最后留下来的名字:" + endName);

    return nameList.indexOf(endName);
}

names = ['aaa','bbb','ccc','ddd','eee']; 
console.log(passGame(names,3));  //3

2.4 优先级队列

2.4.1 优先级队列和普通队列的区别

  • 普通队列插入一个元素,数据会被放在后端,并且需要所有的元素都处理完成后才能处理前面的数据。
  • 优先级队列在插入一个元素的时候回考虑这个数据的优先级。和其他数据优先级进行比较。比较完成后可以知道这个元素在队列中应该插入的位置。
  • 其他方式和普通队列一样

2.4.2 优先级队列的实现

实现优先级队列需要考虑两个方面:

  • 封装元素和优先级放在一起(可以封装一个新的构造函数)
  • 添加元素时,将新插入元素的优先级和队列中已经存在的元素优先级进行比较,以获得自己正确的位置。

代码实现:

//封装优先级队列 以下代码基于ES5
function PriorityQueue() {
    //内部类:在类里面再封装一个类;表示带优先级的数据
    function QueueElement(element,priority) {
        this.element = element;
        this.priority = priority;
    }

    //封装属性
    this.items = [];

    //1.enqueue(): 实现插入方法
    PriorityQueue.prototype.enqueue = function(element,priority) {
        //1.1 创建QueueElement对象
        var queueElement = new QueueElement(element,priority)

        // 2. 判断队列是否为空,如果为空就直接插入进去,如果不为空,就需要从头遍历比较他们的优先级
        if(this.items.length === 0) {
            this.items.push(queueElement);
        }else {
            var added = false;  //用来判断是否插入
            for(var i=0;i<this.items.length;i++) {
                //比较优先级
                if(queueElement.priority < this.items[i],priority) {
                    this.items.splice(i,0,queueElement); //从索引开始删除0个元素,并插入queueElement
                    added = true;
                    break;
                }
            }
            if(!added) {
                this.items.splice(queueElement);
            }
        }
    }

    //toString()
    PriorityQueue.prototype.toString = function() {
        var restString = "";
        for(var i=0;i<this.items.length;i++) {
            restString += this.items[i].element + '-' + this.items[i].priority + " ";
        }
        return restString;
    }
    //其他方法和普通队列一样
}
//测试代码
var pq = new PriorityQueue()

pq.enqueue('abc',111);
pq.enqueue('cba',222);
pq.enqueue('nba',50);
pq.enqueue('nba',300);
console.log(pq.toString());
//nba-300 nba-50 cba-222 abc-111 

3.栈和队列练习

3.1 用两个栈来实现一个队列

完成队列的Push和Pop操作。 队列中的元素为int类型。

思路:定义两个栈1和栈2

栈1:用于存储队列

栈2:出队列的时候,栈1的数据依次出栈并进入栈2中,栈2出栈也就是栈1底部出栈的顺序,也就是队列中出栈的次序。

注意:猪油栈2为空的时候,栈1才能够进数据,否则会打乱出队的次序。

const stack1 = [];
const stack2 = [];
//入栈函数
function push(node) {
    stack1.push(node);
}
//出栈函数
function pop() {
    //先判断栈2是否为空,为空的话才进栈
    if(stack2.length === 0) {
        //并且栈1的不为空,然后栈1出栈,栈2进栈
        while(stack1.length>0) {
            stack2.push(stack1.pop());
        }
    }
    //栈2出栈,如果栈2内没有元素,就直接返回null
    return stack2.pop() || null;
}

扩展:用两个队列实现栈

思路:进栈的时候,如果队列1为空,就进入队列1,如果不为空,就把队列1中的数据灌入到队列2中,然后再把将要入栈的数据插入队列1中, 出栈的时候,如果队列1不为空,就把队列1中的数据出队,如果为空再出队列2的。

const queue1 = [];
const queue2 = [];
//进栈函数
function push(x) {
    if(queue1.length === 0) {
        queue1.push(x);
        //如果queueu2不为空,就一直出队
        while(queue2.length) {
            queue1.push(queue2.shift());  //shift()是删除数组中第一个元素,并返回删除的数的值
            //将队列2删除的数给队列1
        }
    }else if(queue2.length === 0) {
        queue2.push(queue1.shift());
        while(queue1.length) {
            queue2.push(queue1.shift());
        }
    }
}
//出栈的函数
function pop() {
    if(queue1.length != 0) {
        return queue1.shift();
    } else {
        return queue2.shift();
    }
}

3.2 包含min函数的栈

思路:

  • 定义两个栈,一个用来存储数据,一个用来存最小的元素。
  • 首先把数据进入数据栈
  • 每次进栈的时候,都将进栈的数据和最小值栈的顶元素进行比较,如果比栈顶值小就把该数加入最小值栈
  • 如果比最小值栈的栈顶元素大,那就把最小值栈的栈顶元素再次(复制)插入最小值栈中。
  • 在出栈的时候,需要删除最小值栈的栈顶元素,即最小值栈和数据站都应该出栈。
  • 这样最小值栈的栈顶元素就永远都是当前栈的最小值

以数据[3,4,2,7,9,0]为例,让这组数字依次如栈,则栈和其对应的最小值栈如下:

//首先定义两个栈
var dataStack = [];
var minStack = [];
//进栈函数
function push(node) {
    // 首选不管怎么,数据栈都会进栈
    dataStack.push(node);
    // 如果node比最小值栈的栈顶元素小那就把node进入最小值栈,否则就把最小值栈再次(复制)再次入最小值栈
    if (minStack.length === 0 || node < min()) {
        minStack.push(node);
    }else {
        minStack(min());
    }
}
//出栈函数
// 出栈的时候数据栈出栈,最小值栈删掉栈顶元素,
function pop() {
    minStack.pop();
    return dataStack.pop();
}
// 获取最小值栈的栈顶元素
function min() {
    var length = minStack.length;
    return minStack[length - 1]&&length > 0;  //栈不空才能返回
}

3.3 滑动窗口的最大值

给定一个数组nums,有一个大小为k的滑动窗口从数组的最左侧移动到数组的最右侧,你只可以看到在滑动窗口k内的数字,滑动窗口每次向右移动一位,返回滑动窗口最大值。

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7] 
解释: 
  滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

思路
使用一个双端队列(队列两面都可以进出),用于存储处于窗口中的值的下标,保证窗口头部元素永远是窗口的最大值。 遍历每个滑块的起始点。 从起始点开始,遍历后续滑块元素。 对比滑块中元素的最大值,并存入结果。

var maxSlidingWindow = function(nums,k) {
    let result = [];
    for(let i=0;i<nums.length-k+1;i++) {
        let max = nums[i];
        for(let i=0;i<j+k;j++) {
            max = Math.max(max,nums[j]);
        }
        //比对完整所有元素之后,将最大值存入result
        result.push(max);
    }
    return result;
}

3.4 栈的引入和弹出,序列

[题目] 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。 假设压入栈的所有数字均不相等。 例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。 (注意:这两个序列的长度是相等的)

题目来源:牛客网-剑指offer

思路:

  • 首先判断给出的这两个栈序列是否存在并且不为空
  • 然后借助一个工作站,来存放压入栈的弹出过程
  • 遍历压入栈,然后依次存入工作站中
  • 如果工作栈的栈顶元素和弹出栈的栈顶元素相同,工作站就出栈,并且弹出栈的索引往后移
  • 如果不同就继续将压入栈元素压入工作栈继续,相当于入栈
  • 最后如果工作站为空就说明第二个序列是第一个序列的弹出顺序
//传入两个栈序列,一个是压入栈,一个是弹出栈
function IsPopOrder(pushV,popV) {
    //首先判断这两个栈是否存在并且不为空
    if(pushV || popV || pushV.length === 0 || popV.length === 0 ) {
        return;
    } 
    var workStack = [];  //定义一个工作栈
    var outIndex = 0;  //在弹出栈中移动的索引
    for(var i=0;i<pushV.length;i++) {
        // 从栈底开始把压入栈的元素放入工作栈中
        workStack.push(pushV[i])
        //工作栈栈顶元素和弹出栈索引位置相同的话,工作站出栈,并且索引后进一位
        while(workStack.length && workStack[workStack.length - 1] === popV[outIndex]) {
            workStack.pop();
            outIndex ++;
        }
    }
    // 如果工作栈最后为空,说明弹出栈就是压入栈的出栈序列
    return workStack.length === 0;
}