Skip to content

387. First Unique Character in a String #29

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions 387/step1.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
Solve Time: 4:20

Time: O(n)
Space: O(1)

Choose a reason for hiding this comment

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

文字列sの長さがnだとすると、空間計算量もそれに比例して大きくなると思ったのでO(n)になるのかな?と思ったのですが、O(1)になる理由があれば教えていただきたいです!

Copy link
Owner Author

Choose a reason for hiding this comment

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

character_to_countのみ考慮してO(1)って感じです。
引数は考慮していませんでした。

Choose a reason for hiding this comment

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

character_to_countが使うメモリ容量は文字列の長さ(n)に比例して大きくなるのでO(n)かと思ったのですが、どうでしょう?

Copy link
Owner Author

Choose a reason for hiding this comment

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

文字列の長さには比例しません。
文字列に含まれる文字種類数に比例します。

Choose a reason for hiding this comment

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

確かにそうでした!やっと理解できました。ありがとうございます。


一回の走査で解けないかを軽く模索したが思いつかなかったので愚直にmapで記録し、二回ループする手法で解いた。
一回の走査で解くことに固執しかけて時間を無駄に思想になっていたため、そこに気づけた点が個人的によかった
*/
class Solution {
public:
int firstUniqChar(string s) {
map<char, int> character_to_count;
for (const auto& c : s) {
Copy link

Choose a reason for hiding this comment

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

型のサイズがレジスター幅以下の場合、 const 参照ではなく値のコピーで受け取ったほうが、処理が速くなると思います。理由は、 const 参照で受け取ると、値を参照する際にアドレスの計算が 1 回入るためです。
for (auto c : s) {
Effective C++ 第3版の「20項 値渡しよりconst参照渡しを使おう」に、例外事項として書いてあったかもしれません。

Copy link
Owner Author

@colorbox colorbox Nov 28, 2024

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.

現実問題、この場合はどちらも最適化されると思いますが、const 参照にする意図が不明とは感じますね。
リファレンスは、アドレスで実装されることがあるだけであり、概念的には「名前」のようなものともいえるでしょう。(A reference can be thought of as a name of an object. N4860 9.3.3.2 References 第1段落。)

呼出規約を確認ですかねえ。
色々と環境次第なんですが、x64 ABI の場合、大まかに64ビットまではコピーされてレジスターかスタックに積まれ、それ以上はポインターがレジスターかスタックに積まれます。
https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170
https://learn.microsoft.com/ja-jp/cpp/build/x64-calling-convention?view=msvc-170

Copy link
Owner Author

Choose a reason for hiding this comment

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

なるほど、ありがとうございます、参考にします。

character_to_count[c]++;
}
for (int i = 0; i < s.size() ; i++) {
Copy link

Choose a reason for hiding this comment

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

find_if() 関数を使ったほうがよいかと一瞬思ったのですが、こちらのほうがシンプルだと思います。

if (character_to_count[s[i]] == 1) {
return i;
}
}
return -1;
}
};
23 changes: 23 additions & 0 deletions 387/step2_1.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
Time: O(n)
Space: O(1)

step1の改良、マジックナンバーを定数にした
*/
class Solution {
public:
int firstUniqChar(string s) {
map<char, int> character_to_count;
for (const auto& c : s) {
character_to_count[c]++;
}
for (int i = 0; i < s.size(); i++) {
if (character_to_count[s[i]] == 1) {
return i;
}
}

int const kNotFound = -1;
Copy link

Choose a reason for hiding this comment

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

const int kNotFound = -1; のほうがよく見かけるように思います。また、定数には constexpr を優先して使用したほうがよいと思います。

Copy link
Owner Author

Choose a reason for hiding this comment

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

これは書き方をミスっていました。
ありがとうございます、constexpr勉強になります。

return kNotFound;
}
};
32 changes: 32 additions & 0 deletions 387/step2_2.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
Time : O(N log N)
Space : O(N)

https://github.com/kazukiii/leetcode/pull/16/files#diff-f400dc0fa41a78f5b2a9ec5c2d5364dd8cf0da8dc9e8c39d15f52f98ea05394b
を参考にした解法。
平衡木(mapのkey)を利用して文字の初出インデックスを管理。
二度目の出現でインデックスを削除することで、平衡木の先頭には一度しか出現していない文字のインデックスが残る。
3度以上の出現で削除した要素が復帰してしまうが、インデックスが大きいため結果に影響しない。
*/
class Solution {
public:
int firstUniqChar(string s) {
map<char, int> character_to_first_index;
Copy link

Choose a reason for hiding this comment

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

1-pass で処理するにあたり、文字をキューに入れていき、 2 回以上出現する文字を取り除いていくというやり方を考えました。

class Solution {
    struct CharacterAndIndex {
        char c;
        int index;
    };
public:
    int firstUniqChar(string s) {
        map<char, int> char_to_count;
        queue<CharacterAndIndex> characters;
        for (int i = 0; i < s.size(); ++i) {
            char c = s[i];
            characters.push({c, i});
            ++char_to_count[c];

            while (!characters.empty() && char_to_count[characters.front().c] >= 2) {
                characters.pop();
            }
        }

        if (!characters.empty()) {
            return characters.front().index;
        }

        return -1;
    }
};

Copy link
Owner Author

Choose a reason for hiding this comment

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

おぉ、なるほど、これは直感的でわかりやすいですね。
ありがとうございます。

map<int, char> index_to_character;
for (int i = 0; i < s.size(); i++) {
char c = s[i];
if (character_to_first_index.contains(c)) {
int first_index = character_to_first_index[c];
index_to_character.erase(first_index);
continue;
}
character_to_first_index[c] = i;
index_to_character[i] = c;
}
if (index_to_character.empty()) {
const int kNotFound = -1;
return kNotFound;
Comment on lines +27 to +28
Copy link

Choose a reason for hiding this comment

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

これ、私はちょっとやりすぎな気がしていて、

return -1;  // Not Found

くらいのコメントでよいのではないでしょうか。
(実際には、.h ファイルにメソッドの説明が書かれると思います。)
あ、つまり、.h にクラスの宣言をして、.cc に実体を書くのが標準的で、こういうイメージです。
https://source.chromium.org/gn/gn/+/main:src/base/files/file.h;l=155?q=%5C%20%5C%20%2F%2F.*-1%20file:.h$

Copy link
Owner Author

Choose a reason for hiding this comment

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

なるほど、そのあたりの感覚参考になります、ありがとうございます。

}
return index_to_character.begin()->first;
}
};
34 changes: 34 additions & 0 deletions 387/step2_2_suggested.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
https://github.com/colorbox/leetcode/pull/29#discussion_r1861430039
を見てそれを参考に自分で書いた解法、比較用
*/
class Solution {
public:
int firstUniqChar(string s) {
queue<CharacterAndCount> queue;
map<char, int> character_to_count;
for (int i = 0; i < s.size(); i++) {
char c = s[i];
character_to_count[c]++;
queue.push({c, i});
while (true) {
char front_character = queue.front().character;
if (character_to_count[front_character] > 1) {
queue.pop();
} else {
break;
}
}
}
if (queue.empty()) {
return -1;
}
return queue.front().index;
}

private:
struct CharacterAndCount {
char character;
int index;
};
};
31 changes: 31 additions & 0 deletions 387/step2_2_suggested_fixed.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
step2_2_suggested.cpp
https://github.com/colorbox/leetcode/pull/29#discussion_r1861430039
の書き方に沿って修正したコード、参照用
*/
class Solution {
public:
int firstUniqChar(string s) {
queue<CharacterAndCount> characters;
map<char, int> character_to_count;
for (int i = 0; i < s.size(); ++i) {
char c = s[i];
++character_to_count[c];
characters.push({c, i});
while (character_to_count[characters.front().character] >= 2) {
characters.pop();
}
}
if (characters.empty()) {
return -1;
}
return characters.front().index;
}

private:
struct CharacterAndCount {
char character;
int index;
};
};
43 changes: 43 additions & 0 deletions 387/step2_3.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
Time : O(N log N)
Space : O(N)

https://github.com/kazukiii/leetcode/pull/16/files#diff-f5b3276a9df261ed1c1e70d5b6de00ca4544e1a1ae1de6a466add8df5cd3d1b0
を参考にした解法。
ソートで文字種ごとに隣接させ、二度以上出現したものを除外し、min比較で最小インデックスを求める。
*/
class Solution {
public:
int firstUniqChar(string s) {
vector<CharacterIndex> character_indices;
for (int i = 0; i < s.size(); i++) {
character_indices.push_back({s[i], i});
}
sort(character_indices.begin(), character_indices.end());
Copy link

Choose a reason for hiding this comment

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

std::ranges::sort を使う手、あと、lambda で何でソートするかを渡す手もあります。

int count = 1;
int first_unique_character_index = numeric_limits<int>::max();
for (int i = 0; i < s.size(); i++) {
if (i < s.size() - 1 && character_indices[i].character == character_indices[i + 1].character) {
count++;
continue;
}
if (count == 1) {
first_unique_character_index = min(first_unique_character_index, character_indices[i].index);
}
count = 1;
}
Comment on lines +19 to +28
Copy link

Choose a reason for hiding this comment

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

これちょっと読みにくい気がしますね。
二重ループにするのは一つですよ。えー、こんな感じかな?

for (int i = 0; i < character_indices.size();) {
  int same_end;
  for (same_end = i + 1; same_end < character_indices.size() && character_indices[i].character == character_indices[same_end].character; ++same_end);
  if (same_end - i == 1) {
    first_unique_character_index = min(first_unique_character_index, character_indices[i].index);
  }
  i = same_end;
}

Copy link
Owner Author

Choose a reason for hiding this comment

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

なるほど、ありがとうございます。
ループをまたいでcount情報を持つよりもループ内で完結させて二重ループにするほうが読みやすいですね。

if (first_unique_character_index == numeric_limits<int>::max()) {
return -1;
}
return first_unique_character_index;
}

struct CharacterIndex {
char character;
int index;

bool operator<(const CharacterIndex& other) {
return character < other.character;
}
};
};
96 changes: 96 additions & 0 deletions 387/step2_4.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
Time : O(N)
Space : O(1)

LRUを応用してできるとのことだったので実装。
https://www.geeksforgeeks.org/lru-cache-implementation-using-double-linked-lists/
本質的に平衡木による解法と大差ない気がする。
*/
struct Node {
int index;
int count;
Node* prev;
Node* next;

explicit Node(int i) {
index = i;
count = 1;
prev = nullptr;
next = nullptr;
}
};

class LRUCache {
public:
Node* head;
Node* tail;
map<char, Node*> character_to_node;

LRUCache() {
head = new Node(numeric_limits<int>::max());
tail = new Node(numeric_limits<int>::max());
head->next = tail;
tail->prev = head;
}

~LRUCache() {
for (auto [_c, node] : character_to_node) {
delete node;
}
delete head;
delete tail;
}

void Put(char character, int index) {
if (character_to_node.contains(character)) {
Node *node = character_to_node[character];
Remove(node);
node->count++;
return;
}

Node *node = new Node(index);
character_to_node[character] = node;
PushBack(node);
}

bool Empty() {
return head->next->index == tail->index;
}

private:
void Remove(Node* node) {
if (node->prev == nullptr) {
return;
}
Node* prev = node->prev;
Node* next = node->next;
prev->next = next;
next->prev = prev;
node->prev = nullptr;
node->next = nullptr;
}

void PushBack(Node* node) {
Node* prev = tail->prev;
prev->next = node;
node->prev = prev;
node->next = tail;
tail->prev = node;
}
};

class Solution {
public:
int firstUniqChar(string s) {
LRUCache cache = LRUCache();
Copy link

Choose a reason for hiding this comment

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

LRU という名前は Least Recently Used という意味で、使わないやつから削除するということなので、現在の実装である同じものが2つ以上入ると Remove で linked list だけから削除されるというのは、ちょっと誤解を生む気がします。

出現回数を別の map で管理するか、重複したものを入れておく set を作るかして、このクラスをサイズ無制限の普通の LRU にしておくというのも一つでしょう。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます、LRUを改造して今回の問題に対応させましたが、ある程度改造した時点でもうLRUじゃないので名前を変えるなどすべきでした

for (int i = 0; i < s.size(); i++) {
cache.Put(s[i], i);
}
if (cache.Empty() || cache.head->next->count > 1) {
return -1;
}
return cache.head->next->index;
}
};

15 changes: 15 additions & 0 deletions 387/step3.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class Solution {
public:
int firstUniqChar(string s) {
map<char, int> character_to_count;
for (const char& c : s) {
character_to_count[c]++;
}
for (int i = 0; i < s.size(); i++) {
if (character_to_count[s[i]] == 1) {
return i;
}
}
return -1;
}
};