Рекурсія, глибоке копіювання об'єктів

Лекція по рекурсії.

Рекурсія на лурку.

Рекурсивний виклик функції

Рекурсія - самоподібність. Тобто, це коли об'єкт містить свою самоподібну копію в собі.

Зображення на тему рекурсії
Рекурсія в природі

У програмуванні рекурсивним викликом називається випадок, коли функція викликає сама себе в своєму тілі.

З рекурсіями потрібно бути обережним - неакуратний виклик може створити "вічну" рекурсію, яка з'їсть всю доступну їй пам'ять і підвісить браузер.

З одним варіантом рекурсії ми вже познайомилися на прикладі масивів. Додамо до нього рекурсивний виклик функції:

var a = [];
a[0] = a;
console.log(a);

function recur(a){
  console.log(a);
  if (a < 1000000) { recur(a*2); }
}
recur(1);

Глибина рекурсії.

Рекурсія в теорії є нескінченною. Задля того, аби комп'ютери могли працювати з рекурсіями - кількість їх ітерацій обмежують. Кількість ітерацій називають глибиною рекурсії.

Порахуйте глибину рекурсії у попередньому прикладі.

Пряма та непряма рекурсія.

При прямій рекурсія викликає саму себе, як у попередньому прикладі.

При непрямій рекурсії функція A викликає функцію B, а та в свою чергу викликає функцію A:

function fun1(){
  ...
  fun2();
  ...
}
function fun2(){
  ...
  fun1();
  ...
}
fun1();

Ланцюжок може бути значно довшим, функції по черзі викликатимуть одна одну, і остання - викликатиме першу.

Завдання.

Виведіть в консоль рекурсією цифри від 10 до 0.

Виведіть в консоль строки:

1
21
321
4321
54321

Внесіть невеликі зміни, щоб отримати:

54321
4321
321
21
1

Практичне застосування рекурсії

Пошук шляху.

Задача пошуку шляху є класичною для рекурсії. Застосовується у різноманітних напрямках розробки: іграх, розпізнаванні образів, пошуку ефективних моделей розвитку бізнеса, в навігаційних системах, роутінгу інформації і т.п.

Розглянемо приклад пошуку шляху по лабіринту.

labyrinth
Лабіринт

З кожної точки лабіринту ми умовно можемо піти по чотирьох напрямках: вгору, вниз, вліво чи вправо, якщо нам не заважатимуть стіни. Нехай ми знаходимося в точці з координатами x та y. Напишемо функцію, яка обходить весь лабіринт:

function goTo(x, y){
  console.log('we are in position', x, y);
  ...
  if (noWall(x - 1, y)) { goTo(x - 1, y); }    // left
  if (noWall(x + 1, y)) { goTo(x + 1, y); }    // right
  if (noWall(x, y - 1)) { goTo(x, y - 1); }    // up
  if (noWall(x, y + 1)) { goTo(x, y + 1); }    // down
}
goTo(1, 1);

Щоб наш скрипт не підвис - потрібно задати певну глибину, наприклад, максимум - 30 ходів. Якщо в лабіринті виявляться закільцьовані ходи, то без глибини рекурсія буде вічно блукати по колу.

Ігри.

В іграх рекурсії використовуються повсюдно, для пошуку шляхів, для імітації штучного інтелекту, для генерації світу, локацій і т.п.

Рекурсією добре описуються такі ігри, як ханойські вежі, хрестики-нолики, шашки, шахмати та інші.

Фрактали.

Фрактал - нерегулярна, самоподібна структура. Є звичайні деревовидні фрактали, всілякі губки Менгера, трикутники Серпінського, є більш складні структури, що викоритовують комплексні числа з реальними та уявними множинами.

tree
Фрактальне дерево
trees
Фрактальні дерева в іграх

Генерація віртуальних світів.

В жанрі ігор Rogue локації генеруються випадково за допомогою рекурсій.

Різноманітні ігри також будують свої світи за допомогою рекурсій, глобально - всю територію, окремі локації, локально - розташування предметів, скарбів, побудова ландшафту, рослинного світу...

Наприклад, весь світ гри Minecraft будується рекурсіями та фракталами. Завдяки цьому створюються гарні дивовижні структури, унікальні в межах всього світу розмірами 3,6 млрд. км2 (площа всієї планети Земля - 0,51 млрд. км2). До того ж, в Minecraft можна створювати майже безкінечно велику кількість різноманітних світів.

Minecraft
Ландшафт у Minecraft

Генератор ландшафта

За допомогою рекурсій можна створювати правдоподібні ландшафти. Ви їх часто бачите у сучасних фільмах та іграх. Фільм знімають у павільйоні, а всі гори довкола - навіть не натурно зняті, а згенеровані рекурсією.

Є невелика стара програмка Terragen версій 0.9.*, що дозволяє генерувати гірські пейзажі.

Terragen
Інтерфейс программи Terragen 0.9

Сучасна версія програми значно потужніша:

Terragen

Terragen
Пейзажі, згенеровані програмою Terragen 4

Сортування.

За допомогою рекурсії можна сортувати масиви. Часто такі алгоритми виявляються оптимальнішими, швидше працюють, оскільки не перебирають багато разів одні і ті ж значення.

Детальніше можна почитати за посиланнями:
TVD: Рекурсия и рекурсивные алгоритмы,
Хабра: Рекурсия. Тренировочные задачи.

Рекурсивне копіювання об'єктів

Раніше ми розглянули копіювання об'єкта за допомогою оператора for-in.

Цей спосіб буде працювати лише для простих типів даних. Та якщо в якості значення ключа буде масив чи інший об'єкт, ці структури не скопіюються, а скопіюються лише посилання на них. Таким чином, у оригінального та у скопійованого об'єкта будуть спільні масиви та об'єкти.

Тому якщо ми зустрінемо тип даних ключа об'єкт чи масив - їх треба перебрати рекурсивно. Це називається глибоким копіюванням об'єкта.

function copyObj(obj){
  res = {};
  for (var key in obj) {
    if (typeof obj[key] === 'object') {
      if (obj[key].length !== undefined) {
        res[key] = copyArr(obj[key]);
      } else {
        res[key] = copyObj(obj[key]);
      }
    } else {
      res[key] = obj[key];
    }
  }
  return res;
}

Єдний ньюанс - при копіюванні масивів також потрібно враховувати тип даних кожного елемента масива.

Домашнє завдання

  1. Виведіть рекурсією цифри від Z до A в консоль (використовуйте коди символів).

* Пошук скарбів.

Дано лабіринт, його представлено у вигляді матриці розмірами 21 х 13:

var lab = [
  '111111111111111111111','100010000010001000001','111010111010111011101',
  '100010101000000000101','101110101010111110111','100000100010100000101',
  '101111111110101110101','101000100000100010001','111011101110111111111',
  '100010000010000000001','101111101010111111101','100000001010001000001',
  '111111111111111111111'
];

Одиниці - це стіни, нулі - коридори.

У лабіринті довільно розміщується точка початку гри та точка "скарб". Можна створити дві змінні з масивами з двох елементів, що відповідають координатам x та y:

var start = [5, 3],
    gold = [7, 11];

Створіть функцію, яка б вибирала довільно один з коридорів лабіринту і заміняла б його на точку початку.

Застосуйте цю ж функцію для заміни нуля на літеру g.

Відмалюйте лабіринт у браузері, використовуючи теги.

Для відображення клітин коридорів чи стін зручно використовувати тег span, для горизонтальних рядків лабіринту - тег p, для всього лабіринту - тег div.

div { width: 420px; border-left: 1px solid #999; border-top: 1px solid #999;}
p { margin: 0; font-size: 0; line-height: 0; }
span { display: inline-block; width: 19px; height: 19px; border-right: 1px solid #999; border-bottom: 1px solid #999;}
span.start { background: #080;}
span.wall { background: #999;}
span.gold { background: #fa0;}

// приклад маленького лабіринту:
div
 p>.wall*5
 p>.wall+.start+span*2+.wall
 p>.wall+span+.wall+span+.wall
 p>.wall+span+.wall+.gold+.wall
 p>.wall*5

Замість кольорів бекграунду можна задати текстуру кам'яної стіни, зображення шукача скарбів та скрині з золотом.

Щоб створити лабіринт по заданій матриці - вам знадобляться деякі нові команди JavaScript.

Створення тегів відбувається за допомогою метода document.createElement, елемент при цьому створюється в пам'яті.

var span = document.createElement('span');
var p = document.createElement('p');
var div = document.createElement('div');

Щоб задати елементу клас - використовується властивість className:

span.className = 'active';
p.className = 'first';
div.className = 'labyrinth';

Щоб добавити один елемент в інший - використовується метод appendChild:

p.appendChild(span);
div.appendChild(p);
document.body.appendChild(div);

Створіть функцію, яка б відмальовувала лабіринт у браузері.

Керувати шукачем скарбів будемо вручну.

Добавте на сторінку 4 кнопки з написами "up", "right", "down", "left" (або фоновими зображеннями стрілок).

До кожної кнопки добавте onclick, перевіряйте, чи можна піти у заданому напрямку, якщо можна, то перемістіться у сусідню клітину.

По досягненні точки зі скарбом виведіть на екран поздоровлення та запустіть гру знову з початку.

Але який же лабіринт без мінотавра? добавте ще одну точку m.

Після кожного ходу гравця мінотавр має зробити свій хід:
рекурсивно переберіть лабіринт (задайте глибину рекурсії 20), знайдіть гравця і по прокладеному маршруту рухайтеся на зустріч йому.
Якщо мінотавр зловить гравця - кінець гри. Почати гру заново.

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

Наступний етап - автоматична генерація довільних лабіринтів і вже можна починати думати про вихід на комерційний ринок :).