360公司2019校招笔试中Android开发工程师岗位的客观题,我当年也参加过类似的笔试。这类笔试的客观题部分通常涵盖Java基础、Android核心组件、线程与进程管理、网络协议、数据结构与算法等多个知识域,题目覆盖面广、考察点细致,非常考验基本功是否扎实。很多人在准备校招笔试时会陷入一个误区:光刷题、背答案,却不理解知识点背后的运行机制和设计考量。结果就是碰到换个问法的题目就懵了。这篇文章会把这类笔试中最常出现的考点进行系统拆解,结合真实场景说明每个知识点为什么重要、怎么考、怎么答,希望帮助正在准备Android校招的同学少走弯路。
1. 客观题考点全景:笔试到底在筛选什么
1.1 考察范围的四个维度
360这类公司的Android开发笔试客观题,看似随机,其实考察范围非常稳定。我把近几年的题目做了归类,基本逃不出四个维度。
第一个维度是Java语言基础,占比通常在30%以上。集合框架、并发编程、JVM内存模型、异常处理、泛型、反射这些是重灾区。很多Android开发同学觉得Java基础就是大学里学的那些,实际上笔试考察的深度远超课本内容。比如HashMap在JDK 7和JDK 8中的实现差异、ConcurrentHashMap如何保证线程安全、volatile关键字的内存语义,这些在笔试中出现频率极高。
第二个维度是Android核心知识,占比在40%左右。Activity的启动模式、生命周期在不同场景下的回调顺序、Fragment与Activity的通信方式、Service的两种启动方式及其区别、BroadcastReceiver的动态注册与静态注册差异、ContentProvider的权限管理、Handler的消息机制原理、View的绘制流程与事件分发机制,这些是Android开发者的基本功,也是笔试必考内容。
第三个维度是计算机基础知识,占比约20%。数据结构中的链表反转、二叉树遍历、栈和队列的应用场景,算法中的排序和查找,网络中的TCP三次握手四次挥手、HTTP与HTTPS的区别、TCP与UDP的区别,操作系统中的进程与线程、死锁条件、虚拟内存等。这些知识虽然在日常Android开发中不会直接用到,但它们是区分科班出身和半路出家的重要标准。
第四个维度是逻辑推理和智力题,占比约10%。这类题目主要考察思维灵活性和临场反应能力。常见的有排列组合问题、概率计算、逻辑推理,偶尔会出现一些脑筋急转弯类型的题目。360的笔试中这类题目的比例不算高,但一旦出现,往往会让很多人卡住。
1.2 客观题的命题逻辑和答题陷阱
校招笔试客观题的命题逻辑有一个核心特征:就是"看似简单,实则挖坑"。出题人会在几个地方设置陷阱。
第一种陷阱是概念混淆型。例如考察Activity的onSaveInstanceState方法的调用时机,很多人记得"在onStop之前调用",却忽略了"在onPause之后"这个前置条件。实际上onSaveInstanceState在Android P之前是在onStop之前调用的,但在Android P及以后,它的调用时机调整到了onStop之后,这是一个典型的版本差异陷阱。
第二种陷阱是条件遗漏型。比如考察Handler的用法时,出题人可能会设置一个"在主线程中创建Handler"的场景,而实际上Handler在任何线程中都可以创建,只是需要通过Looper.prepare()和Looper.loop()来维护消息队列。如果忽略了"默认主线程有Looper"这个隐含条件,就很容易做错。
第三种陷阱是答案绝对化。很多题目会设置"以下说法正确的是""以下说法错误的是"这种题型,选项中经常出现"一定""必须""只能"这类绝对化词汇,这些选项大概率是错误的。而出现"通常""一般""可能"这类相对化词汇的描述,往往是对的。这不是玄学,而是由技术实现决定的,任何技术方案都有取舍和边界条件。
1.3 从2019年真题反推的备考重点
以360公司2019年的Android开发工程师笔试为例,从考后流出的真题回忆来看,有几个知识点频繁出现。
一是Activity的启动模式和任务栈管理。singleTop、singleTask、singleInstance三种启动模式的区别,onNewIntent方法的回调时机,以及Intent中FLAG_ACTIVITY_NEW_TASK、FLAG_ACTIVITY_CLEAR_TOP等标志位对任务栈的影响。这些知识点在约八成的Android笔试中都会出现。
二是Handler消息机制。Handler、Looper、MessageQueue三者之间的关系,Message的obtain方法和recycle方法如何优化内存,Handler的postDelay并不是精准延时,而是按消息的时间戳排序后依次执行。很多公司笔试都喜欢考Handler,因为这是Android异步通信的核心,也是面试中喜欢追问的知识点。
三是JVM内存模型和GC机制。堆内存中新生代与老年代的比例关系,对象何时从新生代晋升到老年代,GC的触发条件,强引用、软引用、弱引用、虚引用的区别和应用场景。Android开发中内存优化是重中之重,所以笔试中关于JVM内存的题目几乎成了标配。
四是网络编程相关知识点。HTTP请求的完整过程,从DNS解析、TCP三次握手、发送HTTP请求、服务器响应、四次挥手断开连接,这个流程是网络模块的大题必考内容。HTTPS的加密原理,对称加密与非对称加密的应用场景,CA证书的作用,这些也是高频考点。
2. Java基础考点拆解:笔试失分的重灾区
2.1 集合框架的底层原理与线程安全性
Android开发中用得最多的集合类就是HashMap、ArrayList、LinkedList、HashSet,但笔试不会直接问"HashMap怎么用",而是会问它的底层实现。比如HashMap在JDK 7中采用数组加链表的结构,在JDK 8中则优化为数组加链表加红黑树。当链表长度超过阈值8且数组长度大于等于64时,链表会转换为红黑树,这样查找的时间复杂度从O(n)降为O(log n)。这个优化非常实用,但要记住一个前提条件:数组长度必须达到64。如果数组长度不够,即使链表再长也不会树化,而是会先扩容。
关于加载因子,HashMap的默认加载因子是0.75,这个值是基于空间利用率和时间成本的折中。加载因子越小,空间浪费越多,但发生哈希冲突的概率越低,查找效率越高。0.75是经过数学计算和大量测试得出的经验值,笔试时直接记住即可。
HashTable和HashMap的区别,面试和笔试都爱考。HashTable的方法是线程安全的,因为它的方法都加了synchronized锁,但这也导致了性能较低。HashMap是线程不安全的,但在单线程场景下性能更好。在并发场景下,推荐使用ConcurrentHashMap,它采用锁分段技术,把整个Map分成多个段,每段独立加锁,这样多线程访问不同段的数据时可以并发执行,大大提高了并发性能。JDK 8之后ConcurrentHashMap改用了CAS加synchronized的机制,锁的粒度更细了。
ArraList和LinkedList的区别也是经典考点。ArrayList基于动态数组实现,随机访问快,时间复杂度O(1),但插入和删除需要移动元素,时间复杂度O(n)。LinkedList基于双向链表实现,插入和删除快,时间复杂度O(1),但随机访问需要遍历,时间复杂度O(n)。这里有一个容易出错的点:LinkedList的插入删除在已知节点的前提下才是O(1),如果只知道位置索引,查找该节点仍需要O(n)的时间。
2.2 并发编程基础与线程池
并发编程是笔试客观题中技术含量较高的部分。synchronized和ReentrantLock的区别、volatile关键字的作用、ThreadLocal的使用场景,这些几乎成了必考知识点。
synchronized是JVM层面的关键字,可以修饰方法、代码块和静态方法,底层通过Monitor监视器实现,锁的获取和释放由JVM自动管理。ReentrantLock是JDK层面的类,需要手动加锁和解锁,它提供了更灵活的功能,比如可中断锁、公平锁、多个条件变量等。公平锁与非公平锁的对比,ReentrantLock默认使用非公平锁,因为非公平锁的吞吐量更大,但可能出现线程饥饿现象。
volatile关键字是并发编程中的另一个高频考点。volatile有两个语义:保证内存可见性和禁止指令重排序。内存可见性是指当一个线程修改了volatile变量的值,其他线程能立即看到这个修改,这是通过内存屏障实现的。禁止指令重排序是指编译器和处理器不会对volatile变量相关的指令进行重排序,这样能避免多线程环境下出现意外的执行顺序。但要注意,volatile只能保证可见性和有序性,不能保证原子性。比如volatile int count,多线程执行count++操作,依然会出现线程安全问题,因为count++对应了读取、修改、写回三个操作,volatile无法保证这三个操作的原子性。
线程池的考察点集中在参数配置和执行流程上。ThreadPoolExecutor的核心参数有核心线程数、最大线程数、空闲线程存活时间、工作队列、线程工厂、拒绝策略。执行流程是:当提交一个任务时,如果当前线程数小于核心线程数,则创建新线程执行任务;如果当前线程数大于等于核心线程数,且工作队列未满,则将任务放入队列;如果工作队列已满,且当前线程数小于最大线程数,则创建新线程执行任务;如果线程数已到达最大值且队列已满,则执行拒绝策略。四种拒绝策略分别是AbortPolicy抛出异常、CallerRunsPolicy让调用线程执行任务、DiscardPolicy直接丢弃任务、DiscardOldestPolicy丢弃队列中最旧的任务。
Android中的线程池使用需要特别注意主线程与子线程的切换。由于Android规定在主线程中不能进行网络操作,在子线程中不能更新UI,所以实际开发中经常需要使用AsyncTask、HandlerThread、IntentService等工具。AsyncTask在低版本中存在线程池被占满的问题,所以现在很多代码已经不再推荐使用AsyncTask而推荐协程或RxJava。
2.3 JVM内存区域与垃圾回收
Android是基于JVM的,但Android使用的并不是标准的HotSpot JVM,而是Dalvik虚拟机或ART虚拟机。笔试中JVM相关的题目,依然会以标准JVM为背景,因为Android开发者需要理解内存管理的底层机制。
JVM内存区域分为线程共享区和线程私有区。线程共享区包括堆和方法区,线程私有区包括虚拟机栈、本地方法栈和程序计数器。堆是对象分配内存的主要区域,也是垃圾回收的主要区域。方法区存储类信息、常量、静态变量等数据,在JDK 8中改为元空间。
垃圾回收算法有标记清除、复制、标记整理三种。标记清除算法会产生内存碎片,但实现简单;复制算法不会产生内存碎片,但需要额外空间;标记整理算法将存活对象向一端移动,然后清理边界外内存,解决了碎片问题但需要移动对象。
垃圾回收器方面,笔试常考新生代和老年代使用的回收器,Serial、Parallel、CMS、G1等。G1在JDK 9之后成为默认回收器,它把堆分成多个Region,可以并发标记,能够预测停顿时间。
引用类型的四种强度,是Android内存优化中的理论基础。强引用:只要有强引用存在,垃圾回收器永远不会回收它,即使发生OOM,这导致了Android中常见的内存泄漏问题,比如Activity被静态变量持有。软引用:内存不足时回收,常用于图片缓存,但Android从API 9后推荐使用LruCache替代SoftReference,因为LRU策略更可控。弱引用:下次垃圾回收时回收,常用于Handler的Context引用,避免Activity泄漏。虚引用:任何时候都可回收,主要用于对象回收跟踪。
3. Android核心考点拆解:组件、消息与异步
3.1 Activity生命周期与启动模式:最经典的送分题
Activity生命周期是Android笔试中出现频率最高的知识点。完整生命周期、可见生命周期、前台生命周期三个阶段的回调顺序必须烂熟于心。完整流程是onCreate到onDestroy,可见流程是onStart到onStop,前台流程是onResume到onPause。
常考的场景是屏幕旋转。Activity旋转屏幕时,会经历onPause、onStop、onDestroy、onCreate、onStart、onResume的完整流程,因此旋转前后Activity会重建。要避免这种重建,可以在AndroidManifest的activity标签中配置android:configChanges="orientation|screenSize",这样系统就会把配置变更交给onConfigurationChanged方法处理,而不是销毁重建Activity。
另一个高频场景是Activity被系统回收后的状态保存。onSaveInstanceState方法会在Activity被系统回收前调用,适合保存一些临时状态。它的调用时机在onPause之后、onStop之前,但也有上面提到的版本差异。从Android P(API 28)开始,onSaveInstanceState的调用时机调整到了onStop之后。这一点是笔试陷阱题的重灾区。
Activity启动模式在AndroidManifest中通过launchMode属性配置。standard模式每次启动都会创建新实例,这是默认模式。singleTop模式,如果栈顶已有该Activity实例,不会创建新实例,而会调用onNewIntent方法,适合接收推送通知跳转的场景。singleTask模式会检查整个任务栈中是否存在该Activity实例,如果存在就复用并清除其上方的所有Activity,适合作为应用的主页面。singleInstance模式则是独享一个任务栈,适合需要与外部应用共享的Activity,比如来电界面。
Intent的Flag也能影响Activity的启动方式。FLAG_ACTIVITY_NEW_TASK会为新Activity创建新的任务栈,在非Activity上下文(如Application)中启动Activity时必须在Intent上添加这个Flag。FLAG_ACTIVITY_CLEAR_TOP会清除目标Activity之上的所有Activity,它的行为与singleTask类似,可结合使用。
3.2 Handler消息机制:原理与易错点
Handler消息机制是Android异步任务的核心,也是校招笔试中常考的内容。它的组成成员是Handler、Looper、MessageQueue、Message。主线程启动时,ActivityThread会通过Looper.prepareMainLooper创建主线程的Looper,并通过Looper.loop启动消息循环,因此主线程中创建Handler不需要额外操作。
Handler发送消息有两种方式。一种是使用sendMessage方法,通过MessageQueue的enqueueMessage方法将Message按时间排序插入消息队列。这种方式的执行不会精准延时,因为如果消息队列中前面的消息执行时间较长,后续消息会被阻塞。另一种是使用post方法,本质上是把Runnable包装成Message。postDelayed方法与sendMessageDelayed方法本质相同。
使用Handler时最常见的错误之一是内存泄漏。如果Handler是非静态内部类,它会持有外部Activity的隐式引用,当Activity销毁后,Handler仍有可能在消息队列中有待处理的消息,导致Activity无法被垃圾回收。解决办法是使用静态内部类加WeakReference的方式持有Activity,或者在onDestroy方法中调用removeCallbacksAndMessages(null)移除所有未处理的消息。
笔试中关于Handler的经典题目包括:子线程中能不能创建Handler在线程中直接创建Handler,代码运行时会抛出RuntimeException,因为系统不会自动为子线程创建Looper,需要首先调用Looper.prepare()为当前线程初始化Looper,然后创建Handler,最后调用Looper.loop()启动消息循环。还可以考Message的复用机制:使用Message.obtain()从消息池中获取Message而不是直接new。消息池是一个链表结构,容量上限为50,能有效避免频繁创建Message对象带来的内存抖动。
3.3 Service与BroadcastReceiver、ContentProvider:四大组件的联动考察
Service的两种启动方式startService和bindService的区别,是笔试必考题。startService启动的Service与启动它的组件没有绑定关系,即使启动组件销毁,Service依然会继续运行,直到调用stopService或自身执行stopSelf。bindService启动的Service与绑定组件生命周期绑定,组件销毁后Service也随之销毁(如果没有其他组件绑定)。通过bindService可以获取Service的Binder对象,从而调用Service中定义的方法。
Service的生命周期也分两种。startService方式下,生命周期是onCreate到onStartCommand再到onDestroy,onStartCommand可以被多次调用。bindService方式下,生命周期是onCreate到onBind再到onUnbind最后到onDestroy。绑定Service时,可以通过LocalBinder、Messenger、AIDL三种方式进行通信。LocalBinder适用于同一进程内的通信,最简单;Messenger基于Handler实现跨进程通信;AIDL用于需要并发处理的跨进程通信场景。
BroadcastReceiver的注册方式有两种。动态注册是在代码中通过registerReceiver注册,需要在onDestroy中反注册,否则会泄漏;静态注册是在AndroidManifest中声明,应用安装后系统会在应用未启动时也能接收广播。从Android 8.0(API 26)开始,系统对静态注册的隐式广播进行了限制,很多系统广播无法再通过静态注册方式接收。同时,从Android 14(API 34)开始,动态注册广播接收器时也必须指定RECEIVER_EXPORTED或RECEIVER_NOT_EXPORTED标志。
ContentProvider是Android中跨应用共享数据的主要方式。访问ContentProvider需要通过ContentResolver的query、insert、update、delete方法,使用Uri来标识数据。在IPC过程中,ContentProvider的底层使用Binder机制实现跨进程调用。如果访问的数据量较大,应该使用openAssetFile或openFile等方法来传递大文件,避免通过Binder传递过大的数据导致TransactionTooLargeException。
4. 内存与性能优化考点:从原理到实战的结合题
4.1 常见内存泄漏场景与规避方案
内存泄漏是Android开发中非常常见的质量问题,也是笔试中经常结合场景考察的知识点。其中一个最常见的场景是Handler造成的泄漏,这个前面已经说过。另一个经典泄漏场景是静态Context引用。如果把Activity的Context赋值给静态变量,Activity在被销毁后无法被垃圾回收。解决方法是尽量使用Application的Context,或者在合适时机将静态变量置空。
匿名内部类的使用也是内存泄漏的高发场景。比如创建一个匿名Runnable对象并交给Handler,如果这个Runnable需要在Activity销毁后仍执行,就可能造成泄漏。这种情况下需要把生命周期与外部组件解绑,比如在onDestroy时把Runnable移除。
还有单例模式造成的内存泄漏。例如使用单例持有Activity的Context引用,单例的生命周期与应用进程一致,所以被持有的Activity永远无法被回收。正确的做法是单例中只持有ApplicationContext。网络请求回调也可能造成泄漏,如果Activity发起了网络请求,在请求返回之前Activity被销毁,而回调中还需要处理Activity相关操作,就会泄漏。解决方法是使用弱引用引用Activity,或在销毁时取消网络请求。
这些泄漏场景在笔试题中出现时,往往不会直接说"这会导致泄漏",而是用长长的描述覆盖代码场景,需要自己定位问题。答题的核心思路是寻找"生命周期长的对象是否持有生命周期短的对象引用"。
4.2 布局优化与绘制性能
布局优化是Android性能优化的基本功。include标签用于布局复用,merge标签用于减少布局层级,ViewStub用于延迟加载不常用的布局。笔试中常考的一个知识点是:为什么使用merge标签?因为FrameLayout中如果只有一个子View,Root布局直接用merge标签可以让子View直接放到父容器中,减少一层嵌套,提高绘制效率。
布局层级越深,measure和layout阶段的时间花费就越多,因此需要减少过度的嵌套。官方建议布局层级控制在5层以内,一旦超过这个层级,就应该检查是否有必要通过RelativeLayout或ConstraintLayout来压缩层级。笔试中可能会给出一段布局代码,让你分析布局层级如何优化。
View的绘制流程是measure、layout、draw三个阶段。measure阶段确定View的大小,layout阶段确定View的位置,draw阶段绘制View的内容。常考的题目是:在onMeasure中调用setMeasuredDimension来设置测量宽高,ViewGroup在onMeasure中要遍历所有子View并调用它们的measure方法,measure时传递的MeasureSpec包括mode和size两部分,mode有三种取值:UNSPECIFIED表示父容器不对子View的大小做限制,EXACTLY表示父容器确定子View的精确大小,AT_MOST表示子View的大小不能超过父容器给定的最大值。
自定义View是笔试中的高阶考点。常考的题目有:自定义View的属性如何声明和获取,在自定义View中处理padding,需要写一个继承自View的类,在构造方法中获取自定义属性。需要重写onMeasure处理wrap_content的情况。如果自定义ViewGroup想支持子View,需要重写onLayout方法对子View进行布局。
4.3 LruCache与图片加载框架的原理
图片缓存是Android开发中非常高频的性能优化场景。LruCache是Android 3.1之后提供的缓存工具类,全称是Least Recently Used Cache,即最近最少使用算法。它的核心原理是用LinkedHashMap来存储数据,通过accessOrder参数来控制访问顺序。当缓存满时,会淘汰最久未使用的数据,这就是LRU策略。
LruCache在Android中的典型应用场景是图片缓存。同时需要注意:LruCache的总容量通常根据应用可用内存来设置,比如总容量的八分之一。图片加载到内存时,用计算后的字节数,而不是直接计算图片宽高乘以像素大小。在使用Bitmap时要注意复用recycle方法,但在高版本中,recycle方法不是必要的,系统会自动回收Native层内存。复用inBitmap的方式,在一定的API级别才能用,可以复用Bitmap的内存区域避免重新分配。
关于图片加载框架,笔试有时会问Glide、Fresco、Picasso和ImageLoader的选型和底层实现。Glide默认使用RGB_565解码图片(部分版本默认ARGB_8888),内存占用只是其他框架用ARGB_8888的一半,因此内存占用比Picasso要小。Glide的生命周期感知能力很强,与Activity和Fragment的生命周期绑定,能在页面销毁时自动取消加载任务位图。还可以通过DiskCacheStrategy控制磁盘缓存策略,如缓存原图、缓存处理后的结果等。
5. 网络与操作系统:非专业但必须拿下的分数
5.1 TCP与UDP、HTTP与HTTPS:这些网络协议题怎么答
TCP协议的三次握手和四次挥手是校招笔试几乎必考的题目。三次握手建立连接的过程是:客户端发送SYN报文,服务器回复SYN+ACK报文,客户端再发送ACK报文。为了确保双方收发能力都正常。三次握手中每次丢失报文如何处理,SYN Flood攻击如何防御,是区分基础题和进阶题的节点。
四次挥手断开连接的过程是:主动方发送FIN报文,被动方回复ACK,被动方发送FIN报文,主动方回复ACK。因为在TCP连接中可能存在半关闭状态,所以被动方收到FIN后可以先回复ACK告诉主动方"我已收到关闭请求",等自己数据发送完毕后再发送FIN,这就是中间有两次等待的原因。TIME_WAIT状态出现在主动关闭连接的一方,等待2MSL时间确保最后的ACK到达,MSL是报文段最大生存时间,通常为2分钟。
TCP与UDP的区别是基础题。TCP是面向连接的、可靠的、基于字节流的传输层协议,UDP是无连接的、不可靠的、基于数据报的传输层协议。TCP有流量控制和拥塞控制机制,UDP没有。TCP适合传输文件、邮件等需要可靠交付的场景,UDP适合视频通话、实时游戏等对延迟敏感的场景。
HTTP与HTTPS的区别同样是高频考点。HTTP使用明文传输,HTTPS使用SSL/TLS加密传输。HTTPS的默认端口是443,HTTP是80。HTTPS的加密过程是:客户端请求HTTPS连接,服务器返回数字证书,客户端验证证书合法性并通过证书获取服务器的公钥,客户端生成对称加密密钥并用服务器的公钥加密后发送给服务器,服务器用私钥解密得到对称加密密钥,之后双方通过对称加密密钥进行通信。题目的变种是:为什么不用纯非对称加密?因为非对称加密解密速度慢,对称加密速度快但密钥分发不安全,所以使用非对称加密来传输对称密钥。
HTTP的请求方法也是常考点。GET和POST的区别是面试笔试必考题。GET请求的参数放在URL中,POST放在请求体中,GET方式容易因为URL长度限制而截断,POST方式没有这个限制。实际上,GET和POST的语义区别更为根本:GET应该用于获取数据,不应有副作用,POST用于提交操作,允许有副作用。
HTTP状态码也是常考知识点,2xx表示成功,3xx表示重定向,4xx表示客户端错误,5xx表示服务器错误。常考的有:200 OK表示请求成功,301表示永久重定向,302表示临时重定向,304表示未修改(缓存),400是请求错误,401未授权,403禁止访问,404找不到资源,500服务器内部错误,502网关错误,503服务不可用。
5.2 进程与线程、死锁与并发:操作系统笔试要点
操作系统部分的笔试题目主要有进程与线程的区别、死锁的四个必要条件、进程状态转换、虚拟内存等。进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位。同一进程的多个线程共享进程的地址空间和资源,而进程之间拥有独立的地址空间和资源。进程间通信方式有管道、消息队列、共享内存、信号量、套接字,线程间通信可以直接通过共享变量实现。
死锁的四个必要条件是互斥条件、请求与保持条件、不可剥夺条件、循环等待条件。前三个条件中的任何一个如果被破坏,死锁就不会发生。死锁的预防策略有:破坏互斥条件(很难做到)、破坏请求与保持条件(一次性申请所有资源)、破坏不可剥夺条件(适当剥夺资源)、破坏循环等待条件(按序分配资源)。常考的一个细节是:死锁的避免算法有银行家算法。
进程状态转换是经典题目。进程有三种基本状态:就绪、运行、阻塞。就绪状态到运行状态的转换是由调度程序完成的,运行状态到阻塞状态通常是因为等待I/O事件或信号量,阻塞状态到就绪状态是等待的事件完成。画进程状态转换图是这类题目的常见操作型考点。
5.3 数据结构与算法:选择题中的送分题与陷阱题
数据结构部分的笔试题相对基础,主要以选择题形式考察。要掌握各种数据结构的基本特点和适用场景:数组随机访问快、链表增删快、栈先进后出、队列先进先出、二叉树有左右孩子、哈希表以空间换时间、图用于表示多对多关系。
二叉树遍历是必考题。前序遍历的顺序是根左右,中序遍历的顺序是左根右,后序遍历的顺序是左右根。给出两种遍历序求第三种遍历序是常考的题目。如果给出前序和后序,一般无法唯一确定一棵二叉树,除非二叉树是满二叉树或每个节点的度要么为0要么为2。同时要清楚排序算法的时间复杂度:冒泡排序平均O(n^2)、快速排序平均O(n log n)、堆排序O(n log n)、归并排序O(n log n)且空间复杂度O(n)。快速排序在最坏情况下的时间复杂度退化为O(n^2),这种情况发生在每次选择的基准元素都是最小或最大的时候,所以快速排序有时候会加入随机化来避免最坏情况。
二叉树中的经典题目包括完全二叉树的节点个数、二叉搜索树中查找第k小的元素、按层遍历二叉树、二叉树的最大深度与最小深度、两个节点的最近公共祖先。这些题目在笔试中往往以选择题形式考察时间复杂度与实现思路。
哈希表的冲突解决方法有开放定址法和链地址法。开放定址法的探测方式有线性和二次探测与双重散列,当冲突发生时按某种方式寻找下一个空位。链地址法是把哈希值相同的元素放在同一个链表中。HashMap在Java中采用链地址法,当链表过长时转化为红黑树来优化查询效率,这一点在第二章已经细说。
LRU缓存算法在操作系统中是页面置换算法的一种。它是基于"最近使用的数据在不久的将来还会被使用"这个假设。实现思路是用哈希表加双向链表,哈希表负责O(1)的查找,双向链表负责记录访问顺序,链表头部是最新访问的数据,尾部是最久未访问的数据。每次访问时把数据移到链表头部,缓存满时淘汰链表尾部的数据。复杂度分析:get和put操作的时间复杂度都是O(1)。这种数据结构在Android的LruCache中被直接应用。
6. 笔试中的实战策略与备考建议
6.1 时间分配与答题顺序
客观题部分的时间分配很关键。一场笔试通常在60到90分钟,题量大约在50到80题之间,平均每题只有1分钟左右的时间。我的建议是拿到试卷后先快速浏览一遍,标记出自己确定的题和不确定的题。答题顺序上,先做自己有把握的题,把确定的分数牢牢抓在手里,然后再回来啃难题。
客观题中遇到完全没思路的题目,不要死磕,先在草稿纸上标记题号,继续往下做。客观题不像编程题,不会因为代码没写完而整题零分,先做完所有会做的题,再回头处理不会的题,这样的策略能最大程度利用有限的答题时间。
多选题是最容易丢分的题型。多选、少选、错选都不得分,答题时需要非常谨慎。如果对某个选项没有十足把握,我的经验是:可以不选,因为有把握的选项至少能拿部分分,猜一个不确定的选项容易全军覆没。
6.2 错题复盘的正确姿势
备考过程中,做完题不是结束,复盘才是真正拉开差距的环节。刷题时不要只对答案,而要去分析每个错误选项的考察意图。一道考察Activity启动模式的题目,如果做错了,不要只记住正确答案,而要思考出题人为什么设置另外三个错误选项,它们代表了哪些常见的理解偏差。
我习惯用表格记录错题,包含题干、我的错误选项、正确答案、错误原因、关联知识点五列。复习时重点看错误原因和关联知识点,尤其是反复出现的高频错误点。
在即将笔试的冲刺阶段,可以只看错题本和知识图谱,不需要再大量刷新题。把之前总结的知识点框架再过一遍,比盲目刷题更有价值。
6.3 笔试与面试的衔接准备
笔试结束到面试通知的过程,通常有几天到两周不等。这段时间不要干等着,可以把笔试中暴露出来的薄弱知识点整理成面试自我介绍中的讲述素材。
笔试考的内容,面试中大概率会再次出现。比如笔试考了Handler消息机制,面试中可能会让你手写一个Handler消息机制的简化版本,或者追问Handler内存泄漏的原因和解决方案。笔试考了Activity启动模式,面试中可能会给你一个具体场景,问你该用哪种启动模式。
其实很多公司笔试客观题都在筛选基本功,而不需要真正掌握某个高深技术。如果真的把上面这些知识点吃透了,即便笔试不通过,面试环节也大概率能顶住。2019年360的这套笔试客观题,说实话难度并没有超出常规校招范围,关键在于知识体系是否成型。我建议读者可以把这份考点整理当作自检清单,逐项核验自己的知识盲区,然后针对性地补齐,早日拿下理想offer。