hellojs.ru
Главная - Основы JavaScript - Рекурсия

Рекурсия

Размещено в категории "Основы JavaScript"
05.10.2024 / просмотров: 39 / комментариев: 0
Ресурсы:

Object.values()
Array.isArray()
Статья про рекурсию




Рекурсия - это приём, когда функция вызывает сама себя.

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

Код

pow(2, 1); // 2
pow(2, 2); // 4
pow(2, 3); // 8
pow(2, 4); // 16

Чтобы написать функцию возведения в степень, есть два варианта.

Первый вариант:

Код

function pow(x, n) {
  let result = 1; // не 0 потому что иначе все время будет 0 при умножении

  for (let i = 0; i < n; i++) {
  result *= x;
  }

  return result;
}

Логика работы функции здесь такова:

Цитата

// 2, 0

0 < 0 => false => return result = 1

// 2, 1

i = 0; 0 < 1 => true => return result = 1 * 2 = 2

// 2, 2

i = 0; 0 < 2 => true => result = 1 * 2 = 2
i = 1; 1 < 2 => true => return result = 2 * 2 = 4

// 2, 3

i = 0; 0 < 3 => true => result = 1 * 2 = 2
i = 1; 1 < 3 => true => result = 2 * 2 = 4
i = 2; 2 < 3 => true => return result = 4 * 2 = 8


Второй вариант (рекурсия)

Что значит возвести число в степень n?

Возвести число x в степень n – значит найти произведение n множителей, каждый из которых равен x.

Код

function pow(x, n) {
  if (n === 1) {
  return x;
  } else {
  return x * pow(x, n - 1);
  }
}

Разбираем рекурсию на примере вызова нашей функции, например, с параметрами (2, 3).

Функция принимает два аргумента: x = 2, n = 3, и, так как n не равен 1, срабатывает альтернативное условие

Код

function pow(2, 3) {
  return 2 * pow(2, 3 - 1);
}

Внутри функции происходит вызов функцией самой себя, но уже с параметрами x = 2, n = 2

Код

function pow(2, 2) {
  return 2 * pow(2, 2 - 1);
}

Теперь внутри этой функции также происходит вызов функцией самой себя, с параметрами x = 2, n = 1. Так как n = 1, то срабатывает основное условие и возвращается x = 2

Код

function pow(2, 1) {
  return 2;
}

Мы добрались до базы рекурсии, знаем результат её вычисления, это цифра 2, она возвращается и подставляется в вызове функции с параметрами (2, 2)

Код

function pow(2, 2) {
  return 2 * 2; // 4
}

Число 4, как результат работы этой функции, возвращается и подставляется в вызове функции с параметрами (2, 3)

Код

function pow(2, 3) {
  return 2 * 4; // 8
}

Таким образом, функция, вызываемая с параметрами (2, 3), возвращает число 8.

База рекурсии - случай, приводящий сразу к завершению работы функции. В примере выше базой рекурсии будет 1.

Шаг рекурсии - запуск вложенной функции, но уже с другим значением. В примере выше шаг рекурсии это n - 1

Глубина рекурсии - это общее количество вложенных вызовов вместе с самым первым. В примере выше это n (максимальная глубина рекурсии может быть в районе 10 000)

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

Попробуем понять почему на примере.

Есть такой объект со студентами и стоит задача посчитать общий прогресс студентов со всех доступных курсов:

Код

let students = {
  js: [{
  name: 'John',
  progress: 100
  }, {
  name: 'Ivan',
  progress: 60
  }],

  html: {
  basic: [{
  name: 'Peter',
  progress: 20
  }, {
  name: 'Ann',
  progress: 18
  }],

  pro: [{
  name: 'Sam',
  progress: 10
  }]
  }
};

Задачу можно решить как с использованием цикла, так и рекурсией.

Решение задачи можно разбить на этапы:

1. Выяснить общее число студентов

2. Узнать общее число прогресса в процентах

3. Общее число прогресса разделить на количество студентов

Решение задачи с использованием цикла

Создадим функцию, в которую как аргумент будут приходить какие-то данные:

Код

function getTotalProgressByIteration(data) {
  let total = 0; // общий прогресс  
  let studentsTotal = 0; // общее кличество студентов

  return total / studentsTotal;
}

console.log(getTotalProgressByIteration(students));

Цитата

У объектов есть метод Object.values(), возвращающий массив значений перечисляемых свойств объекта.

Пример:

Код

const obj = {
  a: 'something',
  b: 42,
  c: false
};

console.log(Object.values(obj)); // [ 'something', 42, false ]


Внутри нашей функции мы задаём цикл, объявляем внутри него переменную, назовём её course

Код

function getTotalProgressByIteration(data) {
  let total = 0; // общий прогресс  
  let studentsTotal = 0; // общее кличество студентов

  for (let course of Object.values(data)) {

  }

  return total / studentsTotal;
}

Object.values(data) нам вернёт, таким образом, массив, состоящий из значений свойств объекта students, то есть получим такой массив, первый элемент которого также массив, а второй элемент это объект:

Код

[
  [{
  name: 'John',
  progress: 100
  }, {
  name: 'Ivan',
  progress: 60
  }],

  {
  basic: [{
  name: 'Peter',
  progress: 20
  }, {
  name: 'Ann',
  progress: 18
  }],

  pro: [{
  name: 'Sam',
  progress: 10
  }]
  }
]

И в нашем цикле в переменную course будут поочередно попадать эти два элемента.

И мы получаем развилку, элемент, попадающий в course, будет либо массивом, либо чем-то еще, в данном случае, объектом.

Соответственно, нам нужно прописать условие, проверяющее тип элемента, который попадает в переменную course

Цитата

Метод Array.isArray() возвращает true, если объект является массивом и false, если он таковым не является.

Синтаксис:

Код

Array.isArray(obj);


Проверяем, является ли элемент массивом:

Код

function getTotalProgressByIteration(data) {
  let total = 0; // общий прогресс  
  let studentsTotal = 0; // общее кличество студентов

  for (let course of Object.values(data)) {
  if (Array.isArray(course)) {

  }
  }

  return total / studentsTotal;
}

Если условие истинное (элемент является массивом), то:

1. считаем, сколько в этом массиве студентов

2. складываем значения всех свойств progress

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

Соответственно, взяв длину массива, мы узнаем число студентов в нем. Прибавляем его в переменную studentsTotal

Код

function getTotalProgressByIteration(data) {
  let total = 0; // общий прогресс  
  let studentsTotal = 0; // общее кличество студентов

  for (let course of Object.values(data)) {
  if (Array.isArray(course)) {
  studentsTotal += course.length;
  }
  }

  return total / studentsTotal;
}  

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

Код

function getTotalProgressByIteration(data) {
  let total = 0; // общий прогресс  
  let studentsTotal = 0; // общее кличество студентов

  for (let course of Object.values(data)) {
  if (Array.isArray(course)) {
  studentsTotal += course.length;

  for (let i = 0; i < course.length; i++) {
  total += course[i].progress;
  }
  }
  }

  return total / studentsTotal;
}

Если же в переменную course попадает не массив, а что-то другое, в нашем случае это объект, то мы опять с помощью Object.values() получаем доступ к значениям его свойств, а это также два массива.

Пишем перебор массива с помощью for/of, как мы это делали ранее

Код

function getTotalProgressByIteration(data) {
  let total = 0; // общий прогресс  
  let studentsTotal = 0; // общее кличество студентов

  for (let course of Object.values(data)) {
  if (Array.isArray(course)) {
  studentsTotal += course.length;

  for (let i = 0; i < course.length; i++) {
  total += course[i].progress;
  }
  } else {
  for (let subCourse of Object.values(course)) {
  studentsTotal += subCourse.length;

  for (let j = 0; j < subCourse.length; j++) {
  total += subCourse[j].progress;
  }
  }
  }
  }

  return total / studentsTotal;
}

В результате получаем 41.6 средний прогресс студентов по всем доступным курсам.

Решение с использованием рекурсии

Напишем теперь новую функцию, в которой будем использовать рекурсию:

Код

function getTotalProgressByRecursion(data) {

}

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

Код

function getTotalProgressByRecursion(data) {
  if (Array.isArray(data)) {
  let total = 0;

  for (let i = 0; i < data.length; i++) {
  total += data[i].progress;
  }

  return [total, data.length]; // так как нам нужно 2 значения, используем массив
  } else {
  let total = [0, 0];

  for (let subData of Object.values(data)) {
  const subDataArr = getTotalProgressByRecursion(subData);
  total[0] += subDataArr[0];
  total[1] += subDataArr[1];
  }

  return total;
  }
}

const result = getTotalProgressByRecursion(students);

console.log(result[0] / result[1]);

Итак, что здесь происходит:

1. Базовым случаем рекурсии у нас будет, как мы решили, массив, поэтому, когда функция наталкивается на массив, происходит его перебор и вычисляется суммарный прогресс студентов из этого массива.

Число студентов при этом - это длина массива, поскольку каждый его элемент описывает одного студента.

В результате возвращается массив с данными типа [суммарный прогресс, число студентов].

2. Если же функция наталкивается на объект, то:

- с помощью метода Object.values() получает значения свойств объекта в виде массива

- Запускается перебор для этого массива, в котором каждый элемент этого массива (а им может быть и объект, и также массив) попадает в переменную subData

- Теперь функция вызывает сама себя, принимая в качестве аргумента переменную subData и результат вызова сохраняется в переменную subDataArr

- При этом, если в subData попадает массив, то происходит его перебор и вычисляется суммарный прогресс студентов из этого массива, и в переменную subDataArr сохраняется массив с данными типа [суммарный прогресс, число студентов]

- Если же в subData попадает объект, то функция опять вызовет саму себя и так до тех пор, пока функция не дойдёт до массива.

- Новые значения элементов массива в переменной total получаются прибавлением значений элементов массива из переменной subDataArr

Благодаря рекурсии, функция будет работать с любым уровнем вложенности.
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
Сайт управляется системой uCoz