栈
只能在表的同一端进行插入和删除操作,且操作遵循先进后出原则的线性表称为栈。
- 栈的变化端称为栈顶,进行插入、删除操作。
- 栈的封口端称为栈底。
- 栈的先进后出原则:先入栈的元素在栈底,最后放入的元素在栈顶。
若用 表示栈,则:
- 表尾端称为栈顶,表头端称为栈底。
- 称为栈底元素, 称为栈顶元素。
- 当栈中无元素时称为空栈。
栈常见的两种操作:
-
入栈:添加一个元素到栈顶。
-
出栈:删除栈顶最后一个元素。
栈的基本操作
- 栈的初始化
- 判断栈是否为空
- 获取栈顶元素
- 入栈
- 出栈
- 遍历栈
- 清空栈
顺序栈
用顺序存储方式存储元素的栈称为顺序栈。
栈指针的设置及栈空、栈满判断:
- 设 为栈底指针,指示栈底元素在顺序栈中的位置,此指针不随栈的操作而变化
- 设 为栈顶指针,指示栈顶元素的下一个位置,初始值指向栈底:
- 用 表示栈的最大容量
- 当 时栈空, 时栈满
顺序栈的初始化
- 申请容量为 的存储空间, 指向空间的基地址。
- 设栈顶指针 。
- 设 。
顺序栈的入栈
- 判断栈是否为满,若栈满则不再添加元素。
- 否则,将新的元素压入栈顶,栈顶指针加 1: 。
顺序栈的出栈
- 判断栈是否为空,若栈空则不再删除元素。
- 否则,栈顶指针减 1 : ,栈顶元素出栈。
取栈顶元素
- 当栈非空时,返回当前栈顶元素值,栈顶保持不变
链式栈
用链表存储方式实现的栈称为链式栈。
链栈中如何区分栈顶和栈底?
- 若链尾为栈顶,每次栈顶的操作都要对链表进行遍历,时间复杂度为 ;
- 若链头为栈顶,则栈操作的时间复杂度为
;
因此从时间复杂度上分析,宜采用链头作为栈顶。
链栈操作特点:
- 入栈:插入数据到链栈的头部;
- 出栈:删除链栈的首元结点;
因此链栈是只能在头部进行插入和删除的特殊链表。
链栈的初始化
操作步骤如下:
- 构造一个空栈
- 栈顶指针设置为空:
链栈的入栈
将数据元素插入到栈顶:
- 创建新结点
- 将新结点插入到链栈的首元结点之前,修改新结点指针域:
- 修改栈顶指针:
链栈的出栈
删除首元结点:
- 判断链栈是否为空
- 若链栈不为空,获取首元结点 的数据域
- 修改栈顶指针,使其指向首元结点的下一个结点:
- 释放 结点
链栈的取栈顶元素
- 当栈非空时,返回当前栈顶元素值,栈顶保持不变
链栈的特点
- 链栈不需要头结点,链表的头指针指向栈顶,插入和删除仅在栈顶处执行;
- 基本不存在栈满的情况,空栈相当于头指针指向空。
顺序栈和链式栈的比较
存储空间:顺序栈在初始化时必须申请存储空间,若栈不满时会造成存储空间的浪费;
链式栈所需空间是随时申请的,比顺序栈仅存储结点相比,需要额外申请空间存储其指针域。
时间复杂度:只针对栈顶的基本操作(入栈、出栈、栈元素的存取),顺序栈和链式栈的时间复杂度均为 。
栈应用
应用场景:在解决某个问题的时候,只要求关心最近一次的操作,并且在操作完成了之后,需要向前查找到更前一次的操作。
- 进制转换:十进制数字和其他进制之间进行转换。
- 表达式求值:实现计算器的计算功能。
- 括号匹配的检验:给出一串由括号组成的字符串,判断所有括号是否满足两两配对。
- 迷宫求解。
- (逆)波兰表达式求值:根据(逆)波兰表达式计算结果。
- 八皇后问题:在 的国际象棋上摆放 8 个皇后,使其互不攻击(任意两个皇后都不在同一行、同一列、同一斜线上),该如何摆放,共有多少种摆放方式。
- 汉诺塔问题。
算法例题
1.括号匹配的检验
例题:给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串,判断字符串是否有效。
说明:
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 空字符串可被认为是有效字符串。
示例 1:
输入: “()”
输出: true
示例 2:
输入: “(]”
输出: false
解题思路:
利用一个栈,不断地往里压左括号,一旦遇上了一个右括号,我们就把栈顶的左括号弹出来,表示这是一个合法的组合,以此类推,直到最后判断栈里还有没有左括号剩余。
2.每日温度
例题:根据每日气温列表,请重新生成一个列表,对应位置的输入是你需要再等待多久温度才会升高超过该日的天数。如果之后都不会升高,请在该位置用 0 来代替。
说明:气温列表 temperatures 长度的范围是 [1, 30000]。
示例:给定一个数组 T 代表了未来几天里每天的温度值,要求返回一个新的数组 D,D 中的每个元素表示需要经过多少天才能等来温度的升高。
给定 T:[23, 25, 21, 19, 22, 26, 23]
返回 D: [ 1, 4, 2, 1, 1, 0, 0]
解题思路:
第一个温度值是 23 摄氏度,它要经过 1 天才能等到温度的升高,也就是在第二天的时候,温度升高到 24 摄氏度,所以对应的结果是 1。接下来,从 25 度到下一次温度的升高需要等待 4 天的时间,那时温度会变为 26 度。
思路 1:最直观的做法就是针对每个温度值向后进行依次搜索,找到比当前温度更高的值,这样的计算复杂度就是
。
但是,在这样的搜索过程中,产生了很多重复的对比。例如,从 25 度开始往后面寻找一个比 25 度更高的温度的过程中,经历了 21 度、19 度和 22 度,而这是一个温度由低到高的过程,也就是说在这个过程中已经找到了 19 度以及 21 度的答案,它就是 22 度。
思路 2:可以运用一个堆栈 stack 来快速地知道需要经过多少天就能等到温度升高。从头到尾扫描一遍给定的数组 T,如果当天的温度比堆栈 stack 顶端所记录的那天温度还要高,那么就能得到结果。
- 对第一个温度 23 度,堆栈为空,把它的下标压入堆栈;
- 下一个温度 24 度,高于 23 度高,因此 23 度温度升高只需 1 天时间,把 23 度下标从堆栈里弹出,把 24 度下标压入;
- 同样,从 24 度只需要 1 天时间升高到 25 度;
- 21 度低于 25 度,直接把 21 度下标压入堆栈;
- 19 度低于 21 度,压入堆栈;
- 22 度高于 19 度,从 19 度升温只需 1 天,从 21 度升温需要 2 天;
- 由于堆栈里保存的是下标,能很快计算天数;
- 22 度低于 25 度,意味着尚未找到 25 度之后的升温,直接把 22 度下标压入堆栈顶端;
- 后面的温度与此同理。
该方法只需要对数组进行一次遍历,每个元素最多被压入和弹出堆栈一次,算法复杂度是 。
3.八皇后问题
在一个 N×N 的国际象棋棋盘上放置 N 个皇后,每行一个并使她们不能互相攻击。给定一个整数 N,返回 N 皇后不同的的解决方案的数量。
解题思路:
解决 N 皇后问题的关键就是如何判断当前各个皇后的摆放是否合法。
利用一个数组 columns[] 来记录每一行里皇后所在的列。例如,第一行的皇后如果放置在第 5 列的位置上,那么 columns[0] = 6。从第一行开始放置皇后,每行只放置一个,假设之前的摆放都不会产生冲突,现在将皇后放在第 row 行第 col 列上,检查一下这样的摆放是否合理。
方法就是沿着两个方向检查是否存在冲突就可以了。
代码实现:
首先,从第一行开始直到第 row 行的前一行为止,看那一行所放置的皇后是否在 col 列上,或者是不是在它的对角线上,代码如下。
boolean check(int row, int col, int[] columns) {
for (int r = 0; r < row; r++) {
if (columns[r] == col || row - r == Math.abs(columns[r] - col)) {
return false;
}
}
return true;
}
然后进行回溯的操作,代码如下。
int count;
int totalNQueens(int n) {
count = 0;
backtracking(n, 0, new int[n]);
return count;
}
void backtracking(int n, int row, int[] columns) {
// 是否在所有n行里都摆放好了皇后?
if (row == n) {
count++; // 找到了新的摆放方法
return;
}
// 尝试着将皇后放置在当前行中的每一列
for (int col = 0; col < n; col++) {
columns[row] = col;
// 检查是否合法,如果合法就继续到下一行
if (check(row, col, columns)) {
backtracking(n, row + 1, columns);
}
// 如果不合法,就不要把皇后放在这列中(回溯)
columns[row] = -1;
}
}
参考
- 《数据结构(C语言版)》 严魏敏、吴伟民著
- 《数据结构(第3版)》 刘大有等著
- 《搞定数据结构与算法》 苏勇