GAFAコーディング面接こんな感じでした

このあいだ、GAFA数社のコーディング面接を受けて全落ちしました。後続のため、オンサイト面接がこんな感じだったよ、というのをストーリー風に仕立てて公開します。問題と会話はダミーですが、雰囲気はかなり近くできたと思います。なお実際の会話はすべて英語で、バーチャルでの実施でした。

メイン問題はLeetCodeのNo.1472をもとに作成。
https://leetcode.com/contest/weekly-contest-192/problems/design-browser-history/

ちなみに「ぼく」はIQ+30くらいの設定です。それではどうぞ。


入室と自己紹介
面接官「やあ!わたしはシンディ。会えて嬉しいよ!」
ぼく「こんにちは、シンディ。ぼくはyambe2002。調子はどう?」
面「超いい感じだよ。きみは?」
ぼ「ぼくも超いい感じさ」
面「それはよかった。わたしは部署Aのソフトウェアエンジニアで3年目なんだ。社内ツールを作ってるよ。AI関係のツールで、超クールでExcitingなやつなんだ」
ぼ「それはクールだね」
面「簡単な自己紹介をお願いしていいかな?」
ぼ「うん。ぼくは経験豊富なソフトウェアエンジニアで…〇〇で貢献して…リーダー経験が……」
面「Cool(たぶん聴いてない)。じゃ、問題に入ろうか。わたしからの問題はね…」
ぼ「あ、はい」


出題と質疑
面「Webブラウザの履歴を管理するコードを書いて」
ぼ「Webブラウザの履歴…」
面「そう。Webブラウザってさ、『戻る』ボタンで前のページに戻っていけるでしょ?あれを実現するの。『進む』ボタンもあるから注意して」
ぼ「なるほど。えーと、それはChromeみたいな普通のブラウザだよね。えーとえーと」
面「…」
ぼ「えーと、そうだ、APIとか決まってる?」
面「決まってないよ。好きに決めていいよ」
ぼ「うーん。じゃあ履歴って何を保持したらいい?URLとサイト名、タイムスタンプ?」
面「いい質問だね。ここでは簡単のため、そうだね、サイト名だけにしようか」
ぼ「ならサイト名のリストを持つする感じかな」
面「多分そうだね」
ぼ「えーと、そして、『進む』の時に遷移するサイト名、『戻る』のときに遷移するサイト名を返すAPIが要る」
面「うん。あと新しいサイトに遷移したときも」
ぼ「そうだね。じゃあ内部的には、サイト名の履歴を文字列リストで持って、Indexで現在の位置を管理する感じかな…」
面「それで行けそう?」
ぼ「待って。それで、APIはVisit()、Back()、Forward()でいい?」
面「うん、そうだね。とりあえずAPIはそれで良いよ」
ぼ「OK。あ、Back()やForward()で次に行けないときは、どうする?」
面「それもいい質問だ。そうだね、今のサイト名を返すようにしようか」
ぼ「あと何かあるかな…」
面「…」
ぼ「ブラウザだと、履歴一覧を表示したり、そこからサイトにジャンプしたりできるけど…」
面「あとで必要になるかもね」
ぼ「だよね。速度は…当然すべてO(1)でやらないといけない」
面「速いほうがいいね」
ぼ「あとは、えーと、履歴クリアもあとで付けそうだな。まあこれは簡単か」
面「そうだね」
ぼ「まとめると、履歴は文字列のリストで持ち、現在位置をIndexで管理する。Back()、Forward()が呼ばれたら、Indexをデクリメントかインクリメントする。Visit()のときは、Indexをインクリメントして、そこに新しいサイト名を書き込む」
面「Visit()でリストの長さが足りなかったら?」
ぼ「そのときはリストにAdd()する。あ、リストって可変長の配列のことね」
面「それで良さそう?」
ぼ「うーん、多分…なにかあるかな…」
面「『戻る』を何度かしたあと、新しいサイトを訪れたらどうなる?」
ぼ「ん?……あ、だめだ!そうか、『戻る』『戻る』『訪ねる』『進む』で過去のサイトに飛んでしまう!つまり、Visit()が呼ばれたら、それより後ろの履歴はクリアしないと!」
面「そう!ならどうする?」
ぼ「うーん。変数MaxIndexを足せばいいかな。MaxIndexより大きなIndexは無効。Visit()が呼ばれたら更新する」
面「なるほど。大丈夫そうだね」
ぼ「あとはOKかな?…よし、じゃあコード書いてみるよ(マーカーを手に取る)」


コーディング
ぼ「まずクラス外観はこんな感じかな…(カキカキ)」

public class BrowserHistory {
    public void Visit(string name) { }    
    public string Back() { }    
    public string Forward() { }
}

面「ん?これ何の言語?」
ぼ「C#だよ。ぼくはC#使いなんだ(自己紹介で言ったけど…)」
面「Cool」
ぼ「そして、履歴は文字列のリストで持つよ。名前は_historyでいいか」
面「OK」
ぼ「それと現在位置を表す_indexと、有効な最大Indexを保持する_maxIndexが要るね」

public class BrowserHistory {
    int _index = -1;
    int _maxIndex = -1;
    List<string> _history = new List<string>();

    public void Visit(string name) { }    
    public string Back() { }    
    public string Forward() { }
}

面「なんで_indexを-1で初期化したの?」
ぼ「これは『いまの位置』だから。初めはどこのサイトも表示してないのを想定してる」
面「なるほど」

public class BrowserHistory {
    int _index = -1;
    int _maxIndex = -1;
    List<string> _history = new List<string>();

    public void Visit(string name) { 
        if(_index == _maxIndex)
            _history.Add("");
        _history[++_index] = name;
        _maxIndex = _index;
    }

    public string Back() { 
        if(_index == -1) return "";
        _index = Math.Max(0, _index-1);
        return _history[_index];
    }   

    public string Forward() { 
        if(_maxIndex == -1) return "";
        _index = Math.Min(_maxIndex, _index+1);
        return _history[_index];
    }
}

ぼ「Visit()、Back()、Forward()も書くとこんな感じかな…」
面「Back()、Foward()で履歴がないときはブランクを返すようにしたんだね」
ぼ「そう。ちょっと手動テストしてみるね…。えーと履歴が無いときのBack()、Forward()は大丈夫そうかな…。初回のVisit()で_historyのサイズが1になって…そのあとBack()したら…Forward()は……」
面「大丈夫そう?」
ぼ「うん…たぶん…」
面「じゃいくつか聞くよ」


コードについての質疑1
面「まず、それぞれの関数の計算量を教えて」
ぼ「えーと、Visit()、Back()、Forward()すべてO(1)だね」
面「本当に?」
ぼ「え?……あ!Visit()の最悪ケースがO(N)か!リストのAdd()がO(N)になる場合がある!」
面「そうだね。その場合の問題点は?」
ぼ「まれにVisit()の実行が遅い。しかも、履歴がたまるとどんどん遅くなる」
面「その通り」
ぼ「これは大問題だ…」
面「まあ、一般的な使い方なら大丈夫だろうけどね。これを解決するにはどうする?」
ぼ「うーん、データ構造を変えないとダメな気がする…Doubly Linked ListかDequeueあたりかな…」
面「どうして?」
ぼ「Linked ListもDequeueも追加がO(1)だから。リストじゃなくてこっちを使うべきだったか…」
面「まあ良し悪しだけどね。ところでDequeueだと『戻る』『進む』の実装に困らない?」
ぼ「いや、それは2つDequeueを使えば出来るかな。でもDoubly Linked Listのほうが素直に実装できそう」


コードについての質疑2
面「ところで、仕様追加をしたいんだけど」
ぼ「う、うん」
面「古い履歴は自動で捨てるようにしたい」
ぼ「えーと、古い、っていうのは時間で?それとも何回分前かで?」
面「そうだね。じゃ何回分前かでやろう」
ぼ「LRUキャッシュみたいに?」
面「お、そうそう。じゃ、ついでだから、LRUキャッシュについて説明してくれる?」
ぼ「LRUはLeast Recently Usedのことで、容量が閾値を超えたときに『いちばん訪れてない』キャッシュを捨てていくもの。LinkedListを使った実装が一般的」
面「そうだね。君の実装はリストだけど、それで実現できる?」
ぼ「難しいと思う。たとえば『最小の有効Index』を持てば見た目は実現するけど、これ結局メモリを解放してないので意味がない。メモリ解放するためにリストを作り直すと、こんどは実行時間が遅くなる」
面「わたしもそう思う」
ぼ「うーん、Linked Listで実装したほうがいい?」
面「きみがそうしたほうがいいと思うなら」
ぼ「了解(あ、あと15分しか無いじゃん!!急げ!)」

public class BrowserHistory {
    int THRES = 1000;
    LinkedListNode<string> _current;
    LinkedList<string> _history = new LinkedList<string>();

    public void Visit(string name) { 
        var node = new LinkedListNode<string>(name);
        if(_current?.Next == null)
            _history.AddLast(node);
        else
            _current.Next = node;
        _current = node;

        CheckSize();
    }

    void CheckSize(){
        while(_history.Count > THRES)
            _history.RemoveFirst();
    }

    public string Back() { 
        if(_current == null) return "";
        if(_current.Previous != null) 
            _current = _current.Previous;
        return _current.Value;
    }   

    public string Forward() { 
        if(_current == null) return "";
        if(_current.Next != null) 
            _current = _current.Next;
        return _current.Value;
    }
}

ぼ「で、できたよ…(ぜえぜえ)」
面「OK。ちょっと時間がないから、細かいチェックは省こうか」


コードについての質疑3
面「ああ、あと3分で次の面接が始まっちゃうね。じゃ最後にもう一個だけ聞こうか」
ぼ「」
面「このコードはLeast Recent Usedにしたけど、訪れた時間が古いもの、たとえば1日以上前のものを随時クリアする、ならどう実装する?」
ぼ「えーとえーと、サイト名と合わせて時間も保存して…」
面「うんうん」
ぼ「CheckSize()で、サイズの代わりに現在時との差をチェックすればいい…?」
面「それで問題ない?」
ぼ「……いや、ダメか。『戻る』があるから、古い順にソートされてるとは限らないね」
面「『戻る』『進む』で時間も更新するならそうだね」
ぼ「えーっと、あ、これはSortedDictionaryを使えばできる、かな?」
面「SortedDictionaryっていうのは?」
ぼ「Key-ValueのMapなんだけど、Keyがソートされてるんだ。キーの実装はたしかBalanced-BSTだったと思う。Javaで言うTreeMap」
面「ああ、なるほど」
ぼ「これに、更新時間をキー、値をLinkedListNodeで保存してあげれば。キーの小さいほうから、値を取り出して、1日以上前ならそのNodeを削除する。1日未満ならBreakかな」
面「行けそうだね。もしそうしたら、計算量はどうなる?」
ぼ「Back()、Forward()、Visit()がすべてO(LogN)になる。毎回BSTへの更新が発生するから」
面「それは確実?」
ぼ「…いや、古い履歴がたくさんあると、クリアに時間がかかる場合があるか。極端な話、全部が古いとO(NLogN)かかる」
面「一つ一つ消したらそうだろうね」
ぼ「うーん、キーのBalanced-BSTをうまく作れば二分探索で…いや、ちょっと分からないや」
面「難しいね。もしかしたら、完ぺきじゃないかもしれないけど、CheckSize()で単純にFirst()から日付をチェックするほうがベターかもしれないよ。実装も単純だしね」
ぼ「なるほど…」


次の面接へ
面「ああ、時間がなくなっちゃった。きみのほうから何か質問ある?」
ぼ「えーっとえーっと、(何かひねり出さないと…あれっ、この人名前なんだっけ?)あなたは社内ツールを作ってるんだっけ、AI関連の」
面「そうだよ!これはとてもチャレンジングな仕事で~~機械学習がうんぬんかんぬん~」
面2「(前触れなく)ハロー!」
面「やあマイク!紹介するよ、こちらはマイク。こちらはyambe2002」
面2「会えて嬉しいよyambe2002!僕は部署Bで働いてるバックエンドエンジニアだよ!」
ぼ「やあマイク!」
面2「それじゃ、ここからぼくが引き継ぐね。ありがとうシンディ!」
面「ありがとう。それじゃあねyambe2002。Good Luck!」
面2「じゃあ早速だけど、ぼくからの問題はこれ。ミュージシャン名で検索すると、コンサートや開始時間・料金なんかを表示するWebサイトをデザインしてほしいんだ!近くのコンサートを優先してね。細かい要件はね…」
ぼ「」(すでに疲労困憊)


こんな感じのが4~5連続