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;
}