Skip to content
290 changes: 290 additions & 0 deletions arai60/hash-map/first-unique-charaacter-in-a-strings/answer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
# 387. First Unique Character in a String

## STEP1

### 発想
* 文字ごとの出現回数を数える。
* 先頭から1つずつ走査していく。
* 出現回数が1の文字があった際には、そのインデックスを返却する。

```javascript
const firstUniqChar = function(s) {
const char_to_count = new Map()
for (const ch of s) {
if (!char_to_count.has(ch)) {
char_to_count.set(ch, 0)
}
const count = char_to_count.get(ch)
char_to_count.set(ch, count + 1)
}
for (let i = 0; i < s.length; i++) {
const ch = s[i]
if (char_to_count.get(ch) === 1) {
return i
}
}
return -1
};
```

## STEP2

* pythonのdefaultdictのように、mapのkeyが存在しない際に0が入るように、
|| を用いて、書くほうが好みなのでこちらに修正した。

```javascript
const firstUniqChar = function(s) {
const charToCount = new Map()
for (const c of s) {
const count = charToCount.get(c) || 0
charToCount.set(c, count + 1)
}
for (let i = 0; i < s.length; i++) {
if (charToCount.get(s[i]) === 1) {
return i
}
}
return -1
};
```

## STEP3

* 1分、1分、1分

```javascript
const firstUniqChar = function(s) {
const charToCount = new Map()
for (const c of s) {
const count = charToCount.get(c) || 0
charToCount.set(c, count + 1)
}
for (let i = 0; i < s.length; i++) {
if (charToCount.get(s[i]) === 1) {
return i
}
}
return -1
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

読みやすかったです。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

読みやすいです。個人的にはcountは切り出さずにsetのカッコの中にそのまま入れてしまってもいい気がします。

```

## 感想

* For文が2周していた際に、1周でできないかと考えるのは、習慣化できそう。

* LinkedHashMap というデータ構造については、次に解くLRUの問題を解いた際に学習する。

### コメント集を読んで

## 他の人のPRを読んで

* hayashi-ayのコメント
* Mapではなく、配列で出現回数を保持する方法がある。
* 参照: https://discord.com/channels/1084280443945353267/1233603535862628432/1237973103670198292

* UTF-8, UTF-16
https://github.com/ichika0615/arai60/blob/96c3a36d478787dbe6fe0c03a45bc1e392be386d/arai60_15.md#%E4%BB%A5%E4%B8%8B%E5%8B%89%E5%BC%B7

* Ryotaro25
* PR: https://github.com/Ryotaro25/leetcode_first60/pull/16/
* 387.FirstUniqueCharacterinaString/step4.cpp の参照を実装している箇所が読めない。
--> ポインターや参照の箇所が理解できていない。次の問題までに勉強する。
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これ的外れだったら放念してください.
ポインターはデータの置いてあるアドレスを格納した変数です.
C言語で,ローカルスコープの変数を呼び出した関数内で変更しようとすると,デフォルトの挙動では変数がコピーされて渡されるので,もとの変数は書きかわりません.
なので呼び出した先で呼び出し元の変数を変更するには,元の変数のアドレス (=住所) を渡して,これの中身を見る (デリファレンス,*をつけます) ことが必要になります.
以下の例で言うと,fを呼び出した時は呼び出し元の引数は変化せず,gを呼び出した際には変化します.

#include <stdio.h>

void f(int a) {
  a = 10;
}

void g(int *a) {
  *a = 10;
}

int main() {
  int a = 0;
  f(a); // 変化しない
  printf("%d\n", a); // 出力: 0
  g(&a); // 変化する,`&`はアドレスを取得する演算子
  printf("%d\n", a); // 出力: 10
}

そこで,Cではしばしば,ポインタの書き換えを呼び出した関数内で行う,ということになると,ポインタのポインタのポインタの...というような面倒なことになることがあります.

一方,C++ではこの部分のインターフェースを参照という形で直感的で簡潔にしてくれていて,明示的にポインタを用いないでも,内部にポインタを用いて呼び出した関数内での変更を反映できるようになっています.

上の例で行けば,

#include <stdio.h>
// 引数が,`&`付きで,参照になっている
void f(int& a) {
  a = 10; // デリファレンスはしてないが,呼び出し元が書き換わる
}

int main() {
  int a = 0;
  f(a); // 今度はポインタを取らないが
  printf("%d\n", a); // これは10になっている
}

この方は,この参照の機能を自分で,Cライクな様式である,ポインタを用いて再実装してみた,ということをされているのだと思います.


* Yoshiki-Iwasa
* PR: https://github.com/Yoshiki-Iwasa/Arai60/pull/14
* 思考プロセスが記述されていて、すごくレビューがしやすくなると思うので、自分も真似をする。
* rustは文法が理解できていないので、公式ドキュメントを見ながら文法を検索できるようにする。

* olsen-blue
* PR: https://github.com/olsen-blue/Arai60/pull/15/
* return -1とする際には、関数の呼び出し側でコメントを書くことがある。
https://github.com/olsen-blue/Arai60/pull/15/files#r1913610172
* 多言語で予約後となっているものは、変数名として避けた方が良い。
https://github.com/olsen-blue/Arai60/pull/15/files#r1941265089

* hayashi-ay
* PR: https://github.com/hayashi-ay/leetcode/pull/28/
* 個人的に、PythonやJavascriptのMapのkeyの入力順が保存されることを使用した
方法(`*04`)が読みやすくて好み。
https://github.com/hayashi-ay/leetcode/pull/28/files#diff-5ec7c3c87171edf4d61e9eb79fd926cafa27caf068da7474222897c8e9e7ab96R91-R107

* hroc135
* PR: https://github.com/hroc135/leetcode/pull/15/
* 「どの情報から順番に出してあげることで、より早く伝わるかを考える。 」をすぐに取り入れたい。
https://github.com/hroc135/leetcode/pull/15/files#r1731514545
* メモリにおけるキャッシュ効率について理解ができていないので、勉強する。

* kazukiii
* PR: https://github.com/kazukiii/leetcode/pull/16/files
* nodchipさんのコメント
* 以下を理解できていないので、勉強する。
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このよしあしの差はかすかです。ただ、背景の仕組みは分かっていて欲しいです。
レジスターの大きさと値の大きさのどちらが大きいかの問題で、機械語にしたときに値が8バイト以下だと扱いが変わるので参照にしないほうが速そうだという話です。

Copy link
Owner Author

@syoshida20 syoshida20 May 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本を読み、nodchipさんとodaさんの指摘が理解できました。

プロセッサの記憶階層は、以下のようになっている。

  • CPUの汎用レジスタは、 64ビットアーキテクチャだと8バイトを格納できる。 容量は64バイト-256バイトで、1サイクルでアクセスできる。
  • L1キャッシュは、容量が8KB-256KBで、数サイクルでアクセスできる。。
  • L2キャッシュは、1時キャッシュの10倍程度の容量を持ち、10-20サイクルでアクセスできる。
  • メモリは、容量は数GBで、数百サイクルでアクセスできる。

値が8バイト以下に収まるデータを考えた際に、

  • 参照渡しにした場合

    • レジスタには、ポインターが格納される。
    • 実際の値にアクセスするために、初回はメモリアクセス、2回目以降はL1キャッシュ、L2キャッシュへのアクセスが必要となる。
    • だから、定数倍遅くなる。
  • 値渡しにした場合

    • レジスタには、 値が直接格納される。
    • 直接レジスタから値を読み取るため1サイクルで済む。

実際にはコンパイラーが最適化してくれる可能性もあります。
このよしあしの差はかすかです。

コンパイラの中で、以下のような最適化が行われる。 だから、参照渡しでも値渡しと同等の性能が得られることがある。

  • 参照の間接参照を除去
  • 値を直接レジスタで処理

(プロセッサを支える技術の3章の「キャッシュの仕組み」から「プロセッサの記憶階層」 を参照しました。)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

値渡しにしても、なんらかの意味でメモリーから取ってくるはずなので、数百クロックもの差はないでしょう。
ただ、アドレス参照が一回増えるかもしれないという感覚があるからで、それが最適化で消えるかもしれないのも確かです。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます!
コンパイル結果を見て、実際にどうなっているか比較するというのをやってみようと思います。

> x64 アーキテクチャーを仮定するのであれば、値が 64 ビット (8 バイト) 以内に収まる場合は、
> 参照にしないほうが良いと思います。理由は、値が 64 ビット (8 バイト) 以内に収まる場合、
> 汎用レジスター 1 本に格納できるためです。

## その他の方法

* `*01` BruteForceアプローチ

```javascript
const firstUniqChar = function(s) {
for (let i = 0; i < s.length; i++) {
let isIthCharUnique = true
for (let j = 0; j < s.length; j++) {
if (i !== j && s[i] === s[j]) {
isIthCharUnique = false
break
}
}
if (isIthCharUnique) {
return i
}
}
return -1
};
```

* `*02` キューを用いた方法
* 参照 :野田さんのコメント https://github.com/colorbox/leetcode/pull/29/files#r1861430039

```javascript
const firstUniqChar = function(s) {
const container = []
const charToCount = new Map()
for (let i = 0; i < s.length; i++) {
const pair = new Pair(i, s[i])
container.push(pair)
const count = charToCount.get(s[i]) || 0
charToCount.set(s[i], count + 1)

let top = container[0]
while (container.length > 0 && charToCount.get(top.char) > 1){
container.shift()
top = container[0]
}
}
if (container.length > 0) {
return container[0].index
}
return -1
};
```

* `*03` 配列を用いた方法

```javascript
const firstUniqChar = function(s) {
const charCount = new Array(26).fill(0)
const aAsciiCode = 'a'.charCodeAt(0)
const zAsciiCode = 'z'.charCodeAt(0)
for (const c of s) {
const ithCharAsciiCode = c.charCodeAt(0)
if (ithCharAsciiCode < aAsciiCode && zAsciiCode < ithCharAsciiCode) {
throw new Error('invalid input')
}
const idx = ithCharAsciiCode - aAsciiCode
charCount[idx]++
}
for (let i = 0; i < s.length; i++) {
const idx = s[i].charCodeAt(0) - aAsciiCode
if (charCount[idx] === 1) {
return i
}
}
return -1
};
```

* `*04` JavaScriptのMapが順序を保持することを利用した方法

```javascript
const firstUniqChar = function(s) {
const duplicated = new Set()
const uniqueCharToIdx = new Map()
for (let i = 0; i < s.length; i++) {
if (duplicated.has(s[i])) {
continue
}
if (uniqueCharToIdx.has(s[i])) {
uniqueCharToIdx.delete(s[i])
duplicated.add(s[i])
continue
}
uniqueCharToIdx.set(s[i], i)
}
if (uniqueCharToIdx.size === 0) {
return -1
}
const iterator = uniqueCharToIdx.values()
return iterator.next().value
};
```

### コードの良し悪し

* `*10` 出現回数を数えて、1回だけ出現するものを見つけた際に、そのインデックスを返す方法
* 空間計算量 : O(1)
* 時間計算量 : O(N) for文が2周している。

* `*11` Brute Forceアプローチ
* 空間計算量 : O(1)
* 時間計算量 : O(N^2)

* `*12` キューを用いた方法
* 空間計算量 : O(N) キューを用いている。
* 時間計算量 : O(N) for文が1周している。

* `*13` 配列を用いた方法
* 空間計算量 : O(1)
* 時間計算量 : O(N) for文が2周している。

* `*14` JavaScriptのMapが順序を保持することを利用した方法
* 空間計算量 : O(N) duplicatedのSetで、O(N)の空間計算量が必要。
* 時間計算量 : O(N) for文が1周している。

## 調べたこと

* str.encode関数は、UTF-8にエンコードされる.
https://docs.python.org/3/library/stdtypes.html#str.encode

* String.prototype.charCodeAt() https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt

> charCodeAt() always indexes the string as a sequence of UTF-16 code units,
> so it may return lone surrogates. To get the full Unicode code point
> at the given index, use String.prototype.codePointAt().

```javascript
const c = "𩸽";

console.log(c.length); // 2 (2つのコードユニットで構成)
console.log(c.charCodeAt(0)); // 55399 (高位サロゲート)
console.log(c.charCodeAt(1)); // 56893 (低位サロゲート)
console.log(c.codePointAt(0)) // 171581 (UniCodeコードポイント)

const codePoint = c.codePointAt(0)
// codePoint = (highSurrogate - 0xD800) * 0x400 + (lowSurrogate - 0xDC00) + 0x10000
highSurrogate = Math.floor((codePoint - 0x10000) / 0x400) + 0xD800 // 55399
lowSurrogate = ((codePoint - 0x10000) % 0x400) + 0xDC00 // 56893
console.log(highSurrogate === c.charCodeAt(0)) // true
console.log(lowSurrogate === c.charCodeAt(1)) // true
```

* ord() は、Unicode CodePointが返却される。
https://docs.python.org/3/library/functions.html#ord

* PythonのOrderedDictは、双方向連結リストとHash Tableでできている。
https://github.com/python/cpython/blob/a66bae8bb52721ea597ade6222f83876f9e939ba/Lib/collections/__init__.py#L89

* C++のmapは、keyでソートされている。
https://cplusplus.com/reference/map/map/
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::mapは平衡二分木で,pythonのdictやJSのMapに対応するハッシュテーブルベースのデータ構造としてはstd::unordered_mapがあります.


* Pythonのcollections.Counterの内部実装
* https://github.com/python/cpython/blob/a66bae8bb52721ea597ade6222f83876f9e939ba/Lib/collections/__init__.py#L551
* Pure Pythonで書かれている。

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_count_elements についてはCの実装があれば使われるようですね。


* Goにおけるmapの内部実装
https://github.com/golang/go/blob/eb4069127a7dbdaed480aed80ba6ed1b2ea27901/src/internal/runtime/maps/map.go

* ハッシュ表の理解 (kumagiさんのスライド)
* OpenAddressing, ClosedAddressingのところまで読んだ。
https://www.docswell.com/s/kumagi/ZGXXRJ-hash-table-world-which-you-dont-know