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

Я продемонстрирую некоторые общие граничные случаи в базовом алгоритме пузырьковой сортировки:

// bubbleSort.js
function bubbleSort(array) {
  var swapped;
  do {
    swapped = false;
    for(var i = 0; i < array.length; i++) {
      if(array[i] > array[i + 1]) {
        swap(array, i, i + 1);
        swapped = true;
      }
    }
  } while(swapped);
  return array;
}
function swap(array, i, j) {
  var temp = array[i];
  array[i] = array[j];
  array[j] = temp;
}

1. Принятая структура данных

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

var obj = {0: 0, 
           1: 1, 
           2: 2, 
           3: 3}

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

--> obj

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

if (!Array.isArray(arr)) {
 
    return false 
    }

Это также вернет false для ложных значений, таких как null или undefined.

2. Пустота

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

bubbleSort([]) 
--> []

В контексте многих программ мы хотим обрабатывать пустой массив по-другому, чтобы он не сломал что-то в нашей программе ниже по строке:

if (!arr[0]) {
    return false
} 
or 
if (arr.length === 0) {
    return false 
}

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

var str = ''
var obj = {}
var arr = []
var num = 0

Если наш алгоритм проходит через вложенные структуры данных, мы также можем захотеть проверить вложенную пустоту следующим образом:

[[],[],[],[[]],[[[]]]]

3. Допустимые типы

Следующим важным случаем для проверки являются допустимые типы внутри нашего массива. Наш алгоритм пузырьковой сортировки сортирует строки на основе их значений Unicode. Это означает, что заглавные буквы будут стоять перед строчными:

bubbleSort([a,A,b,B])
---> [A,B,a,b]

Если мы хотим, чтобы алгоритм сортировал строки независимо от заглавных букв, мы можем обработать это:

if (arr[j-1].toLowerCase > arr[j].toLowerCase) {            
                var temp = arr[j-1];            
                arr[j-1] = arr[j];            
                arr[j] = temp;         
           }

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

2 > '2' ---> false 
2 < '2' ---> false 
2 === '2' ---> false 
2 == '2' ---> true 

В случае двух равных чисел, где одно является строкой, единственный оператор, который принимает значение «истина», - это ==. Еще сложнее, если в числовой строке есть буква.

1 > '1a' ---> false 
1 < '1a' ---> false 
1 === '1a' ---> false 
1 == '1a' ---> false 
2 > '1a' ---> false 
2 < '1a' ---> false 

Это может привести к некоторым странным результатам в нашем алгоритме сортировки. Следовательно, мы можем захотеть принимать только числа, а не строки, или обрабатывать разные типы отдельно.

Другой тип, о котором следует помнить, - это логическое значение. true примерно равно 1, а false примерно равно 0.

true == 1 ---> true 
false == 0 ---> true 
true > false ---> true 
true < 2 ---> true 
true < 'a' ---> false
false < 'a' ---> false

Возможно, мы не захотим разрешать логические значения в нашем алгоритме, особенно если мы сортируем массив строк. Сюда входят ложные значения, такие как null, undefined и NaN, которые будут вести себя странно при передаче в оператор сравнения.

Все ложные типы данных действуют по-разному в операторах сравнения:

null < 0.0000000001 ---> true
null >= 0 ---> true
null > 0 ---> false
null == 0 ---> false
null === null ---> true
null == undefined ---> true
undefined < 2 ---> false 
undefined > 0 ---> false 
undefined == 1 --> false
undefined === undefined ---> true
NaN == undefined ---> false
NaN === NaN ---> false 
NaN >= 0 ---> false 
NaN <= 2 ---> false 
NaN == null ---> false

Вы можете исключить их из своего алгоритма сортировки. null, пожалуй, самый странный, где он приближается к нулю, но не равен 0 и не больше 0, но == может быть принудительно приведен к значению undefined.

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

!!null == 0 ---> true
!!undefined == 0 ---> true
!!NaN == 0 --> true

Это связано с алгоритмами принуждения в Javascript и тем, как различные ложные значения хранятся в памяти. Подробнее об этом здесь: http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3. и здесь: http://www.ecma-international.org/ecma-262/5.1/#sec-11.8.5

4. Большие ценности

В нашем случае мы должны искать очень большие массивы. Что произойдет, если нашей функции будет передан массив длиной более 1 x 10¹⁰⁰?

Вот здесь-то и пригодится нотация Big O.

Сложность Big O

Сложность Big O - это показатель эффективности алгоритма. Когда мы имеем дело с большими наборами данных, это имеет большое значение. Big O имеет две переменные - время и объем памяти. Память важна, потому что это может нарушить наш алгоритм, если в качестве параметров передаются большие объемы данных. Каждая компьютерная программа хранит данные в стеке и куче. Доступ к стековой памяти осуществляется намного быстрее, но ее меньше. Как это связано с алгоритмами?

Давайте ответим на это двумя версиями пузырьковой сортировки, рекурсивной и итеративной:

// bubbleSort.js
function swap(array, i, j) {
  var temp = array[i];
  array[i] = array[j];
  array[j] = temp;
}
/// Bubble Sort using for loops 
function bubbleSort(array) {
  var swapped;
  do {
    swapped = false;
    for(var i = 0; i < array.length; i++) {
      if(array[i] > array[i + 1]) {
        swap(array, i, i + 1);
        swapped = true;
      }
    }
  } while(swapped);
  return array;
}
// Bubble sort using recursion
var swapped = true
function recursiveSwapping(arr, i = 0) {
  var len = arr.length;
  if (i >= len) {
    return arr
  }
    if (arr[i] > arr[i + 1]) {
      swap(arr, i, i + 1);
      swapped = true
    }
    return recursiveSwapping(arr, i + 1)
}

function recursiveBubbleSort(arr) {
    if (!swapped) {
      return arr
    }
        swapped = false
        recursiveSwapping(arr, i = 0)
        return recursiveBubbleSort(arr)
}

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

Теоретически рекурсивная версия также не должна иметь проблем с памятью. Многие языки нижнего уровня допускают так называемую оптимизацию хвостового вызова, которая позволяет писать рекурсивные функции со сложностью памяти 0 (1). До недавнего времени JS не допускал оптимизацию хвостовых вызовов, но в ECMAScript 6 вы можете включить ее в строгом режиме. Просто введите 'use strict’ в верхней части файла, и вы сможете писать рекурсивно, не сталкиваясь с ошибками переполнения стека.

'use strict'

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

function recursiveSwapping(arr, i = 0) {
  var len = arr.length;
  if (i >= len) {
    return arr
  }
    if (arr[i] > arr[i + 1]) {
      swap(arr, i, i + 1);
      swapped = true
    }
// tail call below 
    return recursiveSwapping(arr, i + 1)
}

5. Некоторые другие крайние случаи, о которых следует помнить.

  1. Нечетный / четный - числа и длина строки
  2. Отрицательные числа
  3. Специальные символы
  4. Повторяющиеся элементы
  5. Длина 1
  6. Длинная строка или большое число
  7. Нулевые или ложные значения