文章目录
1. 认识栈结构
- 栈结构
- 栈结构是一种常见的线性数据结构
- 栈是一种只能在一端进行插入或删除操作的线性表
- 表中允许插入,删除操作的一端称为栈顶,表的另一端称为栈底.
- 向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;
- 从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
栈结构如下图所示:
-
栈的特点
- 栈的主要特点是先进后出
- 即后进入栈的元素先出栈
- 每次进栈的数据元素都放在原来栈顶元素之前称为新的栈顶元素,每次出栈的数据元素都是当前栈顶元素
-
生活中类似于栈的例子
- 若干人走进一个死胡同,假设该胡同的宽度只允许一个人进出,那么走出胡同的顺序和走出的顺序就正好相反.
- 比如我们去食堂打饭的时候都需要拿盘子,最新放上去的, 最先被学生拿走使用.最早放上去的,最后会被使用.
栈结构详细图解
2. 常见的栈笔试题
题目答案: C
- A答案: 65进栈, 5出栈, 4进栈出栈, 3进栈出栈, 6出栈, 21进栈,1出栈, 2出栈
- B答案: 654进栈, 4出栈, 5出栈, 3进栈出栈, 2进栈出栈, 1进栈出栈, 6出栈
- D答案: 65432进栈, 2出栈, 3出栈, 4出栈, 1进栈出栈, 5出栈, 6出栈
用四个动图来展示这个过程(这个过程这样演示应该还是比较清楚的):
A选项:
B选项
C选项
D选项
3.栈结构的实现
我们通过js语言来实现一个类,以此来模拟栈的操作
3.1 栈的创建
- 我们先来创建一个栈的类, 用于封装栈相关的操作
function Stack() {
// 栈中的属性
var items = [];
//栈中的方法
}
代码解析:
- 我们创建了一个Stack构造函数, 用户创建栈的类.
- 在构造函数中, 定义了一个变量, 这个变量可以用于保存当前栈对象中所有的元素.
- 这个变量是一个数组类型. 我们之后无论是压栈操作还是出栈操作, 都是从数组中添加和删除元素.
3.2 栈的常见操作列举
- push(element): 添加一个新元素到栈顶位置.
- pop():移除栈顶的元素,同时返回被移除的元素。
- peek():返回栈顶的元素,不对栈做任何修改(这个方法不会移除栈顶的元素,仅仅返回它)。
- isEmpty():如果栈里没有任何元素就返回true,否则返回false。
- clear():移除栈里的所有元素。
- size():返回栈里的元素个数。这个方法和数组的length属性很类似。
- push方法实现:
注意: 我们的实现是将最新的元素放在了数组的末尾, 那么数组末尾的元素就是我们的栈顶元素
//push
Stack.prototype.push = function(el) {
items.push(el);
}
- pop方法
注意:
-
出栈操作应该是将栈顶的元素删除, 并且返回.
-
因此, 我们这里直接从数组中删除最后一个元素, 并且将该元素返回就可以了
Stack.prototype.pop = function() {
return items.pop();
}
- peek 方法
peek方法是一个比较常见的方法, 主要目的是看一眼栈顶的元素.
注意:
- 和pop不同, peek仅仅的看一眼栈顶的元素,** 并不需要将这个元素从栈顶弹出**.
//peek
Stack.prototype.peek = function() {
return items[item.length - 1]
}
- isEmpty
isEmpty方法
- isEmpty方法用户判断栈中是否有元素.
- 实现起来非常简单, 直接判断数组中的元素个数是为0, 为0返回true, 否则返回false
//isEmpty
Stack.prototype.isEmpty = function() {
return items.length === 0
}
- size方法
- size方法是获取栈中元素的个数.
- 因为我们使用的是数组来作为栈的底层实现的, 所以直接获取数组的长度即可
//size
Stack.prototype.size = function() {
return items.length
}
完整的代码如下:
function Stack() {
// 栈中的属性
this.items = [];
//栈中的方法
//push
Stack.prototype.push = function(el) {
items.push(el);
}
//pop
Stack.prototype.pop = function() {
return items.pop();
}
//peek
Stack.prototype.peek = function() {
return items[item.length - 1]
}
//isEmpty
Stack.prototype.isEmpty = function() {
return items.length === 0
}
//size
Stack.prototype.size = function() {
return items.length
}
//toString
Stack.prototype.toString = function() {
var res = '';
for(let i = 0 ; i < this.items.length ; i++) {
res += this.items[i];
}
return res;
}
}
3.3 封装方法测试
var stack = new Stack();
// 压入元素
stack.push(1)
stack.push(23);
stack.push(245);
stack.push(7);
console.log(stack); // 1 23 245 77
//删除最后一个
stack.pop();
stack.pop();
console.log(stack); // 1 23
//查看第一个元素
var a = stack.peek();
console.log(a) //23
//isEmpty,判断栈是否为为
var b = stack.isEmpty();
console.log(b) //false
//返回栈的长度
var c = stack.size();
console.log(c) //2
4. 栈结构的应用
4.1 简单应用二进制转十进制
如何实现十进制转二进制?
-
要把十进制转化成二进制,我们可以将该十进制数字和2整除(二进制是满二进一),直到结果是0为止。
-
举个例子,把十进制的数字10转化成二进制的数字,过程大概是这样:
代码实现:
//十进制转二进制
function dec2bin(num) {
var stack = new Stack();
var r;
while(num > 0) {
rem = num % 2;
num = Math.floor(num / 2);
//将余数压入栈中
stack.push(rem);
}
var binNum = ''
//
while(!stack.isEmpty()) {
binNum += stack.pop();
}
return binNum;
}
console.log(dec2bin(10)) //1010
4.2 求解迷宫问题
问题描述
给定一个M*N的迷宫图,求一条从指定入口到出口的迷宫路径.假设一个迷宫如下图所示(M = 8 , N = 8),其中每个方块用空白表示通道,黄色表示障碍.
一般情况下,所求迷宫的路径是简单路径,即在求得迷宫的路径上不会出现同一个方块.一个迷宫的路径可能有多条,这些路径又长又短,此时仅仅用栈求一条路径.
算法设计
对于迷宫中的每个方块,有上、下、左、右4个方块相邻第i行第j列的当前方块的位置记为(i,j),规定上方方块为方位0.并按顺时针方向递增编号。在试探过程中假设按从方位0到方位3的方向查找下一个可走的相邻方块。
求迷官问题就是在一个指定的迷宫中求出从人口到出口的一条路径。 在求解时采用“穷举法”,即从人口出发.按方位右下左上
次序试探相邻的方块,一旦找到个可走的相邻方块就继续走下去,并记下所走的方位:若某个方块没有相邻的可走方块,则沿原路退回到前一个方块.换下-个方位再继续试探,直到所有可能的通路都试探完为止。
为了保证在任何位置上都能沿原路退回(称为回溯),需要保存从入口到当前位置的路径上走过的方块,由于回溯的过程是从当前位置退回到前一个方块,体现出后进先出的特点,所以采用栈来保存走过的方块。
- 如果一个非出口方块(x,y)是可走的,则将它进栈
- 然后开始按照
右下左上
的顺序测试他的周围是否是可走的 - 如果是可走的,则将可走的方块压入栈中,.否则继续探测其他方向的方位
- 如果可走,压入栈中之后更新坐标(x,y)的值,并且更新改点的值为
-1
,表示已经探测过,不需要再次探测,继续重复上面的步骤,探测上面是否可走 - 如果四个方向都不能走,内循环结束,判断栈是否为空,然后将栈顶元素出栈
- 一直重复重复循环,知道探测的坐标(x,y)的值与(M,N)相等,结束程序
在算法中应保证试探的相邻可走方块不是已走路径,上的方块。如方块(x,y)已进栈,在试探方块(x+1,y)的相邻可走方块时又会试探到方块(x,y)。也就是说,从方块(x,y)出发会试探方块(x+1,y),而从方块(x+1,y)出发又会试探方块(x,y),这样可能会引起死循环为此在一个方块进栈后将对应的mg数组元素值改为-1(标记为不可走的方块).
该迷宫的基本探测过程如下:
数据组织
为了表示迷宫,我们设置一个数组mg,其中每一个元素代表一个方块的状态,1表示对应方块是障碍物,0表示该方块可通行,为了算法方便,一般在迷宫外围加一堵围墙,均设置为1,表示不可通行.如下:
var mg = [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 0, 0, 0, 0, 1, 1, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
[1, 0, 1, 0, 0, 0, 1, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 1, 1, 0, 1],
[1, 1, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
]
另外用到的栈结构,是一个右行,列,方向组成的三元组,栈类的定义如下:
function Box(x, y, di) {
this.x = x;
this.y = y;
this.di = di;
}
方向探测表示
在探测方向的时候,每个方块都有四个方向:
每一次坐标的变化就(x,y)就会发生变化,即可以设置下面的对象,有x,y两个属性:
function direction(incX, incY) {
this.incX = incX;
this.incY = incY;
}
//该数组为到时候探测方向的检测数组
var Arr = new Array(4);
var dir = [[0, 1], [1, 0], [0, -1], [-1, 0]]
for (let i = 0; i < Arr.length; i++) {
Arr[i] = new direction(dir[i][0], dir[i][1])
}
基本过程
function mgPath(mg, Arr, M, N) {
var temp = new Box();
var x, y, di; //用来保存当前处理单元格的坐标
var row, col; //保存下一次处理单元格的坐标
mg[1][1] = -1; //当前个子无法走
temp.x = 1;
temp.y = 1;
temp.di = -1;
var s = new Stack();
s.push(temp);
while (!s.isEmpty()) {
temp = s.pop();
x = temp.x;
y = temp.y;
di = temp.di + 1;
while (di < 4) {
row = x + Arr[di].incX;
col = y + Arr[di].incY;
//等于0说明当前格子可走,否则di+1换个方向尝试
if (mg[row][col] === 0) {
temp.x = x;
temp.y = y;
temp.di = di;
var a = JSON.parse(JSON.stringify(temp))
s.push(a);
x = row;
y = col;
mg[row][col] = -1;
if (x === M && y === N) {
temp.x = M;
temp.y = N;
temp.di = di;
var a = JSON.parse(JSON.stringify(temp))
//将最后的出口也放入栈中
s.push(a);
return ouput(s);
} else {
di = 0;
}
} else {
di++;
}
}
}
console.log('该迷宫没有路')
}
定义处理输出的函数output,大家可以按照自己想要的方式处理输出
function ouput(data) {
var result = [];
//拿出栈中的所有数据
while (!data.isEmpty()) {
var a = data.pop();
var local = [a.x, a.y]
result.push(local)
}
//逆序输出数组中的数据即为结果
return result.reverse()
}
调用函数
var a = mgPath(mg, Arr, M, N)
console.log(a)
输出的结果如下: