элегантный способ подсчета предметов - PullRequest
9 голосов
/ 18 мая 2011

У меня есть список в форме:

  '(("Alpha" .  1538)
    ("Beta"  .  8036)
    ("Gamma" .  8990)
    ("Beta"  .  10052)
    ("Alpha" .  12837)
    ("Beta"  .  13634)
    ("Beta"  .  14977)
    ("Beta"  .  15719)
    ("Alpha" .  17075)
    ("Rho"   .  18949)
    ("Gamma" .  21118)
    ("Gamma" .  26923)
    ("Alpha" .  31609))

Как подсчитать общее количество вхождений терминов в машину каждого элемента в списке?В основном я хочу:

(("Alpha" . 4)
 ("Beta" . 5)
 ("Gamma" . 3)
 ("Rho" . 1))

Нет, это не домашняя работа.У меня просто пока нет мысли о мышлении на Лиспе.

В C # я бы использовал LINQ для этого.Я могу сделать это и в lisp, используя циклы while и тому подобное, но то, как я думаю об этом, кажется слишком сложным.


РЕДАКТИРОВАТЬ

Этоэто то, что у меня есть:

(defun count-uniq (list)
  "Returns an alist, each item is a cons cell where the car is
a unique element of LIST, and the cdr is the number of occurrences of that
unique element in the list. "
  (flet ((helper (list new)
                 (if (null list)
                     new
                   (let ((elt (assoc (car list) new)))
                     (helper (cdr list)
                             (if elt
                                 (progn (incf (cdr elt)) new)
                               (cons (cons (car list) 1) new)))))))
    (nreverse (helper list nil))))

Ответы [ 9 ]

5 голосов
/ 18 мая 2011
(defun freqs (list &optional test key)
  (let ((h (make-hash-table :test test)))
    (dolist (x list)
      (let ((key (if key (funcall key x) x)))
        (puthash key (1+ (gethash key h 0)) h)))
    (let ((r nil))
      (maphash #'(lambda (k v) (push (cons k v) r)) h)
      (sort r #'(lambda (x y) (< (cdr x) (cdr y)))))))

(freqs '(("Alpha" .  1538)
         ("Beta"  .  8036)
         ("Gamma" .  8990)
         ("Beta"  .  10052)
         ("Alpha" .  12837)
         ("Beta"  .  13634)
         ("Beta"  .  14977)
         ("Beta"  .  15719)
         ("Alpha" .  17075)
         ("Rho"   .  18949)
         ("Gamma" .  21118)
         ("Gamma" .  26923)
         ("Alpha" .  31609))
       #'equal #'car)
4 голосов
/ 19 мая 2011

Объединение функций Common Lisp более высокого уровня:

(defun count-unique (alist) 
  (mapcar
    (lambda (item)
      (cons (car item)
            (count (car item) alist :test #'equal :key #'car)))
    (remove-duplicates alist :test #'equal :key #'car)))

Это не масштабируется до больших списков, хотя. Если вам нужна производительность O (n), используйте вместо этого решение на основе хеш-таблицы, например, менее элегантное:

(defun count-unique (alist)
  (loop
     with hash = (make-hash-table :test #'equal)
     for (key . nil) in alist
     do (incf (gethash key hash 0))
     finally (return
               (loop for key being each hash-key of hash
                  using (hash-value value)
                  collect (cons key value)))))
2 голосов
/ 19 мая 2011

Использование расширений Common Lisp:

(require 'cl)
(loop with result = nil
      for (key . dummy) in original-list
      do (incf (cdr (or (assoc key result)
                        (first (push (cons key 0) result)))))
      finally return (sort result
                           (lambda (a b) (string< (car a) (car b)))))

Вы можете просто сказать finally return result, если вам не нужна сортировка окончательного результата.

2 голосов
/ 18 мая 2011

Не знаю, что это самое элегантное, но кажется разумным:

(defun add-for-cheeso (data)
  (let (result)
    (dolist (elt data result)
      (let ((sofar (assoc (car elt) result)))
        (if sofar
            (setcdr sofar (1+ (cdr sofar)))
          (push (cons (car elt) 1) result))))))
1 голос
/ 04 августа 2014

Каждый раз, когда вы хотите просмотреть список и впоследствии вернуть какое-либо значение, будь то новый список или какой-либо совокупный результат, вы думаете о fold , также называемом «Reduce» в Python и Lisps.Fold - отличная абстракция, поскольку она позволяет писать общий код, применимый для многих вариантов использования, просто настраивая некоторые элементы.Что сходно между поиском суммы из нескольких чисел, поиском товара, поиском минимального целого числа?Все они складываются, потому что вы просматриваете список, а затем возвращаете некоторый результат на основе его содержимого.В Emacs Lisp они выглядят так:

(reduce '+ '(1 2 3 4 5)) ; 15
(reduce '* '(1 2 3 4 5)) ; 120
(reduce 'min '(1 2 3 4 5)) ; 1

Но сгибы даже более общие, чем это.Что сходно между поиском суммы, подсчетом числа четных чисел в списке, удалением каждого нечетного числа и построением списка с каждым числом, увеличенным на 5?Каждая такая функция может быть реализована путем принятия некоторого базового значения, последовательно преобразовывая его, пока вы не получите результат.Вы берете это базовое значение, метафорический шарик глины, называете его «аккумулятор», затем берете один элемент из списка и, основываясь на этом элементе, делаете что-то с этим шариком глины, делая его черновиком великолепной скульптуры.Затем вы берете следующий элемент из списка и делаете что-то новое для вашей скульптуры.Вы повторяете это до тех пор, пока список не станет пустым, и вы не получите шедевр.Как будто каждый элемент списка представляет собой отдельную инструкцию в большом рецепте.Просто имейте в виду, что вы абсолютно свободны делать что-либо с глиной, вам не нужно напрямую использовать элементы списка в результате - технически это означает, что аккумулятор (и, следовательно, результат) может иметьотличается тип .

(reduce '+ '(1 2 3 4 5) :initial-value 0) ; 15
(reduce (lambda (acc x) (if (evenp x) (1+ acc) acc)) '(1 2 3 4 5) :initial-value 0) ; 2
(reduce (lambda (x acc) (if (oddp x) acc (cons x acc))) '(1 2 3 4 5) :initial-value '() :from-end t) ; (2 4)
(reduce (lambda (x acc) (cons (+ x 5) acc)) '(1 2 3 4 5) :initial-value '() :from-end t) ; (6 7 8 9 10)

Примечание о сокращении с конца: списки в Лиспе не являются интеллектуальными массивами, как в Python или Java, они являются связанными списками, поэтому обращаются к или изменяютЭлемент где-то в списке является операцией O (n), в то время как «обращение» к началу списка - это O (1).Другими словами, добавление элемента в конец списка является дорогостоящим, поэтому Лисперс обычно добавляет элементы в начало списка, а затем, наконец, переворачивает список, который называется push / nreverse idiom .Если бы мы делали обычное уменьшение в последних 2 функциях, мы бы взяли 1 к аккумулятору и получили (1), затем 2 к аккумулятору и получили (2 1), пока мы не получим правильный результат, но в обратном порядке.Впоследствии мы могли бы использовать функцию reverse, но, к счастью, reduce Emacs поддерживает :from-end аргумент ключевого слова, поэтому он равен 5, затем 4, затем 3 и т. Д.

Теперь все ясно,что ваша операция складывается, проследите исходный список и посчитайте вхождения каждой клавиши.Прежде чем писать наш фолд, давайте сначала поговорим об алистах.Alist в Lisp - хэш-таблица для бедняков.Вы обычно не возитесь с деталями реализации хэш-таблицы языка программирования, не так ли?Вы работаете с API.В Python этот API выглядит как синтаксис в квадратных скобках (d['a'] = 1) и методы dict (d.keys()).API для списков списков содержит функцию assoc, которая возвращает элемент при условии ключа.

(assoc 'a '((a . 1) (b . 2))) ; (a . 1)

Почему я говорю о деталях реализации?Поскольку вы работаете через assoc и вам все равно, как точно выглядит этот список, вы абстрагируете его.Другая часть API заключается в том, что если вы хотите добавить новый элемент или изменить существующий, вы просто добавляете пунктирную пару в список.Это то, как вы должны работать со списками, независимо от их внутренней структуры.Почему это работает?Например, если я хочу изменить значение для ключа a на 10, я бы просто запустил (cons '(a . 10) my-alist), и my-alist в итоге получило бы '((a . 10) (a . 1) (b . 2)).Но это не проблема, потому что assoc возвращает только первую пару с точками и игнорирует остальные, поэтому вы можете обрабатывать alist точно так же, как и любую другую структуру данных ключ-значение.Имея это в виду, давайте напишем наш первый серьезный фолд.

(reduce (lambda (acc x)
          (let* ((key (car x))
                 (pair (assoc key acc))
                 (count (cdr pair)))
            (if pair
                (cons (cons key (1+ count)) acc)
              (cons (cons key 1) acc))))
        my-alist
        :initial-value '())

Что здесь происходит?Мы берем ваши данные и пустой список, который вскоре станет нашим желаемым результатом.На каждом шаге мы берем пару из данных и спрашиваем: содержит ли наш результат информацию об этой паре?Если нет, то мы добавляем его к результату и ставим 1 - мы встретили этот ключ впервые.Однако, если мы найдем информацию об этой паре в нашем результате, то мы должны снова добавить ее к нашему результату, но на этот раз с числом, увеличенным на 1. Повторите эту процедуру для каждого элемента в ваших данных, и вы получите:

(("Alpha" . 4) ("Gamma" . 3) ("Gamma" . 2) ("Rho" . 1) ("Alpha" . 3)
 ("Beta" . 5) ("Beta" . 4) ("Beta" . 3) ("Alpha" . 2) ("Beta" . 2)
 ("Gamma" . 1) ("Beta" . 1) ("Alpha" . 1))

Помните, что assoc заботится только о первом появлении ключа?Этот список будет вести себя так же, как если бы он был просто (("Alpha" . 4) ("Gamma" . 3) ("Rho" . 1) ("Beta" . 5)), так что мы здесь хорошо.Тем не менее, можем ли мы изменить наш фолд, чтобы получить последний, более короткий результат?Подожди, что за необходимость слишком усложнять наш фолд, если бы мы могли потом просто подправить результат?В конце концов, что такое компьютерное программирование, если не серия преобразований данных?Нет причины, по которой вы не могли бы просто удалить все «устаревшие» пары из своего списка, просто используйте cl-remove-duplicates с правильными аргументами , и все готово.

Итак, мыГордясь собой, мы написали фолд, столп функционального программирования, но тщательное изучение выявляет неэффективность: мы пересекаем аккумулятор с assoc, чтобы найти пару и ее значение для приращения.assoc принимает O (n), reduce само принимает O (n), поэтому наш алгоритм O (n²) (читайте о порядок роста , если вы не понимаете нотацию Big-O).Понятно, что вместо этого нам лучше работать с надлежащей оптимизированной хэш-таблицей и преобразовывать ее в alist, когда это необходимо.Перепишите наш сгиб:

(reduce (lambda (acc x)
          (cl-incf (gethash (car x) acc 0))
          acc)
        my-alist
        :initial-value (make-hash-table :test 'equal))

(gethash k d 0) эквивалентно Python d.get('k', 0), где последний аргумент является значением по умолчанию.cl-incf (эквивалент Common Lisp incf) - это интеллектуальный макрос, который увеличивает свой аргумент на месте (читайте о setf, чтобы понять умные назначения).make-hash-table требует пользовательской тестовой функции, потому что строки нельзя сравнивать со стандартной eql функцией.Чтобы получить список, просто преобразуйте хеш-таблицу результатов нашего фолда с помощью функции ht->alist, которую мы либо берем из библиотеки ht.el Уилфреда, либо пишем сами:

(defun ht->alist (table)
  (let (alist)
    (maphash (lambda (k v)
               (push (cons k v) alist))
             table)
    alist))
1 голос
/ 19 мая 2011

Использование функций старшего порядка сортировка и уменьшение .

Сначала сортировка (с использованием строка <</em>), затем уменьшение (подсчет последовательно string = значения в клетках cons):

(reduce (lambda (r e)
          (if (and r (string= (caar r) e))
              (cons
               (cons (caar r) (1+ (cdar r)))
               (cdr r))
            (cons (cons e  1) r)))
        (sort (mapcar 'car alist) 'string<)
        :initial-value nil)
1 голос
/ 19 мая 2011
(require 'cl)
(defun count-uniq (list)
  (let ((k 1) (list (sort (mapcar #'car list) #'string<)))
    (loop for (i . j) on list
          when (string= i (car j)) do (incf k)
          else collect (cons i k) and do (setf k 1))))
0 голосов
/ 26 января 2017

Это довольно просто и очень просто с помощью библиотеки dash :

(require 'dash)

(defun frequencies (values)
  "Return an alist indicating the frequency of values in VALUES list."
  (mapcar (-lambda ((value . items))
          (cons value (length items)))
        (-group-by #'identity
                   values)))

(frequencies (mapcar #'car my-list))
0 голосов
/ 21 мая 2011

Вот то, что я считаю элегантным функциональным решением, использующим списочные функции Emacs, с возможностью использования многократно используемой функции frequencies, аналогичной ответу Eli:

...