2026华为OD面试题020:日志文件异常检测

题目描述

某系统的日志监控服务需要实时检测日志里的异常模式。系统每天产生大量日志,每条记录包含一个字符串标识。

异常模式的定义是:同一时间段内至少出现 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_404warning_500 次数相同,按首次出现先后,先输出 error_404,再输出 warning_500

(原题文件在说明末尾截断,这里按题目描述和示例补全。题目里"同一时间段内"按整个输入窗口理解,从示例看就是对全部输入做计数。)

讲个故事:值班室的告警轰炸

小张是运维,凌晨三点被电话叫醒,告警群炸了。

几百条日志刷屏,他眼睛都看花。同一类报错反复出现,有的是偶发一下,有的是疯狂刷屏。

组长让他把"短时间冒出来 3 次以上"的日志标识挑出来,按出现次数从多到少列个清单,好优先排查最凶的那几个。

小张抓了下头:这不就是数词频吗?数完挑出大于等于 3 次的,再排个序,次数一样就比谁先登场。

核心原理:哈希计数加双关键字排序

这题本质是词频统计。

扫一遍日志,每碰到一个标识就做两件事:在哈希表里把它的计数加 1,如果是第一次见,顺便记下它最早出现在第几个位置。

扫完之后,把计数大于等于 3 的挑出来,按两个关键字排序:

  • 第一关键字:出现次数,从高到低
  • 第二关键字:首次出现位置,从早到晚

次数相同就比谁先出场,先出场的排前面。要是挑完一个都没有,就输出 NONE

说白了,记 count 是基本功,真正考人的是那个第二关键字。很多人排序只按次数排,忘了次数相同还得看首次出现顺序,用例就过不去。

怎么实现?

  1. 读入所有标识,按空白符切分成数组
  2. 边扫边用哈希表记 countfirstIdx,第一次见就登记位置
  3. 遍历哈希表,筛出 count >= 3
  4. count 降序、firstIdx 升序排序
  5. 结果为空输出 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 还是手搓哈希表?欢迎在评论区聊聊。

相关推荐
炸薯条!1 小时前
从零开始学C++(4) --类和对象
开发语言·c++·算法
haluhalu.1 小时前
prompts.chat:07-few-shot-prompting
人工智能·算法
Jerry1 小时前
LeetCode 150. 逆波兰表达式求值
算法
浩瀚地学1 小时前
【面试算法笔记】0106-数组-区间和
笔记·算法·面试
Jerry7 小时前
LeetCode 1047. 删除字符串中的所有相邻重复项
算法
可编程芯片开发9 小时前
基于霍尔传感器和PID控制器的有功功率检测控制系统simulink建模与仿真
算法
令狐掌门10 小时前
2026华为OD面试题018:智能成绩表
华为od
To_OC10 小时前
LC 17 电话号码的字母组合:我的回溯算法,就是从这道题开窍的
javascript·算法·leetcode
战族狼魂12 小时前
GPT-5.6与Grok 4.5重磅发布
人工智能·算法·大模型·大语言模型