Опубликован: 23.04.2013 | Доступ: свободный | Студентов: 854 / 184 | Длительность: 12:54:00
Лекция 4:

Параллельные алгоритмы

Сортировка массивов

Рассмотрим возможности распараллеливания алгоритмов сортировки массивов.

Пузырьковая сортировка

Начнем с простейшего метода сортировки - пузырьковой сортировки. Идея последовательного алгоритма проста и элегантна. Массив можно рассматривать как некий вертикальный сосуд, заполненный элементами - пузырьками. Для массива из n элементов выполняется n-1 проход. Каждый проход начинается снизу - со дна сосуда, производя последовательный обмен соседних элементов, если нижний элемент "легче" верхнего. В результате прохода "легкие" элементы всплывают. На первом проходе самый легкий элемент окажется вверху сосуда. На i-м проходе на свое место всплывет i-й легкий элемент. Число сравнений на каждом проходе уменьшается на единицу. Временная сложность алгоритма - O(n2). Рис. 3.2 иллюстрирует алгоритм пузырьковой сортировки:

Алгоритм пузырьковой сортировки

Рис. 3.2. Алгоритм пузырьковой сортировки

Приведу текст записи классического варианта алгоритма на языке C#:

/// <summary>
        /// Классический вариант пузырьковой сортировки
        /// Со сложностью O(n * n)
        /// </summary>
        /// <param name="mas">сортируемый массив</param>
        public void BubbleSortClassic(double[] mas)
        {
            int n = mas.Length;            
            double temp = 0;
            for (int k = 0;  k < n - 1; k++)
            { //цикл по числу проходов               
                for (int i = n - 1; i > k; i--)
                {  // цикл всплытия
                    if (mas[i] < mas[i - 1])
                    {//swap
                        temp = mas[i];
                        mas[i] = mas[i - 1];
                        mas[i - 1] = temp;
                    }
                }
            }
        }

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

/// <summary>
        /// Пузырьковая сортировка
        /// Учитывает возможную отсортированность массива
        /// Проходы прекращаются, если отсутствуют обмены 
        /// на предыдущем проходе.
        /// Булевская переменная change следит за обменами
        /// </summary>
        /// <param name="mas">сортируемый массив</param>
        public void BubbleSort(double[] mas)
        {
            int n = mas.Length;
            bool change = true;
            double temp = 0;
            int k = 0;
            for (k = 0; change && k < n-1; k++)
            {
                change = false;
                for(int i = n-1; i > k; i--)
                {
                    if (mas[i] < mas[i - 1])
                    {//swap
                        temp = mas[i];
                        mas[i] = mas[i - 1];
                        mas[i - 1] = temp;
                        change = true;
                    }
                }
            }
        }

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

/// <summary>
        /// Вариант пузырьковой сортировки 
        /// с запоминанием индекса первого обмена
        /// Показывает лучшие результаты. 
        /// </summary>
        /// <param name="mas"></param>
        public void BubbleSortIndex(double[] mas)
        {
            int n = mas.Length;
            bool change = true;
            int index = n - 1;
            int i0 = 0;
            double temp = 0;
            for (int k = 0; change && k < n - 1; k++)
            {
                change = false;
                i0 = index < n - 1 ? index : n - 1;
                for (int i = i0; i > k; i--)
                {
                    if (mas[i] < mas[i - 1])
                    {//swap
                        temp = mas[i];
                        mas[i] = mas[i - 1];
                        mas[i - 1] = temp;
                        if(!change) index = i + 1;
                        change = true;                        
                    }
                }
            }

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

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

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

Слияние фрагментов массива можно выполнять, используя дополнительный массив, в который в нужном порядке будут сливаться элементы отсортированных фрагментов, как это делается в классической процедуре сортировки слиянием, когда сливаются два массива. В этом случае сложность слияния определяется как O(p * n). Нужно поставить на место n элементов, и на каждом шаге элемент нужно выбрать из p кандидатов. В целом сложность параллельного варианта пузырьковой сортировки задается соотношением O(n^2/p^2 +n\cdot p).

Можно выполнять слияние на месте, но тогда сложность в худшем случае будет O(n), поскольку на каждом шаге придется восстанавливать отсортированность того фрагмента массива, который содержал минимальный элемент. После обмена место минимального элемента займет другой элемент, который может нарушить отсортированность фрагмента. Поскольку элемент, который попадает в верхушку отсортированного фрагмента, является минимальным элементом другого фрагмента, то для восстановления отсортированности обычно нужно выполнять небольшое число обменов. Учитывая, что вероятность худшего варианта мала, такая версия алгоритма может представлять практический интерес в ситуации, когда приходится экономить память.

Приведу текст реализации шагового варианта алгоритма, в котором слияние использует дополнительный массив:

/// <summary>
        /// Параллельный вариант пузырьковой сортировки         
        /// </summary>
        /// <param name="mas">сортируемый массив</param>
        /// <param name="p">число процессоров</param>
        public void BubbleParallel(double[] mas, int p)
        {
            int n = mas.Length;
            if (p > n) p = n;
            bool[] change = new bool[p];
            int[] index = new int[p];
            for (int i = 0; i < p; i++)
                index[i] = n - i - 1;
            int i0 = 0, m = 0;
            double temp = 0;
            //Цикл по числу процессоров
            for (int j = 0; j < p; j++)               
               {// процессор j сортирует подпоследовательность элементов, 
                //начинающихся индексом n -j -1 и отстоящих на расстоянии р
                   //цикл по числу проходов m
                   m = index[j] / p;
                   for (int k = 0; k < m; k++)
                   {
                       //цикл всплытия легкого элемента на k-м проходе
                       i0 = index[j] < n - j - 1 ? index[j] : n - j - 1;
                       change[j] = false;
                       for (int i = i0; i - p >= k * p; i = i - p)
                       {
                           if (mas[i] < mas[i - p])
                           {//swap
                               temp = mas[i];
                               mas[i] = mas[i - p];
                               mas[i - p] = temp;

                               if (!change[j]) index[j] = i + p;
                               change[j] = true;
                           }
                       }
                   }
               }
            //Слияние отсортированных последовательностей
            // Merge(mas, p);
            Merge1(mas, p);
        }

Процедура слияния p отсортированных фрагментов массива, где элементы каждого фрагмента отстоят на расстоянии p, имеет следующий вид:

/// <summary>
        /// Слияние упорядоченных последовательностей
        /// Последовательности представляют отрезки с шагом p
        /// Используется дополнительная память
        /// </summary>
        /// <param name="mas">сортируемый массив</param>
        /// <param name="p">число процессоров</param>
        public void Merge1(double[] mas, int p)
        {
            int n = mas.Length;
            int m = n / p;
            int index_min = 0;
            double min = 0;
            int i = 0;
            double[] tmas = new double[n];
            int[] start = new int[p], finish = new int[p];
            for (i = 1; i <= p; i++)
            {
                finish[p-i] = n - i;
                start[p - i] = finish[p - i] % p;               
            }
            for (int k = 0; k < n; k++)
            {//пересылка k-ого элемента
                //поиск кандидата
                i = 0;
                while (start[i] > finish[i]) i++;
                index_min = i; min = mas[start[i]];

                for (int j = i + 1; j < p; j++)
                {
                    //цикл по кандидатам
                    if (start[j] <= finish[j])
                    {
                        if (mas[start[j]] < min)
                        {
                            min = mas[start[j]];
                            index_min = j;
                        }
                    }
                }
                //pass                        
                tmas[k] = mas[start[index_min]];
                start[index_min] += p;
            }
            for (i = 0; i < n; i++)
                mas[i] = tmas[i];
        }

Эксперименты показывают, что этот вариант сортировки работает значительно эффективнее классического варианта пузырьковой сортировки уже на массивах длины 100. И это происходит даже в том случае, когда параллельные вычисления не выполняются и обработка идет последовательно. Объясняется это тем, что из-за пошагового характера алгоритма данный вариант сортировки приближается к версии сортировки Шелла.

Алексей Рыжков
Алексей Рыжков

не хватает одного параметра:

static void Main(string[] args)
        {
            x = new int[n];
            Print(Sample1,"original");
            Print(Sample1P, "paralel");
            Console.Read();
        }

Никита Белов
Никита Белов

Выставил оценки курса и заданий, начал писать замечания. После нажатия кнопки "Enter" окно отзыва пропало, открыть его снова не могу. Кнопка "Удалить комментарий" в разделе "Мнения" не работает. Как мне отредактировать недописанный отзыв?