Элегантный способ найти смежный субарак в массиве в JavaScript?

Я хотел написать функцию для поиска смежного субарама в заданном массиве из заданного начального индекса и вернуть индекс подмассива в массив, если он найдет, и -1, если он не найден. Это похоже на String.indexOf, но для массивов и подмассивов вместо строк и подстрок.

Это мой рабочий код:

var find_csa = function (arr, subarr, from_index) {
    if (typeof from_index === 'undefined') {
        from_index = 0;
    }

    var i, found, j;
    for (i = from_index; i < 1 + (arr.length - subarr.length); ++i) {
        found = true;
        for (j = 0; j < subarr.length; ++j) {
            if (arr[i + j] !== subarr[j]) {
                found = false;
                break;
            }
        }
        if (found) return i;
    }
    return -1;
};

И это мои тесты и их ожидаемые значения:

console.log(find_csa([1, 2, 3, 4, 5], [2, 3, 4]) === 1);
console.log(find_csa([1, 2, 3, 4, 5], [5]) === 4);
console.log(find_csa([1, 2, 3, 4, 5], [1, 3]) === -1);
console.log(find_csa([1, 2, 3, 4, 5], [42]) === -1);
console.log(find_csa([1, 2, 3, 4, 5], []) === 0);
console.log(find_csa([3, 4, 3, 4, 3, 4], [3, 4, 3], 1) === 2);
console.log(find_csa([6, 6, 6, 7], [6, 6, 7]) === 1);
console.log(find_csa([12, 9, 16, 42, 7, 866, 3], [16, 42, 7, 866]) === 2);

Мой код передает тесты, но, как вы можете видеть, он использует логическое значение found во внутреннем цикле, который является всего лишь моим беспорядочным, ad-hoc способом продолжения внешнего цикла из вложенного цикла. есть ли более чистый способ его написания? Я просмотрел Array.prototype.findIndex, но сейчас это экспериментальная технология, поэтому я не могу ее использовать. Мне нужен метод, который работает в большинстве браузеров. Я знаю, что есть фрагмент кода polyfill, написанный на странице Mozilla, но это даже больше, чем мой текущий код, и он будет медленнее из-за вызовов функций, поэтому я бы предпочел избежать этого.

Моя основная цель для этой функции - производительность (субмарины будут очень маленькими, поэтому я считаю, что используя строчный алгоритм поиска Boyer-Moore или try - это слишком много, но моя вторая цель - изящество моей реализации. Учитывая эти две цели, я хотел бы знать, есть ли лучший способ написания этого кода, или если есть какие-либо функции или функции JavaScript, которые мне не хватает, что могло бы помочь мне избежать found boolean.

JSFiddle, если это кому-то помогает: http://jsfiddle.net/qc4zxq2p/

Ответ 1

Есть ли какие-либо функции или функции JavaScript, которые у меня отсутствуют, что может помочь мне избежать found boolean

Да, вы можете использовать label в своем внешнем цикле:

function find_csa(arr, subarr, from_index) {
    var i = from_index >>> 0,
        sl = subarr.length,
        l = arr.length + 1 - sl;

    loop: for (; i<l; i++) {
        for (var j=0; j<sl; j++)
            if (arr[i+j] !== subarr[j])
                continue loop;
        return i;
    }
    return -1;
}

Ответ 2

Это то же самое, что и у вас, просто немного приукрашено (по крайней мере, к моей эстетике):

var find_csa = function (arr, subarr, from_index) {
    from_index = from_index || 0;

    var i, found, j;
    var last_check_index = arr.length - subarr.length;
    var subarr_length = subarr.length;

    position_loop:
    for (i = from_index; i <= last_check_index; ++i) {
        for (j = 0; j < subarr_length; ++j) {
            if (arr[i + j] !== subarr[j]) {
                continue position_loop;
            }
        }
        return i;
    }
    return -1;
};

Ответ 3

Внутренний цикл может быть сведен к одной строке с использованием метода массива every:

if(subarr.every(function(e, j) { return (e === arr[i + j]); })
    return i;

или (предложение ES6):

if(subarr.every( (e, j) => (e === arr[i + j]) ))
    return i;

Но это может быть просто любопытство или образовательный пример, если вы не заботитесь о производительности.

Ответ 4

Внутри вашего цикла вы можете исключить переменную found и избежать этого:

for (j = 0; j < subarr.length; ++j) {
    if (arr[i + j] !== subarr[j]) break;
}
/*
 * the above loop breaks in two cases:
 * normally: j === subarr.length
 * prematurely: array items did not match
 * we are interested in kowing if loop terminated normally
 */
if (j === subarr.length) return i;

Сказав это, вот мое решение, используя Array.join и String.indexOf. Это полезно только для массива чисел:

function find_csa(arr, subarr, from_index) {
    from_index |= 0;
    if (subarr.length === 0) {
        return from_index;
    }
    var haystack = "," + arr.slice(from_index).join(",") + ",",
        needle = "," + subarr.join(",") + ",",
        pos = haystack.indexOf(needle);
    if (pos > 0) {
        pos = haystack.substring(1, pos).split(",").length + from_index;
    }
    return pos;
}
console.log("All tests should return true");
console.log(find_csa([1, 2, 3, 4, 5], [1, 2, 3]) === 0);
console.log(find_csa([1, 2, 3, 4, 5], [2, 3, 4]) === 1);
console.log(find_csa([1, 2, 3, 4, 5], [5]) === 4);
console.log(find_csa([1, 2, 3, 4, 5], [6]) === -1);
console.log(find_csa([1, 2, 3, 4, 5], [1, 3]) === -1);
console.log(find_csa([6, 6, 6, 7], [6, 6, 7]) === 1);
console.log(find_csa([1, 2, 3, 4, 5], []) === 0);
console.log(find_csa([3, 4, 3, 4, 3, 4], [3, 4, 3], 1) === 2);
console.log(find_csa([1, 2, 3, 4, 5], [], 1) === 1);

Ответ 5

Прочитав начальную дискуссию на основе предложения zerkms, мне было интересно попробовать решение с использованием JSON.stringify, несмотря на неблагоприятные мнения.

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

var find_csa = function (arr, subarr, from_index) {
  var start=from_index|0,
      needle=JSON.stringify(subarr),
      matches=JSON.stringify(arr.slice(start)).
      match(new RegExp('^\\[(.*?),?'+
        needle.substr(1,needle.length-2).replace(/([\[\]])/g,'\\$1')
      ));
  return !!matches?(matches[1].length?matches[1].split(',').length:0)+start:-1;
}

Вышеприведенный код принимает массивы массивов, как было предложено Shashank, но не обрабатывает элементы, содержащие запятые.

Итак, я разработал другое решение, которое также принимает запятые (спасибо Стивену Левитану за элегантный отзыв о while(str!=(str=str.replace(regexp,replacement)));).

Но это только для удовольствия, так как:

  • код не так короток, теперь... Вздох!
  • он, вероятно, потребляет много процессорного времени
  • он не работает с пустыми элементами (они игнорируются)
  • Я подозреваю (и не копал глубже:-) он может терпеть неудачу со сложными объектами как элементы

В любом случае, вот он:

var find_csa = function (arr, subarr, from_index) {
  var start=from_index|0,
      commas=new RegExp('(?:(\')([^,\']+),([^\']+)\'|(")([^,"]+),([^"]+))"'),
      strip_commas='$1$2$3$1$4$5$6$4',
      haystack=JSON.stringify(arr.slice(start)),
      needle=JSON.stringify(subarr).replace(/^\[(.*)\]$/,'$1');
  while(haystack!=(haystack=haystack.replace(commas,strip_commas)));
  while(needle!=(needle=needle.replace(commas,strip_commas)));
  matches=haystack.match(new RegExp('^\\[(.*?),?'+needle.replace(/([\[\]])/g,'\\$1')));
  return !!matches?(matches[1].length?matches[1].split(',').length:0)+start:-1;
}