2.1 面试官谈基础知识
- 基础很重要
- 算法、复杂的
- 编程能力
- 数据结构
2.2 编程语言
- 程序员写代码总是基于某一种编程语言,因此技术面试的时候直接或者间接都会涉及至少一种编程语言。在面试的过程中,面试官要么直接问语言的语法,要么让应聘者用-~种编程语言写代码解决一个问题,通过写出的代码来判断应聘者对他使用的语言的掌握程度。现在流行的编程语言很多,不同公司开发用的语言也不尽相同。做底层开发比如经常写驱动的人更习惯用C, Linux下有很多程序员用C++开发应用程序,基于Windows的C#项目已经越来越多,跨平台开发的程序员则可能更喜欢Java,随着苹果iPad、iPhone 的热销已经有很多程序员投向了Objective C的阵营,同时还有很多人喜欢用脚本语言如Perl、Python 开发短小精致的小应用软件。因此,不同公司面试的时候对编程语言的要求也有所不同。每一种编程语言都可以写出一本大部头的书籍,本书限于篇幅不可能面面俱到。本书中所有代码都用C/C++/C#实现,下 面简要介绍一些C++/C#常见的面试题。
面试题1:赋值运算符函数
- 题目:如下为类型CMyString的声明,请为该类型添加赋值运算符函数。
- 思路
-
将返回值类型声明为该类型的引用
-
把传入的参数类型声明为常量引用
-
释放实例自身已有的内存
-
判断传入的参数和当前的实例是不是同一个实例
-
- 不适用java
面试题2:实现Singleton模式
- 题目:设计一个类,我们只能生成该类的一个实例。
- https://www.cnblogs.com/cielosun/p/6582333.html
- 1. 懒汉模式
- public class SingletonDemo {
- private static SingletonDemo instance;
- private SingletonDemo(){
- }
- public static SingletonDemo getInstance(){
- if(instance==null){
- instance=new SingletonDemo();
- }
- return instance;
- }
- }
- 如上,通过提供一个静态的对象instance,利用private权限的构造方法和getInstance()方法来给予访问者一个单例。缺点是,没有考虑到线程安全,可能存在多个访问者同时访问,并同时构造了多个对象的问题。之所以叫做懒汉模式,主要是因为此种方法可以非常明显的lazy loading。针对懒汉模式线程不安全的问题,我们自然想到了,在getInstance()方法前加锁,于是就有了第二种实现。
- 2. 线程安全的懒汉模式
- public class SingletonDemo {
- private static SingletonDemo instance;
- private SingletonDemo(){
- }
- public static synchronized SingletonDemo getInstance(){
- if(instance==null){
- instance=new SingletonDemo();
- }
- return instance;
- }
- }
- 然而并发其实是一种特殊情况,大多时候这个锁占用的额外资源都浪费了,这种打补丁方式写出来的结构效率很低。
- 3. 饿汉模式
- public class SingletonDemo {
- private static SingletonDemo instance=new SingletonDemo();
- private SingletonDemo(){
- }
- public static SingletonDemo getInstance(){
- return instance;
- }
- }
- 直接在运行这个类的时候进行一次loading,之后直接访问。显然,这种方法没有起到lazy loading的效果,考虑到前面提到的和静态类的对比,这种方法只比静态类多了一个内存常驻而已。
- 4. 静态类内部加载
- public class SingletonDemo {
- private static class SingletonHolder{
- private static SingletonDemo instance=new SingletonDemo();
- }
- private SingletonDemo(){
- System.out.println("Singleton has loaded");
- }
- public static SingletonDemo getInstance(){
- return SingletonHolder.instance;
- }
- }
- 使用内部类的好处是,静态内部类不会在单例加载时就加载,而是在调用getInstance()方法时才进行加载,达到了类似懒汉模式的效果,而这种方法又是线程安全的。
- 本题考点:
-
- 考查对单例(Singleton) 模式的理解。
- 考查对Java的基础语法的理解,如静态构造函数等。
- 考查对多线程编程的理解。
2.3 数据结构
数据结构一直是技术面试的重点,大多数面试题都是围绕着数组、字符串、链表、树、栈及队列这几种常见的数据结构展开的,因此每一个应聘者都要熟练掌握这几种数据结构。
数组和字符串是两种最基本的数据结构,它们用连续内存分别存储数字和字符。链表和树是面试中出现频率最高的数据结构。由于操作链表和树需要操作大量的指针,应聘者在解决相关问题的时候-要留意代码的鲁棒性,否则容易出现程序崩溃的问题。是一个与递归紧密相关的数据结构,同样队列也与广度优先遍历算法紧密相关。深刻理解这两种数据结构能帮助我们解决很多算法问题。
2.3.1 数组
面试题3:二维数组中的查找
- 题目:在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
-
思路:从右上角或左下角开始找,逐行删除,或者用二分法查找
- 代码实现 右上角开始实现
- publicclassTestc {
- publicstaticvoidmain(String[] args) {
- int[][] arr= { { 1, 2, 8, 9 }, { 2, 4, 9, 12 }, { 4, 7, 10, 13 }, { 6, 8, 11, 15 } };
- System.out.println(find(arr, 7));
- }
- publicstaticbooleanfind(int[][] array, inttarget) {
- if(array== null) {
- returnfalse;
- }
- introw= 0;
- intcolumn= array[0].length- 1;
- while(row< array.length&& column>= 0) {
- if(array[row][column] == target) {
- returntrue;
- }
- if(array[row][column] > target) {
- column--;
- } else{
- row++;
- }
- }
- returnfalse;
- }
- }
- 测试用例:
-
- 二维数组中包含查找的数字(查找的数字是数组中的最大值和最小值,查找的数字介于数组中的最大值和最小值之间)。
- 二维数组中没有查找的数字(查找的数字大于数组中的最大值,查找的数字小于数组中的最小值,查找的数字在数组的最大值和最小值之间但数组中没有这个数字)。
- 特殊输入测试(输入空指针)。
- 本题考点:
-
- 考查应聘者对二维数组的理解及编程能力。二维数组在内存中占据连续的空间。在内存中从上到下存储各行元素,在同一-行中按照从左到右的顺序存储。因此我们可以根据行号和列号计算出相对于数组首地址的偏移量,从而找到对应的元素。
- 考查应聘者分析问题的能力。当应聘者发现问题比较复杂时,能不能通过具体的例子找出其中的规律,是能否解决这个问题的关键所在。这个题目只要从一个具体的二维数组的右上角开始分析,就能找到查找的规律,从而找到解决问题的突破口。
2.3.2 字符串
面试题4:替换空格
- 题目:请实现一个函数,把字符串中的每个空格替换成"%20"。例如输入“We are happy.",则输出“We%20are%20happy.”。
- 思路:从后往前复制,数组长度会增加,或使用StringBuilder、StringBuffer类 , 先遍历空格个数直接扩容
- 代码实现
- publicclassTestc {
- publicstaticvoidmain(String[] args) {
- System.out.println(replaceSpace(newStringBuffer("We Are Happy.")));
- }
- publicstaticString replaceSpace(StringBuffer str) {
- if(str== null)
- returnnull;
- StringBuilder sb= newStringBuilder();
- for(inti= 0; i< str.length(); i++) {
- if(String.valueOf(str.charAt(i)).equals(" ")) {
- sb.append("%20");
- } else{
- sb.append(str.charAt(i));
- }
- }
- returnString.valueOf(sb);
- }
- }
- 方法二
- "We Are Happy.".replace(" ", "%20")
- 测试用例:
-
- 输入的字符串中包含空格(空格位于字符串的最前面,空格位于字符串的最后面,空格位于字符串的中间,字符串中有连续多个空格)。
- 输入的字符串中没有空格。
- 特殊输入测试(字符串是个NULL指针、字符串是个空字符串、字符串只有一个空格字符、字符串中只有连续多个空格)。
- 本题考点:
-
- 考查对字符串的编程能力。
- 考查分析时间效率的能力。我们要能清晰地分析出两种不同方法的时间效率各是多少。
- 考查对内存覆盖是否有高度的警惕。在分析得知字符串会变长之后,我们能够意识到潜在的问题,并主动和面试官沟通以寻找问题的解决方案。
- 考查思维能力。在从前到后替换的思路被面试官否定之后,我们能迅速想到从后往前替换的方法,这是解决此题的关键。
- 相关题目:
-
- 有两个排序的数组A1和A2,内存在A1的末尾有足够多的空余空间容纳A2。请实现一个函数,把A2中的所有数字插入到A1中并且所有的数字是排序的。和前面的例题一样,很多人首先想到的办法是在A1中从头到尾复制数字,但这样就会出现多次复制一个数字的情况。更好的办法是从尾到头比较A1和A2中的数字,并把较大的数字复制到A1的合适位置。
- 举一反三:
- 合并两个数组(包括字符串)时,如果从前往后复制每个数字(或字符)需要重复移动数字(或字符)多次,那么我们可以考虑从后往前复制,这样就能减少移动的次数,从而提高效率。
2.3.3 链表
我们说链表是一种动态数据结构,是因为在创建链表时,无须知道链
表的长度。当插入一一个结点时,我们只需要为新结点分配内存,然后调整指针的指向来确保新结点被链接到链表当中。内存分配不是在创建链表时.次性完成,而是每添加一个结点分配一 次内存。由于没有闲置的内存,链表的空间效率比数组高。
面试题5:从尾到头打印链表
- 题目:输入一个链表的头结点,从尾到头反过来打印出每个结点的值。
- 面试小提示:在面试中如果我们打算修改输入的数据,最好先问面试官是不是允许做修改。
-
思路:借助栈实现,或使用递归的方法。
- 代码实现
- publicArrayList<Integer> printListFromTailToHead(ListNodelistNode) {
- ArrayList<Integer> list= newArrayList<>();
- if(listNode== null)
- returnlist;
- Stack<ListNode> stack= newStack<>();
- while(listNode!= null) {
- stack.push(listNode);
- listNode= listNode.next;
- }
- while(!stack.isEmpty()) {
- list.add(stack.pop().val);
- }
- returnlist;
- }
- 简单演示 栈
- publicclassTestc {
- publicstaticvoidmain(String[] args) {
- ArrayList<Integer> arr= newArrayList<Integer>();
- arr.add(1);
- arr.add(2);
- arr.add(3);
- arr.add(4);
- arr.add(5);
- printListFromTailToHead(arr);
- }
- publicstaticvoidprintListFromTailToHead(ArrayList<Integer> list) {
- Stack<Integer> stack= newStack<Integer>();
- for(Integer integer: list) {
- stack.push(integer);
- }
- while(!stack.isEmpty()) {
- System.out.println(stack.pop());
- }
- }
- }
- 简单演示 递归
- publicclassTestc {
- publicstaticvoidmain(String[] args) {
- int[] arr= {1,2,3,4,5};
- printListFromTailToHead(arr,0);
- }
- publicstaticvoidprintListFromTailToHead(int[] arr,intnum) {
- if(arr.length> num){
- printListFromTailToHead(arr,num+1);
- System.out.println(arr[num]);
- }
- }
- }
- 测试用例:
- 功能测试(输入的链表有多个结点,输入的链表只有一个结点)。
- 特殊输入测试(输入的链表头结点指针为NULL)。
- 本题考点:
- 考查对单项链表的理解和编程能力。
- 考查对循环、递归和栈3个相互关联的概念的理解。
2.3.4 树
树是一种在实际编程中经常遇到的数据结构。它的逻辑很简单:除了根结点之外每个结点只有一个父结点,根结点没有父结点;除了叶结点之外所有结点都有一个或多个子结点,叶结点没有子结点。父结点和子结点之间用指针链接。由于树的操作会涉及大量的指针,因此与树有关的面试题都不太容易。当面试官想考查应聘者在有复杂指针操作的情况下写代码的能力,他往往会想到用与树有关的面试题。
面试的时候提到的树,大部分都是二叉树。所谓二叉树是树的一种特殊结构,在二叉树中每个结点最多只能有两个子结点。在二叉树中最重要的操作莫过于遍历,即按照某一-顺序访问树中的所有结点。通常树有如下几种遍历方式:
- 前序遍历:先访问根结点,再访问左子结点,最后访问右子结点。图2.5中的二叉树的前序遍历的顺序是10、6、4、8、14、12、16。
- 中序遍历:先访问左子结点,再访问根结点,最后访问右子结点。图2.5中的二叉树的中序遍历的顺序是4、6、8、10、12、14、16。
- 后序遍历:先访问左子结点,再访问右子结点,最后访问根结点。图2.5中的二叉树的后序遍历的顺序是4、8、6、12、16、14、10。
![](https://i-blog.csdnimg.cn/blog_migrate/ee5b4f0b8eb3865b170b9e5ed58a0868.png)
面试题6:重建二叉树
- 题目:输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
-
思路:先找出根节点,然后利用递归方法构造二叉树
- 代码实现
- classTreeNode {
- intval;
- TreeNode left;
- TreeNode right;
- TreeNode(intx) {
- val= x;
- }
- }
- publicclassTestc {
- publicTreeNode reConstructBinaryTree(int[] pre, int[] in) {
- if(pre== null|| in== null) {
- returnnull;
- }
- if(pre.length== 0 || in.length== 0) {
- returnnull;
- }
- if(pre.length!= in.length) {
- returnnull;
- }
- TreeNode root= newTreeNode(pre[0]);
- for(inti= 0; i< pre.length; i++) {
- if(pre[0] == in[i]) {
- root.left= reConstructBinaryTree(Arrays.copyOfRange(pre, 1, i+ 1), Arrays.copyOfRange(in, 0, i));
- root.right= reConstructBinaryTree(Arrays.copyOfRange(pre, i+ 1, pre.length),
- Arrays.copyOfRange(in, i+ 1, in.length));
- }
- }
- returnroot;
- }
- }
- 测试用例:
- 普通二叉树(完全二叉树,不完全二叉树)。
- 特殊二叉树(所有结点都没有右子结点的二叉树,所有结点都没有左子结点的二叉树,只有一一个结点的二叉树)。
- 特殊输入测试(二叉树的根结点指针为NULL、输入的前序遍历序列和中序遍历序列不匹配)。
- 本题考点:
-
考查应聘者对二叉树的前序遍历、中序遍历的理解程度。只有对二叉树的不同遍历算法有了深刻的理解,应聘者才有可能在遍历序列中划分出左、右子树对应的子序列。考查应聘者分析复杂问题的能力。我们把构建二叉树的大问题分解成构建左、右子树的两个小问题。我们发现小问题和大问题在本质上是一致的,因此可以用递归的方式解决。
-
2.3.5 栈和队列
栈是一个非常常见的数据结构,它在计算机领域中被广泛应用,
比如操作系统会给每个线程创建-一个栈用来存储函数调用时各个函数的参数、返回地址及临时变量等。栈的特点是后进先出,即最后被压入(push)栈的元素会第一个被弹出(pop)。 在面试题22“栈的压入、弹出序列”中,我们再详细分析进栈和出栈序列的特点。
通常栈是一“个不考虑排序的数据结构,我们需要O(n)时间才能找到栈中最大或者最小的元素。如果想要在O(1)时间内得到栈的最大或者最小值,我们需要对栈做特殊的设计,详见面试题21“包含min函数的栈”。
队列是另外一种很重要的数据结构。和栈不同的是,队列的特点是先进先出,即第一个进入队列的元素将会第一个出来。 在2.3.4节介绍的树的宽度优先遍历算法中,我们在遍历某一层树的结点时,把结点的子结点放到一个队列里,以备下一层结点的遍历。详细的代码参见面试题23“从上到下遍历二叉树”。
栈和队列虽然是特点针锋相对的两个数据结构,但有意思的是它们却相互联系。请看面试题7“用两个栈实现队列”,同时读者也可以考患如何用两个队列实现栈。
面试题7:用两个栈实现队列
-
题目:用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
- 思路:一个栈压入元素,而另一个栈作为缓冲,将栈1的元素出栈后压入栈2中。也可以将栈1中的最后一个元素直接出栈,而不用压入栈2中再出栈。
- 代码
- publicclassTestc {
- Stack<Integer> stack1= newStack<Integer>();
- Stack<Integer> stack2= newStack<Integer>();
- publicstaticvoidmain(String[] args) throwsException {
- Testc t= newTestc();
- t.push(1);
- t.push(2);
- System.out.println(t.pop());
- t.push(3);
- System.out.println(t.pop());
- System.out.println(t.pop());
- System.out.println(t.pop());
- }
- publicvoidpush(intnode) {
- stack1.push(node);
- }
- publicintpop() throwsException {
- if(stack1.isEmpty() && stack2.isEmpty()) {
- thrownewRuntimeException("栈为空!");
- }
- if(stack2.isEmpty()) {
- while(!stack1.isEmpty()) {
- stack2.push(stack1.pop());
- }
- }
- returnstack2.pop();
- }
- }
- 测试用例:
- 往空的队列里添加、删除元素。
- 往非空的队列里添加、
- 删除元素。
- 连续删除元素直至队列为空。
- 本题考点:
- 考查对栈和队列的理解。
- 考查写与模板相关的代码的能力。
- 考查分析复杂问题的能力。本题解法的代码虽然只有只有20几行
- 代码,但形成正确的思路却不容易。应聘者能否通过具体的例子分
- 析问题,通过画图的手段把抽象的问题形象化,从而解决这个相对
- 比较复杂的问题,是能否顺利通过面试的关键。
2.4 算法和数据操作
和数据结构一样,考查算法的面试题也备受面试官的青睐,其中排序和查找是面试时考查算法的重点。在准备面试的时候,我们应该重点掌握二分查找、归并排序和快速排序,做到能随时正确、完整地写出它们的代码。
有很多算法都可以用递归和循环两种不同的方式实现。通常基于递归的实现方法代码会比较简洁,但性能不如基于循环的实现方法。在面
试的时候,我们可以根据题目的特点,甚至可以和面试官讨论选择合适.的方法编程。
位运算可以看成是一- 类特殊的算法,它是把数字表示成二进制之后对0和1的操作。由于位运算的对象为二进制数字,所以不是很直观,但掌握它也不难,因为总共只有与、或、异或、左移和右移5种位运算。
2.4.1 查找和排序
查找和排序都是在程序设计中经常用到的算法。查找相对而言较为简
单,不外乎顺序查找、二分查找、哈希表查找和二叉排序树查找。在试
的时候,不管是用循环还是用递归,面试官都期待应聘者能够信手拈来写出完整正确的二分查找代码,否则可能连继续面试的兴趣都没有。
- 面试小提示:如果面试题是要求在排序的数组( 或者部分排序的数组)中查找一个数字或者统计某个数字出现的次数,我们都可以尝试用二分查找算法。
面试题8:旋转数组的最小数字
- 题目:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非递减排序的数组的一 个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0
- 思路:
- 代码实现
- publicclassTestc {
- publicstaticvoidmain(String[] args) {
- System.out.println(minNumberInRotateArray(newint[] { 1, 0, 1, 1, 1 }));
- }
- publicstaticintminNumberInRotateArray(int[] array) {
- if(array== null|| array.length== 0)
- return0;
- intleft= 0;
- intright= array.length- 1;
- intmid= 0;
- while(array[left] >= array[right]) {
- if(right- left<= 1) {
- mid= right;
- break;
- }
- mid= (left+ right) / 2;
- if(array[left] == array[mid] && array[mid] == array[right]) {
- if(array[left+ 1] != array[right- 1]) {
- intreslut= array[left];
- for(inti= left+ 1; i<= right; i++) {
- if(reslut>array[i]) {
- reslut= array[i];
- }
- }
- returnreslut;
- }
- } else{
- if(array[left] <= array[mid]) {
- left= mid;
- } else{
- right= mid;
- }
- }
- }
- returnarray[mid];
- }
- }
- 测试用例:
- 功能测试(输入的数组是升序排序数组的一个旋转,数组中有重复数字或者没有重复数字)。
- 边界值测试(输入的数组是一一个升序排序的数组、只包含一一个数字的数组)。
- 特殊输入测试(输入NULL指针)。
- 本题考点:
- 考查对二分查找的理解。本题变换了二分查找的条件,输入的数组不是排序的,而是排序数组的一个旋转。这要求我们对二分查找的过程有深刻的理解。
- 考查沟通学习能力。本题面试官提出了一个新的概念:数组的旋转。我们要在很短时间内学习理解这个新概念。在面试过程中如果面试官提出新的概念,我们可以主动和面试官沟通,多问几个问题把概念弄清楚。
- 考查思维的全面性。排序数组本身是数组旋转的一一个特例。另外,我们要考虑到数组中有相同数字的特例。如果不能很好地处理这些特例,就很难写出让面试官满意的完美代码。
2.4.2 递归和循环
如果我们需要重复地多次计算相同的问题,通常可以选择用递归或者循环两种不同的方法。递归是在一一个函数的内部调用这个函数自身。而循环则是通过设置计算的初始值及终止条件,在一个范围内重复运算。
面试小提示:通常基于递归实现的代码比基于循环实现的代码要简洁很多,更加容易实现。如果面试官没有特殊要求,应聘者可以优先采用递归的方法编程。
- 递归虽然有简洁的优点,但它同时也有显著的缺点。递归由于是函数调用自身,而函数调用是有时间和空间的消耗的:每一次函数调用,都需要在内存栈中分配空间以保存参数、返回地址及临时变量,而且往栈里压入数据和弹出数据都需要时间。这就不难理解.上述的例子中递归实现的效率不如循环。
- 另外,递归中有可能很多计算都是重复的,从而对性能带来很大的负面影响。递归的本质是把一一个问题分解成两个或者多个小问题。如果多个小问题存在相互重叠的部分,那么就存在重复的计算。在面试题9“斐波那契数列”及面试题43“n个骰子的点数”中我们将详细地分析递归和循环的性能区别。
- 除了效率之外,递归还有可能引起更严重的问题:调用栈溢出。前面分析中提到需要为每一次函数调用在内存栈中分配空间,而每个进程的栈的容量是有限的。当递归调用的层级太多时,就会超出栈的容量,从而导致调用栈溢出。在上述例子中,如果输入的参数比较小,如10,它们都能返回结果55。但如果输入的参数很大,如5000,那么递归代码在运行的时候就会出错,但运行循环的代码能得到正确的结果12502500。
面试题9:斐波那契数列
- 题目一:写一个函数,输入n,求斐波那契( Fibonacci )数列的第n项斐波那契数列的定义如下:
![](https://i-blog.csdnimg.cn/blog_migrate/602d853e542e26cf7a7218a16aae5d83.png)
- 思路:递归实现效率低,循环实现。
- 循环代码实现
- publicclassTestc {
- publicstaticvoidmain(String[] args) {
- System.out.println(fibonacci(9));
- }
- publicstaticlongfibonacci(intn) {
- longresult= 0;
- longpreOne= 1;
- longpreTwo= 0;
- if(n== 0) {
- returnpreTwo;
- }
- if(n== 1) {
- returnpreOne;
- }
- for(inti= 2; i<= n; i++) {
- result= preOne+ preTwo;
- preTwo= preOne;
- preOne= result;
- }
- returnresult;
- }
- }
- 递归代码实现
- publicclassTestc {
- publicstaticvoidmain(String[] args) {
- System.out.println(fibonacci(9));
- }
- publicstaticlongfibonacci(intn) {
- if(n< 0) {
- return0;
- }
- if(n== 1) {
- return1;
- }
- returnfibonacci(n- 1) + fibonacci(n- 2);
- }
- }
- 题目二:一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法
- 把问题一 0 1 改成 1 2
- 假设,一级台阶,有f(1)种方法,二级有f(2)种,以此类推,n级有f(n)种方法。
- 可以看出,f(1)=1;f(2)=2。
- 那么,假设n级台阶,那么第一步就有两种情况,跳一步,跟跳两步。
- 情况一:跳一步,那么接下去的就是f(n-1);
- 情况二:跳两步,那么接下去的就是f(n-2)。
- 测试用例:
- 功能测试(如输入3、5、10 等)。
- 边界值测试(如输入0、1、2)。
- 性能测试(输入较大的数字,如40、50、100 等)。
- 本题考点:
- 考查对递归、循环的理解及编码能力。
- 考查对时间复杂度的分析能力。
- 如果面试官采用的是青蛙跳台阶的问题,那同时还在考查应聘者的数学建模能力。
2.4.3 位运算
五种运算:与、或、异或、左移、右移
面试题10:二进制中的1的个数
- 题目:请实现一个函数,输入一个整数,输出该数二进制表示中1的个数。例如把9表示成二进制是1001,有2位是1。因此如果输入9,该函数输出2。
- 思路:
- 1.可能引起死循环的解法 判断尾数 右移。面试官接下来可能要问的第二个问题就是:上面的函数如果输入一个负数,比如0x80000000,运行的时候会发生什么情况?把负数0x80000000右移一位的时候 并不是简单地把最高位的1移到第二位变成0x4000000而是0xC000000。这是因为移位前是个负数,仍然要保证移位后是个负数,因此移位后的最高位会设为1。如果一直做右移运算,最终这个数字就会变成0xFFFFFFFF而陷入死循环。
- 2.为了避免死循环,我们可以不右移输入的数字i。首先把i和1做与运算,判断i的最低位是不是为1。接着把1左移一-位得到2,再和i做与运算,就能判断i的次低位是不是.....这样反复左移,每次都能判断i的其中一位是不是1。
- 代码实现
- publicclassTestc {
- publicstaticvoidmain(String[] args) {
- System.out.println(NumberOf(5));
- }
- publicstaticintNumberOf(intn) {
- intcount= 0;
- intflag= 1;
- while(flag!= 0){
- if((n& flag) != 0){
- count++;
- }
- flag= flag<< 1;
- }
- returncount;
- }
- }
- 思路二:把一个整数减去1,再和原整数做与运算,会把该整数最右边一个1变成0。那么一一个整数的二进制表示中有多少个1,就可以进行多少次这样的操作。
- 代码实现
- publicclassTestc {
- publicstaticvoidmain(String[] args) {
- System.out.println(NumberOf(7));
- }
- publicstaticintNumberOf(intn) {
- intcount= 0;
- while(n> 0){
- if(n!= 0)
- n= n&(n-1);
- count++;
- }
- returncount;
- }
- }
- 测试用例:
- 正数(包括边界值1、0x7FFFFFFF)。
- 负数(包括边界值0x80000000、0xFFFFFFFF)。
- 本题考点:
- 考查对二进制及位运算的理解。
- 考查分析、调试代码的能力。如果应聘者在面试过程中采用的是第
- 一种思路,当面试官提示他输入负数将会出现问题时,面试官会期待他能在心中运行代码,自己找出运行出现死循环的原因。这要求应聘者有一定的调试功底。
- 相关题目:
- 用一条语句判断一“个整数是不是2的整数次方。一个整数如果是2的整数次方,那么它的二进制表示中有且只有一位是1,而其他所有位都是0。根据前面的分析,把这个整数减去1之后再和它自己做与运算,这个整数中唯一的1就会变成0。
- 输入两个整数m和n,计算需要改变m的二进制表示中的多少位才能得到n。比如10的二进制表示为1010, 13 的二进制表示为1101,需要改变1010中的3位才能得到1101。我们可以分为两步解决这个问题:第一步求这两个 数的异或,第二步统计异或结果中1的位数。
- 举一反三:
- 把一个整数减去1之后再和原来的整数做位与运算,得到的结果相当于是把整数的二进制表示中的最右边一个1变成0.很多二进制的问题都可以用这个思路解决。
2.5 本章小结
本章着重介绍应聘者在面试之前应该认真准备的基础知识。为了应对编程面试,应聘者需要从编程语言、数据结构和算法3方面做好准备。
面试官通常采用概念题、代码分析题及编程题这3种常见题型来考查应聘者对某一编程语言 的掌握程度。本章的2.2节讨论了C++/C#语言这3种题型的常见面试题。
数据结构题目一直是面试官考查的重点。数组和字符串是两种最基本的数据结构。链表应该是面试题中使用频率最高的一种数据结构。如果面试官想加大面试的难度,他很有可能会选用与树(尤其是二叉树)相关的第2章面试需要的基础知识
面试题。由于栈与递归调用密切相关,队列在图(包括树)的宽度优先遍历中需要用到,因此应聘者也需要掌握这两种数据结构。
算法是面试官喜欢考查的另外一个重点。查找(特别是二分查找)和排序(特别是快速排序和归并排序)是面试中最经常考查的算法,应聘者一定要熟练掌握。另外,应聘者还要掌握分析时间复杂度的方法,理解即使是同一思路,基于循环和递归的不同实现它们的时间复杂度可能大不相同。
位运算是针对二进制数字的运算规律。只要应聘者熟练掌握了二进制的与、或、异或运算及左移、右移操作,就能解决与位运算相关的面试题。