[数据结构]-05栈

只能在表的同一端进行插入和删除操作,且操作遵循先进后出原则的线性表称为栈。

  • 栈的变化端称为栈顶,进行插入、删除操作。
  • 栈的封口端称为栈底。
  • 栈的先进后出原则:先入栈的元素在栈底,最后放入的元素在栈顶。

若用 S = ( a 1 , a 2 , a 3 , . . . , a n ) S=(a_1,a_2,a_3,...,a_n) 表示栈,则:

  • 表尾端称为栈顶,表头端称为栈底。
  • a 1 a_1 称为栈底元素, a n a_n 称为栈顶元素。
  • 当栈中无元素时称为空栈。

栈常见的两种操作:

  • 入栈:添加一个元素到栈顶。

  • 出栈:删除栈顶最后一个元素。

栈的基本操作

  • 栈的初始化
  • 判断栈是否为空
  • 获取栈顶元素
  • 入栈
  • 出栈
  • 遍历栈
  • 清空栈

顺序栈

用顺序存储方式存储元素的栈称为顺序栈。

栈指针的设置及栈空、栈满判断:

  • b a s e base 为栈底指针,指示栈底元素在顺序栈中的位置,此指针不随栈的操作而变化
  • t o p top 为栈顶指针,指示栈顶元素的下一个位置,初始值指向栈底: t o p = = b a s e top == base
  • s t a c k S i z e stackSize 表示栈的最大容量
  • b a s e = = t o p base == top 时栈空, t o p b a s e = s t a c k S i z e top - base = stackSize 时栈满

顺序栈的初始化

  • 申请容量为 m a x S i z e maxSize 的存储空间, b a s e base 指向空间的基地址。
  • 设栈顶指针 t o p = = b a s e top == base
  • s t a c k s i z e = = m a x S i z e stacksize == maxSize

顺序栈的入栈

  • 判断栈是否为满,若栈满则不再添加元素。
  • 否则,将新的元素压入栈顶,栈顶指针加 1: t o p = t o p + 1 top = top + 1

顺序栈的出栈

  • 判断栈是否为空,若栈空则不再删除元素。
  • 否则,栈顶指针减 1 : t o p = t o p 1 top = top - 1 ,栈顶元素出栈。

取栈顶元素

  • 当栈非空时,返回当前栈顶元素值,栈顶保持不变

链式栈

用链表存储方式实现的栈称为链式栈。

链栈中如何区分栈顶和栈底?

  • 若链尾为栈顶,每次栈顶的操作都要对链表进行遍历,时间复杂度为 O ( n ) O(n)
  • 若链头为栈顶,则栈操作的时间复杂度为 O ( 1 ) O(1)
    因此从时间复杂度上分析,宜采用链头作为栈顶。

链栈操作特点:

  • 入栈:插入数据到链栈的头部;
  • 出栈:删除链栈的首元结点;
    因此链栈是只能在头部进行插入和删除的特殊链表。

链栈的初始化

操作步骤如下:

  • 构造一个空栈
  • 栈顶指针设置为空: t o p = = n u l l top == null

链栈的入栈

将数据元素插入到栈顶:

  • 创建新结点 e e
  • 将新结点插入到链栈的首元结点之前,修改新结点指针域: e . n e x t = t o p e.next = top
  • 修改栈顶指针: t o p = e top = e

链栈的出栈

删除首元结点:

  • 判断链栈是否为空
  • 若链栈不为空,获取首元结点 a n a_n 的数据域
  • 修改栈顶指针,使其指向首元结点的下一个结点: t o p = a n . n e x t top = a_n.next
  • 释放 a n a_n 结点

链栈的取栈顶元素

  • 当栈非空时,返回当前栈顶元素值,栈顶保持不变

链栈的特点

  1. 链栈不需要头结点,链表的头指针指向栈顶,插入和删除仅在栈顶处执行;
  2. 基本不存在栈满的情况,空栈相当于头指针指向空。

顺序栈和链式栈的比较

存储空间:顺序栈在初始化时必须申请存储空间,若栈不满时会造成存储空间的浪费;
链式栈所需空间是随时申请的,比顺序栈仅存储结点相比,需要额外申请空间存储其指针域。

时间复杂度:只针对栈顶的基本操作(入栈、出栈、栈元素的存取),顺序栈和链式栈的时间复杂度均为 O ( 1 ) O(1)

栈应用

应用场景:在解决某个问题的时候,只要求关心最近一次的操作,并且在操作完成了之后,需要向前查找到更前一次的操作。

  1. 进制转换:十进制数字和其他进制之间进行转换。
  2. 表达式求值:实现计算器的计算功能。
  3. 括号匹配的检验:给出一串由括号组成的字符串,判断所有括号是否满足两两配对。
  4. 迷宫求解。
  5. (逆)波兰表达式求值:根据(逆)波兰表达式计算结果。
  6. 八皇后问题:在 8 8 8*8 的国际象棋上摆放 8 个皇后,使其互不攻击(任意两个皇后都不在同一行、同一列、同一斜线上),该如何摆放,共有多少种摆放方式。
  7. 汉诺塔问题。

算法例题

1.括号匹配的检验
例题:给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串,判断字符串是否有效。
说明:
有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 空字符串可被认为是有效字符串。

示例 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:最直观的做法就是针对每个温度值向后进行依次搜索,找到比当前温度更高的值,这样的计算复杂度就是 O ( n 2 ) O(n^2)
但是,在这样的搜索过程中,产生了很多重复的对比。例如,从 25 度开始往后面寻找一个比 25 度更高的温度的过程中,经历了 21 度、19 度和 22 度,而这是一个温度由低到高的过程,也就是说在这个过程中已经找到了 19 度以及 21 度的答案,它就是 22 度。

思路 2:可以运用一个堆栈 stack 来快速地知道需要经过多少天就能等到温度升高。从头到尾扫描一遍给定的数组 T,如果当天的温度比堆栈 stack 顶端所记录的那天温度还要高,那么就能得到结果。

  1. 对第一个温度 23 度,堆栈为空,把它的下标压入堆栈;
  2. 下一个温度 24 度,高于 23 度高,因此 23 度温度升高只需 1 天时间,把 23 度下标从堆栈里弹出,把 24 度下标压入;
  3. 同样,从 24 度只需要 1 天时间升高到 25 度;
  4. 21 度低于 25 度,直接把 21 度下标压入堆栈;
  5. 19 度低于 21 度,压入堆栈;
  6. 22 度高于 19 度,从 19 度升温只需 1 天,从 21 度升温需要 2 天;
  7. 由于堆栈里保存的是下标,能很快计算天数;
  8. 22 度低于 25 度,意味着尚未找到 25 度之后的升温,直接把 22 度下标压入堆栈顶端;
  9. 后面的温度与此同理。
    该方法只需要对数组进行一次遍历,每个元素最多被压入和弹出堆栈一次,算法复杂度是 O ( n ) O(n)

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版)》 刘大有等著
  • 《搞定数据结构与算法》 苏勇
发布了19 篇原创文章 · 获赞 0 · 访问量 761

猜你喜欢

转载自blog.csdn.net/qq_39953750/article/details/103895944