3.5. Демонстрация - Разбор предыдущей задачи
Шаг 1
Итак, рассмотрим пару вариантов решения.
import {log} from './logger';
/*
searchScore(data, 4) => {
"league": 1,
"placement": 4,
}
searchScore(data, 14) => null
*/
function searchScore(leaderboard, leaguePoints) {
return null;
}
const data = [
[
{
"login": "stypeano",
"leaguePoints": 4
},
{
"login": "rstrazir",
"leaguePoints": 45
},
{
"login": "cathead",
"leaguePoints": 143
},
{
"login": "cavernous",
"leaguePoints": 324
}
],
[
{
"login": "ConspiracyLil",
"leaguePoints": 356
},
{
"login": "CzarStories",
"leaguePoints": 568
},
{
"login": "GottaSaiyan",
"leaguePoints": 598
},
{
"login": "Mountaintrid",
"leaguePoints": 785
}
],
[
{
"login": "Rectionom",
"leaguePoints": 930
},
{
"login": "JoshChase",
"leaguePoints": 931
},
{
"login": "DreamLess",
"leaguePoints": 956
},
{
"login": "BlondiePlanet",
"leaguePoints": 1045
}
],
[
{
"login": "Breakingbing",
"leaguePoints": 1056
},
{
"login": "Goldenelox",
"leaguePoints": 1130
},
{
"login": "SaiyanBroadway",
"leaguePoints": 1432
},
{
"login": "BoostScooby",
"leaguePoints": 1476
}
]
]
log(4, searchScore(data, 4));
log(14, searchScore(data, 14));
Шаг 2
Самый простой и неоптимальный способ, который отрабатывает за O(n*m) (где n — количество лиг, а m — количество игроков в самой большой) — перебрать полностью все счета (линейный поиск). Но на миллионах человек он будет работать очень медленно — вдруг нам понадобится рассмотреть вообще всех игроков, чтобы найти лучшего.
import {log} from './logger';
/*
searchScore(data, 4) => {
"league": 1,
"placement": 4,
}
searchScore(data, 14) => null
*/
function searchScore(leaderboard, leaguePoints) {
for (let i = 0; i < leaderboard.length; i++) {
for (let j = 0; j < leaderboard[i].length; j++) {
const player = leaderboard[i][j];
if (player.leaguePoints === leaguePoints) {
const league = i + 1;
const placement = leaderboard[i].length - j;
return {league, placement};
}
}
}
return null;
}
const data = [
[
{
"login": "stypeano",
"leaguePoints": 4
},
{
"login": "rstrazir",
"leaguePoints": 45
},
{
"login": "cathead",
"leaguePoints": 143
},
{
"login": "cavernous",
"leaguePoints": 324
}
],
[
{
"login": "ConspiracyLil",
"leaguePoints": 356
},
{
"login": "CzarStories",
"leaguePoints": 568
},
{
"login": "GottaSaiyan",
"leaguePoints": 598
},
{
"login": "Mountaintrid",
"leaguePoints": 785
}
],
[
{
"login": "Rectionom",
"leaguePoints": 930
},
{
"login": "JoshChase",
"leaguePoints": 931
},
{
"login": "DreamLess",
"leaguePoints": 956
},
{
"login": "BlondiePlanet",
"leaguePoints": 1045
}
],
[
{
"login": "Breakingbing",
"leaguePoints": 1056
},
{
"login": "Goldenelox",
"leaguePoints": 1130
},
{
"login": "SaiyanBroadway",
"leaguePoints": 1432
},
{
"login": "BoostScooby",
"leaguePoints": 1476
}
]
]
log(4, searchScore(data, 4));
log(14, searchScore(data, 14));
Шаг 3
Можно вспомнить про более оптимальный поиск, который работает по отсортированным данным — бинарный поиск. Однако в своём самом простом проявлении он работает только по одномерным массивам. Тогда давайте просто сложим наш массив массивов в массив! Есть небольшая проблема: у нас пропадёт знание о том, в каком месте находится игрок. Поэтому данную информацию придётся как-то добавлять в новый массив. Есть и более серьёзная проблема. Наш поиск будет работать за O(log(n*m)) (напомним, бинарный поиск работает за log(количество элементов), а элементов у нас аж n столбцов и m строк) — и это гораздо оптимальнее нашего первого поиска. Однако само перекладывание в новый массив займёт те же O(n*m), поэтому и общая оценка алгоритма придёт к такому же значению...
import {log} from './logger';
/*
searchScore(data, 4) => {
"league": 1,
"placement": 4,
}
searchScore(data, 14) => null
*/
function searchScore(leaderboard, leaguePoints) {
const leaderboardArray = [];
for (let i = 0; i < leaderboard.length; i++) {
for (let j = 0; j < leaderboard[i].length; j++) {
const {leaguePoints} = leaderboard[i][j];
leaderboardArray.push({
league: i + 1,
placement: leaderboard[i].length,
leaguePoints,
})
}
}
// бинарный поиск...
return null;
}
const data = [
[
{
"login": "stypeano",
"leaguePoints": 4
},
{
"login": "rstrazir",
"leaguePoints": 45
},
{
"login": "cathead",
"leaguePoints": 143
},
{
"login": "cavernous",
"leaguePoints": 324
}
],
[
{
"login": "ConspiracyLil",
"leaguePoints": 356
},
{
"login": "CzarStories",
"leaguePoints": 568
},
{
"login": "GottaSaiyan",
"leaguePoints": 598
},
{
"login": "Mountaintrid",
"leaguePoints": 785
}
],
[
{
"login": "Rectionom",
"leaguePoints": 930
},
{
"login": "JoshChase",
"leaguePoints": 931
},
{
"login": "DreamLess",
"leaguePoints": 956
},
{
"login": "BlondiePlanet",
"leaguePoints": 1045
}
],
[
{
"login": "Breakingbing",
"leaguePoints": 1056
},
{
"login": "Goldenelox",
"leaguePoints": 1130
},
{
"login": "SaiyanBroadway",
"leaguePoints": 1432
},
{
"login": "BoostScooby",
"leaguePoints": 1476
}
]
]
log(4, searchScore(data, 4));
log(14, searchScore(data, 14));
Шаг 4
Тогда модифицируем обычный бинарный поиск так, чтобы он работал с нашим массивом массивов! Например, тем же бинарным поиском найдём сначала нужную лигу. Только вместо попадания по точному количеству очков будем делить таблицу пополам и смотреть, попадёт ли наше количество очков в промежуток этой лиги, и делить пополам дальше уже относительно этой середины. Чтобы не переусложнять функцию, вынесем поиск по лигам отдельно. Если вы не дошли до решения с бинарным поиском и так и не написали его самостоятельно, внимательно присмотритесь к функции поиска и убедитесь, что ничего сложного в этом нет. Мы всё так же делим пополам наш интервал поиска и идём искать дальше в одну из половинок, если нам не подходит середина. Попробуйте пройтись по алгоритму и поискать с его помощью.
{
time: 0,
isEntering: true, // если в этот момент человек входил — true, выходил — false
}
И перегоним наблюдения системы в него. Это займет у нас O(n) времени.
import {test} from "./tester.js"
import {log} from './logger';
/*
searchScore(data, 4) => {
"league": 1,
"placement": 4,
}
searchScore(data, 14) => null
*/
function searchScore(leaderboard, leaguePoints) {
const leagueIndex = searchLeagueByScore(leaderboard, leaguePoints)
return null;
}
function searchLeagueByScore(leaderboard, leaguePoints) {
let left = 0;
let right = leaderboard.length - 1;
const firstPlacePoints = leaderboard[right][leaderboard[right].length - 1].leaguePoints;
const lastPlacePoints = leaderboard[0][0].leaguePoints;
// Если количество очков вообще не входит в промежутки в таблице
// (меньше минимального или больше максимального)
if (lastPlacePoints > leaguePoints
|| firstPlacePoints < leaguePoints) {
// значит, такой лиги точно нет
return null;
}
// пока концы промежутка, в котором мы ищем, не сошлись
while (left <= right) {
// делим наш промежуток (примерно) пополам
const middleIndex = Math.floor((right + left) / 2);
const middle = leaderboard[middleIndex];
const firstPlayerPoints = middle[middle.length - 1].leaguePoints;
const lastPlayerPoints = middle[0].leaguePoints;
// если очки входят в лигу по середине - значит, это то, что мы ищем
if (lastPlayerPoints <= leaguePoints && leaguePoints <= firstPlayerPoints) {
return middleIndex;
}
// если очков для этой лиги слишком мало
if (lastPlayerPoints > leaguePoints) {
// то двигаем правый край нашего поиска до серединки
// (ищем от начала до текущей середины)
right = middleIndex - 1;
// а если наоборот слишком много
} else if (leaguePoints > firstPlayerPoints) {
// то ищем справа
left = middleIndex + 1;
}
}
// если края всё-таки сошлись - значит, такой лиги нет
return null;
}
const data = [
[
{
"login": "stypeano",
"leaguePoints": 4
},
{
"login": "rstrazir",
"leaguePoints": 45
},
{
"login": "cathead",
"leaguePoints": 143
},
{
"login": "cavernous",
"leaguePoints": 324
}
],
[
{
"login": "ConspiracyLil",
"leaguePoints": 356
},
{
"login": "CzarStories",
"leaguePoints": 568
},
{
"login": "GottaSaiyan",
"leaguePoints": 598
},
{
"login": "Mountaintrid",
"leaguePoints": 785
}
],
[
{
"login": "Rectionom",
"leaguePoints": 930
},
{
"login": "JoshChase",
"leaguePoints": 931
},
{
"login": "DreamLess",
"leaguePoints": 956
},
{
"login": "BlondiePlanet",
"leaguePoints": 1045
}
],
[
{
"login": "Breakingbing",
"leaguePoints": 1056
},
{
"login": "Goldenelox",
"leaguePoints": 1130
},
{
"login": "SaiyanBroadway",
"leaguePoints": 1432
},
{
"login": "BoostScooby",
"leaguePoints": 1476
}
]
]
log(4, searchScore(data, 4));
log(14, searchScore(data, 14));
Шаг 5
После того как мы нашли нужную лигу (либо сразу отдали null, если не нашли), нам остаётся написать ещё один бинарный поиск уже по ней.
import {log} from './logger';
/*
searchScore(data, 4) => {
"league": 1,
"placement": 4,
}
searchScore(data, 14) => null
*/
function searchScore(leaderboard, leaguePoints) {
const leagueIndex = searchLeagueByScore(leaderboard, leaguePoints);
if (leagueIndex === null) {
return null;
}
const placementIndex = searchInLeague(leaderboard[leagueIndex], leaguePoints);
return null;
}
function searchLeagueByScore(leaderboard, leaguePoints) {
let left = 0;
let right = leaderboard.length - 1;
const firstPlacePoints = leaderboard[right][leaderboard[right].length - 1].leaguePoints;
const lastPlacePoints = leaderboard[0][0].leaguePoints;
// Если количество очков вообще не входит в промежутки в таблице
// (меньше минимального или больше максимального)
if (lastPlacePoints > leaguePoints
|| firstPlacePoints < leaguePoints) {
// значит такой лиги точно нет
return null;
}
// пока концы промежутка, в котором мы ищем, не сошлись
while (left <= right) {
// делим наш промежуток (примерно) пополам
const middleIndex = Math.floor((right + left) / 2);
const middle = leaderboard[middleIndex];
const firstPlayerPoints = middle[middle.length - 1].leaguePoints;
const lastPlayerPoints = middle[0].leaguePoints;
// если очки входят в лигу по середине - значит, это то, что мы ищем
if (lastPlayerPoints <= leaguePoints && leaguePoints <= firstPlayerPoints) {
return middleIndex;
}
// если очков для этой лиги слишком мало
if (lastPlayerPoints > leaguePoints) {
// то двигаем правый край нашего поиска до серединки
// (ищем от начала до текущей середины)
right = middleIndex - 1;
// а если наоборот слишком много
} else if (leaguePoints > firstPlayerPoints) {
// то ищем справа
left = middleIndex + 1;
}
}
// если края всё-таки сошлись - значит, такой лиги нет
return null;
}
function searchInLeague(league, leaguePoints) {
let left = 0;
let right = league.length - 1;
while (left <= right) {
const middleIndex = Math.floor((right + left) / 2);
const {leaguePoints: middleLeaguePoints} = league[middleIndex];
if (middleLeaguePoints === leaguePoints) {
return middleIndex;
}
if (middleLeaguePoints > leaguePoints) {
right = middleIndex - 1;
} else if (leaguePoints > middleLeaguePoints) {
left = middleIndex + 1;
}
}
return null;
}
const data = [
[
{
"login": "stypeano",
"leaguePoints": 4
},
{
"login": "rstrazir",
"leaguePoints": 45
},
{
"login": "cathead",
"leaguePoints": 143
},
{
"login": "cavernous",
"leaguePoints": 324
}
],
[
{
"login": "ConspiracyLil",
"leaguePoints": 356
},
{
"login": "CzarStories",
"leaguePoints": 568
},
{
"login": "GottaSaiyan",
"leaguePoints": 598
},
{
"login": "Mountaintrid",
"leaguePoints": 785
}
],
[
{
"login": "Rectionom",
"leaguePoints": 930
},
{
"login": "JoshChase",
"leaguePoints": 931
},
{
"login": "DreamLess",
"leaguePoints": 956
},
{
"login": "BlondiePlanet",
"leaguePoints": 1045
}
],
[
{
"login": "Breakingbing",
"leaguePoints": 1056
},
{
"login": "Goldenelox",
"leaguePoints": 1130
},
{
"login": "SaiyanBroadway",
"leaguePoints": 1432
},
{
"login": "BoostScooby",
"leaguePoints": 1476
}
]
]
log(4, searchScore(data, 4));
log(14, searchScore(data, 14));
Шаг 6
Осталось привести наше решение к виду, который хотели наши заказчики! В итоге мы получили гораздо больше кода, чем в изначальном решении с полным перебором, но работает он быстрее. Если оценить его, то мы получим O(log(n) + log(m)), что преобразуется в наше обещанное O(log(n*m)) по свойству сложения логарифмов. Запомните его для последующей оценки алгоритмов.
import {log} from './logger';
/*
searchScore(data, 4) => {
"league": 1,
"placement": 4,
}
searchScore(data, 14) => null
*/
function searchScore(leaderboard, leaguePoints) {
const leagueIndex = searchLeagueByScore(leaderboard, leaguePoints);
if (leagueIndex === null) {
return null;
}
const placementIndex = searchInLeague(leaderboard[leagueIndex], leaguePoints);
if (placementIndex === null) {
return null;
}
const league = leagueIndex + 1;
const placement = leaderboard[leagueIndex].length - placementIndex;
return {league, placement};
}
function searchLeagueByScore(leaderboard, leaguePoints) {
let left = 0;
let right = leaderboard.length - 1;
const firstPlacePoints = leaderboard[right][leaderboard[right].length - 1].leaguePoints;
const lastPlacePoints = leaderboard[0][0].leaguePoints;
// Если количество очков вообще не входит в промежутки в таблице
// (меньше минимального или больше максимального)
if (lastPlacePoints > leaguePoints
|| firstPlacePoints < leaguePoints) {
// значит такой лиги точно нет
return null;
}
// пока концы промежутка, в котором мы ищем, не сошлись
while (left <= right) {
// делим наш промежуток (примерно) пополам
const middleIndex = Math.floor((right + left) / 2);
const middle = leaderboard[middleIndex];
const firstPlayerPoints = middle[middle.length - 1].leaguePoints;
const lastPlayerPoints = middle[0].leaguePoints;
// если очки входят в лигу по середине - значит, это то, что мы ищем
if (lastPlayerPoints <= leaguePoints && leaguePoints <= firstPlayerPoints) {
return middleIndex;
}
// если очков для этой лиги слишком мало
if (lastPlayerPoints > leaguePoints) {
// то двигаем правый край нашего поиска до серединки
// (ищем от начала до текущей середины)
right = middleIndex - 1;
// а если наоборот слишком много
} else if (leaguePoints > firstPlayerPoints) {
// то ищем справа
left = middleIndex + 1;
}
}
// если края всё-таки сошлись - значит, такой лиги нет
return null;
}
function searchInLeague(league, leaguePoints) {
let left = 0;
let right = league.length - 1;
while (left <= right) {
const middleIndex = Math.floor((right + left) / 2);
const {leaguePoints: middleLeaguePoints} = league[middleIndex];
if (middleLeaguePoints === leaguePoints) {
return middleIndex;
}
if (middleLeaguePoints > leaguePoints) {
right = middleIndex - 1;
} else if (leaguePoints > middleLeaguePoints) {
left = middleIndex + 1;
}
}
return null;
}
const data = [
[
{
"login": "stypeano",
"leaguePoints": 4
},
{
"login": "rstrazir",
"leaguePoints": 45
},
{
"login": "cathead",
"leaguePoints": 143
},
{
"login": "cavernous",
"leaguePoints": 324
}
],
[
{
"login": "ConspiracyLil",
"leaguePoints": 356
},
{
"login": "CzarStories",
"leaguePoints": 568
},
{
"login": "GottaSaiyan",
"leaguePoints": 598
},
{
"login": "Mountaintrid",
"leaguePoints": 785
}
],
[
{
"login": "Rectionom",
"leaguePoints": 930
},
{
"login": "JoshChase",
"leaguePoints": 931
},
{
"login": "DreamLess",
"leaguePoints": 956
},
{
"login": "BlondiePlanet",
"leaguePoints": 1045
}
],
[
{
"login": "Breakingbing",
"leaguePoints": 1056
},
{
"login": "Goldenelox",
"leaguePoints": 1130
},
{
"login": "SaiyanBroadway",
"leaguePoints": 1432
},
{
"login": "BoostScooby",
"leaguePoints": 1476
}
]
]
log(4, searchScore(data, 4));
log(14, searchScore(data, 14));
File logger.js
const results = document.getElementById('results')
export function log(points, placement) {
const result = document.createElement('li');
if (!placement) {
result.innerHTML = `Вы — единствнный игрок со счетом ${points}`;
} else {
result.innerHTML = `Ваш счет — ${points}. Скорее всего, вы попадете в лигу ${placement.league} и займете там ${placement.placement} место`;
}
results.appendChild(result);
}