鹅厂员工吐槽:毕业5年,感觉码农的路愈发难走,刚进公司拿2万感觉很轻松,买了房之后,现在拼命还很慌!
鹅厂员工吐槽,毕业5年,越干越慌,我看完第一反应:这不就是很多码农的现状吗。
刚进公司那会儿,月薪两万,感觉自己挺能打。代码一写,工资一发,周末还能点个外卖犒劳自己,甚至觉得买房也没那么远。
结果真上车之后,画风直接变了。
房贷一扣,生活费一走,父母那边再有点事,账户余额立马老实了。
以前加班是为了涨薪,现在加班像是在保命。你说不拼吧,怕被优化;你说拼吧,又不知道拼到哪天算个头。
最难受的是,技术这行还一直催你更新。新人越来越卷,AI又在旁边敲门,35岁像个倒计时一样挂在头上。
以前觉得进大厂就是上岸,现在才发现,上岸之后还得拼命划水
面试题:最大栈
popMax() 一加,原来的 Stack<Integer> 基本就废了。
我见过有人这么写:每次要最大值,就把栈扫一遍。测试数据十几个元素,看不出问题;线上一批任务压进来,接口耗时突然抖一下,日志里全是这种东西:
cost=183ms, action=popMax, stackSize=48291
cost=211ms, action=popMax, stackSize=53120
这种实现我第一眼就不太信。
最大栈要解决的不是“怎么找到最大值”这么简单,而是这几个操作都得稳:
push(x)
pop()
top()
peekMax()
popMax()
普通栈只关心栈顶。
最大栈还要随时知道当前最大值,甚至能把“最靠近栈顶的最大值”删掉。
这里容易写错的地方在 popMax()。比如栈里是:
5, 1, 5, 3
栈顶在右边。
popMax() 应该删掉后面那个 5,不是前面那个。这个细节不处理,很多题目能过一半,然后在重复最大值上翻车。
我一般不会用一个栈硬搞。
比较顺手的写法是:一个双向链表维护真实入栈顺序,一个 TreeMap 维护值到节点集合的映射。
双向链表负责:
push / pop / top
TreeMap 负责:
peekMax / popMax
因为 TreeMap.lastKey() 可以直接拿到当前最大值。
关键是,popMax() 找到最大值后,还要能从链表中间把那个节点摘掉。所以节点必须是双向的,不能只存值。
代码我写成下面这样,没放什么教学 demo,都是关键逻辑:
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.TreeMap;
publicclassMaxStack{
privatestaticclassNode{
int val;
Node prev;
Node next;
Node(int val) {
this.val = val;
}
}
privatefinal Node head = new Node(0);
privatefinal Node tail = new Node(0);
privatefinal TreeMap<Integer, Deque<Node>> valueIndex = new TreeMap<>();
publicMaxStack(){
head.next = tail;
tail.prev = head;
}
publicvoidpush(int x){
Node node = new Node(x);
linkBeforeTail(node);
valueIndex.computeIfAbsent(x, k -> new ArrayDeque<>()).addLast(node);
}
publicintpop(){
Node node = tail.prev;
if (node == head) {
thrownew IllegalStateException("stack is empty");
}
unlink(node);
Deque<Node> bucket = valueIndex.get(node.val);
bucket.removeLast();
if (bucket.isEmpty()) {
valueIndex.remove(node.val);
}
return node.val;
}
publicinttop(){
if (tail.prev == head) {
thrownew IllegalStateException("stack is empty");
}
return tail.prev.val;
}
publicintpeekMax(){
if (valueIndex.isEmpty()) {
thrownew IllegalStateException("stack is empty");
}
return valueIndex.lastKey();
}
publicintpopMax(){
if (valueIndex.isEmpty()) {
thrownew IllegalStateException("stack is empty");
}
int max = valueIndex.lastKey();
Deque<Node> bucket = valueIndex.get(max);
Node node = bucket.removeLast();
unlink(node);
if (bucket.isEmpty()) {
valueIndex.remove(max);
}
return max;
}
privatevoidlinkBeforeTail(Node node){
Node last = tail.prev;
last.next = node;
node.prev = last;
node.next = tail;
tail.prev = node;
}
privatevoidunlink(Node node){
Node left = node.prev;
Node right = node.next;
left.next = right;
right.prev = left;
node.prev = null;
node.next = null;
}
}
这里有个小点别改错了。
valueIndex 里面我用的是 Deque<Node>,不是 Set<Node>。
原因很简单:重复值要按入栈顺序处理。最大值如果出现多次,popMax() 要删离栈顶最近的那个,也就是最后入栈的那个最大值。
所以这里用:
bucket.removeLast();
不是随便删一个。
跑一下刚才那个例子:
MaxStack stack = new MaxStack();
stack.push(5);
stack.push(1);
stack.push(5);
stack.push(3);
System.out.println(stack.peekMax()); // 5
System.out.println(stack.popMax()); // 5
System.out.println(stack.top()); // 3
System.out.println(stack.pop()); // 3
System.out.println(stack.top()); // 1
这时候栈里剩下的是:
5, 1
说明删的是靠近栈顶的那个 5,没删错。
复杂度也比较干净。
push() 要写链表,再写 TreeMap,主要是 O(log n)。
pop() 要从 TreeMap 找对应桶,删除最后一个节点,整体也是 O(log n),严格说删除桶本身才走树操作。
top() 是 O(1)。
peekMax() 是 O(log n) 或接近树取尾节点的成本。
popMax() 是 O(log n)。
有人会问,能不能用两个栈?
能,但只能解决 peekMax(),解决不了漂亮的 popMax()。
双栈写法大概是这样:
push(x) 时顺手记录当前最大值
pop() 时两个栈一起弹
peekMax() 直接看最大值栈顶
但是 popMax() 要删中间元素,两个栈就开始别扭了。你得把上面的元素倒出来,删完再倒回去。数据量小没事,数据量一大,日志里就会出现前面那种几十毫秒、几百毫秒的抖动。
最大栈这个题,看着像栈,其实考的是两个结构怎么互相留后门。
链表给 TreeMap 留了一个“按节点删除”的口子。
TreeMap 给链表留了一个“快速定位最大值”的口子。
只用一个结构硬扛,最后代码不是慢,就是乱。这个地方别省。