Android程序員面試會遇到的算法系列:前端
今年可謂是跌宕起伏的一年,幸虧結局還算是圓滿。開年的時候因爲和公司CTO有過節,被"打入冷宮",到下半年開始找工做,過程仍是蠻艱辛。先分享一下offer的狀況數據結構
國內的有函數
1.阿里口碑(offer)
2.Wish(offer)
3.Booking(Offer)
4.今日頭條(Offer)
5.Airbnb(北京)被拒
最讓我開心的是拿到了硅谷的offer!
FaceBook Menlo Park總部的offer
Amazon 西雅圖總部 offer
在面試的過程當中我深深的感覺到,對於一個優秀的安卓開發來講,首先擺在第一位的仍是他/她做爲一個軟件工程師的基本素養。不管你是作前端仍是後端,最後定義你的優秀程度的仍是做爲軟件工程師的基本素養,學習能力和編程能力,還有設計能力。我本身在如今的公司也作過面試官,發現新加坡的大部分碼農(東南亞的碼農),對基礎的編程能力實在是有所欠缺,熟練的使用API卻不能理解爲何。
不少同窗會在長久以往的業務邏輯開發中慢慢迷失,逐漸的把寫代碼變成了一種習慣,而沒有再去思考本身代碼的優化,結構的調整。這個現象不止是安卓開發的小夥伴有,任何大公司的朋友都會遇到。因此我這一系列的文章打算深刻的講解一下對於安卓程序員面試中可能遇到的算法。也但願能培養你們多思考,業餘時間多動手寫好代碼,優質代碼的習慣。
那麼第一篇我打算着重講一下二叉樹的問題。
相信你們之前在學習算法與數據結構的時候都遇到過。好比說,打印二叉樹前序,中序,後序的字符串這種問題。通常來講咱們會選擇使用遞歸的形式來打印,好比說
/** ** 二叉樹節點 **/
public class TreeNode{
TreeNode left;
TreeNode Right;
int value;
}
//中序
public void printInoderTree(TreeNode root){
//base case
if(root == null){
return;
}
//遞歸調用printTree
printInoderTree(root.left);
System.out.println(root.val);
printInoderTree(root.right);
}
//中序
public void printPreoderTree(TreeNode root){
//base case
if(root == null){
return;
}
//遞歸調用printTree
System.out.println(root.val);
printPreoderTree(root.left);
printPreoderTree(root.right);
}
複製代碼
一開始上學的時候,我這幾段代碼都是背下來的,徹底沒有理解其中的奧妙。對於二叉樹的遞歸操做,其實正確的理解方式
- 把每次遞歸想象成對其子集(左右子樹)的一個操做,假設該遞歸已經能夠處理好左右子樹,那麼根據已經處理好的左右子樹在調整根節點。
這樣的思想其實和分而治之 分治法 類似,就是把一個大問題先分紅小問題,再去解決。咱們仍是以二叉樹的中序打印爲例子。
由於中序打印咱們須要以左中右的順序打印二叉樹,如下圖爲例子咱們分解一下問題。
上面這個gif詳細的解釋了怎麼叫分而治之,首先,咱們假設A節點的左右子樹分開並且已經打印完畢,那麼只剩下A節點須要單獨處理,那麼久打印它。對於B子樹來講,咱們以一樣的思惟處理。因此動圖裏面是B子樹先鋪平,而後輪到A節點,最後到C子樹。
最後咱們須要考慮一下這個遞歸的結束條件。咱們假設A節點左右子樹都爲空,null,那麼在調用該方法的時候咱們須要在Node爲空的時候直接返回不作任何操做。該條件咱們通常稱爲遞歸的Base Case。每一個遞歸都是這樣,先想好咱們怎麼把問題分治
, 再考慮base case
是哪些,怎麼處理,咱們的遞歸就結束了。
問題來了,咱們明明要講深度優先,爲何講起遞歸了。二者的聯繫是什麼?
其實遞歸對於不少數據結構來講,就是深度優先,好比二叉樹,圖。由於在遞歸的過程當中,咱們就是在一層一層的往下走,好比對於二叉樹的中序打印來講,咱們遞歸樹的左節點,除非左節點爲空,咱們會一直往下走,這自己就是深度優先了。因此通常來講
,對於深度優先,咱們都會用遞歸來解決,由於寫起來最方便。固然咱們深度優先若是不想用遞歸,還可使用棧(Stack)
來解決,咱們在之後的文章來說(不過你們須要知道的是,遞歸自己就是使用方法棧的一種操做,聯想一下咱們經常聽到的StackOverFlow,你應該能明白其中的奧妙了吧)。
好!相信我已經勾起了你們對大學算法課的記憶了!那麼咱們來鞏固一下。使用分治思想+遞歸,咱們就已經能夠解決大部分二叉樹的問題了。 咱們來看一道題目->
這道題是一個經典的題目,Mac上著名軟件HomeBrew的做者曾經在面試Google的時候被問到了,還沒作出來,所以最後被拒。。。。因而他在我的推特上抱怨到:
Google: 90% of our engineers use the software you wrote (Homebrew), but you can’t invert a binary tree on a whiteboard so fuck off.
最後你們的關注點就慢慢從做者被拒自己轉移到了題目上了。。。那咱們看看這道題到底有多難。
翻轉前
翻轉後
看起來好像很麻煩的樣子,每一個子樹自己都被翻轉一遍。可是咱們使用分治的思惟,假如說咱們有個函數,專門翻轉二叉樹的。假如咱們把B子樹翻轉好,再把C子樹翻轉好,那麼咱們要作的豈不就是簡單的把A節點的左賦給C(原來是B),再把A節點的右賦給B(原來是C)。這個問題是否是就解決了?
對於B和C咱們能夠用一樣的分治思惟去遞歸解決。用一段代碼來描述一下
public TreeNode reverseBinaryTree(TreeNode root){
//先處理base case,當root ==null 時,什麼都不須要作,返回空指針
if(root == null){
return null;
}
else{
//把左子樹翻轉
TreeNode left = reverseBinaryTree(root.left);
//把右子樹翻轉
TreeNode right = reverseBinaryTree(root.right);
//把左右子樹分別賦值給root節點,可是是翻轉過來的順序
root.left = right;
root.right = left;
//返回根節點
return root;
}
}
複製代碼
根據這個例子,再加上中序打印的題目,咱們應該已經能夠很輕鬆的理解到了,對於二叉樹的題目或者算法,分而治之
和 遞歸
的核心思想了,就是把左右子樹分開處理,最後在把結果合併(把處理好的左右子樹對應根節點進行處理)。
那麼接下來咱們來一個複雜一點點的題目
這個題目咱們須要把一個二叉樹變成一個相似於鏈表的結構,全部的子節點都移到右節點去,看圖爲例。
轉變以後
從圖中咱們能夠看出來,把二叉樹鋪平的這個過程,是先把左子樹鋪平,連接到根節點的右節點上面,再把右子樹鋪平,連接到已經鋪平的左子樹的最後一個節點上。最後返回根節點。那麼咱們從一個宏觀的角度來講,須要作的就是先把左右子樹鋪平。
假設咱們有一個方法叫flatten()
,它會把一個二叉樹鋪平最後返回根節點
public TreeNode flatten(TreeNode root){
}
複製代碼
那麼從宏觀的角度,咱們對鋪平這個操做,已經作完了!!!接下來就是第二步,仍是以一個動畫來闡述這個過程。
最終代碼以下,附上註釋
public TreeNode flatten(TreeNode root){
//base case
if(root == null){
return null;
}
else{
//用遞歸的思想,把左右先鋪平
TreeNode left = flatten(root.left);
TreeNode right = flatten(root.right);
//把左指針和右指針先指向空。
root.left = null;
root.right = null;
//假如左子樹生成的鏈表爲空,那麼忽略它,把右子樹生成的鏈表指向根節點的右指針
if(left == null){
root.right = right;
return root;
}
//若是左子樹生成鏈表不爲空,那麼用while循環獲取最後一個節點,而且它的右指針要指向右子樹生成的鏈表的頭節點
root.right = left;
TreeNode lastLeft = left;
while(lastLeft != null && lastLeft.right != null){
lastLeft = lastLeft.right;
}
lastLeft.right = right;
return root;
}
}
複製代碼
至此,咱們已經作完了這道題了,但願你們最後能好好理解咱們所謂的分而治之的思想和二叉樹中對左右子樹遞歸的處理。大部分的二叉樹算法題也就是圍繞着這個思想爲中心,只要從宏觀上能把對左右子樹處理的邏輯想清楚,那麼就不難解決了。
那麼對於安卓開發中,咱們會不會遇到相似的問題呢?或者說安卓開發中會遇到樹形結構的算法麼?
答案是確定有!
咱們都知道在安卓系統裏面,每一個ViewGroup
裏面又會包含多個或者零個View
,每個View
或者 ViewGroup
都有一個整型的Id,那麼每次咱們在使用ViewGroup
的findViewById(int id)
的時候,咱們是以什麼方式來查找並返回在當前ViewGroup下面,咱們要查找的View呢?
這個也是我很是喜歡對來我司應聘的求職者的問題,不過很遺憾,目前爲止能完完整整寫出來的就一個。。。。(再次可見東南亞開發者的水平,不忍吐槽)
那麼題目來了
請完成如下方法
//返回一個在vg下面的一個View,id爲方法的第二個參數
public static View find(ViewGroup vg, int id){
}
複製代碼
可使用的方法有:
View -> getId()
返回一個int 的 idViewGroup -> getChildCount()
返回一個int的孩子數量ViewGroup -> getChildAt(int index)
返回一個孩子,返回值爲View。這個題目就能夠說很是經典了,以往的樹形結構的題目,咱們都是作一個二叉樹的處理,除了左就是右,可是這裏咱們每一個ViewGroup均可能有多個孩子,每一個孩子既多是ViewGroup,也可能只是View(ViewGroup是View的子類,這裏是一個知識點!)
我這裏就不作過多的解釋了,直接貼代碼,並且安卓系統自己也是用這種方式進行View的查找的。
//返回一個在vg下面的一個View,id爲方法的第二個參數
public static View find(ViewGroup vg, int id){
if(vg == null) return null;
int size = vg.getChildCount();
//循環遍歷全部孩子
for(int i = 0 ; i< size ;i++){
View v = vg.getChildAt(i);
//若是當前孩子的id相同,那麼返回
if(v.getId == id) return v;
//若是當前孩子id不一樣,可是是一個ViewGroup,那麼咱們遞歸往下找
if(v instance of ViewGroup){
//遞歸
View temp = find((ViewGroup)v,id);
//若是找到了,就返回temp,若是沒有找到,繼續當前的for循環
if(temp != null){
return temp;
}
}
}
//到最後還沒用找到,表明該ViewGroup vg 並不包含一個有該id的孩子,返回空
return null;
}
複製代碼
說到廣度優先,大部分同窗可能會想到圖,不過畢竟樹結構自己就是一種特殊的圖。因此通常說樹,尤爲是二叉樹的廣度優先咱們指的通常是層序遍歷。
好比說樹
層序打印的結果就是A->B->C->D->D->E->F->G
對於層序遍歷的相關算法,真理只有一個!
就是用隊列(Queue)
!
道理很簡單,每次遍歷當前節點的時候,把該節點從隊列拿出來,而且把它的子節點所有加入到隊列中。over~
上一個簡單的打印代碼
public void printTree(TreeNode root){
if(root == null){
return;
}
Queue queue = new LinkedList();
queue.add(root);
while(!queue.isEmpty()){
TreeNode current = queue.poll();
System.out.println(current.toString());
if(current.left != null){
queue.add(current.left);
}
if(current.right != null){
queue.add(current.right);
}
}
}
複製代碼
這段代碼很簡單,利用隊列先進先出的性質,咱們能夠一層層的打印二叉樹的節點們。
因此對於二叉樹的層序遍從來說,通常都會使用隊列,這都是套路。所以,二叉樹的層序遍歷相對來講比較簡單,你們下次見到二叉樹的層序遍歷相關的面試題,先大膽的和麪試官說出你打算使用隊列,確定沒錯!
最後對於層序遍從來說咱們再來一個比較具備表明性的題目!
這個題目要求你們在擁有一個二叉樹節點的左右節點指針之餘,還要幫它找到它的next指針指向的節點。
大概是這樣:
在上面這個圖中,紅色的箭頭表明next指針的指向
邏輯很簡單,每個的節點的next指向同一層中的下一個節點,不過若是該節點是當前層的最後一個節點的話,不設置next,或者說next爲空。
其實這個題目就是典型的層序遍歷,使用隊列就能夠輕鬆解決,每次poll出來一個節點,判斷是否是當前層的最後一個,若是不是,把其next設置成queue中的下一個節點就ok了。至於怎麼判斷當前節點是哪一層呢?咱們有個小技巧,使用當前queue的size作for循環,且看代碼
public void nextSibiling(TreeNode node){
if(node == null){
return;
}
Queue queue = new LinkedList();
queue.add(node);
//這個level沒有實際用處,可是能夠告訴你們怎麼判斷當前node是第幾層。
int level = 0;
while(!queue.isEmpty()){
int size = queue.size();
//用這個for循環,能夠保證for循環裏面對queue無論加多少個子節點,我只處理當前層裏面的節點
for(int i = 0;i<size;i++){
//把當前第一個節點拿出來
TreeNode current = queue.poll();
//把子節點加到queue裏面
if(current.left != null){
queue.add(current.left);
}
if(current.right != null){
queue.add(current.right);
}
if(i != size -1){
//peek只是獲取當前隊列中第一個節點,可是並不把它從隊列中拿出來
current.next = queue.peek();
}
}
}
level++;
}
}
複製代碼
二叉樹的知識點我就大概講這些,下次的文章我會接着詳細的講深度優先和廣度優先的算法。深度優先是一個很是很是寬泛並且難以徹底掌握的知識點,我會用詳細的篇幅來覆蓋全部的深度優先的基本題型,包括對樹,圖的深度優先搜索,集合的回朔等等。
Part2 的連接: Android程序員面試會遇到的算法(part 2 廣度優先搜索)