程序员老鬼

某大厂员工:现在QA开发出来一堆东西,也不好好测试,让研发来测试,AI工具越来越完善,写代码变成了挂机~

刚看到个贴子,说某大厂员工吐槽:现在 QA 开发一堆工具和流程,也不好好测,全丢给研发自己回归;再加上 AI 写代码越来越方便,结果写代码变成“挂机”,验证和背锅成了主业。

Image

我觉得这事吧,本质不是“QA 摸鱼”或者“AI 抢饭碗”,而是分工和责任越来越糊了。

从我的角度看,AI 上线之后,更需要的是:谁负责“对结果负责”。工具再多,没人设计用例、没人思考边界条件,最后翻车一样是全员挨骂。QA 如果只会点点工具不懂业务,确实会被边缘化;研发如果只想写爽代码不愿意测,也早晚会被现实教育。

别指望 AI 和别人替你负责。

面试题:最近的三笔订单

想象你在电商做后端,一个接口是“我的订单”。产品说:列表第一页,只要给我最近的三笔就行,越新越靠前。数据存在库里也好,先查出来放在内存里也好,最后都可以抽象成一个问题:

给定一堆订单,每个订单有下单时间,找到最近的三笔订单,按时间从新到旧返回。

这里我先默认是“某一个用户的一堆订单里找最近三笔”。如果要做“每个用户最近三笔”,只是在这个基础上多一层分组,思路是一样的。

先把模型定下来

先用一个最简单的订单类,里面只保留和题目有关的字段:

publicclassOrder{
privatefinallong id;
privatefinallong createdTime; // 时间戳,越大越新

publicOrder(long id, long createdTime){
this.id = id;
this.createdTime = createdTime;
    }

publiclonggetId(){
return id;
    }

publiclonggetCreatedTime(){
return createdTime;
    }

@Override
public String toString(){
return"Order{id=" + id + ", createdTime=" + createdTime + "}";
    }
}

假设你方法的入参就是 List<Order> orders,里面可能有几条,也可能有好几万条。

多数同学第一反应都是:按时间排序,然后取前三个,这个思路完全没问题,而且在订单量不大的时候也很好用。

import java.util.*;

publicclassOrderService{

// 方案一:排序后取前三
public List<Order> latest3BySort(List<Order> orders){
if (orders == null || orders.isEmpty()) {
return Collections.emptyList();
        }

// 从新到旧排序
        orders.sort((o1, o2) -> Long.compare(o2.getCreatedTime(), o1.getCreatedTime()));

int end = Math.min(3, orders.size());
// 注意要 new 一个新的 List,避免直接 subList 导致外面改影响这里
returnnew ArrayList<>(orders.subList(0, end));
    }
}

时间复杂度很好算:排序是 O(n log n),后面取前三是 O(1),整体就当 O(n log n)。

这种写法的优点:

  • 代码短,好读
  • 运行不会太慢,小数据量情况下完全够用

缺点也很明显:其实我只想要三条,却把所有订单都排了一个序,有点浪费。

如果订单量是几十个、几百个,这种浪费不值一提;但如果你是批量离线任务,一次拉几十万条、几百万条订单,就会感觉到排序还是挺费劲的。

换个思路:我只要前三名,没必要全员排序,我只需要在扫描列表的过程中,随时知道“当前为止最大的三个人”。

这种场景特别适合用堆(PriorityQueue),维护一个固定大小为 3 的小顶堆:

  • 堆里最多放 3 个订单

  • 堆顶永远是“当前这三笔里最旧的那一笔”

  • 扫描每个订单:

    • 堆没满,直接丢进去
    • 堆满了,就看新订单是不是比堆顶更新,如果更近,就把堆顶踢出去,放进新订单

扫一遍结束,堆里留下的就是“最近的三笔”,只是顺序还要再调一下。

import java.util.*;

publicclassOrderService{

// 方案二:小顶堆,时间复杂度 O(n log 3) ≈ O(n)
public List<Order> latest3ByHeap(List<Order> orders){
if (orders == null || orders.isEmpty()) {
return Collections.emptyList();
        }

// 小顶堆:时间越早(越旧)排在越前
        PriorityQueue<Order> heap = new PriorityQueue<>(3,
                Comparator.comparingLong(Order::getCreatedTime));

for (Order order : orders) {
if (heap.size() < 3) {
                heap.offer(order);
            } else {
// 堆顶是当前三条里最旧的订单
                Order oldestInTop3 = heap.peek();
if (oldestInTop3 != null &&
                        order.getCreatedTime() > oldestInTop3.getCreatedTime()) {
                    heap.poll();       // 踢掉最旧的
                    heap.offer(order); // 把更新的塞进去
                }
            }
        }

// 这时 heap 里的元素从旧到新,需要翻个顺序
        List<Order> result = new ArrayList<>(heap);
        result.sort((o1, o2) -> Long.compare(o2.getCreatedTime(), o1.getCreatedTime()));
return result;
    }
}

这个方案的特点:

  • 每次操作堆的复杂度是 O(log 3),但 log 3 是常数,可以当成 1
  • 整体就是一遍线性扫描:O(n)
  • 特别适合“订单很多,但是只要前 k 条”的场景

如果哪天产品说,不是三笔了,要“最近的一百笔”,这个方案改一下堆的大小就行,整体复杂度还是 O(n log k),对大数据量很友好。

如果你不想引入堆,或者希望少一点对象创建,其实还可以更原始一点:用三个变量,手动维护前三大的时间戳。

思路类似“找数组中最大的三个数”:

publicclassOrderService{

// 方案三:一趟扫描 + 三个指针
public List<Order> latest3Manual(List<Order> orders){
if (orders == null || orders.isEmpty()) {
return Collections.emptyList();
        }

        Order first = null;   // 最新
        Order second = null;  // 第二新
        Order third = null;   // 第三新

for (Order order : orders) {
if (first == null ||
                    order.getCreatedTime() > first.getCreatedTime()) {
// 全新的第一名
                third = second;
                second = first;
                first = order;
            } elseif (second == null ||
                    order.getCreatedTime() > second.getCreatedTime()) {
// 排在第一和第二之间
                third = second;
                second = order;
            } elseif (third == null ||
                    order.getCreatedTime() > third.getCreatedTime()) {
// 排在第三
                third = order;
            }
        }

        List<Order> result = new ArrayList<>();
if (first != null) result.add(first);
if (second != null) result.add(second);
if (third != null) result.add(third);
return result;
    }
}

这个方式:

  • 时间复杂度 O(n)
  • 额外空间几乎是 O(1)
  • 代码读起来略微绕一点,但多看两遍也不难

如果 k 固定很小,比如一直是 3,这种写法是最省的。

上面这些都是“单个用户的一堆订单”。如果是“每个用户最近三笔订单”,常见写法是:

  • 先按用户 id 分组(比如 Map<userId, List>)
  • 每个用户内部用上面任意一个算法

或者更省内存一点:一边流式读取订单,一边用:

Map<Long, PriorityQueue<Order>> map = new HashMap<>();

每个用户维护一个小顶堆,逻辑和上面那个 latest3ByHeap 完全一样,只不过是多套一层 map.get(userId)。

这个思路在实际业务里非常常见,比如按用户取最近几次登录、最近几次付款等等,跟我们平时做高并发订单系统时处理的一些场景非常像

总体上,这道“最近的三笔订单”的题,难度不在语法,而在你能不能把场景拆干净、把不同数据量下合适的方案说清楚: 小数据就排序,大数据用堆,k 很小可以手搓前三,这样面试官一般就会觉得你思路还挺全面的。

-END-

我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html

最后给大家分享一份不错的副业资料,点击下方公众号,回复关键字: 副业 领,也可以链接我微信:hls404