Как ответить
Задача сводится к группировке записей таблицы users по категориям для заданного пользователя, подсчёту количества кликов в каждой группе и выбору категории с максимальным значением. В Haskell это естественно решается через комбинацию filter, groupBy (или group после сортировки) и maximumBy.
Допустим, таблица users представлена списком кортежей (Int, String) — (userId, category). Тогда решение будет выглядеть так:
import Data.List (group, sort, maximumBy)
import Data.Ord (comparing)
mostPopularCategory :: Int -> [(Int, String)] -> String
mostPopularCategory uid rows =
let userRows = filter ((== uid) . fst) rows
categories = map snd userRows
grouped = group (sort categories)
counted = map (\g -> (head g, length g)) grouped
(bestCat, _) = maximumBy (comparing snd) counted
in bestCat
Разбор по шагам:
- Фильтрация —
filter ((== uid) . fst) rowsоставляет только строки нужного пользователя. - Извлечение категорий —
map snd userRowsдаёт список категорий для этого пользователя. - Группировка —
group (sort categories)собирает одинаковые категории в подсписки. Сортировка обязательна, иначеgroupсгруппирует только соседние элементы. - Подсчёт —
map (\g -> (head g, length g))превращает каждую группу в пару (категория, количество). - Выбор максимума —
maximumBy (comparing snd)находит пару с наибольшим вторым элементом.
Если частоты одинаковые, maximumBy вернёт первую попавшуюся максимальную категорию — это удовлетворяет условию «любую». Можно уточнить поведение: если список после фильтрации пуст (пользователь не найден), код упадёт с ошибкой. На практике стоит добавить обработку через Maybe:
mostPopularCategorySafe :: Int -> [(Int, String)] -> Maybe String
mostPopularCategorySafe uid rows = case counted of
[] -> Nothing
_ -> Just bestCat
where
userRows = filter ((== uid) . fst) rows
categories = map snd userRows
grouped = group (sort categories)
counted = map (\g -> (head g, length g)) grouped
(bestCat, _) = maximumBy (comparing snd) counted
Для больших объёмов данных (миллионы строк) лучше использовать Data.Map.Strict с аккумулятором — это даст O(n log n) вместо O(n log n) через сортировку, но с меньшими константами и без лишнего хранения:
import qualified Data.Map.Strict as Map
mostPopularCategoryMap :: Int -> [(Int, String)] -> String
mostPopularCategoryMap uid rows =
let userRows = filter ((== uid) . fst) rows
freqMap = Map.fromListWith (+) [(cat, 1) | (_, cat) <- userRows]
(bestCat, _) = maximumBy (comparing snd) (Map.toList freqMap)
in bestCat
В реальном проекте я бы выбрал второй вариант: он короче, не требует сортировки и легко адаптируется под потоковую обработку (например, через foldl').