Перевернуть строку на PHP

Самая популярная задача на собеседованиях это переворот строки без использования функции srtrev. Решил добавить свои 5 копеек, какой я вижу эту задачу.

Первое решение на знание других встроенных функций (никто ведь не запрещал использовать их).

$s = "123abc";

preg_match_all('/./u', $s, $a);
echo implode('', array_reverse($a[0]));

Второе решение с использованием операции конкатенации выглядит еще проще.

$s = "123abc";

for ($i = strlen($s); $i >= 0; $i--) {
    $s .= $s[$i];
    $s[$i] = '';
}

И напоследок решение которым можно удивить своего работодателя, если он слабо понимает булеву алгебру.

$s = "123abc";
$a = -1;
$b = strlen($b);

while (++$a < --$b) {
    $s[$a] = $s[$a] ^ $s[$b];
    $s[$b] = $s[$a] ^ $s[$b];
    $s[$a] = $s[$a] ^ $s[$b];
}

Сниппет для форматирования телефонных номеров

Задача. Есть большой список телефонных номеров, которые нужно переформатировать под единый формат (или форматы). Номера есть нескольких типов 7, 10 и 11 значные. Для каждого из этих типов необходимо вывести номер телефона в своем формате.

В php есть такие функции как money_format и number_format, но нет такой функции как phone_format, этот пробел я и решил восполнить написав такую функцию для форматирования телефонных номеров.

/**
 * Форматирование телефонного номера
 * по шаблону и маске для замены
 *
 * @param string $phone
 * @param string|array $format
 * @param string $mask
 * @return bool|string
 */
function phone_format($phone, $format, $mask = '#')
{
    $phone = preg_replace('/[^0-9]/', '', $phone);

    if (is_array($format)) {
        if (array_key_exists(strlen($phone), $format)) {
            $format = $format[strlen($phone)];
        } else {
            return false;
        }
    }

    $pattern = '/' . str_repeat('([0-9])?', substr_count($format, $mask)) . '(.*)/';

    $format = preg_replace_callback(
        str_replace('#', $mask, '/([#])/'),
        function () use (&$counter) {
            return '${' . (++$counter) . '}';
        },
        $format
    );

    return ($phone) ? trim(preg_replace($pattern, $format, $phone, 1)) : false;
}

Использование

Передаем в качестве параметров не отформатированный номер телефона, шаблон и маску для замены. В качестве шаблона также можно передать массив ключи которого это количество цифр в телефоне, а значение шаблоны замены

$phones = array(
    '926 111-2233',
    '9261112233',
    '8 (926) 111 22 33',
    '8 926 111-22-33',
    '559-8833',
    '5598833',
    '',
    'qweqwe'
);

$formats = array(
    '7' => '###-##-##',
    '10' => '+7 (###) ### ####',
    '11' => '# (###) ### ####'
);

foreach ($phones AS $phone) {
    echo phone_format($phone, $formats, '#');
}

Результат выполнения радует глаз:

+7 (926) 111 2233
+7 (926) 111 2233
8 (926) 111 2233
8 (926) 111 2233
559-88-33
559-88-33

Данный сниппет не ставит перед собой цель определить город, регион или какой-либо другой параметр. Кроме того возникнут проблемы при использовании шаблонов для номеров “8 (123) 111-22-33” и “+7 (123) 111-22-33”. Возможно потом придумаю как поступать с такими номерами.

Развертывание Node.js приложения на DotCloud

Dotcloud - это облачная платформа для развертывания приложений. Стек на DotCloud включает в себя более 10 различных сервисов среди которых есть и Node.js.

Мои первые впечатления от DotCloud были весьма положительные. Можно практически забыть о том как развертывать приложение и полностью сосредоточится на коде. Работа по развертыванию сводится к установке DotCloud CLI и настройке файла окружения dotcloud.yml. Установка клиента под Linux/MacOS тривиальна:

sudo easy_install pip
sudo pip install dotcloud

Подготовка к развертыванию

Для развертывания приложения на Dotcloud необходим файл dotcloud.yml, который описывает сервисы к которым приложение будет иметь доступ. Полный список приложений можно посмотреть по ссылке. Для доступа к стеку NodeJS + Redis файл dotcloud.yml может иметь следующий вид:

www:
  type: nodejs

data:
  type: redis

Если приложение на NodeJS использует дополнительные пакеты, то можно использовать файл package.json, в котором указать зависимости от других пакетов. Пакеты будут автоматически установлены при развертывании. Пример файла package.json:

{
  "engines": {
    "node": ">=v0.4.5"
  },
  "dependencies": {
    "redis": ">=0.6.6",
    "express": ">=2.4.2"
  }
}

Для автоматического запуска приложений на NodeJS необходимо создать третий файл с названием supervisord.conf и указать в нем путь запуска

[program:node]
command = node index.js
directory = /home/dotcloud/current

Использование environment.json

Получить доступ к стеку сервисов которые были указаны в файле dotcloud.yml очень просто. После развертывания приложения к домашней директории создается файл environment.json, содержащий информацию для доступа к сервисам. Используя этот файл можно настроить приложение для автоматического использования настроек к базам данных не заботясь о ручной настройке. Поскольку я указал Redis в качестве базы данных в файле dotcloud.yml и назвал его data, то для получения настроек к Redis из приложения на NodeJS достаточно написать:

var envfilepath = '/home/dotcloud/environment.json',
    environment = JSON.parse(require('fs').readFileSync(envfilepath));

var host = environment['DOTCLOUD_DATA_REDIS_HOST'],
    port = environment['DOTCLOUD_DATA_REDIS_PORT'],
    pass = environment['DOTCLOUD_DATA_REDIS_PASSWORD'];

Развертывание

Для создания приложения на Dotcloud необходимо написать в консоле:

dotcloud create appname

appname - в данном случае имя приложения, которое может быть любым. Ну и непосредственно пуш приложения на Dotcloud

dotcloud push appname ~/path-to-node-app/

При успешном деплое выдадут www адрес для доступа к приложению

Deployment finished successfully. Your application is available at the following URLs
www: http://d07c100d.dotcloud.com/

Что еще можно сделать

Использование собственного доменного имени. Для этого надо прописать DNS запись для соответствующего домена:

CNAME gateway.dotcloud.com.

И добавить алиас к своему приложению на Dotcloud:

dotcloud alias add appname.www www.example.com

Также можно подключится по SSH

dotcloud ssh appname.www

Полный список команд можно получить в подробной документации на официальном сайте.

Сокращатель ссылок на Node и Redis

Началось с того, что я решил попробовать Redis в каком-нить проекте, а поскольку давно планировал сделать собственную сокращалку ссылок, то решил именно ее и написать. Связка NodeJS + Redis вообще показалась наиболее легко реализуемой. Закончилось тем что помимо Redis написал еще и реализацию для MongoDB.

Не буду вдаваться в подробности сравнения MongoDB и Redis, на эту тему итак уже написано куча статей (Comparing MongoDB And Redis Part 1 и Part 2. Для реализации сокращалки Redis’а хватает за глаза, он вообще более дружественен и прост в реализации к такого рода задачам.

Скачать мой Node Url Shortener можно на Github.

Для установки надо подтянуть дополнительные пакеты из npm. Прежде всего это ExpressJS, а также пакет для работы с Redis или MongoDB.

git clone https://github.com/dotzero/node-url-shortener nus
cd nus
npm install

Далее надо отредактировать файл ./config.js и запустить app.js через NodeJS. Планирую пользовать исключительно через API, поэтому с веб-интерфейсом особо не заморачивался. Реализация API почти такая же как у goo.gl.

Для сокращения ссылок:

GET /api/v1/shorten/?long_url=http://www.google.com

JSON ответ

{
  status_code: 200
  status_txt: "OK"
  hash: "Mw"
  url: "http://localhost/Mw"
  long_url: "http://www.google.com"
}

И для разворачивания коротких ссылок

GET /api/v1/expand/?short_url=http://localhost/Mw

Исправление ошибок при установке Redmine 1.2 на Ubuntu Server 11.04

Как я убедился на собственном опыте, большинство мануалов по установке Redmine 1.2 являются просто копипастом старых мануалов, в которых меняют номера версий. Из всех просмотренных мною статей, больше всего мне понравилась статья Manage Projects with Redmine on Ubuntu 11.04. Но даже используя ее у меня возникло пару ошибок, поэтому захотелось сделать этот мини howto.

При попытке миграции базы данных, вылетает ошибка:

rake/rdoctask is deprecated.  Use rdoc/task instead (in RDoc 2.4.2+)

Связано с тем что по-умолчанию ставится более новая версия Rake. Для исправления можно удалить свою версию (посмотреть версию rake –version) и поставить принудительно версию 0.8.7 которая подходит для RubyOnRails 2.3.11

gem uninstall -v=0.9.2 rake
gem install -v=0.8.7 rake

Следующая ошибка которая у меня возникла. Ошибка в методе version_requirements

undefined local variable or method `version_requirements'' for #<:gemdependency:0x7face0b79690>

Чтобы это исправить надо отредактировать файл /config/environment.rb. Найти в начале файла строки:

# Bootstrap the Rails environment, frameworks, and default configuration
require File.join(File.dirname(__FILE__), ''boot'')

И дописать сразу после них условие

if Gem::VERSION >= "1.3.6"
    module Rails
        class GemDependency
            def requirement
                r = super
                (r == Gem::Requirement.default) ? nil : r
            end
        end
    end
end

Других проблем при установке у меня не возникло.

Как установить OS X 10.7 Lion с USB

Купив Льва сразу захотелось сделать себе бэкап флешку, на случай если что-то поломается. Оказалось что хоть Lion и устанавливается через App Store но из него можно вполне изготовить установочную флешку, для этого надо:

  1. Купив льва не запускать установку, а зайти в папку /Программы/
  2. Найти там Install Mac OS X Lion.app (Установка Mac OS X Lion)
  3. Нажать правой кнопкой и выбрать Показать содержимое пакета
  4. В открывшемся окне заходим в Contents/SharedSupport
  5. Копируем файл InstallESD.dmg это и есть имидж со Львом
  6. Вставляем флешку
  7. Открываем /Программы/Утилиты/Дисковая утилита
  8. Выбираем флешку в списке слева
  9. А в списке справа выбираем вкладку Восстановить
  10. Источник тот самый InstallESD.dmg
  11. Восстановить

Для того чтобы установить с нее надо во время загрузки Mac’а зажать Alt

Разграничение доступа с использованием Gitosis

Gitosis - это система управления несколькими git-репозиториями и управления доступом к ним без необходимости создавать под каждого разработчика отдельного пользователя системы. Все все разработчики будут входить через общий аккаунт и индивидуальные ssh ключи. Преимущества такого решения:

  • Нет необходимости заведения пользователей под каждого разработчика
  • Простая и быстрая настройка доступа к репозиториям
  • Работа через SSH-протокол

Установка Gitosis

Т.к. gitosis написан на python, то для его установки необходим python и пакет python-setuptools

sudo apt-get install python python-setuptools

Ставим непосредственно gitosis. В конечном счете все сводится к клонированию репозитория gitosis и установке через setuptools

git clone git://eagain.net/gitosis.git
cd gitosis
python setup.py install

Настройка на сервере

Нам нужен пользователь под которым будет работать gitosis. Как правило такого пользователя называют git. Добавляем пользователя и группу.

sudo adduser --system --group --disabled-password --home /home/git git

Далее необходим ssh ключ с помощью которого можно будет управлять настройками gitosis, можно использовать свой публичный ключ, если есть или сгенерировать новый (не забыв при этом приватную часть перенести на тот компьютер с которого планируется администрирование gitosis).

ssh-keygen -b 2048 -t rsa -f /tmp/gitosis -C "Gitosis admin"

Запускаем gitosis от имени пользователи git

sudo -H -u git gitosis-init < /tmp/gitosis.pub

Т.к. конфиг gitosis хранится в виде репозитория и применяется при git push, то необходимо сделать исполняем post-update хук.

sudo chmod 755 /home/git/repositories/gitosis-admin.git/hooks/post-update

Управление репозиториями

Теперь для добавление новых репозиториев и установки доступов к ним необходимо с клиентской машины, на которой есть приватная часть ключа от gitosis’а, клонировать репозиторий конфигурации.

git clone git@АДРЕС_СЕРВЕРА:gitosis-admin.git
cd gitosis-admin

В папке будет локальная версия конфигурации gitosis.conf и папка keydir. Открываем файл gitosis.conf, содержимое будет примерно следующее:

[group gitosis-admin]
writable = gitosis-admin
members = username

Для удобства я рекомендую добавить уровень отладки, который позволит выводить намного больше информации при push’e коммитов в репозитории, для этого надо добавить следующие строки в файл конфигурации:

[gitosis]
loglevel = DEBUG

Для добавления нового репозитория, в конфиг необходимо добавить следующее:

[group projectname]
writable = project_name
members = developer1 developer2

[group projectname-read]
readonly = project_name
members = developer3

Мы добавили две группы projectname и projectname-read. В первой группе мы разрешили разработчикам developer1 и developer2 писать в репозиторий project_name, а во второй группе мы разрешили разработчику developer3 только читать из этого же репозитория.

Можно не создавать секцию с доступом readonly, когда нет необходимости предоставлять доступ только на чтение. Имена групп в конфиг файле не на что не влияют но должны быть уникальны в рамках файла.

Открытые ключи указанных в конфиге пользователей нужно скопировать в каталог gitosis-admin/keydir. Важно чтобы файлы ключей имели имена вида имя_пользователя.pub. В данном примере в папку необходимо добавить публичные ключи developer1.pub, developer2.pub, developer3.pub.

Для сохранения всех настроек надо закоммитить изменения в репозиторий и push’ить на сервер.

git commit -am "Добавлен проект project_name"
git push

Теперь разработчик developer1 может начать работать на проектом. Для этого он просто создает локальный репозиторий project_name и добавляет в качестве удаленного репозитория сервер с gitosis.

mkdir project_name
cd project_name
git init
git remote add origin git@АДРЕС_СЕРВЕРА:project_name.git

Теперь, можно пушить свой репозиторий на сервер с gitosis. Удаленный репозиторий создастся автоматически при первом коммите.

git push origin master

На этом простейшая настройка gitosis закончена.

Обязательная авторизация на Yii

Есть такие сайты на которых необходимо чтобы каждый посетитель сначала авторизовался, прежде чем увидеть содержимое сайта. К таким проектам в основном относятся системы администрирования, хотя и не только. Поиски в интернете не дали никаких вменяемых ответов как реализовать такой механизм на Yii, так что пришлось разбираться самому.

Как оказалось сделать подобную конструкцию довольно просто. Для этого в контроллере необходимо переопределить метод beforeAction, например так:

class Controller extends CController
{
    // ...

    protected function beforeAction($action)
    {
        if (Yii::app()->user->isGuest AND $this->id . '/' . $action->id !== 'account/login') {
            Yii::app()->user->loginRequired();
        }

        return true;
    }

    // ...
}

После этого все контроллеры, к которым надо закрыть доступ, наследовать от этого контроллера вместо CController. Если проект создавался через yiic webapp, то такой контроллер можно найти в /protected/components/Controller.php. Таким образом все не авторизованные пользователи обращаясь к любому контроллеру будут принудительно перенаправлены на страницу авторизации, установленную в конфиге (loginUrl).

Использование Sphinx для нахождения ближайших объектов по координатам

Для одного из проектов потребовалось реализовать программный функционал поиска ближайших объектов на карте в определенном радиусе от заданной точки с координатами широты и долготы. Такие задачи как правило требуют решения Прямой и Обратной геодезических задач, а поскольку с геодезией я совершенно не дружу, то обратил свое внимание на готовые решения, одним из который стал Sphinx.

Наверняка многие воспринимают Sphinx исключительно как полнотекстовый поисковый движок, я же хотел бы обратить внимание на магическую функцию @geodist, которую можно использовать для нахождения в индексе объектов на заданном расстоянии по их координатам (широте и долготе). Для нахождения ближайших объектов требуется не много:

  • База данных поддерживаемая Sphinx’ом с координатами объектов поиска
  • Установленный Sphinx Search
  • А также PHP + sphinxapi (или любой другой язык для которого есть sphinxapi)

База данных

Я не буду описывать процесс установки Sphinx’а на хостинг, мануалов в инете полно да и сам процесс установки банален и не требует каких-то особенных умений. Ниже привожу примерный файл конфига который понадобится для индексации таблицы с координатами объектов

# Источник данных для поиска
source main
{
    # параметры подключения к mysql
    type = mysql
    sql_host = localhost
    sql_user = user
    sql_pass = password
    sql_db = database
    sql_port = 3306

    # запросы после установки соединения
    sql_query_pre = SET NAMES utf8
    sql_query_pre = SET SESSION query_cache_type=OFF

    # не засыпать между шагами индексации
    sql_ranged_throttle = 0
}

# Источник данных для гео-поиска (наследует блок main)
source geo : main
{
    # запрос выборки широты и долготы в радианах для индексации
    sql_query = \
        SELECT `id`,
            radians(`latitude`), \
            radians(`longitude`) \
        FROM table_with_objects

    # обрабатывать поля как float
    sql_attr_float = latitude
    sql_attr_float = longitude
}

# настройка индекса
index geo
{
    # использовать соответствующий блок из source
    source = geo

    # путь для хранения файлов индекса
    path = /usr/home/www/sphinx/indexes/closest

    docinfo = extern
    mlock = 0
    min_word_len = 1
    charset_type = utf-8
}

# настройки демона
searchd
{
    # прослушивание порта
    listen = 9312

    # хранение логов
    log = /usr/home/www/sphinx/log/searchd.log
    query_log = /usr/home/www/sphinx/log/query.log
    pid_file = /usr/home/www/sphinx/log/searchd.pid
}

Индексация

При первоначальном запуске просто скармливаем созданных конфиг индексатору

indexer --config /usr/home/www/sphinx/sphinx.conf --all

Для переиндексации текущего индекса необходимо использовать параметр «–rotate», он добавит к созданному индексу новые данные

indexer --config /usr/home/www/sphinx/sphinx.conf --rotate

Запускаем демона

searchd --config /usr/home/www/sphinx/sphinx.conf

PHP + sphinxapi

Для проверки работы создадим простую форму

<form action="" method="get">
    Долгота <input name="longitude" size="40" value="<?=$_GET['longitude']?>"><br>
    Широта <input name="latitude" size="40" value="<?=$_GET['latitude']?>" /><br>
    Радиус <input name="radius" size="40" value="<?=$_GET['radius']?>"><br>
    <input type="submit" value="Искать!">
</form>

<?php
if(isset($_GET['longitude']) and strlen($_GET['longitude']) > 2)
{
    require_once('sphinxapi.php');

    function collectIds($arr)
    {
        return $arr['id'];
    }

    $_longitude = $_GET['longitude'];
    $_latitude = $_GET['latitude'];
    $_radius = (intval($_GET['radius']) > 0) ? intval($_GET['radius']) : 10;

    $search = new SphinxClient(); // Создание экземпляра клиента

    $search->SetServer("localhost", 9312); // Подсключаемся
    $search->SetMatchMode(SPH_MATCH_ALL); //
    $search->SetArrayResult(true); // Не использовать id документов в качестве ключей
    $search->SetLimits(0, 5); // Ограничить поиск 5 объектами
    $search->SetSortMode ( SPH_SORT_EXTENDED, "@geodist asc"); // Сортировать объекты по расстоянию от исходной точки поиска
    $search->SetGeoAnchor('latitude', 'longitude', deg2rad($_latitude), deg2rad($_longitude));

    $circle = (float) $_radius;
    $search->SetFilterFloatRange('@geodist', 0.0, $circle);

    $result = $search->Query('', 'closestStores');

    if($result AND $result['total'] > 0)
    {
        print_r($result);

        $ids = array_map('collectIds', $result['matches']);

        print_r($ids);
    }
}
?>

Если все настроено правильно, то под формой отобразится массив с результатами выборки.

Простой алгоритм случайной выборки с учетом веса

Иногда вам может понадобиться выбрать случайный элемент из списка с учетом того, что некоторые элементы имеют больший шанс выбора, чем другие (имеют больший “вес”). Например, вы можете взять список приложений и количество загрузок, и случайным образом выбрать “Популярное приложение” в зависимости от количества загрузок.

В этой заметке я покажу вам два подхода к “взвешенному” случайному выбору - один подходит для небольших списков, а другой оптимизирован для большего числа элементов.

Простой алгоритм случайной выборки с учетом веса

В общем виде этот алгоритм можно описать так:

  1. Выбрать случайное число между единицей и суммой “весов” всех элементов
  2. Спускаться по списку элементов добавляя к счетчику вес текущего элемента
  3. Проверить, если счетчик (шаг №2) больше или равен случайному числу (шаг №1), то закончить цикл и вернуть текущий элемент. В противном случае перейдите к шагу №2.

Этот алгоритм прост в реализации и довольно быстр, когда число элементов не велико, или когда вам нужно сделать выбор один раз. Ниже приводится функция, которая принимает массив элементов для выбора, а также массив соответствующих им весов, и возвращает случайно выбранный элемент из первого массива. Вы можете использовать любое целое положительное число, как вес.

/**
 * Выборка случайного элемента с учетом веса
 *
 * @param array $values индексный массив элементов
 * @param array $weights индексный массив соответствующих весов
 * @return mixed выбранный элемент
 */
function weighted_random_simple ( $values, $weights )
{
    $total = array_sum( $weights );
    $n = 0;

    $num = mt_rand( 1, $total );

    foreach ( $values as $i => $value )
    {
        $n += $weights[$i];

        if ( $n >= $num )
        {
            return $values[$i];
        }
    }
}

Вот пример скрипта который выведет либо A, B, C или с вероятностью 15%, 35% и 50% соответственно:

$values = array('A', 'B', 'C');
$weights = array(3, 7, 10);

echo weighted_random_simple($values, $weights);

Алгоритм случайной выборки из тысяч элементов

Описанный выше алгоритм может работать очень медленно, когда список элементов велик, и вам необходимо сделать несколько выборок. Это потому, что он должен пройтись по всему массиву каждый раз при обращении к функции.

Однако, алгоритм может быть расширен, чтобы сделать его значительно быстрее. Вместо вычисления общего веса (шаг №1) и счетчика (шаг №2) каждый раз, можно сделать это один раз и сохранить значения счетчиков в массиве. Тогда мы сможем использовать бинарный поиск, чтобы быстро выбрать правильный элемент. Ниже приведен модифицированный вариант функции:

/**
 * Случайно выбирает один из элементов на основе их веса.
 * Оптимизирован для большого числа элементов.
 *
 * @param array $values индексный массив элементов
 * @param array $weights индексный массив соответствующих весов
 * @param array $lookup отсортированный массив для поиска
 * @param int $total_weight сумма всех весов
 * @return mixed выбранный элемент
 */
function weighted_random($values, $weights, $lookup = null, $total_weight = null)
{
    if ($lookup == null OR $total_weight == null)
    {
        list($lookup, $total_weight) = calc_lookups($values, $weights);
    }

    $r = mt_rand(1, $total_weight);

    return $values[binary_search($r, $lookup)];
}

/**
 * Создание массива используемого в бинарном поиске
 *
 * @param array $values
 * @param array $weights
 * @return array
 */
function calc_lookups($values, $weights)
{
    $lookup = array();
    $total_weight = 0;

    for ($i=0; $i < count($weights); $i++)
    {
        $total_weight += $weights[$i];
        $lookup[$i] = $total_weight;
    }

    return array($lookup, $total_weight);
}

/**
 * Ищет в массиве элемент по номеру и возвращает элемент если он найден.
 * В противном случае возвращает позицию, где он должен быть вставлен,
 * или count($haystack)-1, если $needle больше чем любой элемент в массиве.
 *
 * @param int $needle
 * @param array $haystack
 * @return int
 */
function binary_search($needle, $haystack)
{
    $high = count($haystack) - 1;
    $low = 0;

    while ( $low < $high )
    {
        $probe = (int)(($high + $low) / 2);

        if ($haystack[$probe] < $needle)
        {
            $low = $probe + 1;
        }
        elseif ($haystack[$probe] > $needle)
        {
            $high = $probe - 1;
        }
        else
        {
            return $probe;
        }
    }

    if ( $low != $high )
    {
        return $probe;
    }
    else
    {
        return ($haystack[$low] >= $needle) ? $low : $low + 1;
    }
}

Описанный выше скрипт также содержит две новые функции - calc_lookups которая вычисляет массив для использования в бинарном поиске, и непосредственно функция binary_search которая осуществляет бинарный поиск. Пример использования скрипта:

// Рассчет массивов (1 раз)
list($lookup, $total_weight) = calc_lookups($values, $weights);
//....
// Каждый раз когда вам необходимо выбрать случайный элемент:
$val = weighted_random($values, $weights, $lookup, $total_weight);

В заключение

Чтобы дать вам представление о том, какова скорость этих алгоритмов: Для каждого из них я использовал массив включающий 10 000 элементов, 10 000 раз подряд. Первый алгоритм отработал за 13 секунд, а второй всего 0,09 секунд.