你猜他通過了嗎
一面2020/12/20/14:00-14:501、自我介紹(2分鐘)
2、你說你讀過 redis 原始碼,那說一下常用的資料結構。
字串、跳錶、字典、壓縮列表、連結串列
3、介紹一下跳錶:
它在初始化的時候會分配一個頭,它也是和連結串列一樣鏈起來一個表嘛,它會給每個節點隨機分配一個層,然後它從它的頭部給每個節點進行每一層的連線,它的層數越高,它就會直接連到後面去,中間低的層就不連了,包括它的遍歷也是一樣,從高到低來找哪個數,這樣它就可以跳著遍歷了。
4、查詢的時間複雜度是多少:log(n)
5、空間佔用呢,相對於連結串列多了什麼:主要是多出了那個層嘛,它每個節點的層數是不一樣的,如果層都是1,也就退化成連結串列了。
6、vector push_back 擴容:2倍擴容,實際上可能更復雜一點
7、擴容以後,它的記憶體地址會變化嘛:會變化。以前的話,它是會把原來的元素重新複製過去,開闢一個新空間,複製過去,再把它之前的空間 free 掉。但是現在的話,是透過移動語義 move 來的
8、說一說 move 語義:相對於以前的複製來說,是把元素複製一遍,再把之前的銷燬掉。現在的 move 相當於是把它直接搬走了。也就是,我再想一種說法,也就是延長了它的生命週期,然後把新的指標直到原先的那個位置去,然後把原先的指標賦為空,差不多這樣。
9、move 底層的實現:emm,我看到了有 static_cast ,但是具體的,沒有完全瞭解過
10、右值引用:把那種臨時的變數,或者說馬上就要消亡,那個叫將亡值,把它的生命週期給延長,變成左值。
11、什麼時候被釋放:我的理解是它相當於一個左值了(意思和左值一樣)
12、怎麼把 vector 裡面存資料的所有記憶體空間都釋放掉:先說的 resize ,後面改說是另一個函式,不記得名字了
13、說一下虛擬函式:子類對父類的函式進行一個重寫。
14、舉個例子詳細說一下吧:舉例說了動物(父類)吃東西,其他子類吃的動作不一樣。這個時候對吃這個函式使用虛擬函式。
15、虛擬函式和函式重寫有什麼區別呢:(亂七八糟猜了一同)我這樣猜的
16、再具體說說:但是我不知道這樣猜的對不對
17、那換個問題吧,說說引用和指標:1. 記憶體佔用;2. 初始化;3. 可修改指向
18、還有嘛:沒了
19、引用可以銷燬嘛:不知道了
20、智慧指標瞭解過嘛,說說怎麼實現的:增加了一個引用計數
21、具體說說:不同的智慧指標指向同一個物件,有一個指標指向,它的引用計數就+1,最後變為0的時候,它就自動的銷燬了。
22、多執行緒中可以使用智慧指標嘛:不同執行緒裡面的智慧指標都是指向同一個物件嘛?是的:我覺得應該是適用的吧
23、你不知道是吧,確實是適用的,你是怎麼想的呢:它不同的執行緒是共享資源的吧,所以它們是可以指到同一個物件上的吧
24、會有讀寫衝突嘛:會有的
25、怎麼解決:應該透過加鎖來解決
手撕1. 中序遍歷(迭代)給定二叉樹的,構造一箇中序遍歷的迭代器,對應的功能是返回二叉樹上的下一個元素。class Iterator {public: TreeNode* next();}
1.1 思路討論(6分鐘左右)
我:在實現的過程中,需要把中序遍歷給記錄下來嘛面:你具體說說我:比如說,我在初始化的時候直接把它的中序遍歷存在 vector 中,直接用 next 這樣來訪問面:說說有什麼優點和缺點我:優點就是在呼叫 next 的時候複雜度是 O(1),缺點就是在初始化的時候要把它完全遍歷一遍,空間複雜度是O(n)面:你可以試試其他方法我:是不用額外的空間嘛面:也不是不用,但是比 O(n) 小點,比如 O(logn) 就行了。我:哦哦,那就是用一個棧,記錄它balabala....面:那你開始寫吧面:你先定義一個TreeNode我:需要定義Tree嘛面:這個不用我:這個需要執行嘛面:先不需要
1.2中途程式碼編寫(八分鐘左右)面:根節點從建構函式傳入
#include <iostream>#include <stack>using namespace std; struct TreeNode { int val; TreeNode* left, *right;}; class Iterator {public: stack<TreeNode*> s; Iterator(TreeNode* root) { // s.push(root); TreeNode* t = root; while (t) { s.push(t); t = root->left; } } TreeNode* next() { if (s.empty()) return nullptr; // 面試官後面提醒 TreeNode* ret = s.top(); TreeNode* t = ret; if (t&&t->right) { t = t->right; while (t) { s.push(t); t = t->left; } } else { TreeNode* tRight = t; s.pop(); if (s.empty()) return ret; // 面試官後面提醒 t = s.top(); while (t->right == tRight) { s.pop(); if (s.empty()) return ret; // 面試官後面提醒 tRight = t; t = s.top(); } } return ret; }}
1.3 程式碼解釋(4分鐘)寫完之後,讓我解釋程式碼,並指出幾個沒有加特殊情況的位置。
2. 全排列給定一個數組,列印它的全排列[1,2]-> [1,2] [2,1]
2.1 說思路(2分鐘)面:說不太清,你覺得沒問題就直接寫吧2.2 程式碼(7分鐘)
#include <iostream>#include <vector>using namespace std; vector<vector<int>> ans; void sol(vector<int>& v, vector<int> ret, vector<bool> flag) { if (ret.size() == v.size()) ans.push_back(ret); for (int i = 0; i < v.size(); ++ i) { if (flag[i]) { ret.push_back(v[i]); flag[i] = false; sol(v, ret, flag); flag[i] = true; ret.pop_back(); } }} int main() { vector<int> v; for (int i = 1; i <= 3; ++ i) v.push_back(i); vector<int> ret; vector<bool> flag(v.size(), true); sol(v, ret, flag); for (int i = 0; i < ans.size(); ++ i) { for (int j = 0; j < ans[i].size(); ++ j) cout << ans[i][j] << ' '; cout << endl; }}
反問我:你覺得我還有什麼需要加強的地方嘛面:你的廣度還行,但是深度不夠,比如虛擬函式和智慧指標二面2020/12/21/14:05-15:05自我介紹
簡歷有寫c++,接受其他語言嘛:具體什麼語言
golang,python:可以接受,只是偏向c++
實習時間:沒有其他問題,可以實習到轉正
說明轉正時間(提醒我時間很長):沒特殊情況,希望實習到轉正
base上海?:是的
闡述實習的工作(3分鐘)
說說程序、執行緒、協程:說了程序是資源排程單位,執行緒是cpu排程單位,協程有個通道可以傳輸資料。粒度上:程序 > 執行緒 > 協程
可以試著說說它們的優缺點:程序切換的缺點,時間開銷;其他的我可能不太瞭解,就說這麼多吧
說說程序、執行緒的通訊:程序:通道(口誤把管道說成了通道。。)、訊號,socket也算吧;執行緒用訊號比較多吧。
演算法題說一說lru,快取大小為K,怎麼設計資料結構:用連結串列儲存快取資料,說了訪問的過程;繼續說連結串列查詢上的缺點,時間複雜度為O(n),所以用雜湊來查詢,時間複雜度可以降為O(1)。(2分鐘)
如果加上過期時間呢:在連結串列節點中加一個更新時間,在訪問資料的時候,判斷一下時間有沒有過期,就行了吧。(3分鐘)
寫一下吧:
(剛開始的時候自己實現的 list 來寫的,挺麻煩的,我突然反應過來之前是用stl的。這時候已經過了15分鐘)我:我可以改成用stl麼。。。。
面:可以的,你改
(15分鐘後,寫完了)
#include <iostream>#include <unordered_map>#include <list>#include <pair>using namespace std; struct ListNode { int val; ListNode* next, *pri; double update;}; class lru { // ListNode* head, *tail; list<pair<int, double>> l; unordered_map<int, list<pair<int, double>>::iterator > m; int K; int nums; double T; lru(int k, double t) { K = k; nums = 0; T = t; } void set(int x) { if (m.find(x) == m.end()) { auto p = make_pair(x, time()); l.push_back(p); m[x] = l.rbegin(); nums ++; } else { auto p = m[x]; p->first = x; p->second = time(); l.erase(p); l.push_back(*p); m[x] = l.rbegin(); } if (nums > K) { l.pop_front(); nums --; } } int get(int x) { if (m.find(x) == m.end()) { return 0; } else { auto p = m[x]; if (time() - p->second >= T) { m.erase(x); l.erase(p); return 0; } else { p->second = time(); l.erase(p); l.push_back(*p); m[x] = l.rbegin(); return x; } } }}
如果是多執行緒裡面會出錯嘛:會出錯
那怎麼解決呢:在對資料結構內部進行修改的時候加鎖
具體點:
// 這裡加寫鎖l.erase(p);l.push_back(*p);m[x] = l.rbegin();
mysql 的索引資料結構:B+樹
說說為什麼用 B+ 樹:說了範圍查詢的優點
說說 git merge 和 git rebase 哪個好:rebase
說說有什麼優點:不太清楚
反問看原始碼和做專案,從面試和自我提升的角度哪個更好:都很重要。後者重要一點
面的哪個組:廣告創意
面試官介紹了4分鐘:感覺還挺有意思的
三面2020/12/24/14:00-14:55自我介紹(2分鐘)
實習介紹(12分鐘)
redis常用資料結構:字串、字典、連結串列、跳錶、壓縮列表、集合、有序集合
有序集合的實現方式:跳錶和壓縮列表
具體說說:壓縮列表用於資料量小的情況,儲存方式是線性的;資料量大的時候會自動轉換為跳錶,跳錶和連結串列一樣,由一個一個節點鏈起來,但是它的節點和連結串列不一樣,它的節點會被隨機分配一個層數,相同的層數連結起來。
跳錶查詢的時間複雜度:log(n)
有序集合中有查詢時間複雜度為O(1)的實現嗎:有序集合應該是沒有的。
redis 的過期策略:有好幾種吧,常見的lru
之前用的資料庫是什麼:mysql
引擎是什麼:innodb
隔離級別是:可重複讀
怎麼實現的可重複讀:MVCC
使用的樂觀鎖還是悲觀鎖:樂觀鎖
MVCC的實現:增加了版本號,有個版本鏈,每次事務開始的時候有一個快照,其他事務讀取的時候,讀取那個快照。事務內部修改的時候有一個日誌,日誌會記錄事務內部對這一行的修改,會用於事務的回滾。
binlog、redolog(也可能是undo log)聽說過嗎:聽說過,但是沒深入瞭解過
innodb的索引資料結構是什麼:B+樹
主鍵和非主鍵的索引呢:它一開始只會對主鍵建索引吧
非主鍵呢:不清楚
B+樹的葉子節點存放的什麼:整體的資料,包括索引,都在裡面
瀏覽器輸入一個url,返回了一個error,怎麼找出錯的地方:透過狀態碼
沒有狀態碼,只有一個error:啥都沒有嗎。。。
dns的返回標誌:那可能是dns上的錯誤,可能不存在這個域名
但是我輸入的toutiao.com,我知道是存在的:是的。那可能是dns查詢的時候出現了錯誤,那可以一步一步的查,它剛出去的時候訪問的那個dns伺服器(應該都是有的,小聲bb)。可能最近的那個路由器或者電腦裡面沒有配dns。有沒有可能(強顏歡笑)
你怎麼知道最近的呢:查詢dns的路徑,好像是有一個命令的
什麼命令呢:這命令名字我不記得了。。。
linux怎麼檢視系統的狀態呢:top
top出來顯示什麼:cpu佔用率,記憶體佔用率、一些程序的資訊
實習中,python後端,是怎麼實現的http連線,是django嗎:emm,是透過webpy的api直接呼叫的。。。。
你的request,post,具體實現是什麼:emmm,我可以從c++方面來說嗎
嗯:它要建立一個套接字,給套接字分配一個埠,等待連線的到來,連線來了之後,像redis的話,它收到連線,會複製一個socket,分配一個新埠,把這個連線分配給新socket,原先的socket繼續listen。連線之後,讀取socket裡面緩衝區的內容,再內部進行處理,再透過socket傳輸出去。
socket監聽是哪個層:tcp
之前問的是http,這方面是不是不太瞭解:嗯
還有什麼領域沒問你的嗎:作業系統,計算機網路,你們應該用golang,python比較多吧
也有用c++的:那還有c++這些,瞭解的多一些
段頁式儲存執行的時候會訪問幾次記憶體:訪問記憶體會透過頁的級數也有關係,如果它是一級頁表的話,它取頁號一次,取物理地址一次,如果沒有在記憶體中的話,還會訪問磁碟
我問的段頁式:段頁式先取它的段內偏移量,然後再訪問物理地址,是兩次
程序的儲存分佈:棧、堆、資料區、程式碼區、共享庫
程式碼放在哪:程式碼區
變數放在哪:靜態變數放在靜態的變數區,區域性變數會放在棧中
指標呢:指標也是有靜態的吧(小聲(不確定)),直接宣告的會放在棧中
指標指向的地址分配的空間呢,malloc的:放在堆中
演算法做題吧,我先看看你之前做的什麼題
求s1中包含s2中字元(不用考慮順序)的最小子串(長度最小)s1 = "abcedeabd"s2 = "abe"
思路
滑動視窗:
可以先找到第一個包含s2字元的字串,比如例子中的abce,然後往右移,把a提出去,找到右邊第一個提出去的a,這就是下一個子串。把所有子串找出來,再找最短的。
面:你這個會有問題,你先寫程式碼吧。
#include <iostream>#include <unordered_map>#include <unordered_set>#include <vector>#include <string>using namespace std;int main() { unordered_map<char, int> um1, um2; unordered_set<char> us; string s1 = "abcedeabd"; string s2 = "abe"; vector<string> ans; int left, right; left = right = 0; int sum = 0; for (int i = 0; i < s2.length(); ++ i) { us.insert(s2[i]); um1[s2[i]] ++; sum ++; } string s; um2 = um1; int i = 0; while (i < s1.length() && um2[s1[i]] == 0) i ++; while (sum > 0) { if (um2[s1[i]]) { um2[s1[i]] --; sum --; } s += s1[i]; i ++; } ans.push_back(s); for (; i < s1.length(); ++ i) { char ch = s[0]; int cnt = 1; // 找到擷取的地方 if (um1[s[0]] == 1) { while (us.find(s[cnt]) == us.end()) { // TODO } } while (s[cnt] ) s = s.strsub(1, s.length() -1); while () if (sum == 0){ ans.push_back(s); um2 = um1; } } cout << "Hello World!" << endl;}
(17分鐘後)
面:沒什麼時間了,說說程式碼吧
我:(對著程式碼)先找到第一個符合條件的子串,然後移除掉第一個字元s[0],這裡需要把s中,非s2字元的其他字元也移除掉,這裡就有問題了,如果後面的s[cnt](已經是s[cnt] in s2了),正好是移除掉的s[0],這裡有兩種情況,如果s2中只有一個s[0],那麼這個時候right不用右移,如果s2有多個s[0],則與其他字元做同樣的處理。感覺後面還有挺多要寫的。。。
面:還有更好的解法,你可以之後去想一想。
反問問:推薦一本書
面:這種主觀的推薦我們是規定不能說的哈
問:上下班時間
面:10105.5,你們應該都知道
我:不同組也有差異吧哈哈
面:那你想要多呢還是少呢(笑)
三面沒過,換了個組繼續加面兩輪