题目描述
某系统的日志监控服务需要实时检测日志里的异常模式。系统每天产生大量日志,每条记录包含一个字符串标识。
异常模式的定义是:同一时间段内至少出现 3 次的日志标识。
请写一个程序,找出所有出现次数大于等于 3 次的日志标识,按下面规则输出:
- 先按出现次数从高到低排序
- 出现次数相同,按首次出现的先后顺序排序
输入描述:所有日志标识以空格分割。标识由大小写字母、数字和下划线组成,长度不超过 50。日志数量范围 [1, 10000]。
输出描述:输出所有出现次数大于等于 3 次的日志标识,按上述规则排序,以空格分割。如果没有符合条件的,输出 NONE。
用例 1:
输入
error_404 error_404 warning_500 error_404 info_200 warning_500 warning_500 info_200
输出
error_404 warning_500
说明:
error_404出现 3 次(第 0、1、3 个),首次出现在第 0 个warning_500出现 3 次(第 2、5、6 个),首次出现在第 2 个info_200只出现 2 次(第 4、7 个),不符合条件error_404和warning_500次数相同,按首次出现先后,先输出error_404,再输出warning_500
(原题文件在说明末尾截断,这里按题目描述和示例补全。题目里"同一时间段内"按整个输入窗口理解,从示例看就是对全部输入做计数。)
讲个故事:值班室的告警轰炸
小张是运维,凌晨三点被电话叫醒,告警群炸了。
几百条日志刷屏,他眼睛都看花。同一类报错反复出现,有的是偶发一下,有的是疯狂刷屏。
组长让他把"短时间冒出来 3 次以上"的日志标识挑出来,按出现次数从多到少列个清单,好优先排查最凶的那几个。
小张抓了下头:这不就是数词频吗?数完挑出大于等于 3 次的,再排个序,次数一样就比谁先登场。
核心原理:哈希计数加双关键字排序
这题本质是词频统计。
扫一遍日志,每碰到一个标识就做两件事:在哈希表里把它的计数加 1,如果是第一次见,顺便记下它最早出现在第几个位置。
扫完之后,把计数大于等于 3 的挑出来,按两个关键字排序:
- 第一关键字:出现次数,从高到低
- 第二关键字:首次出现位置,从早到晚
次数相同就比谁先出场,先出场的排前面。要是挑完一个都没有,就输出 NONE。
说白了,记 count 是基本功,真正考人的是那个第二关键字。很多人排序只按次数排,忘了次数相同还得看首次出现顺序,用例就过不去。
怎么实现?
- 读入所有标识,按空白符切分成数组
- 边扫边用哈希表记
count和firstIdx,第一次见就登记位置 - 遍历哈希表,筛出
count >= 3的 - 按
count降序、firstIdx升序排序 - 结果为空输出
NONE,否则空格拼接输出
读入用按 token 读的方式,单行多行都能兜住,更稳。
代码实现
C 语言
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define HASHSIZE 20011
typedef struct Node {
char key[64];
int count;
int firstIdx;
struct Node *next;
} Node;
Node *table[HASHSIZE];
unsigned int hashStr(const char *s) {
unsigned int h = 0;
for (; *s; s++) h = h * 131u + (unsigned char)*s;
return h % HASHSIZE;
}
Node *findOrInsert(const char *key, int idx) {
unsigned int h = hashStr(key);
Node *p = table[h];
while (p) {
if (strcmp(p->key, key) == 0) return p;
p = p->next;
}
p = (Node *)malloc(sizeof(Node));
strcpy(p->key, key);
p->count = 0;
p->firstIdx = idx;
p->next = table[h];
table[h] = p;
return p;
}
typedef struct {
char key[64];
int count;
int firstIdx;
} Item;
static Item items[10005];
int cmp(const void *a, const void *b) {
const Item *x = a, *y = b;
if (x->count != y->count) return y->count - x->count; // 次数降序
return x->firstIdx - y->firstIdx; // 首次出现升序
}
int main() {
char tok[64];
int idx = 0;
while (scanf("%63s", tok) == 1) {
Node *p = findOrInsert(tok, idx);
p->count++;
idx++;
}
int m = 0;
for (int i = 0; i < HASHSIZE; i++) {
for (Node *p = table[i]; p; p = p->next) {
if (p->count >= 3) {
strcpy(items[m].key, p->key);
items[m].count = p->count;
items[m].firstIdx = p->firstIdx;
m++;
}
}
}
if (m == 0) {
printf("NONE\n");
} else {
qsort(items, m, sizeof(Item), cmp);
for (int i = 0; i < m; i++) {
if (i) printf(" ");
printf("%s", items[i].key);
}
printf("\n");
}
return 0;
}
C++
cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
unordered_map<string, pair<int,int>> mp; // {count, firstIdx}
string tok;
int idx = 0;
while (cin >> tok) {
if (mp.find(tok) == mp.end()) mp[tok] = {0, idx};
mp[tok].first++;
idx++;
}
vector<tuple<int,int,string>> v;
for (auto &p : mp) {
if (p.second.first >= 3)
v.push_back(make_tuple(p.second.first, p.second.second, p.first));
}
if (v.empty()) {
cout << "NONE" << endl;
} else {
sort(v.begin(), v.end(), [](const auto &a, const auto &b) {
if (get<0>(a) != get<0>(b)) return get<0>(a) > get<0>(b); // 次数降序
return get<1>(a) < get<1>(b); // 首次出现升序
});
for (size_t i = 0; i < v.size(); i++) {
if (i) cout << " ";
cout << get<2>(v[i]);
}
cout << endl;
}
return 0;
}
Java
java
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
Map<String, int[]> mp = new HashMap<>(); // [count, firstIdx]
int idx = 0;
while (sc.hasNext()) {
String tok = sc.next();
if (!mp.containsKey(tok)) mp.put(tok, new int[]{0, idx});
mp.get(tok)[0]++;
idx++;
}
List<String> keys = new ArrayList<>();
for (Map.Entry<String, int[]> e : mp.entrySet()) {
if (e.getValue()[0] >= 3) keys.add(e.getKey());
}
if (keys.isEmpty()) {
System.out.println("NONE");
} else {
keys.sort((a, b) -> {
int ca = mp.get(a)[0], cb = mp.get(b)[0];
if (ca != cb) return cb - ca; // 次数降序
return mp.get(a)[1] - mp.get(b)[1]; // 首次出现升序
});
StringBuilder sb = new StringBuilder();
for (int i = 0; i < keys.size(); i++) {
if (i > 0) sb.append(" ");
sb.append(keys.get(i));
}
System.out.println(sb);
}
}
}
JavaScript
javascript
const readline = require('readline');
const rl = readline.createInterface({ input: process.stdin });
let buf = [];
rl.on('line', (line) => buf.push(line));
rl.on('close', () => {
const toks = buf.join(' ').split(/\s+/).filter(t => t.length > 0);
const mp = new Map(); // key -> {count, firstIdx}
let idx = 0;
for (const t of toks) {
if (!mp.has(t)) mp.set(t, { count: 0, firstIdx: idx });
mp.get(t).count++;
idx++;
}
const arr = [];
for (const [k, v] of mp) {
if (v.count >= 3) arr.push({ key: k, count: v.count, firstIdx: v.firstIdx });
}
if (arr.length === 0) {
console.log("NONE");
} else {
arr.sort((a, b) => {
if (a.count !== b.count) return b.count - a.count; // 次数降序
return a.firstIdx - b.firstIdx; // 首次出现升序
});
console.log(arr.map(x => x.key).join(" "));
}
});
Python
python
import sys
def main():
toks = sys.stdin.read().split()
mp = {} # key -> [count, firstIdx]
for idx, t in enumerate(toks):
if t not in mp:
mp[t] = [0, idx]
mp[t][0] += 1
# 筛出次数 >= 3 的,按次数降序、首次出现升序排
arr = [(v[0], v[1], k) for k, v in mp.items() if v[0] >= 3]
if not arr:
print("NONE")
else:
arr.sort(key=lambda x: (-x[0], x[1]))
print(" ".join(x[2] for x in arr))
main()
复杂度分析
- 扫描计数:
O(n),n 是日志条数,最多 10000 - 筛选:
O(m),m 是不同标识数 - 排序:
O(m * log m) - 总时间复杂度:
O(n + m * log m) - 空间复杂度:
O(m)
n 最大 10000,哈希表加排序轻松跑完,毫无压力。
总结一下
日志异常检测 = 词频统计 + 双关键字排序。
套路记牢:
- 哈希表一边计数一边记首次出现位置
- 筛
count >= 3 - 按 count 降序、firstIdx 升序排
- 没结果输出
NONE
最容易被坑的是第二关键字。只按次数排,次数相同的几个顺序就乱了,用例直接挂。顺手记个 firstIdx 当 tiebreaker,稳。
词频统计是 OD 的常客,类似的还有热点网站统计、API 集群负载统计,套路都是这一套,换个壳子而已。
你做词频类的题,习惯用 map 还是手搓哈希表?欢迎在评论区聊聊。