Алгоритм пузырьковой сортировки никак не следит за текущим состоянием массива.

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

Оптимизация сортировки пузырьком

Быстродействие пузырьковой сортировки в JavaScript можно улучшить, если добавить флаг (логическую переменную) который будет следить за тем был ли хотя бы один обмен на текущей итерации.

Если нет, то массив отсортирован и задача выполнена.

Чтобы убедиться в уменьшении количества итераций алгоритма, добавим счетчик outerLoopIterationCount. Будем увеличивать его на каждом проходе внешнего цикла и возвращать когда массив окажется отсортирован.

const optimizedBubbleSort = (arr) => {
  let hasSwapped = false;
  let outerLoopIterationCount = 0;
  
  for (let i = 0; i < arr.length; i++) {
    for (let j = 0; j < arr.length - i; j++) {
      if (arr[j] > arr[j + 1]) {
        hasSwapped = true;
        let tmp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = tmp;
      }
    }
    if (!hasSwapped) {
      return outerLoopIterationCount;
    } else {
      hasSwapped = false;
    }
    outerLoopIterationCount++;
  }
  return outerLoopIterationCount;
}

Для проверки возьмем два массива. Второй в два раза длиннее, но в нем всего лишь один элемент не на своем месте.

  • выводим на экран начальное состояние массивов
  • сортируем их и сохраняем количество итераций, которое вернет функция сортировки optimizedBubbleSort
  • выводим на экран массивы еще раз, чтобы убедиться в том, что они отсортированы и проверить количество итераций, которое понадобилось для сортировки
const testData = [ 0, -1, 4, 5, 2, -3 ];
const almostSortedTestData = [ 12, -3, -1, 0, 2, 4, 5, 7, 8, 9, 10 ];

console.log(testData, `Initial testData state`);
console.log(almostSortedTestData, `Initial almostSortedTestData state`);

const iterationsTestData = optimizedBubbleSort(testData);
const iterationsAlmostSortedTestData = optimizedBubbleSort(almostSortedTestData);

console.log(testData, `Total iterations: ${iterationsTestData}`);
console.log(almostSortedTestData, `Total iterations: ${iterationsAlmostSortedTestData}`);

В консоли увидим такой результат:

[ 0, -1, 4, 5, 2, -3 ] Initial testData state
[ 12, -3, -1, 0, 2, 4, 5,  7, 8, 9, 10 ] Initial almostSortedTestData state

[ -3, -1, 0, 2, 4, 5 ] Total iterations: 6
[ -3, -1, 0, 2,  4, 5, 7, 8, 9, 10, 12 ] Total iterations: 2

Хотя второй массив и оказался в 2 раза длиннее, нам нужно было всего две итерации внешнего цикла, чтобы отсортировать его. Уже на втором проходе, флаг hasSwapped не изменился. Это значит, что обменов не было и массив уже отсортирован. Мы сразу же завершили алгоритм улучшенной сортировки пузырьком и не тратили лишнее время.

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

const testData = [ 0, 1, 2, 3, 4, 5, 6 ];

console.log(testData, `Initial testData state`);

const iterationsTestData = optimizedBubbleSort(testData);

console.log(testData, `Total iterations: ${iterationsTestData}`);

Вывод на экран:

[ 0, 1, 2, 3, 4, 5, 6 ] Initial testData state
[ 0, 1, 2, 3, 4, 5, 6 ] Total iterations: 1

Шейкерная сортировка

Coctail sort или Шейкерная сортировка - это еще одно улучшение “пузырька”. Альтернативные названия этой сортировки - сортировка перемешиванием или bidirectional sort.

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

Оказавшись в начале массива, на своих местах будет уже 2 элемента - первый и последний. Остановимся когда дойдем до середины массива. Таким образом сделаем в 2 раза меньше итераций внешнего цикла. За счет этого скорость шейкерной сортировки будет немного выше, чем у обычного пузырька.

Начнем с небольшого рефакторинга и вынесем функцию обмена наружу. Назовем ее swap :

function swap(arr, i, j) {
  let tmp = arr[i];
  arr[i] = arr[j];
  arr[j] = tmp;
}

Реализуем алгоритм шейкерной сортировки на JavaScript:

function cocktailSort(arr) {
  let left = 0;
  let right = arr.length - 1;
  let hasSwapped = false;
  let outerLoopIterationCount = 0;

  while (left < right) {
    outerLoopIterationCount++;
    for (let i = left; i < right; i++) {
      if (arr[i] > arr[i + 1]) {
        swap(arr, i, i + 1);
        hasSwapped = true;
      }
    }
    right--;
    for (let i = right; i > left; i--) {
      if (arr[i] < arr[i - 1]) {
        swap(arr, i, i - 1);
        hasSwapped = true;
      }
    }
    left++;
    if (!hasSwapped) {
      return outerLoopIterationCount;
    } else {
      hasSwapped = false;
    }
  }
  return outerLoopIterationCount;
}

И убедимся на том же тестовом массиве, что алгоритм работает и действительно делает в 2 раза меньше итераций внешнего цикла:

const testData = [ 0, -1, 4, 5, 2, -3 ];

console.log(testData, `Initial testData state`);
const iterationsTestData = cocktailSort(testData);
console.log(testData, `Total iterations: ${iterationsTestData}`);

Как видим, массив отсортирован, и количество итераций 3 вместо 6 у optimizedBubbleSort:

[ 0, -1, 4, 5, 2, -3 ] Initial testData state
[ -3, -1, 0, 2, 4, 5 ] Total iterations: 3