За последние 24 часа нас посетил 22651 программист и 1042 робота. Сейчас ищут 647 программистов ...

Сопоставлении массивов — как ускорить?

Тема в разделе "Прочие вопросы по PHP", создана пользователем starryknight, 19 ноя 2016.

  1. starryknight

    starryknight Новичок

    С нами с:
    19 ноя 2016
    Сообщения:
    8
    Симпатии:
    0
    Друзья, передо мной стоит задача исключить из списка словосочетаний те, которые полностью входят в другие. Реализовано с помощью такой функции. Все прекрасно работает, пока массив маленький. 1000 записей обрабатываются за 3 секунды, а вот 3000 за 28 (?!) секунд. Мне нужно пройтись по 100к. Наверняка, есть способ сделать этот код лучше и быстрее. Но какой?
    з.ы. протестить на 1000 можно тут
    PHP:
    1. function leave_good_tails($array){  
    2.         $array = explode("\n", $array);
    3.         $array = array_map('rtrim', $array);
    4.             foreach($array as $str) {                
    5.                     //разбиваем строку на слова
    6.                     $wds = explode(" ", $str);          
    7.                         foreach($array as $str2) {              
    8.                             $wds2 = explode(" ", $str2);              
    9.                                 //не будем сопоставлять строку саму с собой
    10.                                 if ($str2 != $str){
    11.                                     $intersect = array_intersect($wds2, $wds);
    12.                                     $count_needle = count($wds2);                  
    13.                                     $count_intersect = count($intersect);                      
    14.                                     //Если все слова меньшего массива найдены
    15.                                     if ($count_intersect == $count_needle){                                  
    16.                                         $all_doubles[] = $str2;
    17.                                     }                      
    18.                                 }              
    19.                         }
    20.                 }
    21.        
    22.         //Теперь отнимем дубли из основного массива
    23.         echo "<p>Список без вхождений, очищенный от дубликатов:";
    24.         $new_array = array_diff($array,$all_doubles);
    25.             foreach ($new_array as $word){
    26.                 echo "</br>".$word;
    27.             }
    28. }
    29.  
    30. $start = microtime(true);
    31. $array = "ура
    32. солнце светит
    33. бодро светит солнце ";
    34. leave_good_tails($array);
    35. echo 'Время выполнения скрипта: '.(microtime(true) - $start).' сек.';
     
  2. Fell-x27

    Fell-x27 Суперстар
    Команда форума Модератор

    С нами с:
    25 июл 2013
    Сообщения:
    12.155
    Симпатии:
    1.769
    Адрес:
    :сердА
    1)
    Ну дык, там у вас сложность O(n^2), а не O(n); Потому и время растет не линейно.

    3000 ровно в 3 раза больше, чем 1000. Три в квадрате - девять. Итого, время выполнения вырастает в 9 раз. Тысяча отрабатывала за 3 секунды, значит, новое время будет 9*3 = 27 секунд. Очень даже похоже на то, что вы описали, если учитывать погрешности и допуски измерений.

    2) У вас не по словосочетаниям поиск идет, а по словам. Это несколько разные вещи.
    3) Входящий массив точно должен быть просто строкой со словами, разделенными пробелами, а не набором словосочетаний, каждое в своей ячейке? Потому что массив - это массив. А у вас просто строка.
     
    #2 Fell-x27, 19 ноя 2016
    Последнее редактирование: 19 ноя 2016
    denis01 нравится это.
  3. starryknight

    starryknight Новичок

    С нами с:
    19 ноя 2016
    Сообщения:
    8
    Симпатии:
    0
    1) похоже
    2) Беру первое словосочетание "солнце светит", разбиваю на слова. Беру второе "бодро светит солнце", разбиваю на слова — с тем, чтобы сопоставить каждое слово с каждым. Сопоставляю слова, по их сопоставлению делаю вывод о совпадении словосочетаний : если каждое слово 1 словосочетания нашлось во 2 словосочетании считаю 1 словосочетание дубликатом. Может криворуко написала, но сама желаемая логика выполняется, только медленно.
    3) Входящий массив мне дается as is — это просто строка, каждое словосочетания с новой строки. Я его explode по переводу строки — получается массив строк, с которым я дальше и работаю.
     
  4. Fell-x27

    Fell-x27 Суперстар
    Команда форума Модератор

    С нами с:
    25 июл 2013
    Сообщения:
    12.155
    Симпатии:
    1.769
    Адрес:
    :сердА
    А если они в другом порядке даны?
    Или в этом и есть цель, чтобы идентифицировать "светит солнце" и "солнце светит" как одинаковые?
    Если нет, то
    Это лишнее.
    В таком случае достаточно просто сделать сплит по переводу строки, как у вас, а потом вызвать https://php.net/manual/ru/function.array-unique
     
  5. starryknight

    starryknight Новичок

    С нами с:
    19 ноя 2016
    Сообщения:
    8
    Симпатии:
    0
    Вы совершенно правы, в этом и цель. "светит солнце", "солнце светит бодро": считаем 1 дублем (хоть и порядок разный, и слово "бодро" добавилось), а второе оставляем.
     
  6. Fell-x27

    Fell-x27 Суперстар
    Команда форума Модератор

    С нами с:
    25 июл 2013
    Сообщения:
    12.155
    Симпатии:
    1.769
    Адрес:
    :сердА
    Хм..вечером попробую поковырять интереса академического ради.
    Но, сходу вопрос, установлен ли какой-нибудь xDebug или еще что-то? Оно может заметно влиять на производительность кода.
     
  7. Fell-x27

    Fell-x27 Суперстар
    Команда форума Модератор

    С нами с:
    25 июл 2013
    Сообщения:
    12.155
    Симпатии:
    1.769
    Адрес:
    :сердА
    Ваш алгоритм, кстати, работает некорректно. Если ему скормить
    Код (Text):
    1.  
    2. ура
    3. солнце светит
    4. бодро светит солнце
    5. бодро светит солнце
    6. бодро светит солнце
    7. бодро светит солнце
    то результатом будет

    Код (Text):
    1. ура
    2. бодро светит солнце
    3. бодро светит солнце
    4. бодро светит солнце
    5. бодро светит солнце
    Дубликатов останется полным полно в итоге.
     
  8. Fell-x27

    Fell-x27 Суперстар
    Команда форума Модератор

    С нами с:
    25 июл 2013
    Сообщения:
    12.155
    Симпатии:
    1.769
    Адрес:
    :сердА
    Попробуйте вот такую функцию:
    PHP:
    1. function leave_good_tails($array)
    2. {
    3.     $array = explode("\n", $array);
    4.     echo 'Обработано строк: ' . sizeof($array);
    5.  
    6.     $array = array_map('rtrim', $array);
    7.     $array = array_unique($array);
    8.  
    9.     foreach ($array as $first_key => $first_operand) {
    10.  
    11.         if ($first_operand === '') {
    12.             unset($array[$first_key]);
    13.             continue;
    14.         }
    15.  
    16.         $exploded_first_operand = explode(' ', $first_operand);
    17.         foreach ($array as $second_key => $second_operand) {
    18.             if ($first_key == $second_key) {
    19.                 continue;
    20.             }
    21.             $exploded_second_operand = explode(' ', $second_operand);
    22.             $intersection = sizeof(array_intersect($exploded_first_operand, $exploded_second_operand));
    23.             if ($intersection === sizeof($exploded_first_operand)) {
    24.                 unset($array[$first_key]);
    25.                 break;
    26.             }
    27.         }
    28.     }
    29.  
    30.     echo "<p>Список без вхождений, очищенный от дубликатов:<hr>";
    31.     foreach ($array as $word) {
    32.         echo "</br>" . $word;
    33.     }
    34. }
    Кроме того, что работать будет гарантированно в разы быстрее, она во много раз экономичнее по расходу памяти. Ну и результат выдает корректнее, чем у вас в данный момент.
    --- Добавлено ---
    А если подумать...можно сделать иначе.
    --- Добавлено ---
    У меня данный вариант отработал 65+ тысяч "типа уникальных значений" за 4 минуты.
    Ваш исходный алгоритм отрабатывал бы примерно 3.5 часа.

    Но можно, по идее, сделать еще быстрее.
     
  9. Fell-x27

    Fell-x27 Суперстар
    Команда форума Модератор

    С нами с:
    25 июл 2013
    Сообщения:
    12.155
    Симпатии:
    1.769
    Адрес:
    :сердА
    Вот еще как вариант.
    PHP:
    1. function leave_good_tails($array)
    2. {
    3.     $result = [];
    4.     $array = explode("\n", $array);
    5.     echo 'Обработано строк: ' . sizeof($array);
    6.  
    7.     $array = array_unique($array);
    8.     $array_copy = array_map(
    9.         function ($element) {
    10.             $element = trim($element);
    11.             $result = explode(' ', $element);
    12.             natcasesort($result);
    13.             return implode(' ', $result);
    14.         }, $array);
    15.  
    16.     $array_copy = array_unique($array_copy);
    17.  
    18.     foreach ($array_copy as $first_key => $first_operand) {
    19.  
    20.         if ($first_operand === '') {
    21.             unset($array_copy[$first_key]);
    22.             continue;
    23.         }
    24.  
    25.         $exploded_first_operand = explode(' ', $first_operand);
    26.         foreach ($array_copy as $second_key => $second_operand) {
    27.             if ($first_key == $second_key) {
    28.                 continue;
    29.             }
    30.             $exploded_second_operand = explode(' ', $second_operand);
    31.             $intersection = sizeof(array_intersect($exploded_first_operand, $exploded_second_operand));
    32.             if ($intersection === sizeof($exploded_first_operand)) {
    33.                 unset($array_copy[$first_key]);
    34.                 break;
    35.             }
    36.         }
    37.     }
    38.  
    39.     foreach (array_keys($array_copy) as $key) {
    40.         $result[] = $array[$key];
    41.     }
    42.  
    43.  
    44.     echo "<p>Список без вхождений, очищенный от дубликатов:<hr>";
    45.     foreach ($result as $word) {
    46.         echo "</br>" . $word;
    47.     }
    48. }
    В идеальных условиях может быть до 2 раз быстрее предыдущего. И экономичнее.
    Но надо тестить на реальных данных. Там видно будет.
     
  10. starryknight

    starryknight Новичок

    С нами с:
    19 ноя 2016
    Сообщения:
    8
    Симпатии:
    0
    Насколько изящное и простое решение! Мы просто не будем по сто раз перебирать одно и то же.
    Этот сервис phpfiddle исполнял мой скрипт за 28 секунд (3000 записей), ваш первый вариант за 17 секунд, а второй — 18 секунд. Протестить большой объем на нем не вышло — у них ограничение на 30 секунд. На простом апаче ваш первый вариант обработал у меня 35 тысяч за 120 минут. Сейчас запустила на fast-cgi, но там тоже крутится уже около 40 минут.
    Думаю, дело в настройках сервера, вам же удалось за 4 минуты в 2 раза больший объем прогнать. Большое вам спасибо за помощь. Буду думать, что с настройками не так может быть.
     
  11. Fell-x27

    Fell-x27 Суперстар
    Команда форума Модератор

    С нами с:
    25 июл 2013
    Сообщения:
    12.155
    Симпатии:
    1.769
    Адрес:
    :сердА
    Еще, быть может, дело в мощностях. И входные данные входным данным рознь. По-хорошему надо постараться обойтись только лишь встроенными функциями, без велосипедов. Они - это чистый Си. Они работают на порядки быстрее. Тысячи элементов жуют за сотые доли секунды.
    --- Добавлено ---
    А какая версия php у вас используется? Это тоже очень важно. Нужна хотя бы 5.6, в идеале - 7.
     
  12. Fell-x27

    Fell-x27 Суперстар
    Команда форума Модератор

    С нами с:
    25 июл 2013
    Сообщения:
    12.155
    Симпатии:
    1.769
    Адрес:
    :сердА
    И да, если у вас стоит задача как можно быстрее обсчитать огромные объемы данных, то, вероятно, проблема в постановке задачи или в выборе инструментария. PHP создан не для таких целей. Ну и да, было бы гораздо легче, если бы выложили сюда архивчиком актуальные входные данные. Хотя бы те же тысяч 30. Уже будет более менее достаточно для тестирований.
     
  13. starryknight

    starryknight Новичок

    С нами с:
    19 ноя 2016
    Сообщения:
    8
    Симпатии:
    0
    Понимаю, что php не самый подходящий инструмент, но это, то чем я хотя бы минимально владею, вот и пытаюсь здесь реализовать. Судя по всему, дело еще и в версии php (5.3), буду в этом направлении двигаться. Так-то без таких задач оно вроде и без разницы, но видно пришла пора ;) А входные данные самые простые — поисковые запросы, загрузила пример на 35К.
     

    Вложения:

    • 35000.zip
      Размер файла:
      364,3 КБ
      Просмотров:
      4
  14. Fell-x27

    Fell-x27 Суперстар
    Команда форума Модератор

    С нами с:
    25 июл 2013
    Сообщения:
    12.155
    Симпатии:
    1.769
    Адрес:
    :сердА
    Разница есть.
    На php 5.3 у вас бы строка
    выдала бы ошибку. Этот синтаксис был введен начиная с 5.4
    --- Добавлено ---
    А теперь это стало похоже на проблему "X-Y".
    Расскажите, какие цели преследуются? Зачем это нужно, что в конечном счете должно получиться и как должно использоваться? Быть может, вы решаете задачу "не с той стороны".
     
  15. Fell-x27

    Fell-x27 Суперстар
    Команда форума Модератор

    С нами с:
    25 июл 2013
    Сообщения:
    12.155
    Симпатии:
    1.769
    Адрес:
    :сердА
    Взял из вашей выборки на обум пачку строк, оказалось, 2325. Обработал. Получил...2325 строк.
    Полистал ваш массив. Он состоит из уникальных запросов на 99%. Обработка не имеет смысла. По крайней мере обработка по той схеме, которую вы предложили. Любой из приведенных выше алгоритмов лишь впустую будет тратить время.

    Из-за этого вопросы по части преследуемых целей становятся еще острее. Потому как путь их достижения был выбран не верный. Вы пытаетесь собрать какую-то аналитику?
     
  16. starryknight

    starryknight Новичок

    С нами с:
    19 ноя 2016
    Сообщения:
    8
    Симпатии:
    0
    Вы правы, $result=[]; и выдала ошибку. Версия php из phpinfo. Этот файл на 35 тысяч мне удалось обработать за 7170 секунд (2 часа) с помощью вашего 1 скрипта. 26 тысяч осталось, около 9 отсеялось. Результат проверили вручную частично — все отлично работает, то, что нужно осталось, что не нужно отфильтровалось. Боюсь, по целям рассказать особенно нечего, кроме того, что уже было. Запрос, полностью входящий в другой запрос, нужно удалить, чтобы избежать дублирования. "солнце светит ярко" удаляем, если есть запрос "солнце светит ярко нам". Удалим "солнце светит ярко нам", если найдется запрос "солнце светит ярко нам всем". Оставим только самый длинный.
     
  17. Chushkin

    Chushkin Активный пользователь

    С нами с:
    17 дек 2010
    Сообщения:
    1.062
    Симпатии:
    91
    Адрес:
    Мещёра, Центр, Болото N3
    Коллеги, вы зациклились на классическом алгоритме "сравнение массивов".
    Откажитесь от этой идеи и сделайте 2-х/3-х проходной алгоритм - скоростя вырастут на порядки. Думаю, цель/задача позволяет.
     
  18. Fell-x27

    Fell-x27 Суперстар
    Команда форума Модератор

    С нами с:
    25 июл 2013
    Сообщения:
    12.155
    Симпатии:
    1.769
    Адрес:
    :сердА
    А зачем? Куда эти запросы идут? Что это даст?

    Это 0.02% от проверенной массы. Неужели оно стоит таких затрат?

    Да, была такая мысль, но я стопорнулся, увидев, что входящий контент и без того уникален и в фильтре нужды и нет по сути. Тут хоть сколько проходов делай, эффекта не будет. На финальном проходе все равно придется обработать почти полный объем данных. Хотя, смотря что ты имел ввиду. Я сейчас про деление на равные части, независимую их проверку, потом слияние результатов, деление на n-1 частей, проверку и так далее, пока в конце не окажется пласт финальный.

    Если ты просто про нарезку на равные части и их независимый прогон, то это не зайдет - совпадение из первого фрагмента может лежать только в последнем. Тогда фильтр не отработает.
     
  19. Chushkin

    Chushkin Активный пользователь

    С нами с:
    17 дек 2010
    Сообщения:
    1.062
    Симпатии:
    91
    Адрес:
    Мещёра, Центр, Болото N3
    >> ... деление на n-1 частей, проверку и так далее, пока в конце не окажется пласт финальный.

    Это нелинейный алгоритм, а я про линейный. Ну, или "почти линейный".
     
  20. Fell-x27

    Fell-x27 Суперстар
    Команда форума Модератор

    С нами с:
    25 июл 2013
    Сообщения:
    12.155
    Симпатии:
    1.769
    Адрес:
    :сердА
    Или ты что-то иное имел ввиду? Делись.
     
  21. igordata

    igordata Суперстар
    Команда форума Модератор

    С нами с:
    18 мар 2010
    Сообщения:
    32.410
    Симпатии:
    1.768
    про солнце я не вкурил
    1. есть "солнце светит". мы ищем.
    2. находим "солце светит нам", херим первый, который короче, т.е. "солце светит"
    3. натыкаемся на "солнце светит всем". Возникает вопросы: кто главнее? нафига удаляли? и почему именно его?..
     
  22. Fell-x27

    Fell-x27 Суперстар
    Команда форума Модератор

    С нами с:
    25 июл 2013
    Сообщения:
    12.155
    Симпатии:
    1.769
    Адрес:
    :сердА
    Потому что пункт два не правильный у тебя. "солнце светит нам" - это расширенная версия "солнце светит". Значит, "солнце светит" не нужно.
     
  23. igordata

    igordata Суперстар
    Команда форума Модератор

    С нами с:
    18 мар 2010
    Сообщения:
    32.410
    Симпатии:
    1.768
    я написал именно это, как мне кажется. Именно по этому, не понятно, что делать в случае нахождения фразы, которая совпадает с изначальной.

    Короче мне насрать конечно, но я вижу как минимум два взаимоисключающих подхода, которые зависят от задачи. А задачу я не понимаю толком.
     
    denis01 нравится это.
  24. Poznakomlus

    Poznakomlus Активный пользователь

    С нами с:
    12 сен 2014
    Сообщения:
    96
    Симпатии:
    19
    Адрес:
    Киев
    Странно, что здесь не упомянули про diff
    С помощью данных алгоритмов и следовало решать задачу, упростив алгоритм для сравнения слов
     
  25. Chushkin

    Chushkin Активный пользователь

    С нами с:
    17 дек 2010
    Сообщения:
    1.062
    Симпатии:
    91
    Адрес:
    Мещёра, Центр, Болото N3
    Я был неправ, у этой задачи нет линейного алгоритма. Я так думаю...
    Но можно заметно ускорить, если использовать индексацию.
    Например, так:
    PHP:
    1. function f0($text) {
    2.     $source = explode("\n", mb_strtolower($text));
    3.     $b = $bb = $idx = [];
    4.     foreach($source as $k=>$v) {
    5.         if(!$v) continue;
    6.         $b[$k] = explode(' ', $v);
    7.         foreach($b[$k] as $kk=>$w) {
    8.             if($w == '') { unset($b[$k][$kk]); continue; };
    9.             $idx[$w][$k] = 1;
    10.         }
    11.     }
    12.     foreach($b as $k=>$v) { // (самый медленный кусок)
    13.         $c = [];
    14.         foreach($v as $kk=>$w) {
    15.             $c = $kk == 0 ? $idx[$w] : array_intersect_key($c, $idx[$w]);
    16.         }
    17.         $bb[$k] = $c;
    18.     }
    19.     foreach($bb as $k=>$v) {
    20.         foreach($v as $kk=>$vv) {
    21.             if($kk != $k and isset($bb[$kk])) {
    22.                 unset($bb[$k]);
    23.             }
    24.         }
    25.     }
    26.     // Восстановить массив исходных данных (строк) и отфильтровать
    27.     return array_intersect_key(explode("\n", $text), $bb);
    28. }
    У меня те 35933 строки от ТС отрабатывает за 6.5 секунд, оставляя 26386 строк. (php-5.6.18, Win7, 3Ггц). Для обработки требуется ~60 Мб RAM.
    Зависимость времени от числа строк квадратичное (примерно).

    п.с. Вообще, задачка оказалась интересной, не тривиальной.