🌊

JavaScript三十六景Tips集

に公開2

[背景]

このようなまとめが欲しかった。
LeetCodeを解きながら長い間書き留めてきたJavaScript Tips集です。
チーム開発ではもちろん可読性が重視されますので、その点もご留意ください。

elseブロックを段落で表現する

if(!l1) return l2
else if(!l2) return l1
else if (l1.val <= l2.val) // ...some
else return l2

いっぺんに代入する

// let left = 0;
// let current = 0;
// let answer = 0;

let left = current = answer = 0

[i][0]のような値にのみアクセスしたい場合

    for(let i = 0; i < queries.length; i++){
        res.push(limit > prefix[queries[i][1]] - prefix[queries[i][0]])
    }   

to

    for(let query of queries){
        res.push(limit > prefix[query] - prefix[query])
    }

i(index)を単独で使っていないのであればfor of

to

var reverseWords = function(s) {
    // some
    for (let c of s) {
        if (c === ' ') {
            res += word + c;
            word = '';
        } else {
            word = c + word;
        }
    }
    // some
};

includesしないで存在チェックをしないでSetを使う

 let numsSet = new Set(nums);

    for (const num of nums) {
        if (!numsSet.has(num + 1) && !numsSet.has(num - 1)) {
            ans.push(num);
        }
    }

n, kどちらかが0の場合0を返す

if(n === 0 || k === 0) return 0

to

if(n * k === 0) return 0

条件に合った時だけ動くfor文

let nextInsertIndex = 3
let k = 1
for(let r = 0; nextInsertIndex < k; r++){
    console.log("never console")
}

setする時端的に書く

let nums = [1, 2, 1]
let map = new Map()
for(let n of nums){
    if(map.has(n)){
        map.set(n, map.get(n) + 1)
        continue
    }
    map.set(n, 1)    
}
console.log(map)

to

let nums = [1, 2, 1]
let map = new Map()
for(let n of nums){
    map.set(n, (map.get(n) || 0) + 1)    
}
console.log(map)

iteratorを使う

let m = new Map([[1,1], [2,1], [3,1], [4,2]])
let arr = m.keys()
for(let e of arr){
    if(m.get(e) > 1){
        console.log(e)
    }
}

to

let m = new Map([[1,1], [2,1], [3,1], [4,2]])
let arr = m.keys()
let current = arr.next().value
while(current >= 0){
    if(m.get(current) > 1){
        console.log(current)
    }
    current = arr.next().value
}

whileを使わないforの書き方

  let start = 0, end = numbers.length-1
  while(start < end){
   ...some
  }

to

  for(let start = 0, end = numbers.length-1; start < end;){
     ...some
  }

cacheする

// cacheする
for (let i = 0, len = arry.length; i < len; i++) {
	console.log(ary[i])
}

短く書く

const a = {}
if(a["a"] === undefined) a["a"] = []
a["a"].push(1)

to

const a = {}
a["a"] ||= []
a["a"].push(1)

二次元配列の作り方

let dp = Array.from({length: word1.length + 1}).map(e => Array.from({length: word2.length +1}).fill(0))

to

let dp = Array(word1.length + 1).fill(0).map(() => Array(word2.length + 1).fill(0));

or

let dp = Array.from({ length: word1.length + 1 }, () => Array(word2.length + 1).fill(0));

よりシンプルになる。

maxを得たい時、spread構文を使う

12

 let max = 0;
  for (const val of candies) {
    val > max && (max = val);
  }

to

 const max = Math.max(...candies);

引数を得て何か再定義したい場合、ボディで定義する必要がないかもしれない

function some(candies, extraCandies, max = Math.max(...candies)){...}

overflowを考慮したmid値を得る

let mid = Math.floor((right + left) / 2)

to

let mid = Math.floor(left + (right - left) / 2)) // lower mid
// let mid = Math.floor(left + (right - left + 1) / 2)) // uper mid

object内のundefinedだけignoreする

to

// objはコピー後
Object.keys(obj).forEach(e => {
 if(obj[e] === undefined) delete obj[e]
})

奇数と偶数

from

if(nums[i] % 2 === 1 && nums[j] % 2 === 0){
 ...some
}

to

// 奇数の場合true
if(nums[i] % 2 > nums[j] % 2){
 ...some
}

Mapのvalue値をsortしたMapを返す

Mapを値でソートする場合は、entries()で[key, value]の配列を取り出し、それを値でソートしてからMapに戻す必要がある

let map = new Map([
  ['b', 2],
  ['a', 1],
  ['c', 3]
]);

// 値でソート
let sortedByValue = new Map([...map.entries()].sort(([, v1], [, v2]) => v1 - v2));

console.log([...sortedByValue]); 
// [['a', 1], ['b', 2], ['c', 3]]

keyでsort

let map = new Map([
  ['b', 2],
  ['a', 1],
  ['c', 3]
]);

// キーでソート
let sortedByKey = new Map([...map.entries()].sort(([k1], [k2]) => k1.localeCompare(k2)));

console.log([...sortedByKey]); 
// [['a', 1], ['b', 2], ['c', 3]]

returnすればメモリがO(n)じゃなくなる?

WIP
https://leetcode.com/problems/kids-with-the-greatest-number-of-candies/editorial/comments/1866073

forの終え方

(breakでもいいけど)

 for(let i = 2; i <= n; i++){
       if(n % i === 0){
           count++;
           if(count === k){
               res = i;
               i = n+1; // ここで条件以上にしてループを終える
           }
       }
   }
 return res

headがnullになるまで回す

 for(let i = 0; head; head = head.next){
      hs[i++] = head
 }

One line Fizz Buzz

const fizzBuzz = function(n) {
    let res = []
    for(let i = 1; i <= n; i++){
        const isDivisibleFive = (i % 5) === 0
        const isDivisibleThree = (i % 3) === 0
        if(isDivisibleFive && isDivisibleThree){
            res.push("FizzBuzz")
        } else if(isDivisibleThree){
            res.push("Fizz")
        } else if(isDivisibleFive){
            res.push("Buzz")
        } else {
            res.push(i.toString())
        }
    }
    return res
};

to

const fizzBuzz = function(n) {
   return new Array(n).fill(0).map((a, i) => (++i % 3 ? '' : 'Fizz') + (i % 5 ? '' : 'Buzz') || '' + i); 
};
// 常に++iをしていくと1から始まり0でFizzが付くことがない

https://leetcode.com/problems/fizz-buzz/solutions/89949/1-line-javascript-o-n-solution

簡単にdictionalyを作成する

const dict = "abcdefghijklmnopqrstuvwxyz";
const charMap = new Map();
for(let i = 0; i < dict.length; i++){
 if(!charMap.get(dict[i])) charMap.set(dict[i], i)
}

to

const dict = "abcdefghijklmnopqrstuvwxyz";
const charMap = new Map([...dict].map((v, i) => [v, i]));

ビットシフトを検討する

let right1 = 12345;
let right2 = 12345;

// 乗算
console.time('Multiplication');
for (let i = 0; i < 100000000; i++) {
  right1 *= 2;
}
console.timeEnd('Multiplication');

// ビットシフト
console.time('Bitshift');
for (let i = 0; i < 100000000; i++) {
  right2 <<= 1;
}
console.timeEnd('Bitshift');

// Multiplication: 588.912109375 ms
// Bitshift: 165.72705078125 ms

数字の桁数を数えるとき

let n = 100;
n.toString().length // 3

to

Math.log10(100) + 1 // 3

桁の長さが事前にわからない場合、toStringはO(n)の空間複雑度になる。一方Math.log10はO(1)

Setは果たしてコスト減になっているか

このようなintersectionを求める関数の例
重複を気にしないようにSet.addで積んでいって最後に配列に戻していますが、最初から配列で書くことで不要な変換を避けることができる

const intersection = function(nums1, nums2) {
    nums1.sort((a, b) => a - b)
    nums2.sort((a, b) => a - b);
    let p1 = 0;
    let p2 = 0;
    let set = new Set()
    while(p1 < nums1.length && p2 < nums2.length){
        if(nums1[p1] === nums2[p2]){
            set.add(nums1[p1])
            p1++
            p2++
        } else if(nums1[p1] < nums2[p2]){
            p1++
        } else {
            p2++
        }
    }
    return Array.from(set)
};

to

const intersection = function(nums1, nums2) {
    nums1.sort((a, b) => a - b);
    nums2.sort((a, b) => a - b);
    let p1 = 0;
    let p2 = 0;
    const result = [];
    
    while (p1 < nums1.length && p2 < nums2.length) {
        if (nums1[p1] === nums2[p2]) {
            // 重複を避けるために、最後に追加された値と比較
            if (result.length === 0 || result.at(-1) !== nums1[p1]) {
                result.push(nums1[p1]);
            }
            p1++;
            p2++;
        } else if (nums1[p1] < nums2[p2]) {
            p1++;
        } else {
            p2++;
        }
    }
    
    return result;
};

そもそもtwo pointerを使わないでも書けますが上記は説明のため
other

const intersection = function(nums1, nums2) {
    const set1 = new Set(nums1); // nums1 の要素をハッシュセットに追加
    const result = new Set(); // 結果を格納するためのセット
    for (const num of nums2) {
        if (set1.has(num)) {
            result.add(num); // nums2の要素がset1に含まれていれば結果に追加
        }
    }
    return Array.from(result); // 結果を配列に変換して返す
};

時間効率: ソートする必要がないため、計算量は O(n + m) で済む。(nとmはそれぞれnums1とnums2の長さ)。ソートを使用したアプローチ(O(n log n + m log m))よりも高速。

結論:多くの場合 Set が最適

循環する配列の中で最後に値を加える

Circular Queueで活躍する


let a = [undefined, undefined, 1, 2, 3]
const count = 3
const length = a.length;
const headIndex = 2;
const b = a[(headIndex + count) % length] = 4
b // [4, undefined, 1, 2, 3]

more

dequeueの際のheadIndexの移動

this.headIndex = (this.headIndex + 1) % this.capacity;

文字が持つ数字をindex位置にして重複チェックに利用する

let dis = Array(26).fill(false);
const index = s[right].charCodeAt() - 97; // 97 は文字 'a' の ASCII コード値
dis[index] = true

ラベル付きループで外側のループを抜ける

outerLoop: for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
        if (i === 1 && j === 1) {
            break outerLoop; // 外側のループを抜ける
        }
        console.log(`i = ${i}, j = ${j}`);
    }
}

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/label

ビット演算子で整数に変換

const num = 5.75;
console.log(num | 0) // 5
// ビット単位のOR演算。|が内部的に符号付き整数に変換するため、5|0ということが起きている

この方法は、通常の Math.floor よりも高速であることが多い。32ビット整数の範囲(約-2^31から2^31-1)の数値にしか適用できないので注意

シンボルを使った隠しプロパティ

const hidden = Symbol('hiddenProperty');
const obj = {
    [hidden]: 'This is hidden',
    visible: 'This is visible'
};

console.log(obj.visible); // "This is visible"
console.log(obj[hidden]); // "This is hidden"
console.log(Object.keys(obj)); // ["visible"]

Intlオブジェクトで文字列のフォーマッティング

const number = 1234567.89;
const formatted = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(number);
console.log(formatted); // "$1,234,567.89"

// これを使うと、数値のフォーマットや通貨の表示などを簡単に行える

XOR の性質を利用して重複排除

// 配列の中で一つだけ出現する数字を求める
let nums = [2, 3, 2, 4, 3];
let res = 0;
for (let n of nums) res ^= n;
console.log(res); // 4
  • a ^ a = 0, a ^ 0 = a の性質で重複を消せる
  • Set を使わず O(1) メモリで済む

~~ で高速に整数化

console.log(~~5.75); // 5
  • Math.floor の省略形としてよく使われる
  • ただし32bit 整数範囲に限定される

注意

数値をsortする場合はカスタム比較関数を渡すこと

sort()メソッドはデフォルトで文字列としてのソートを行う。各要素を文字列として扱い、Unicodeのコードポイントに基づいてソートするため数値が正しい順序でソートされない

[2, 34, 4, 100,4343, 2, 244, 34,400, 41].sort()
// [100, 2, 2, 244, 34, 34, 4, 400, 41, 4343]

参考資料

Discussion

shiracamusshiracamus

競技プログラミングならありだけど、プログラミング教育なら動作の違いも把握しておくのがいいですね。

いっぺんに代入する

let left = current = answer = 0

これ、left だけが let(ローカル変数) で current と answer はグローバル変数になります。

function sample() {
    let left = current = answer = 0
}

sample();
console.log(current)  // グローバル変数なので表示できる
console.log(left)       // ReferenceError: left is not defined

n, kどちらかが0の場合0を返す

これ、厳密比較ではなくなります。

> let n = '0'
'0'
> let k = '0'
'0'
> n === 0 || k === 0
false
> n * k === 0
true