题目描述

✅ 735. 小行星碰撞

image-20260928222401120

image-20260928222401121

题意分析

数组顺序表示小行星从左到右的位置,正数向右、负数向左,绝对值表示大小。相撞时较小者消失,一样大则双方消失,幸存者的大小和方向不变。所有小行星速度相同,因此同向的不会追上,左负右正的会远离,只有左正右负可能相撞。

解法:栈模拟连续碰撞

核心思路

[!blue]

从左到右扫描,用栈保存已处理前缀的幸存者,栈底到栈顶保持原位置顺序,并保证前缀内部的碰撞已处理完。新行星位于这些幸存者的右侧;若有碰撞,必然先遇到离它最近的栈顶,所以只需要从栈顶开始判断。

只有当前 asteroid < 0 且栈顶 top > 0 时,两者才相向移动。比较 top 与 -asteroid:栈顶较小就弹出,当前行星继续与新的栈顶碰撞;相等就弹出栈顶并令当前行星死亡;栈顶较大则只令当前行星死亡。alive 用来记录当前行星是否还存在,死亡后不能继续碰撞或入栈。

当前行星仍存活时,若栈空或栈顶与它不再相向,就可以入栈,已处理前缀又恢复到没有待发生碰撞的状态。每轮碰撞要么永久弹出一个栈顶,要么结束当前行星的处理,不会反复扫描已经消失的行星。

解题步骤

  1. 初始化空栈,依次读取每颗小行星 asteroid,先标记它仍存活。
  2. 当当前值为负、栈顶为正且当前仍存活时,进入碰撞循环。
  3. 栈顶更小时弹栈并继续;相等时弹栈且让当前值消失;栈顶更大时只让当前值消失。
  4. 碰撞循环结束后,若当前值仍存活,就把它压栈。
  5. 扫描完成后,栈中元素就是最终结果,顺序无需反转。

代码实现

class Solution {
    public int[] asteroidCollision(int[] asteroids) {
        int[] stack = new int[asteroids.length];
        int size = 0;

        for (int asteroid : asteroids) {
            boolean alive = true;

            // 只有左侧向右、当前向左时会相撞,存活者可能继续向左碰撞。
            while (alive && asteroid < 0 && size > 0 && stack[size - 1] > 0) {
                int top = stack[size - 1];

                if (top < -asteroid) {
                    size--;
                } else if (top == -asteroid) {
                    size--;
                    alive = false;
                } else {
                    alive = false;
                }
            }

            // 当前行星未被更大或等大的栈顶消灭时,才可加入结果。
            if (alive) {
                stack[size++] = asteroid;
            }
        }

        return Arrays.copyOf(stack, size);
    }
}
func asteroidCollision(asteroids []int) []int {
    stack := make([]int, 0, len(asteroids))

    for _, asteroid := range asteroids {
        alive := true
        // 只有左侧向右、当前向左时会相撞,存活者可能继续向左碰撞。
        for alive && asteroid < 0 && len(stack) > 0 && stack[len(stack)-1] > 0 {
            top := stack[len(stack)-1]
            if top < -asteroid {
                stack = stack[:len(stack)-1]
            } else if top == -asteroid {
                stack = stack[:len(stack)-1]
                alive = false
            } else {
                alive = false
            }
        }
        // 当前行星未被更大或等大的栈顶消灭时,才可加入结果。
        if alive {
            stack = append(stack, asteroid)
        }
    }
    return stack
}

复杂度分析

  • 时间复杂度:$O(n)$。每颗小行星最多入栈、出栈各一次;不弹栈的碰撞会使当前行星死亡,每颗也至多发生一次,因此所有碰撞操作合计为线性次数。
  • 空间复杂度:$O(n)$。最坏情况下没有碰撞,栈保存全部小行星。

关键点总结

[!green]

  • 是否碰撞取决于相邻幸存者的方向:只有「正数在左、负数在右」需要处理。
  • 当前负数可能连续撞碎多个较小的正数,因此必须使用 while。
  • 三种大小关系要完整区分;相等时双方都消失。
  • 栈始终保存已处理前缀的最终幸存者,返回时保持原顺序,不需要反转。

易错点总结

[!yellow]

  • 判断符号相反还不够,必须是栈顶为正、当前为负;顺序相反时两者正在远离。
  • 当前行星撞碎较小栈顶后仍然存活,必须继续循环;相等时则既要弹栈,也要把 alive 设为 false。
  • 只能在 alive 为真时入栈,否则会把已经消失的当前行星重新放回结果。
  • Java 的定长数组只有 [0, size) 有效,需截取这个前缀;从栈底到栈顶已是答案顺序,不应反转。

相似题目

题目 难度 关联与区别
1047. 删除字符串中的所有相邻重复项 简单 同样用栈保存仍存活的前缀,新元素可能触发连续消除,本题需额外判断方向和绝对值。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/74257874
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!