JavaScriptでのアルゴリズムとデータ構造 part1

# 初めに
連結リスト、スタックやキューなどデータ構造のコア概念を模索しながらC言語で練習してみて思ったのは、理解したうえでどんな場面で応用していくかが大事なんですね。

しかし応用の前段階から、アルゴリズムの理解に時間がかかったり不慣れなところ（解決法の仕組みとかどうしてそういう発想ができるのかとか）もあったりして、自分はやはり初歩的な段階からもう一度勉強してみたいと思います。

今回はリハビリを兼ねて長い間触れてないJavaScriptでアルゴリズムとデータ構造の実現を理解してまとめていきたいと思います。（箇条書きが多い。）

教材はこちらです。

https://www.udemy.com/course/js-algorithms-and-data-structures-masterclass/

（この記事では単なる自分の理解や気になるところをトピックにして書いたメモです。トピックは基本的に教材と関連しているが、語る方向が全然違う可能性もあります。なお、有料映像であったため、映像のコードそのまま出すのを控えております。なので記事に書いてあるのは正確性を問わず自分なりに考えたコードだったり、あるいはほかの参考文章から挙げたものでもあります。）

# Memo
- 文字の検査では、普通に`if`条件式でコードポイント（code point）にするほうが正規表現より早い可能性は十分ある。（可読性が悪くなるんですが...。）
    - 正規表現は文字列の中身をチェックしたり、メソッドにより無害化して置き換えたりするメリットがある。それに可読性が高い。

# Big O Notation
## 時間計算量（Time complexity）
- `O(n^2)`、`O(n log n)`、`O(n)`、`O(log n)`、`O(1)`。
    - linear: `O(n)`, quadratic: `O(n^2)`, constant: `O(1)`
- 基本的にループの実行回数で分けられている。
- `O(n)`と`O(1)`との違いは、`O(n)`はインプットの`n`でループの回数を決める。`O(1)`は`n`はどうであれ最大は固定された回数で実行する。
    - 場合によって`O(n)`と`O(1)`の計算量が同じだったこともある。

```js:O(1)
function atMost5(n) {
  for (let i = 1; i <= Math.min(n, 5); i++) {
    console.log(i);
  }
}
atMost5(10);
// 1
// 2
// 3
// 4
// 5
atMost5(3);
// 1
// 2
// 3
atMost5(-3); // not work
```

---

```js:O(n)
function atLeast5(n) {
  for (let i = 1; i <= Math.max(n, 5); i++) {
    console.log(i);
  }
}
atLeast5(5);
// 1
// 2
// 3
// 4
// 5
atLeast5(7);
// 1
// 2
// 3
// 4
// 5
// 6
// 7
```
- `O(log n)`は基数`2`ベースにして、毎回の処理ずつ入力データの半分を除外することで、全探索`O(n)`より少ない。

```
log2(8) = x => 2^3 = 8, x = 3
log2(n) = exponent => 2^exponent = n
```

## 空間計算量（Space complexity）
- 処理が実行される際にどのぐらいのメモリが使われるかを表す。
- 空間計算量（Space complexity）＝（実行のための）補助的メモリ空間（Auxiliary space）＋入力データが占めるメモリ空間（Input space）
- JavaScriptでは`boolean`、`number`、`undefined`、`null`は定数空間（constant space）とされている。
- 文字列は長さ`n`で大きさを決めるので`O(n)`となる。
- 配列は長さ、オブジェクトはキーの数で`O(n)`となる。

```js
// there is one number every time => O(1) space
function sum(arr) {
  let result = 0;
  for (let i = 0; i < arr.length; i++) {
    result += arr[i];
  }
  return result;
}
console.log(sum([1, 2, 3])); // 6

// arr: n, newArr: n => O(n) space
function doubledArr(arr) {
  let newArr = [];
  for (let i = 0; i < arr.length; i++) {
    newArr.push(arr[i] * 2);
  }
  return newArr;
}
console.log(doubledArr([1, 2, 3])); // [ 2, 4, 6 ]
```

## Arrays, Objects, Built-in Methods in JavaScript
**Objects**
- Insertion: `O(1)`
- Removal: `O(1)`
- Searching: `O(n)`
- Access: `O(1)`

**Built-in Methods**
- `Object.keys()`: `O(n)`
- `Object.values()`: `O(n)`
- `Object.entries()`: `O(n)`
- `hasOwnProperty()`: `O(1)`

**Arrays**
- Insertion: `push()`は`unshift()`より早い。
- Removal: `pop()`は`shift()`より早い。
- Searching: `O(n)`
- Access: `O(1)`

**Built-in Methods**
`push()`: `O(1)`
`pop()`: `O(1)`
`shift()`: `O(n)` *reindex
`unshift()`: `O(n)` *reindex
`sort()`: `O(n log n)`
...

# Explore examples
- 一般的な事例
- さらに複雑な事例
- 端的な事例
    - 空（empty）や有効ではない（Invalid）入力の検証など。

https://qiita.com/hu-yu/items/74c867b3de95ef59758a#empty-values

https://qiita.com/hu-yu/items/21490dcb8fa5e0ce9256#undefined-vs-empty-slots

https://qiita.com/hu-yu/items/a05c484df82d1a187fd6

# Problem solving patterns
**基本的に自分で考えたコードですが、アイディア・発想は映像の影響を受けております。**
## Frequency Counter
出現頻度などの問題は基本的に二重ループでなくオブジェクトで解決する。
オブジェクトのこの使い方をあまり考えたことありませんでした。とても賢い方法だと思います。
```js
function validAnagram(str1, str2) {
  // length check
  if (str1.length !== str2.length) {
    return false;
  }
  // type check
  if (
    Object.prototype.toString.call(str1) !== '[object String]' ||
    Object.prototype.toString.call(str2) !== '[object String]'
  ) {
    return false;
  }

  // basically the input is assumed to be lower case. just making sure
  str1.toLowerCase();
  str2.toLowerCase();

  let obj1 = {};

  // not like array is indexing with index number,
  // object key is efficient and get value precisely
  for (let letter of str1) {
    obj1[letter] ? ++obj1[letter] : (obj1[letter] = 1);
  }

  for (let i = 0; i < str2.length; i++) {
    if (!obj1[str2[i]]) {
      return false;
    } else {
      obj1[str2[i]]--;
    }
  }
  return true;
}
console.log(validAnagram('aaz', 'zza')); // false
console.log(validAnagram('anagram', 'nagaram')); // true
```

### Practices
```js
function sameFrequency(num1, num2) {
  const typeChecker = Object.prototype.toString;
  if (
    typeChecker.call(num1) !== '[object Number]' ||
    typeChecker.call(num2) !== '[object Number]'
  ) {
    return 'Invalid input';
  }
  let str1 = JSON.stringify(num1);
  let str2 = JSON.stringify(num2);
  if (str1.length !== str2.length) return false;

  let obj = {};
  for (let num of str1) {
    obj[num] ? ++obj[num] : (obj[num] = 1);
  }
  for (let num of str2) {
    if (!obj[num]) {
      return false;
    } else {
      obj[num]--;
    }
  }
  return true;
}
console.log(sameFrequency(182, 281)); // true
console.log(sameFrequency(34, 14)); // false
```

## Mutiple pointers
先頭・最後尾のポインタで両者の和が`0`になるペア探し出す。
```js
function sumZeroPair(arr) {
  let left = 0;
  let right = arr.length - 1;
  let newArr = [];
  while (right > left) {
    let sum = arr[left] + arr[right];
    if (sum === 0) {
      newArr.push([arr[left], arr[right]]);
      right--;
      left++;
    } else if (sum > 0) {
      right--;
    } else {
      left++;
    }
  }
  if (!newArr.length) {
    return 'No pair';
  }
  return newArr;
}
console.log(sumZeroPair([-4, -3, -2, -1, 0, 1, 2, 3, 10]));
// [ [ -3, 3 ], [ -2, 2 ], [ -1, 1 ] ]
console.log(sumZeroPair([-4, -3, -2, -1, 0, 10]));
// No pair
```

### Practices
一部違うタイプの入力も考えてみました。
```js
const typeChecker = Object.prototype.toString;
function areThereDuplicates() {
  // check value type
  const type = typeChecker.call(arguments[0]);
  for (let [index, value] of Object.entries(arguments)) {
    if (typeChecker.call(value) !== type) {
      return differentType(...arguments);
    }
  }

  let obj = {};
  for (let [index, value] of Object.entries(arguments)) {
    // skip empty string
    if (value === '') {
      continue;
    }
    obj[value] ? ++obj[value] : (obj[value] = 1);
    if (obj[value] >= 2) return true;
  }
  return false;
}

function differentType() {
  let arr = [];
  for (let [index, value] of Object.entries(arguments)) {
    // skip empty string
    if (value === '') {
      continue;
    }
    arr.push([value, 1]);
  }

  for (let i = 0; i < arr.length; i++) {
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[i][0] === arr[j][0]) {
        ++arr[i][1];
        if (arr[i][1] >= 2) {
          return true;
        }
      }
    }
  }
  return false;
}

console.log(areThereDuplicates(1, 2, 3)); // false
console.log(areThereDuplicates('a', 'b', 'c', 'a')); // true
console.log(areThereDuplicates('1', 1, '2', 3)); // false
console.log(areThereDuplicates('', '', '1', '2')); // false
console.log(areThereDuplicates('', '', '1', 1, 2, 2)); // true
```
配列風オブジェクトの処理は`for...of`と`Object.entries()`の組み合わせよりもっといい方法（`Array.from()`、`...args`など）があると思います。リンク先はオブジェクトのイテレータについてです。

https://qiita.com/hu-yu/items/4fa1e00991a6f6141c55#forof

---

そして一意のキーしか置かれない`Set`も使えることに気づいて、
```js
function areThereDuplicates() {
  // Set only store unique value, whether primitives or objects
  // also the empty string ''
  let set = new Set();
  let emptyCount = 0;
  for (let i = 0; i < arguments.length; i++) {
    if (arguments[i] !== '') {
      set.add(arguments[i]);
    }
  }
  if (set.size - emptyCount === 0 || set.size === arguments.length) {
    return false;
  }
  return true;
}

console.log(areThereDuplicates(1, 2, 3)); // false
console.log(areThereDuplicates('a', 'b', 'c', 'a')); // true
console.log(areThereDuplicates('1', 1, '2', 3)); // false
console.log(areThereDuplicates('', '')); // false
console.log(areThereDuplicates('', '', '1', 1, 2, 2)); // true
```

https://qiita.com/hu-yu/items/9ce95b2d322855acc243#set-1

## Count unique values
重複しない要素の個数を求める。
```js
function countUniqueValues(arr) {
  if (Object.prototype.toString.call(arr) !== '[object Array]') {
    return 'Invalid input';
  }
  if (arr.length === 0) return 0;

  let obj = {};
  let count = 0;

  for (let key of arr) {
    obj[key] ? ++obj[key] : (obj[key] = 1);
  }
  for (let [key, value] of Object.entries(obj)) {
    count++;
  }
  return count;
}
console.log(countUniqueValues([1, 1, 1, 1, 1, 2])); // 2
console.log(countUniqueValues([1, 2, 3, 4, 4, 4, 7, 7, 12, 12, 13])); // 7
console.log(countUniqueValues([])); // 0
console.log(countUniqueValues([-2, -1, -1, 0, 1])); // 4
```

---

下は元の配列から重複しない要素の配列を新しく作る解法です。
```js
function countUniqueValues(arr) {
  if (Object.prototype.toString.call(arr) !== '[object Array]') {
    return 'Invalid input';
  }
  if (arr.length === 0) {
    console.log([]);
    return 0;
  }

  let left = 0;
  let newArr = [];
  newArr.push(arr[left]);

  for (let i = 1; i < arr.length; i++) {
    if (arr[left] !== arr[i]) {
      ++left;
      arr[left] = arr[i];
      newArr.push(arr[left]);
    }
  }
  console.log(newArr);
  return left + 1;
}
console.log(countUniqueValues([1, 1, 1, 1, 1, 2]));
// [ 1, 2 ]
// 2
console.log(countUniqueValues([1, 2, 3, 4, 4, 4, 7, 7, 12, 12, 13]));
// [1, 2, 3, 4, 7, 12, 13];
// 7
console.log(countUniqueValues([]));
// []
// 0
console.log(countUniqueValues([-2, -1, -1, 0, 1]));
// [ -2, -1, 0, 1 ]
// 4
```

### Practices
配列から指定された平均値で数字のペアを見つけ出す。
```js
// arr is sorted
function averagePair(arr, average) {
  if (arr.length === 0) return false;

  let left = 0;
  let right = arr.length - 1;
  let tempAverage = 0;
  while (right > left) {
    tempAverage = (arr[left] + arr[right]) / 2;
    if (tempAverage > average) {
      right--;
    } else if (tempAverage < average) {
      left++;
    } else {
      return true;
    }
  }
  return false;
}
console.log(averagePair([1, 2, 3], 2.5)); // true
console.log(averagePair([1, 3, 3, 5, 6, 7, 10, 12, 19], 8)); // true
console.log(averagePair([-1, 0, 3, 4, 5, 6], 4.1)); // false
console.log(averagePair([], 4)); // false
```

---

二つ文字列の入力で、前者の文字要素が後者に入っているかどうか。（前者の文字順に追う必要がある。）
```js
function isSubsequence(str1, str2) {
  const typechecker = Object.prototype.toString;
  if (
    typechecker.call(str1) !== '[object String]' ||
    typechecker.call(str2) !== '[object String]'
  ) {
    return 'Invalid input';
  }

  let compareIndex = 0;
  for (let i = 0; i < str2.length; i++) {
    // we have to follow str1's order
    if (str2[i] === str1[compareIndex]) {
      compareIndex++;
    }
  }
  if (compareIndex === str1.length) return true;
  return false;
}

console.log(isSubsequence('hello', 'hello world')); // true
console.log(isSubsequence('sing', 'sting')); // true
console.log(isSubsequence('abc', 'abracadabra')); // true
console.log(isSubsequence('abc', 'acb')); // false
```

## Sliding window
指定の長さで配列最大和を求める。
`counter`が`digits`に到達するまで`tempTotal`に今までの和を保存させる。`digits`に到達すると`tempTotal`が返り値の`result`と比べて`result`より大きいなら`result`にアサインする。
`startIndex`は`0`から、毎回の間隔`digits`が終わると`+1`をして新しいスタートになります。これは毎回の間隔が終わるとスタートが更新されるので二重ループと似た仕組みになっている気がします。
```js
function maxSubarraySum(arr, digits) {
  const typeChecker = Object.prototype.toString;
  if (
    typeChecker.call(arr) !== '[object Array]' ||
    typeChecker.call(digits) !== '[object Number]'
  ) {
    return 'Invalid input';
  }
  if (arr.length < digits) return null;

  let startIndex = 0;
  let tempTotal = 0;
  let counter = 0;
  let result = -Infinity;

  for (let j = 0; j < arr.length; j++) {
    tempTotal += arr[j];
    ++counter;
    if (counter === digits) {
      if (tempTotal > result) {
        result = tempTotal;
      }
      tempTotal = 0; // reset
      counter = 0; // reset
      ++startIndex;
      j = startIndex - 1; // -1 for j++
    }
  }
  return result;
}
console.log(maxSubarraySum([1, 2, 5, 2, 8, 1, 5], 2)); // 10
console.log(maxSubarraySum([1, 2, 5, 2, 8, 1, 5], 4)); // 17
console.log(maxSubarraySum([4, 2, 1, 6], 1)); // 6
console.log(maxSubarraySum([4, 2, 1, 6, 2], 4)); // 13
console.log(maxSubarraySum([], 4)); // null
```
少し前にC言語アルゴリズムの練習で少し触れたので取り入れてみました。

https://qiita.com/hu-yu/items/fbd8957fad2dcdc7bac3#%E6%8C%87%E5%AE%9A%E3%81%AE%E9%95%B7%E3%81%95%E3%81%A7%E9%85%8D%E5%88%97%E6%9C%80%E5%A4%A7%E5%92%8C%E3%82%92%E6%B1%82%E3%82%81%E3%82%8B

これだとスタート位置を更新せず、さきの解法より早くなると思います。
```js
function maxSubarraySum(arr, digits) {
  const typeChecker = Object.prototype.toString;
  if (
    typeChecker.call(arr) !== '[object Array]' ||
    typeChecker.call(digits) !== '[object Number]'
  ) {
    return 'Invalid input';
  }
  if (arr.length < digits) return null;

  let startIndex = 0;
  let tempTotal = 0;
  let result = -Infinity;

  for (let i = 0; i < arr.length; i++) {
    tempTotal += arr[i];
    if (i >= digits - 1) {
      if (tempTotal > result) {
        result = tempTotal;
      }
      tempTotal -= arr[startIndex];
      startIndex++;
    }
  }
  return result;
}
```

### Practices
```js
function maxSubarraySum(arr, digits) {
  if (
    arr.length < digits ||
    Object.prototype.toString.call(arr) !== '[object Array]'
  ) {
    return null;
  }

  let temp = 0;
  let result = -Infinity;
  for (let i = 0; i < digits; i++) {
    temp += arr[i];
  }

  result = temp;
  for (let i = digits; i < arr.length; i++) {
    temp = temp - arr[i - digits] + arr[i];
    if (temp > result) {
      result = temp;
    }
  }
  return result;
}
console.log(maxSubarraySum([100, 200, 300, 400], 2)); // 700
console.log(maxSubarraySum([1, 4, 2, 10, 23, 3, 1, 0, 20], 4)); // 39
console.log(maxSubarraySum([-3, 4, 0, -2, 6, -1], 2)); // 5
console.log(maxSubarraySum([3, -2, 7, -4, 1, -1, 4, -2, 1], 2)); // 5
console.log(maxSubarraySum([2, 3], 3)); // null
```

# Recursion
いまでも苦手なトピックです。これを機に練習ができてよかったと思います。

## Practices
```js
function reverseStr(str) {
  if (!str) return '';
  return str[str.length - 1].concat(reverse(str.slice(0, str.length - 1)));
}
console.log(reverseStr('awesome')); // emosewa
console.log(reverseStr('rithmschool')); // loohcsmhtir
```

---

```js
function isPalindrome(str) {
  // special case
  if (str.length === 1) return true;
  if (str.length === 2) return str[0] === str[1];
  // base case
  // String.prototype.slice(-1): extract the last element
  if (str[0] === str.slice(-1)) {
    return isPalindrome(str.slice(1, -1));
  }
  return false;
}
console.log(isPalindrome('awesome')); // false
console.log(isPalindrome('foobar')); // false
console.log(isPalindrome('tacocat')); // true
```

---

```js
const isOdd = (value) => value % 2 !== 0;
function someRecursive(arr, callback) {
  // special case, base case
  if (arr.length === 0) return false;
  // other base case
  if (callback(arr[0])) return true;
  return someRecursive(arr.slice(1), callback);
}
console.log(someRecursive([1, 2, 3, 4], isOdd)); // true
console.log(someRecursive([4, 6, 8, 9], isOdd)); // true
console.log(someRecursive([4, 6, 8], isOdd)); // false
console.log(someRecursive([4, 6, 8], (val) => val > 10)); // false
```

---

```js
function flatten(arr) {
  let newArr = [];
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      newArr = newArr.concat(flatten(arr[i]));
    } else {
      newArr.push(arr[i]);
    }
  }
  return newArr;
}
console.log(flatten([1, 2, 3, [4, 5]])); // [ 1, 2, 3, 4, 5 ]
console.log(flatten([1, [2, [3, 4], [[5]]]])); // [ 1, 2, 3, 4, 5 ]
console.log(flatten([[1], [2], [3]])); // [ 1, 2, 3 ]
console.log(flatten([[[[1], [[[2]]], [[[[[[[3]]]]]]]]]])); // [ 1, 2, 3 ]
```
これを見て、前も配列の平坦化をやったことあると思いだしました。まだまだ書き方に慣れてないときだったんですが、振り返りとして残しておきます。

https://qiita.com/hu-yu/items/bc512eb2fc9cf6351e7f#flatten-multidimensional-array%E5%A4%9A%E6%AC%A1%E5%85%83%E9%85%8D%E5%88%97%E3%81%AE%E4%B8%80%E6%AC%A1%E5%85%83%E5%8C%96

---

```js
function capitalizeFirst(arr) {
  if (arr.length === 0) return [];
  let newArr = [];
  newArr.push(arr[0][0].toUpperCase().concat(arr[0].slice(1)));
  return newArr.concat(capitalizeFirst(arr.slice(1)));
}
console.log(capitalizeFirst(['car', 'taco', 'banana'])); // [ 'Car', 'Taco', 'Banana' ]
```

---

```js
const obj1 = {
  outer: 2,
  obj: {
    inner: 2,
    otherObj: {
      superInner: 2,
      notANumber: true,
      alsoNotANumber: 'yup',
    },
  },
};
const obj2 = {
  a: 2,
  b: { b: 2, bb: { b: 3, bb: { b: 2 } } },
  c: { c: { c: 2 }, cc: 'ball', ccc: 5 },
  d: 1,
  e: { e: { e: 2 }, ee: 'car' },
};

function nestedEvenSum(obj) {
  let sum = 0;
  for (let key of Object.keys(obj)) {
    if (typeof obj[key] === 'object') {
      // traverse next depth of obj
      sum += nestedEvenSum(obj[key]);
    } else if (typeof obj[key] === 'number' && obj[key] % 2 === 0) {
      // only even number can add
      sum += obj[key];
    }
  }
  return sum;
}
console.log(nestedEvenSum(obj1)); // 6
console.log(nestedEvenSum(obj2)); // 10
```
オブジェクトのコピーや探索について、

https://qiita.com/hu-yu/items/0ccd1b57067feeb35a2d#method

---

```js
function capitalizeWords(arr) {
  if (arr.length === 0) return [];
  let newArr = [];
  newArr.push(arr[0].toUpperCase());
  return newArr.concat(capitalizeWords(arr.slice(1)));
}

console.log(capitalizeWords(['i', 'am', 'learning', 'recursion']));
```

---

```js
let obj2 = {
  num: 1,
  test: [
    {
      num: 2,
    },
    [5],
  ],
  data: {
    val: 4,
    info: {
      isRight: true,
      random: 66,
    },
  },
};
// change olo obj
function stringifyNumbers(obj) {
  for (let key of Object.keys(obj)) {
    if (typeof obj[key] === 'object') {
      stringifyNumbers(obj[key]);
    } else if (typeof obj[key] === 'number') {
      obj[key] = obj[key].toString();
    }
  }
  return obj;
}
// return new obj
function stringifyNumbers(oldObj) {
  let copy = {};
  for (let key of Object.keys(oldObj)) {
    copy[key] = oldObj[key];
  }
  function recursivePart(newObj) {
    for (let key of Object.keys(newObj)) {
      if (typeof newObj[key] === 'object') {
        recursivePart(newObj[key]);
      } else if (typeof newObj[key] === 'number') {
        newObj[key] = newObj[key].toString();
      }
    }
  }
  recursivePart(copy);
  return copy;
}
console.log(stringifyNumbers(obj2));
```
`Number`→`String`は簡単にできますが、`String`→`Number`はそうはいきませんでした。

https://qiita.com/hu-yu/items/8bfc4a528b856e782986#coercion

（ちなみに`'0O0'`は`Number`型に`true`判定されたのは8進数の`0o000`、`'0X0'`は16進数の`0x0000`。大文字小文字問わず、ここでは分かりやすいように小文字にしました。2進数なら`'0b'`/`'0B'`から始まる。）

---

```js
const obj = {
  stuff: 'foo',
  data: {
    val: {
      thing: {
        info: 'bar',
        moreInfo: {
          evenMoreInfo: {
            weMadeIt: 'baz',
          },
        },
      },
    },
  },
};
function collectStrings(obj) {
  let arr = [];
  for (let key of Object.keys(obj)) {
    if (typeof obj[key] === 'object') {
      arr = arr.concat(collectStrings(obj[key]));
    } else if (typeof obj[key] === 'string') {
      arr.push(obj[key]);
    }
  }
  return arr;
}
console.log(collectStrings(obj));
```