導入
なぜデザイン タイプの質問を最後に置くのかというと、一般的なデータ タイプとアルゴリズムを理解した後でも、問題を解決するというプログラミングの本質に戻る必要があるからです。自分たちで要件を設計し、解決するという行動は一見クールに見えますが、実際には少し愚かな行動であり、現場で問題を解決する必要があります。このタイプの質問は、よりオープンで不確かであり、面接官のレベルをより適切に測定できるため、一般的なタイプの面接の 1 つでもあります。ここでちょっとした裏技ですが、普段設計問題をやる場合、データ構造で簡単に解ける問題はせいぜい中程度の問題で、アルゴリズムやデータ構造を完成させる必要がある設計問題は一般的に難しいです。
理論的根拠
このラベルの問題を設計するには、問題シナリオを組み合わせ、学習したデータ構造を使用して、現在の問題シナリオに適した新しいデータ構造を組み立てる必要があります。理論的基礎について話さなければならない場合、データ構造が非常に重要であり、その後に、以前に関係するほとんどのアルゴリズムが続きます。質問メーカーの操作性が大きすぎるため、今日は電流制限を設計し、明日は負荷を設計する場合、ルールやテンプレートを見つけることができますか? 見つからない場合は、これまでに学習した内容がすべて重要なポイントであり、もう 1 つはそれを実現する能力です。達成方法はわかっている場合もありますが、調査範囲はあまりにも幅が広すぎるので、自分の弱点をテストするだけでは完成させることができないため、あらゆる種類のコードを書くことに慣れておく必要があります。設計に関する質問を含む、leetcode に関するすべての質問には、単純すぎるという
欠点があります。場合によっては、問題を解決するためにどのような知識を使用する必要があるかがトピックからわかることがあります。実はこれは学校の定型質問のようなもので、面接や仕事をしていると応用問題に直面することが多く、テーマに応じて適切なデータ構造やアルゴリズムを抽象化して選択する必要があります。これには、特定の抽象的な能力が必要ですが、Leetcode では十分に発揮できないかもしれませんが、研究に取り組む際には、この方向の発展にも注意を払う必要があります。
問題解決の経験
- デザインタイプの質問は総合的な能力をテストするもので、特にさまざまなデータ構造に精通している必要があります。
- 場合によっては、問題を解決するために、特定の抽象化能力を備え、適切なデータ構造を選択することが必要になります。
- いくつかのソース コードを読んだ後、それは設計クラスに非常に役立ち、実装したものがソース コードの簡易バージョンであることがわかります。
- 設計に関する質問については、コードの習熟度を比較してください。
- 一般的なデザインの質問では、特定の抽象化能力と問題解決能力が必要です。
アルゴリズムのトピック
146. LRUキャッシュ
トピック分析: これはよくあるインタビューの質問です。ハッシュ テーブルの助けを借りて、リンク リストに高速検索の特性が与えられ、リンク リストを使用して LRU (最も最近使用されていない) キャッシュ アルゴリズムを迅速に実装できます。
コードは以下のように表示されます。
/**
* 哈希表、双向链表
*
*/
class LRUCache{
class Node {
int value;
Node prev;
Node next;
public Node() {
}
public Node(int _value) {
value = _value;
}
}
int cap;
int size;
// 用数组优化HashMap,因为值范围固定,且数组更快
// 索引为key,值为value的hashMap
Node[] map = new Node[10001];
// 双向链表,两个方向
Node head;
Node tail;
public LRUCache(int capacity) {
cap = capacity;
size = capacity;
head = new Node();
tail = new Node();
head.next = tail;
tail.prev = head;
size = 0;
}
public int get(int key) {
Node node = map[key];
if (node == null || node.value == -1) return -1;
// 被查找后,删除,再添加,相当于更新为最新位置
remove(node);
addHead(node);
return node.value;
}
public void put(int key, int value) {
// 为空添加后,如果超长,需要删除旧的
if (map[key] == null || map[key].value == -1) {
Node node = new Node(value);
addHead(node);
if (size == cap) removeTail();
else ++size;
map[key] = node;
// 若已存在,更新为最新值,和最新位置
} else {
map[key].value = value;
remove(map[key]);
addHead(map[key]);
}
}
public void remove(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
public void addHead(Node node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
public void removeTail() {
Node last = tail.prev.prev;
last.next.value = -1;
last.next = tail;
tail.prev = last;
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
155. 最小限のスタック
トピック分析: リンク リストを使用し、各ノードを格納するときに、ノードの場合は最小値のみを格納します。
コードは以下のように表示されます。
/**
* 链表
*/
class MinStack {
private Node head;
public void push(int x) {
if(head == null)
head = new Node(x, x);
else
head = new Node(x, Math.min(x, head.min), head);
}
public void pop() {
head = head.next;
}
public int top() {
return head.val;
}
public int getMin() {
return head.min;
}
private class Node {
int val;
int min;
Node next;
private Node(int val, int min) {
this(val, min, null);
}
private Node(int val, int min, Node next) {
this.val = val;
this.min = min;
this.next = next;
}
}
}
/**
* Your MinStack object will be instantiated and called as such:
* MinStack obj = new MinStack();
* obj.push(val);
* obj.pop();
* int param_3 = obj.top();
* int param_4 = obj.getMin();
*/
173. 二分探索木反復子
トピック分析: 順序反復にスタックを使用します。
コードは以下のように表示されます。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
/**
* 二叉树
*/
class BSTIterator {
Deque<TreeNode> stack;
// 思路:因为要中序遍历,所以把根节点先入栈然后入栈左孩子,每当输出一个节点后把那个节点的右孩子以及右孩子的所有左边的孩子加入进栈(想象递归的思想,空间复杂度O(h):因为最多存入树的高度个节点)
public BSTIterator(TreeNode root) {
stack = new LinkedList<>();
TreeNode t = root;
while(t!=null){ // 初始化栈:把根节点和根节点的所有左孩子及左孩子的左孩子加入进栈
stack.addLast(t);
t = t.left;
}
}
public int next() {
TreeNode t = stack.pollLast();
int res = t.val;
if(t.right!=null){ // 把取出节点的右孩子和右孩子的所有左孩子及左孩子的左孩子加入进栈
stack.addLast(t.right);
t = t.right.left;
while(t!=null){
stack.addLast(t);
t = t.left;
}
}
return res;
}
public boolean hasNext() {
return !stack.isEmpty();
}
}
/**
* Your BSTIterator object will be instantiated and called as such:
* BSTIterator obj = new BSTIterator(root);
* int param_1 = obj.next();
* boolean param_2 = obj.hasNext();
*/
208. Trie(プレフィックスツリー)の実装
トピック分析: 配列を使用した辞書ツリーの実装。
コードは以下のように表示されます。
/**
* 设计
*/
class Trie {
private class TrieNode { // 每个节点最多有26个不同的小写字母
private boolean isEnd;
private TrieNode[] next;
public TrieNode() {
isEnd = false;
next = new TrieNode[26];
}
}
private TrieNode root;
/** Initialize your data structure here. */
public Trie() {
root = new TrieNode();
}
/** Inserts a word into the trie. */
public void insert(String word) {
TrieNode cur = root;
for (int i = 0, len = word.length(), ch; i < len; i++) {
ch = word.charAt(i) - 'a';
if (cur.next[ch] == null)
cur.next[ch] = new TrieNode();
cur = cur.next[ch];
}
cur.isEnd = true; // 加上一个标记,表示为一个单词
}
/** Returns if the word is in the trie. */
public boolean search(String word) {
TrieNode cur = root;
for (int i = 0, len = word.length(), ch; i < len; i++) {
ch = word.charAt(i) - 'a';
if (cur.next[ch] == null)
return false;
cur = cur.next[ch];
}
return cur.isEnd;
}
/**
* Returns if there is any word in the trie that starts with the given prefix.
*/
public boolean startsWith(String prefix) {
TrieNode cur = root;
for (int i = 0, len = prefix.length(), ch; i < len; i++) {
ch = prefix.charAt(i) - 'a';
if (cur.next[ch] == null)
return false; // 若还没遍历完给定的前缀子串,则直接返回false
cur = cur.next[ch];
}
return true; // 直接返回true
}
}
/**
* Your Trie object will be instantiated and called as such:
* Trie obj = new Trie();
* obj.insert(word);
* boolean param_2 = obj.search(word);
* boolean param_3 = obj.startsWith(prefix);
*/
211. 単語の追加と検索 - データ構造の設計
トピック分析: 辞書ツリーは 208. Trie (接頭辞ツリー) の実現に似ており、. を個別に処理するだけで済みます。
コードは以下のように表示されます。
/**
* 设计
*/
class WordDictionary {
private WordDictionary[] items;
boolean isEnd;
public WordDictionary() {
items = new WordDictionary[26];
}
public void addWord(String word) {
WordDictionary curr = this;
int n = word.length();
for(int i = 0; i < n; i++){
int index = word.charAt(i) - 'a';
if(curr.items[index] == null)
curr.items[index] = new WordDictionary();
curr = curr.items[index];
}
curr.isEnd = true;
}
public boolean search(String word) {
return search(this, word, 0);
}
private boolean search(WordDictionary curr, String word, int start){
int n = word.length();
if(start == n)
return curr.isEnd;
char c = word.charAt(start);
if(c != '.'){
WordDictionary item = curr.items[c - 'a'];
return item != null && search(item, word, start + 1);
}
for(int j = 0; j < 26; j++){
if(curr.items[j] != null && search(curr.items[j], word, start + 1))
return true;
}
return false;
}
}
/**
* Your WordDictionary object will be instantiated and called as such:
* WordDictionary obj = new WordDictionary();
* obj.addWord(word);
* boolean param_2 = obj.search(word);
*/
225. キューを使用したスタックの実装
トピック分析:1つのキューでも2つのキューでも実現可能 1つのキューの場合:毎回スタックの先頭を削除し、再度ポップされた値以外をキューの最後尾に追加し、ポップされた値を削除。2 つのキューの状況: 1 つのキューがバックアップに使用され、新しい要素が追加されるたびに、新しい要素を除く他の要素がキューの最後に再追加されます。
コードは以下のように表示されます。
/**
* 队列
*/
class MyStack {
private Queue<Integer> inQ; // 输入队列
private Queue<Integer> outQ; // 输出队列
public MyStack() {
inQ = new LinkedList<>();
outQ = new LinkedList<>();
}
public void push(int x) {
inQ.offer(x);
// 把outQ中元素全部转到inQ队列
while (!outQ.isEmpty()) {
inQ.offer(outQ.poll());
}
// 交换两队列
Queue temp = inQ;
inQ = outQ;
outQ = temp;
}
public int pop() {
return outQ.poll();
}
public int top() {
return outQ.peek();
}
public boolean empty() {
return outQ.isEmpty();
}
}
/**
* Your MyStack object will be instantiated and called as such:
* MyStack obj = new MyStack();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.top();
* boolean param_4 = obj.empty();
*/
232. スタックを使用したキューの実装
トピック分析: 2 つのスタックを使用し、相互に誘導し合うことでキューを実現できます。
コードは以下のように表示されます。
/**
* 栈
*/
class MyQueue {
private Stack<Integer> inStack; // 输入栈
private Stack<Integer> outStack; // 输出栈
public MyQueue() {
inStack = new Stack<>();
outStack = new Stack<>();
}
public void push(int x) {
inStack.push(x);
}
public int pop(){
// 如果outStack栈为空,则将inStack栈全部弹出并压入b栈中,然后outStack.pop()
if (outStack.isEmpty()) {
while (!inStack.isEmpty()) {
outStack.push(inStack.pop());
}
}
return outStack.pop();
}
public int peek() {
if (outStack.isEmpty()) {
while (!inStack.isEmpty()) {
outStack.push(inStack.pop());
}
}
return outStack.peek();
}
public boolean empty() {
return inStack.isEmpty() && outStack.isEmpty();
}
}
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue obj = new MyQueue();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.peek();
* boolean param_4 = obj.empty();
*/
284. 最上位反復子
トピック分析: 最も直観的な方法は、リストを使用してイテレータ内の各要素を格納し、リスト内の要素を走査してイテレータをシミュレートすることです。
コードは以下のように表示されます。
/**
* 迭代器
*/
class PeekingIterator implements Iterator<Integer> {
private Iterator<Integer> iterator;
private Integer nextElement;
public PeekingIterator(Iterator<Integer> iterator) {
this.iterator = iterator;
nextElement = iterator.next();
}
public Integer peek() {
return nextElement;
}
@Override
public Integer next() {
Integer ret = nextElement;
nextElement = iterator.hasNext() ? iterator.next() : null;
return ret;
}
@Override
public boolean hasNext() {
return nextElement != null;
}
}
295. データストリームの中央値
トピック分析: 2 つの優先キュー queMax と queMin を使用して、それぞれ中央値を超える数値と中央値以下の数値を記録します。加算される数値の累積数が奇数の場合、queMin の数値の数は queMax より 1 つ多くなり、中央値が queMin のキューの先頭になります。加算された番号の累積数が偶数の場合、2 つの優先キューの番号の数は同じであり、中央値はキューの先頭の平均になります。
コードは以下のように表示されます。
/**
* 优先队列
*/
class MedianFinder {
PriorityQueue<Integer> queMin;
PriorityQueue<Integer> queMax;
public MedianFinder() {
queMin = new PriorityQueue<Integer>((a, b) -> (b - a));
queMax = new PriorityQueue<Integer>((a, b) -> (a - b));
}
public void addNum(int num) {
if (queMin.isEmpty() || num <= queMin.peek()) {
queMin.offer(num);
if (queMax.size() + 1 < queMin.size()) {
queMax.offer(queMin.poll());
}
} else {
queMax.offer(num);
if (queMax.size() > queMin.size()) {
queMin.offer(queMax.poll());
}
}
}
public double findMedian() {
if (queMin.size() > queMax.size()) {
return queMin.peek();
}
return (queMin.peek() + queMax.peek()) / 2.0;
}
}
297. バイナリツリーのシリアル化と逆シリアル化
トピック分析: バイナリ ツリーのシリアル化は、本質的にその値、そしてさらに重要なことにその構造をエンコードします。ツリーをトラバースして上記のタスクを実行できます。
コードは以下のように表示されます。
/**
* 深度优先搜索
*/
public class Codec {
public String serialize(TreeNode root) {
return rserialize(root, "");
}
public TreeNode deserialize(String data) {
String[] dataArray = data.split(",");
List<String> dataList = new LinkedList<String>(Arrays.asList(dataArray));
return rdeserialize(dataList);
}
public String rserialize(TreeNode root, String str) {
if (root == null) {
str += "None,";
} else {
str += str.valueOf(root.val) + ",";
str = rserialize(root.left, str);
str = rserialize(root.right, str);
}
return str;
}
public TreeNode rdeserialize(List<String> dataList) {
if (dataList.get(0).equals("None")) {
dataList.remove(0);
return null;
}
TreeNode root = new TreeNode(Integer.valueOf(dataList.get(0)));
dataList.remove(0);
root.left = rdeserialize(dataList);
root.right = rdeserialize(dataList);
return root;
}
}