剑指offer读书笔记

一、面试的流程

1.浅谈面试的重点

(1)对于初级程序员,更偏重算法和数据结构,看应聘者的基本功;对于高级程序员,更多关注专业技能和项目经验。
(2)事先需要对公司近况有所了解,对所聘岗位有热情,准备一些合适的问题最后问面试官。
(3)技术岗位就是要踏实写代码,但是不要急于写代码,先了解清楚所需要解决的问题,开始做一些整体规划和设计,编写高质量和高可读性的代码,写完代码以后不要马上提交,自己写一些测试用例测试几遍,找出可能出现的错误。

2.面试的形式

(1)电话面试:尽可能用形象化的语言把细节说清楚,没听清楚问题,一定要再次向面试官确认。
(2)共享桌面面试:一定要思考清楚以后再开始写代码,先想清楚思路,比如算法的时间、空间复杂度,有哪些特殊情况要处理等。注意代码命名和缩进,一目了然。最好先写测试用例再写解决问题的函数,写完以后可以对函数进行全面的单元测试。如果代码有bug,能设置断点,单步跟踪、查看内存、分析调用栈,很快发现并解决问题,当然是最好的。
(3)现场面试:规划好路线,准备好衣服,注意面试邀请流程,面试官会通过应聘者的语言和行动考察沟通能力、学习能力、编程能力等综合实力。

3.面试的环节

(1)行为面试
面试官在此环节注意应聘者的性格特点,深入了解简历中的项目经历。一般会让你做个简单的自我介绍,介绍学习、工作经历、项目经验等。项目经验方面最好用STAR模型描述项目,即:
Situation:简短的项目背景,如项目规模、软件功能和目标用户等。
Task:自己完成的任务。注意区分参与和负责,你负责的话,大概率会被问到核心算法和整体框架设计等问题。
Action:为了完成工作,做了哪些工作,以及如何做的,如:基于什么工具在哪个平台下应用了哪些技术。
Result:自己的贡献,如按时完成了多少功能,修改了多少bug,碰到的最大的问题,如何解决的,在项目中学到了什么,如何解决与其他人的冲突等。关于跳槽,绝对不要抱怨,可以说想要找一份更有挑战的工作。

(2)技术面试
面试官关注的5种素质:
1.基础知识扎实全面,包括编程语言、数据结构、算法等。
比如:C++基础知识,链表、树、栈、队列、哈希表等数据结构,二分查找、归并排序、快速排序、动态规划、贪心的算法。

2.能写出正确的、完整的、鲁棒的高质量代码。
比如:边界条件,特殊输入、错误处理等细节。写完代码以后,最好自己用测试用例检查一下。

3.能思路清晰地分析、解决复杂问题。
遇到复杂的问题可以通过画图(形象化)、举具体例子分析(具体化)和分解复杂问题(简单化)等方法厘清思路。

4.能从时间、空间复杂度两方面优化算法效率。
尤其需要注意排序、查找方面题型的优化。

5.具备优秀的沟通、学习、发散思维能力。

(3)应聘者提问
不要问和职位无关的问题。
不要问薪水,和HR谈。
不要立即打听面试结果。

二、面试的基础知识

1.基础知识

  1. C++的基础知识,如面向对象的特性、构造函数、析构函数、动态绑定等。
  2. 设计模式、UML图。
  3. 内存管理。
  4. 数据结构和算法、编程能力、部分数学知识,如概率、问题的分析和推理能力。
  5. OS的文件操作、程序性能、多线程、程序安全等。
  6. 并发控制、算法复杂度、字符串处理。

2.数据结构

数据结构围绕着数组、字符串、链表、树、栈和队列。

  1. 数组和字符串用于连续内存分别存储数字和字符。
  2. 链表和树需要操作大量的指针,一定要留意代码的鲁棒性。
  3. 栈是一个与递归密切相关的数据结构。
  4. 队列与广度优先遍历算法相关。

数组
面试题3:数组中重复的数字

面试题4:二维数组中的查找

字符串
面试题5:替换空格
总结:在合并两个数组(包括字符串)时,如果从前往后复制每个数字(或字符)则需要重复移动数字(或字符)多次,那么可以考虑从后往前复制,减少移动次数,提高效率。

链表
面试题6:从尾到头打印链表
总结:递归本质上就是一个栈结构。


面试7:重建二叉树
面试8:二叉树的下一个节点
总结:常考前序、中序、后序遍历、宽度优先遍历(层序遍历);堆和红黑树。

栈和队列
面试9:用两个栈实现队列

3.算法和数据操作

递归和循环
通常基于递归的实现方法代码会比较简洁,但性能不如基于循环的实现方法。
面试题10:斐波那契数列
总结:
1.用递归,代码简单,但递归过程中会进行大量的重复计算,时间效率是以n的指数倍增长的。
2.用循环,将数列中间项保存起来,时间复杂度为O(n)。
3.斐波那契数列的应用:青蛙跳台阶问题、小矩形覆盖成大矩形问题。

查找和排序
重点掌握二分查找、归并排序和快速排序。
1.查找:顺序查找、二分查找、哈希查找、二叉排序树查找。
哈希查找和二叉排序树查找重点考察对应的数据结构。
哈希表时间效率最高O(1),但需要额外的空间来实现。
二叉排序查找是利用二叉搜索树这种数据结构来进行查找。

2.排序:比较插入排序、冒泡排序、归并排序、快速排序。
快速排序总体的平均效率最好,但是在数组本身有序时,效率是O(n²)。
在选择排序算法时,一定要先搞清楚排序应用的环境,约束条件、辅助内存等信息。

面试题11.旋转数组的最小数字
测试用例:
功能测试:输入的数组是升序排序数组的一个旋转,数组中有重复数字或者没有重复数字。
边界值测试:只包含一个数字。
特殊输入:输入为空。

回溯法
通常在二维数组(迷宫或棋盘)上搜索路径,回溯法会尝试用递归来实现代码,如果要求不能用递归,再考虑用栈来模拟递归的过程。
回溯法适合由多个步骤组成的问题,并且每个步骤都有多个选项,当我们在某一步选择了其中一个选项时,就进入下一步,然后面临新的选项,这样重复选择,直至最终状态。
面试题12:矩阵中的路径
面试题13:机器人的运动范围
总结:
通常物体或者人在二维方格运动类问题都可以用回溯法解决。

动态规划与贪婪算法
1.应用动态规划求解的问题的特点:
(1)求一个问题的最优解(最大值或最小值)。
(2)整体问题的最优解是依赖各个子问题的最优解。
(3)将大问题分解成若干个小问题,这些小问题之间还有互相重叠的更小的子问题。
(4)从上往下分析问题,从下往上求解问题。从解决最小的问题开始,并把已解决的子问题的最优解存储下来,并把子问题的最优解组合起来逐步解决大问题。

2.贪婪算法是在每一步做出一个贪婪的选择,基于这个选择,能够确定得到最优解。应用贪婪算法时需要用数学方式来证明贪婪选择的正确性。

面试题14:剪绳子
总结:
动态规划的关键是从上到下分析问题、从下到上解决问题的能力;
贪婪算法则需要扎实的数学基本功。

位运算
总共有与、或、异或、左移、右移五种位运算。
面试题15:二进制中1的个数
总结:
1.把一个整数减去1以后,再和原来的数做与运算,得到的结果相当于把整数的二进制表示中最右边的1变成0,很多二进制的问题都是基于这个思路。
2.移位运算的效率要比除法运算效率高得多。

三、高质量代码

1.代码的规范性

1.代码要书写清晰,尤其是手写代码时。
2.代码布局清晰,展现清晰的逻辑,注意缩进。
3.命名规范合理,常用驼峰式命名法。如:int myStudentCount;第一个单词是全部小写,后面的单词首字母大写,常用于函数名。如:public class DataBaseUser;相比小驼峰法,大驼峰法(即帕斯卡命名法)把第一个单词的首字母也大写了,常用于类名,属性,命名空间等。

2.代码的完整性

1.面试官主要通过检查代码是否完成了基本功能、输入边界值是否能得到正确的输出、是否对各种不合规范的非法输入做出了合理的错误处理等方面,来考察应聘者考虑问题是否周全、思维是否全面。
2.通常从功能测试、边界测试和负面测试三方面来设计测试用例。面试时写出来的代码需要将未来需求可能的变化考虑进去,尽量展现自己对可扩展性和可维护性的理解。
3.三种错误处理的方法
(1)函数用返回值来告知调用者是否出错。和系统API一致,但不能方便地使用计算结果。
(2)当错误发生时设置一个全局变量,在返回值中传递计算结果。能够方便地使用计算结果,但是用户可能会忘记检查全局变量。
(3)当函数运行出错时,抛出异常。可以为不同的出错原因定义不同的异常类型,逻辑清晰明了。但有些语言不支持异常,抛出异常时对性能会有影响。

面试题16:数值的整次方
主要考察应聘者思维的全面性,以及快速做乘方的能力。

面试题17:打印从1到最大的n位数
主要考察应聘者解决大数问题的能力,如果面试题是关于n位的整数并且没有限定n的取值范围,或者输入任意大小的整数,那么很有可能考察的是大数问题。字符串和数组常用于表示大数。

面试题18:删除链表的节点
考察应聘者创新思维,当想删除一个节点时,并一定删除这个节点本身,可以先把下一个节点的内容复制出来覆盖被删除节点的内容,然后把下一个节点删除。除此以外,还要全面考虑删除节点位于链尾或者只有一个节点的特殊情况。

面试题19:正则表达式匹配
全面考虑普通字符、·和*的排列组合匹配模式,尽量考虑所有可能的测试用例。

面试题20:表示数值的字符串
表示数值的字符串遵循模式A[.[B]][e|EC]或者.B[e|EC],其中A为数值的整数部分,B为小数部分,e或E表示指数,C表示指数部分,A和C是可能以+或-开头的0~9数位串,B是不能带正负号的数位串。开始扫描A部分,遇到小数点时开始扫描B部分,当遇到E/e时,开始扫描C部分。

面试题21:调整数组顺序使奇数位位于偶数位前面
进一步考虑可扩展的方法,将判断的标准变成函数指针,即用一个单独的函数来判断数字是不是符合标准,这样就将整个函数解耦成两部分:一是判断数字应该在数组的前半部分还是后半部分,二是拆分数组的操作。

3.代码的鲁棒性

鲁棒性(Robust)是指程序能够判断输入是否合乎规范要求,并对不符合要求的输入予以合理的处理。容错性是鲁棒性的一个重要体现。
提高代码的鲁棒性的有效途径是进行预防性编程,是指预见在什么地方可能会出现问题,并为这些可能出现的问题制定处理方式。
最简单实用的防御性编程是在函数入口添加代码以验证用户输入是否符合要求。

面试题22:链表中倒数第k个节点
鲁棒性的三个考点:1.输入指针为空;2.节点数小于k;3.输入的参数k为0。

面试题23:链表中环的入口节点
可以将问题分解成三个步骤:1.找出环中任意一个节点;2.得到环中节点的数目;3.找到环的入口节点。

面试题24:反转链表
测试用例:1.输入链表为空;2.输入链表只有一个节点;3.输入的链表有多个节点。

面试题25:合并两个排序的链表
递归函数,首先确定递归结束条件,并判断特殊情况,从而解决鲁棒性问题。

面试题26:树的子结构
注意:1.与二叉树相关的代码有大量的指针操作,在每次使用指针的时候,都要问自己这个指针有没有可能是nullptr,如果是空该怎么处理。2.由于计算机表示小数(float或double)都有误差,所以不能直接用==来判断两个小数是否相等,如果两个小数的差的绝对值很小,如0.0000001,就可以认为它们相等。

四、解决题目的思路

编码前讲清自己的思路是一个考查指标,解释清楚问题本身和问题解决方案同样重要,应聘者应该明白自己究竟要做什么以及该怎么做。

1.画图让抽象问题形象化

图像能使抽象的问题具体化、形象化,比如二叉树、二维数组、链表等问题都通过画图找到规律,从而找到问题的解决方案。

面试题27:二叉树的镜像
画图以后发现,实质上是利用树的遍历算法。先前序遍历这棵树的每个节点,如果遍历到的节点有子节点,就交换它的两个子节点。当交换完所有非叶子节点的左右子节点以后,就得到了树的镜像。

面试题28:对称的二叉树
通过比较前序遍历序列和对称前序遍历序列来判断两棵二叉树是否对称。

面试题29:顺时针打印矩阵

2.举例让抽象问题具体化

面试题30:包含min函数的栈
方法:借助辅助栈存储最小的元素。
测试用例:1.新压入栈的数字比之前的最小值大;2.新压入栈的数字比之前的最小值小;3.弹出栈的数字不是最小元素;4.弹出栈的数字是最小元素。

面试题31:栈的压入、弹出序列
判断一个序列是不是栈的弹出序列的规律:如果下一个弹出的数字刚好是栈顶数字,那么直接弹出;如果下一个弹出的数字不在栈顶,则把压栈序列中还没有入栈的数字压入辅助栈,直到把下一个需要弹出的数字压入栈顶为止;如果所有数字都压入栈后,仍然没有找到下一个弹出的数字,那么该序列不可能是一个弹出序列。

面试题32:从上到下打印二叉树
总结:不管是广度优先遍历有向图还是二叉树,都要用到队列。首先,把起始节点放入到队列,然后每次从队列的头部取出一个节点,遍历这个节点之后把它能到达的节点都依次放入队列。重复遍历过程,直到队列中的节点全部被遍历为止。

面试题33:二叉搜索树的后序遍历序列
如果面试题要求处理一颗二叉树的遍历序列,则可以先找到二叉树的根节点,再基于根节点把整棵树的遍历序列拆分成左子树对应的子序列和右子树对应的子序列,接下来再递归地处理这两个子序列。

面试题34:二叉树中和为某一值的路径
用vector实现一个栈来保存路径,每次用push_back在路径末尾添加节点,用pop_back在路径的末尾删除节点,如果用stack没办法打印整个路径。

3.分解让复杂问题简单化

核心算法思想是分治法,即分而治之,逐个击破。

面试题35:复杂链表的复制
思路:首先,根据原始链表的每个节点N创建对应的N’,再将N’链接到N的后面,然后,如果原始链表上的节点N的m_pSibling指向S,则它对应的复制节点N’的m_Sibling指向S的复制节点S’。

面试题36:二叉搜索树与双向链表
思路:将树分为三部分:根节点、左子树、右子树,然后把左子树中最大的节点、根节点、右子树中最小的节点链接起来。至于如何把左子树和右子树内部的节点链接成链表,那和原来的问题本质一样,可以用递归解决。

面试题37:序列化二叉树
思路:把二叉树分解成3部分:根节点、左子树、右子树,在处理(序列化和反序列化)它的根节点之后再分别处理它的左、右子树,这是典型的将问题递归分解逐个解决的过程。

面试题38:字符串的排列
思路:求整个字符串的排列,分为两步:1.求所有可能出现在第一个位置的字符,即把第一个字符和后面所有的字符交换。2.固定第一个字符,求后面所有字符的排列。

五、优化时间和空间效率

1.时间效率

1.C/C++程序员要养成采用引用(或指针)传递复杂类型参数的习惯,如果采用值传递的方式,则从形参到实参会产生一次多余的复制操作,尽量避免。
2.递归实现算法的代码很简洁,但是时间效率很差,可以用递归的思路分析问题,用数组来保存中间结果基于循环来实现。
3.常见的查找和排序算法的时间效率要牢记于心。

面试题39:数组中出现次数超过一半的数字
思路:遍历数组的同时保存两个值:一个是数组中的一个数字,另一个是次数。如果当前数字等于保存的数字,次数加一,否则,次数减一。如果次数为零,那么保存的数字更新为当前数字,并将次数置为1。由于要找的数字出现的次数比其他所有数字出现的次数之和还要多,所以结果肯定是最后一次把次数设为1时对应的数字。除此以外,要额外判断输入的数组是否满足条件,以及输入参数为空的特殊条件。

面试题40:最小的K个数
注:partition() 函数定义于algorithm头文件中,partition() 函数只负责对指定区域内的数据进行分组,并不保证各组中元素的相对位置不发生改变。而如果想在分组的同时保证不改变各组中元素的相对位置,可以使用 stable_partition() 函数。
思路:构建一个有k个节点的大根堆二叉树,如果当前数字小于根节点,就将根节点删除,并添加当前数字到堆中,最后堆中的k个节点即为结果。堆采用红黑树来实现的容器,从而保证查找、删除和插入的操作都只需要O(logk)时间,STL中set和multiset都是基于红黑树实现的。总的时间复杂度为O(nlogk),适用于海量数据中找k个较小数的情景。

面试题41:数据流中的中位数
思路:用一个大根堆实现左边的数据容器,用一个小根堆来实现右边的数据容器。往堆里面插入一个数据时间是O(logn),得到中位数的时间为O(1)。首先要保证数据平均分配到两个堆中,其次要保证小根堆中的所有数据大于大根堆中的数据:将新数据插入到大根堆,接着把大根堆中的最大的数字拿出来插入到小根堆。

面试题42:连续子数组的最大和
思路:动态规划,f(i)表示以i个数字结尾的连续子数组的最大和,当f(i-1)<=0时,f(i)=pData[i],当f(i-1)>0时,f(i)=f(i-1)+pData[i]。

面试题43:整数中1出现的次数(从1到n整数中1出现的次数)
思路:找规律,每次去掉最高位进行递归,递归的次数和位数相同。时间复杂度O(logn)。

面试题44:数字序列中某一位的数字
找规律:
// 0
// 1 ~ 9 | digit = 1 start = 1 * 1 count = 1 * 9 * 1
// 10 ~ 99 | digit = 2 start = 1 * 10 count = 10 * 9 * 2
// 100 ~ 999 | digit = 3 start = 1 * 10 * 10 count = 100 * 9 * 3

面试题45:把数组排成最小的数
思路:1.这是一个隐形的大数问题,需要将数字转换成字符串,重新定义大小关系,由于m和n拼接起来的位数相同,所以只需要按照字符串大小的比较规则即可,时间复杂度O(logn)。2.比较规则有效的证明:自反性、对称性、传递性。再用反证法证明最后拼接起来的数字就是最小的。

面试题46:把数字翻译成字符串
思路:定义一个f(i)表示从第i位数字开始的不同翻译的数目,f(i)=f(i+1)+g(i,i+1)×f(i+2),当第i位和第i+1位两位数字拼接起来的数字在10~25的范围时,g(i,i+1)的值为1,否则为0。
递归从最大的问题开始自上而下解决问题,基于循环的方法从最小的问题开始自下而上解决问题,消除重复的子问题,即从数字的末尾开始,然后从右到左翻译并计算不同翻译的数目。

面试题47:礼物的最大价值
思路:典型动态规划问题,先用递归分析问题f(i,j)=max(f(i-1,j),f(i,j-1))+gift[i,j],再用一个辅助二维数组缓存中间结果。进阶优化的话,可以用一维辅助数组来缓存,暂时还没想明白。。。

面试题48:最长不含重复字符的子字符串
思路:用动态规划解决。

2.时间效率与空间效率的平衡

面试题49:丑数
思路:创建一个数组,里面的数字是排好序的丑数,每个丑数都是前面的丑数乘以2、3、5得到的。每次丑数的生成:先记录已有最大丑数M,然后找到第一个乘以2/3/5后大于M的结果M2\M3\M5,新生成的丑数就是M2\M3\M5中最小的那个。

面试题50:第一个只出现一次的字符
思路:定义一个哈希表,键值key是字符,值value是该字符出现的次数,从头开始扫描字符串两次,第一次扫描在哈希表中把次数加一,第二次扫描查看次数,第一个次数为1的字符。时间复杂度为O(n)。

面试题51:数组中的逆序对
思路:统计逆序对的过程:先把数组分隔成子数组,统计出子数组内部的逆序对的数组,然后再统计出两个相邻子数组之间的逆序对的数目。在统计逆序对的过程,还需要对数组进行排序。这个排序的过程是归并排序,时间复杂度:O(nlogn),空间复杂度:O(n)。

面试题52:两个链表的第一个公共结点
思路:先遍历两个链表得到它们的长度,就知道哪个链表比较长,以及长的链表比短链表多几个节点,在第二次遍历时,在长链表上先走若干步,接着同时在两个链表上遍历,找到的第一个相同的节点就是第一个公共节点。时间复杂度O(m+n)。

六、面试中的各项能力

通过介绍自己和项目经历时,考察沟通和表达能力;学习能力通过问看书和关注什么来考察。

1.知识迁移能力

面试题53:在排序数组中查找数字
思路:用二分查找算法找到第一个k和最后一个k,即可得到k在数组中出现的次数。时间复杂度是O(logn)。

面试题54:二叉搜索树的第k个节点
思路:中序遍历遍历一颗二叉搜索树,遍历序列的数值是递增的。

面试题55:二叉树的深度
思路:递归:depth=max(nLeft,nRight)+1.

面试题56:数组中数字出现的次数
位运算,异或的性质:任何数字异或它自己都等于0。
思路:将所有数字相加,找到最右边是1的bit位,用该位是否为1来将数组一分为二,再将两个数组分别异或的结果即为所求。

面试题57:和为S的两个数字
思路:双指针,数组有序,所以当left+right<s时,left向右移动,left+right>s时,right向左移动。

面试题58:翻转字符串
思路:第一步翻转句子中所有的字符,第二步翻转每个单词中字符的顺序。

面试题59:队列的最大值
思路:使用一个双端开口的队列deque,用来保存有可能是滑动窗口最大值的数字的下标。在存入一个数字的下标之前,首先判断队列里已有数字是否小于待存入的数字。如果已有的数字小于待存入的数字,那么这个数字已经不可能是滑动窗口的最大值了,因此将它们从队列的尾部删除,同时,如果队列头部的数字已经从窗口滑出,那么滑出的数字也需要从队列的头部删除。

2.抽象建模能力

将现实问题抽象成数学模型,即建模的步骤:1.选择合理的数据结构来表述问题;2.分析模型中的内在规律,并用编程语言表述这种规律。

面试题60:n个骰子的点数
动态规划:用两个数组来存储骰子的点数的每个总数出现的次数。在第一轮循环中,第一个数组中的第n个数字表示骰子和为n出现的次数。在下一轮循环中,再加上一个新的骰子,此时和为n的骰子出现的次数应该等于上一轮循环中骰子点数和为n-1、n-2、n-3、n-4、n-5与n-6的次数的总和,所以把另一个数组的第n个数字设为前一个数组对应的第n-1、n-2、n-3、n-4、n-5与n-6个数字之和。

面试题61:扑克牌顺子
思路:首先把数组排序;其次统计数组中0的个数;最后统计排序之后的数组中相邻数字之间的空缺的总数。如果空缺的总数小于或等于0的个数,就连续,反之不连续。其中,如果数组中非0数字重复,则一定不连续。

面试题62:圆圈中最后剩下的数
思路:经典约瑟夫环问题,解题方法:1.用环形链表模拟圆圈的经典解法,时间复杂度O(mn),空间O(n);2.分析数学规律直接计算出圆圈中最后剩下的数字,时间复杂度O(n),空间复杂度O(1)。

面试题63:股票的最大利润
思路:存储前i-1个数字中的最小值,然后算出当前价位卖出时可能得到的最大利润,时间复杂度O(n)。

3.发散思维能力

发散思维的特点是思维活动的多向性和变通性,即运用多思路、多方案、多途径来解决问题。除此以外,还会考察应聘者知识面的广度和深度。

面试题64:求1+2+3+…+n
方法:1.围绕循环,利用构造函数求解;2.围绕递归,利用虚函数求解;3.利用函数指针;4.利用模板类型求解。

面试题65:不用加减乘除做加法
思路:二进制的位运算。

面试题66:构建乘积数组

总注:书中涉及的程序源码可到http://www.broadview.com.cn/31092进行下载。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沉浮一湘蕉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值