Android程序员面试会遇到的算法系列:前端
今年可谓是跌宕起伏的一年,幸亏结局还算是圆满。开年的时候因为和公司CTO有过节,被"打入冷宫",到下半年开始找工做,过程仍是蛮艰辛。先分享一下offer的状况markdown
国内的有数据结构
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 广度优先搜索)