《剑指offer学习笔记-第二章:面试需要的基础知识》

面试题对应的答案在我的文章https://mp.csdn.net/mdeditor/87880918#,答案都是自己编写并通过编译的,可能不是最优的解法,后面慢慢优化。

2.2 编程语言
面试过程中面试官要么直接问语言语法,要么让应聘者编写代码解决一个问题,以此判断对语言掌握程度。
(1) C++
通常语言面试有三种类型:
a. 直接问对C++概念的理解,如对关键字的理解。
b. 面试官拿出准备好的代码,让你分析代码运行结果。
c. 要求写一个类型或者实现类型中的一个成员函数。
(2) C#

2.3 数据结构
大多数面试题围绕数组、字符串、链表、树、栈、队列这些常见数据结构展开。

(1) 数组
数组占据一块连续的内存并按照顺序储存数据,由于它的内存连续,可以在O(1)时间读写任何元素,可以用来实现简单的哈希表。它的缺点是空间效率不高,常有空闲区域,vector解决了这个问题。
数组和指针是两个相关联又有区别的两个概念,数组名也是一个指向数组首元素的指针,可以通过指针访问数组。
在32位系统对任意指针求sizeof,结果都是4。当数组做函数参数传递时,数组退化为同类型的指针。

**面试题3:**在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。
例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。

思路:
加粗样式a.先把数组排序(排序长度为n的数组需要O(nlogn)的时间),再遍历一遍数组就可找出是否有重复数字。
b.从头到尾扫描数组,同时将数字存入哈希表,当存入数字时可以判断该哈希表是否已包含了该数字,时间复杂度为O(n),但需要一个大小为O(n)的哈希表,当数组中出现较大的数字时,会浪费很多空间。
c. 从头到尾扫描数组,当扫到下表为i的数字m,首先比较该数字m是否等于i,如果是则扫描下一个数字
增加题目要求:不能修改数组找出重复的数字。
思路:
a.创建一个辅助数组,再根据原数组元素值移到对应下标处,同时判断对应下标是否已经有值,该方案需要O(n)的辅助空间。

b.对数组中的数进行二分查找,比如数组长度为8,就查数组中1-4的个数,超过4个说明有重复数字,否则重复数字在5-8中,然后继续二分查找剩余的数,直到查到最后两个数中的一个。

**面试题4:**在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

思路:从数组右上角开始,当前数字和查找数相等则返回true并退出,大于查找数列号减一,小于查找数行号加1,超出边界值返回false并退出。
在这里插入图片描述
图来自(https://blog.csdn.net/qq_38277033/article/details/81127816)

(2) 字符串
字符串是由若干字符组成的序列,C/C++中每个字符串都以字符‘\0’结尾,这样就能方便找到尾部,因此每个字符串都会多一个字符,需要注意字符串越界,如:

char str[3];
strcopy(str,”abc”);//实际字符串“abc”大小为4

为了节省内存,C/C++把常量字符串放到单独的一个内存区域,当指针赋值给相同的常量字符串时,它们会指向同一内存地址,但用常量内存初始化数组却不同,数组会新分配空间然后将字符串内容复制过去,所以地址会不同。

面试题5:请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are
Happy.则经过替换之后的字符串为We%20Are%20Happy。

思路:首先应该考虑到替换空格之后整个字符串会变长,然后考虑题目对时间复杂度和空间复杂度的要求。先遍历一次字符串,统计出字符串中空格数,推算出替换后字符串总长度。从字符串的后面开始复制和替换,准备两个指针,P1指向原始字符串末尾,P2指向替换后的字符串的头部,然后将原始字符串从尾部开始往后移动,遇到空格则替换,直到P1=P2,此时空格已替换完。

(3) 链表
链表是面试被提及最频繁的数据结构,它是一种动态数据结构,在创建链表时无需知道链表长度。当插入节点时,只需要为新节点分配内存,然后调整指针确保新节点被链接到链表中,因为没有闲置内存,它的空间效率比数组高。但由于链表的内存不连续,只能遍历整个链表来查找其中的元素,它的时间效率为O(n)。
注意下面在末尾添加节点的代码,pHead是指向指针的指针,因为需要改动头指针的值。

面试题6:输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。

在这里插入图片描述
(4) 树
当面试官想考察应聘者在有复杂指针操作下写代码的能力,他往往想到用树相关的题目。通常考察二叉树,它有前序、中序、后序三种遍历方式,每种遍历方式又有递归和循环两种实现方法,一般递归代码更简洁。
宽度优先便利:从上往下,从左往右依次遍历二叉树。
二叉树特例:
a.二叉搜索树:左子节点小于等于根节点,右子节点大于等于根节点,平均查找时间O(logn).
b.堆:分为最大堆和最小堆,最大堆中根节点值最大,最小堆中根节点值最小,它可以用来快速找最大或最小值。
c.红黑树:把树中节点定义为红黑两种颜色,并确保从根节点到叶节点的最长路径长度不超过最短路径的两倍,STL中set\map都是基于红黑树实现。

面试题7.
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

面试题8.
给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。

(5) 栈和队列

栈的特点是先进后出,通常栈不考虑排序,需要O(n)时间才能找到其中的最值元素,如果想要在O(1)时间找到其中最大或最小元素,请见 30题。
队列特点是先进先出,树的宽度优先算法就用到了队列,详细见32题。

面试题9. 用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。

2.4 算法和数据操作
很多算法可以用递归和循环两种不同方式实现,通常递归代码更简洁,循环代码性能更好,面试时根据面试官要求选择合适方法。
排序和查找是面试算法的重点,重点掌握二分查找、归并排序和快速排序,能随时正确完整地写出它们的代码。
若面试要求在二维数组搜索路径可以尝试回溯法,通常回溯适合用递归实现,若面试官禁用,可用栈模拟。
如果面试官求某个问题最优解,并且该问题可分为多个子问题,可尝试用动态规划。在用自上而下的递归思路分析动态规划问题时,我们发现子问题之间存在重叠的更小的子问题,为避免重复计算,应采用自下而上的循环代码实现,即把子问题最优解先算出来用数组保存,接下来基于子问题的解计算最大的解。
如果告诉面试官动态规划的思路后,面试官还在提醒分解子问题是不是存在某个特殊选择,如果采用特殊选择一定能得到最优解,那面试官通常在暗示使用贪婪算法。
位运算可看成一类特殊算法,它是把数字表示成二进制后对01操作,需要掌握与、或、异或、左移、右移五种位运算。

2.4.1 递归和循环
若需要重复计算多次相同的问题,通常可以考虑选择用递归或者循环两种方法,递归是在函数内部调用自身,循环是达到终止条件前一直在某范围重复计算。如果面试官没要求可尽量用递归实现。
在这里插入图片描述

面试题10. 大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0),n<=39。

F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)
如果使用下面的递归方法时间复杂度太高解法一:
int Fibonacci(int n) {
if(n<1)return n;
else
return Fibonacci(n-1)+Fibonacci(n-2);
}
因为其中包含了大量的重复计算,所以应该从下往上计算如解法二:
class Solution {
public:
int Fibonacci(int n) {
int f1=1,f0=0,f2=0;
int i;
if(n<2)
return n;
for(i=2;i<=n;i++)
{
f2=f1+f0;
f0=f1;
f1=f2;

    }
    return f2;
}

};
解法三:此外还有一种时间复杂度为O(logn)的解法,但是是一个很生僻的数组公式:

再利用乘方的如下性质,用递归实现:

在这里插入图片描述
方法一直观但效率太低,方法二用循环实现提高了效率,方法三比较有创意但较复杂,综上方法二较适合面试。

面试题10.1:一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果) 把n级台阶时的跳法看成n的函数,记为f(n),当n>2时,第一次跳的时候就有两种不同的选择:1是第一次跳1级,此时后面n-1阶跳法数目为f(n-1);二是第一次跳2级,此时后面n-2阶跳法为f(n-2),因此n级台阶不同跳法为f(n)=
f(n-1)+ f(n-2),这就不难看出是斐波拉契数列了。

面试题10.2 一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。 推导一下就可以看出规律了f(n)=2^(n-1); 这里第一次跳n阶,则只有一种跳法记为f(0); 第一次跳n-1阶也有f(1)种跳法;
第一次跳n-2阶也有f(2)种跳法; … 第一次跳1阶也只有一种跳法记为f(n-1); 所以总共有f(n)= f(0)+ f(1)+
f(2)+… f(n-1)种跳法 而f(0)=f(1)=1; f(2)=f(1)+f(0)=2 f(3)=
f(2)+f(1)+f(0)=f(2)+f(2)=2f(2) f(4)=
f(3)+f(2)+f(1)+f(0)=f(2)+f(2)=2
f(3) …
f(n)=2*f(n-1)=22*f(n-2)=…=2(n-2)*f(2)=2^(n-1);

面试题10.3 我们可以用21的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2n的大矩形,总共有多少种方法?
同样也是斐波拉契问题,当用第一个2
1矩形覆盖时,有两种选择:横着放或竖着放,横着放右边还有26的区域,覆盖方法记为f(6),竖着放还有27区域,覆盖方法记为f(7),因此f(8)=f(7)+f(6)。

2.4.2 查找和排序
查找和排序是为常用算法,查找一般有顺序查找、二分查找、哈希表查找、二叉排序树查找,面试时一定要能手写出二分查找的循环和递归代码。如果要求在排序的数组查找一个数字或某个数字出现次数,都可尝试二分查找。
哈希表和二叉排序树考察重点在于数据结构,哈希表优点为在O(1)时间查找某一元素,缺点是需要额外空间实现哈希表;二叉查找树查找算法对应数据结构为二叉搜索树。
排序相对更复杂,面试官经常要求比较冒泡排序、归并排序、快速排序等算法的优劣,一般从额外空间消耗、平均时间复杂度、最差时间复杂等方面比较优缺点,而且面试官常要求写出快排的代码。

2.4.3 回溯法
回溯法可看成蛮力法升级版,他从解决问题每一部所有可能中选出一个可行方案,非常适合由多个步骤组成的问题,且每个步骤由多个选项,当我们在某一步选择了其中一个选项,进入下一步又面临新选择,一直重复直达最终状态。
用回溯法解决的问题的所有选项可用树状结构表示,解决问题的步骤可看为树的一个节点,每个选项可看为节点间的连接线,树的叶节点代表终结状态,如果叶子结点满足题目要求则是一个可行的解决方案。通常回溯法适合用递归实现。

面试题12:请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则之后不能再次进入这个格子。
例如 a b c e s f c s a d e e 这样的3 X 4
矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。

思路:这个题其实理解了就觉得不难,只是写代码可能会遇到一些问题。程序我就参考的剑指offer的例程了,程序大概流程为
1.检查边界值和初始化
2.用两层for循环从二维矩阵中每个元素开始尝试路径
3.进入一个格子,判断当前格子是否满足路径条件,如果满足则做好标记并递归判断其周围格子是否满足路径条件;否则返回上一层。
4.结束条件是当进入某一个格子后发现路径已经到str的末尾’\0’。

面试题13:地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。
例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8
= 19。请问该机器人能够达到多少个格子?

思路:思路和矩阵中找字符串相似,也是采用递归遍历的方法,其实还比上一题简单一些:

  1. 判断边界值
  2. 进入一个方格判断该方格是否满足条件
    (1) 若满足条件,做好标记,并递归遍历周围的格子,返回可进入的格子数。
    (2) 不满足则返回0,退回上一层。
    (3) 结束条件是当周围没有可进入的格子(已标记的格子也不能再进入)。
    2.4.4动态规划与贪婪算法
    动态规划问题三个特点:
    (1) 求一个问题的最优解(通常是求最值)
    (2) 该问题能分成若干个问题的子问题
    (3) 并且子问题之间还有重叠的更小的子问题
    (4) 从上往下分析问题,从下往上解决问题
    满足上面的条件可以考虑使用动态规划。求解动态规划问题时,总是从最小问题开始解决,并将已解决的子问题的最优解存储下来,并把子问题最优解组合逐步解决大的问题。
    贪婪算法每一步都可做一个贪婪选择,基于该选择可以得到最优解,比如剪绳子问题中剪出一段长度为3的绳子就是每一步做出的贪婪选择。

面试题14:给你一根长度为n的绳子,请把绳子剪成m段
(m和n都是整数,n>1并且m>1)每段绳子的长度记为k[0],k[1],…,k[m].请问k[0]k[1]…*k[m]可能的最大乘积是多少?
例如,当绳子的长度为8时,我们把它剪成长度分别为2,3,3的三段,此时得到的最大乘积是18.

很可惜牛客网上没有把这个题放上去,只能自己在vs上做了。

动态规划的解题思路:首先定义函数f(n)为把长度为n的绳子剪成若干段后乘积的最大值,在剪第一刀时,我们有n-1种选择,也就是说第一段绳子的可能长度分别为1,2,3…,n-1。因此f(n)=max(f(i)*f(n-i)),其中0<i<n。这是一个自上而下的递归公式。由于递归会有大量的不必要的重复计算。一个更好的办法是按照从下而上的顺序计算,也就是说我们先得到f(2),f(3),再得到f(4),f(5),直到得到f(n)。当绳子的长度为2的时候,只能剪成长度为1的两段,所以f(2) = 1,当n = 3时,容易得出f(3) = 2;

动态规划解题步骤:(时间复杂度O(n*2),空间复杂度O(n))
(1) 判断长度小于4的情况,并返回对应值。
(2) 计算长度大于等于4时,子问题分解成小于4的长度的最优解,并保存下来。
(3) 用两层循环从下往上求解最优解f(n),第一层循环负责从4开始求最优解f(4),一直求到f(n),第二层循环负责求具体每一个最优解的值,通过对每种剪法一一比较就可以求出。

贪婪算法解题思路:当n>=5时,尽可能多剪长度为3的绳子,当剩下的绳子长度为4时,把绳子剪成两段长度为2的绳子.
解题步骤:(时间复杂度O(1),空间复杂度O(1))
(1) 判断长度小于4的情况,并返回对应值。
(2) 长度大于等于4时,计算能剪长度为3绳子的个数,还要考虑最后长度为4的情况,长度为则应剪为22>31.
(3) 计算长度为2的绳子数。
(4) 计算所有长度为2和3的绳子的乘积。

2.4.5 位运算
位运算是把数字用二进制表示后,对每一位上0或者1的运算。二进制的位运算有五种运算。
异或:两个元素相异为1,相同为0,0^0=0, 01=1,11=0;
左移:m<<n,将m左移n位,左边n位丢弃,右边n位补0;
右移:m>>n,将m右移n位,右边n位丢弃,如果是无符号值,用0将左边n位补齐,如果是有符号值,用符号位填补最左边n位。有符号数右移:10001010>>3=11110001

面试题15:输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。

这个题看似简单,但其实做题时你会发现,如果对二进制不是太熟悉,做起来还是不简单。
常规解法
把n和1做与运算,再判断该位是否为1,接着将1左移1位得到2,再判断该位是否为1,然后一直左移直到整数二进制的最左边,也就是,循环的次数为整数二进制的位数。
高级解法
把一个整数减去1,再和原来的整数相与,可将原整数最左边的1变为0,一个二进制整数中有多少个1,就能进行多少次这样的操作。

猜你喜欢

转载自blog.csdn.net/HuYingJie_1995/article/details/87953494