程序员老鬼

被公司辞退了,领到了22万补偿金。结果准备入职下一家公司时,原公司HR电话叫我回去,涨薪7000,但要把赔偿金还回去

刚看到个贴子,说一哥们被公司辞退拿了22万补偿,结果下一家公司都谈好准备入职了,原公司HR突然杀回马枪:开口涨薪7000,但补偿金要还回去。

Image

这事吧…本质就是公司发现离不开人了,但又想把成本压到最低,说白了就是“既要马儿跑,又要马儿不吃草”。

网友们有人说“快跑”,也有人觉得“钱到位就行”。我看了看,嗯…都各有道理。

从我的角度看,这种回头找你的操作,诚意是关键。如果真觉得你有价值,就应该在你被裁的时候说清楚,而不是等你找到下家了再来“收回一半好处”。

不过话说回来,职场嘛,都是算账的地方。你要是真看重涨薪,也能接受退钱,那就理性谈,合同白纸黑字写清楚;要是不想被反复横跳折腾,那就继续走你的。

机会很多,但自尊值钱,选择让自己心安的那条路就行。

面试题:设计日志存储系统

先说结论哈,这题其实不难,本质就是一句话:把一堆带时间戳的日志存起来,然后按时间范围 + 精度去查。难点不在算法多花哨,而是细节别翻车。

一般题目长这样(简化版):

  • 日志有 id 和时间戳,时间格式是:YYYY:MM:DD:HH:MM:SS

  • 你要实现两个方法:

    • put(int id, String timestamp):存一条日志
    • retrieve(String start, String end, String gra):根据时间范围 [start, end],加上一个“粒度”(比如到年、到月、到天…),返回所有在这个范围内的 id

粒度一般是:"Year" / "Month" / "Day" / "Hour" / "Minute" / "Second"。

听起来有点绕,但想象一下: “查 2017 年 1 月这个月的日志”,那我其实就是把时间都截成 “到月”,然后比大小对吧。

核心点:时间戳怎么跟“粒度”打配合?

这个时间格式很乖:YYYY:MM:DD:HH:MM:SS固定长度,补零,对字符串排序是等价于按时间排序的(这一点很关键)。

比如说有两个时间:

2017:01:01:23:59:59
2017:02:01:00:00:00

直接用字符串比较大小,结果和按时间比较一模一样,不用自己写什么时间解析、转 long 之类的,省很多事。

那粒度是干嘛的? 比如用户传:

start  = 2017:01:01:23:59:59
end    = 2017:12:31:00:00:00
gra    = Year

如果按“年”来算,我们只关心前 4 位,后面全部按最小 / 最大补齐就行了:

  • 对 start:按年 -> 变成 2017:00:00:00:00:00(最小可能)
  • 对 end:按年 -> 变成 2017:12:31:23:59:59(最大可能)

这样一来,问题就变成:给定一堆字符串形式的时间戳,查在 [start', end'] 之间的日志。

存储结构怎么选?

最直接的思路:

  • 所有日志塞进一个 List
  • 每条日志存成一个小对象 {id, timestamp}
  • 每次 retrieve 时,线性扫一遍,把在范围里的挑出来

这么干时间复杂度是 O(N) 一次查询,如果日志特别多、查询也多,就有点顶不住了。

稍微正规一点的做法:

  • 我们让日志按时间有序

  • 用 Java 自带的 TreeMap<String, List<Integer>>

    • key:完整时间戳字符串
    • value:这一个时间戳下的所有 id(可能同一秒多条日志)

有序之后,我们就能用 subMap(startKey, true, endKey, true) 一把取出中间那段时间的所有日志,复杂度大概是 O(logN + K)(K 是命中的条数),会比无脑扫一遍好不少。

粒度对应的“前缀长度”

我们要做的,其实就是: 根据粒度,把字符串“截断”到对应位置,然后补上最小/最大值。

可以先做一个粒度到下标的映射(注意下标是“字符串位置”而不是字段个数):

  • Year   -> 4("YYYY")
  • Month  -> 7("YYYY:MM")
  • Day    -> 10
  • Hour   -> 13
  • Minute -> 16
  • Second -> 19(整个串)

这样 start 和 end 处理方式是:

prefix = 原时间戳.substring(0, idx)
startBound = prefix + 最小尾巴
endBound   = prefix + 最大尾巴

比如粒度是 Day,idx = 10:

  • 最小尾巴::00:00:00
  • 最大尾巴::23:59:59

这样就能把“按天查询”的区间,映射成完整时间串的区间。

代码实现(Java)

直接上一个比较标准、能用的实现:

import java.util.*;

publicclassLogSystem{

// 按时间有序存日志
private TreeMap<String, List<Integer>> logs = new TreeMap<>();

// 粒度 -> 截取长度
privatestaticfinal Map<String, Integer> GRA_INDEX = new HashMap<>();
static {
        GRA_INDEX.put("Year", 4);
        GRA_INDEX.put("Month", 7);
        GRA_INDEX.put("Day", 10);
        GRA_INDEX.put("Hour", 13);
        GRA_INDEX.put("Minute", 16);
        GRA_INDEX.put("Second", 19);
    }

publicLogSystem(){}

// 存一条日志
publicvoidput(int id, String timestamp){
        logs.computeIfAbsent(timestamp, k -> new ArrayList<>()).add(id);
    }

// 按时间范围 + 粒度查
public List<Integer> retrieve(String start, String end, String gra){
int idx = GRA_INDEX.get(gra);

        String startPrefix = start.substring(0, idx);
        String endPrefix = end.substring(0, idx);

// 根据粒度补齐最小 / 最大时间
        String startKey = startPrefix + getMinTail(idx);
        String endKey = endPrefix + getMaxTail(idx);

// TreeMap 子区间查询
        NavigableMap<String, List<Integer>> sub =
                logs.subMap(startKey, true, endKey, true);

        List<Integer> res = new ArrayList<>();
for (List<Integer> ids : sub.values()) {
            res.addAll(ids);
        }
return res;
    }

// 不同截断位置,对应的最小尾巴
private String getMinTail(int idx){
switch (idx) {
case4:  return":00:00:00:00:00"; // 年
case7:  return":00:00:00:00";    // 月
case10: return":00:00:00";       // 日
case13: return":00:00";          // 时
case16: return":00";             // 分
case19: return"";                // 秒本身就完整
default: thrownew IllegalArgumentException("idx = " + idx);
        }
    }

// 不同截断位置,对应的最大尾巴
private String getMaxTail(int idx){
switch (idx) {
case4:  return":12:31:23:59:59";
case7:  return":31:23:59:59";
case10: return":23:59:59";
case13: return":59:59";
case16: return":59";
case19: return"";
default: thrownew IllegalArgumentException("idx = " + idx);
        }
    }
}

这个写法有几个小点可以注意:

  1. 时间戳直接用字符串因为格式稳定,所以不必转成 long 或 LocalDateTime,省转换成本,字符串比较就够用了。

  2. 多个日志同一时间戳用 List<Integer> 存 id,一次性都吐出去。

  3. 时间复杂度

  • put():O(logN)(往 TreeMap 插入)
  • retrieve():O(logN + K),K 是命中的日志条数。

还能怎么优化?

如果你知道所有日志都会一次性给你,后面只查不再新增,可以:

  • 先把所有日志放 List
  • 按时间排序
  • 查询时用二分查上下界,就不要 TreeMap 了

但一般面试里,设计成现在这种“随时 put,随时查”更符合题意,用 TreeMap 比较顺手。

大概就这样,这道题关键就是认清几点:

  • 时间戳是“字符串可排序”的
  • 粒度 = 截断 + 补最小/最大尾巴
  • 有序结构上用区间查询,就非常自然了

你真要写的时候,别在时间解析和边界判断上纠结太久,这种题面试官主要看你思路是否清晰、数据结构选得顺不顺。

-END-

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

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