《剑指offer》,作者:何海涛,微软资深软件工程师。
寒假到了,趁着春节时间,准备看一下《剑指offer》,学习一下技术面试和笔记的技巧:
技术面试主要考察下面的几个技术问题:
1、基础知识,包括编程语言、数据结构、算法等;
2、能写出正确的、完整的、鲁棒的高质量代码;
3、能从时间、空间复杂度两方面优化算法效率。
面试题1:为类添加赋值运算符函数
如下所示的base类:
class base
{
public:
base(char *p_data=nullptr);
base(const base & str);
~base(void);
private:
char *p_data;
};
赋值运算符函数:
base & base::operator==(const base & str)
{
if(this!=str)//判断不是自己给自己进行赋值
{
base strTemp(str);//首先生成一个实例
char* pTemp = strTemp.p_data;
strTemp.p_data = p_data;//生成实例数据指针指向原数据内存,在作用域外会被相应的析构函数释放掉
p_data = pTemp;
}
return *this;
}
1、首先要判断不是自己给自己赋值,如果是自己给自己赋值;
2、为了再赋值过程中不违背一场安全性原则(即不导致内存泄漏和原始数据丢失)。可以先创建一个临时实例,再交换临时实例和原始实例的值;
3、这种方法地巧妙之处在于,临时实例数据指针与该类数据指针交换指向之后,就代表着赋值成功,然后再跳出作用域之后,临时实例会调用析构函数释放掉相应地内存。
面试题3:找出数组中重复的数字
例如数组{2,3,1,0,2,5,3},其中重复的数字是1、3。
传统的做法就是先对数组及进行排序,然后找重复的数字。其中排序的时间复杂度为O(nlogn)。
为了提高时间复杂夫,更好的算法是,从头到尾依次扫描数组中的
面试题4:二维数组中的查找
把二位数组看作一个方形矩阵,每次将矩阵右上角的数据与查找数据进行对比,然后再确定下一步的策略。
例:在数组
{1,2,8,9,
2,4,9,12,
4,7,10,13,
6,8,11,15}中查找是否存在7,代码如下
bool find(int *matrix,int rows,int columns,int number)
{
bool found = false;
if (matrix != nullptr && rows > 0 && columns > 0)
{
int row = 0;
int column = columns-1;
while (row < rows && column >= 0)
{
if (matrix[row*columns + column] == number)
{
found = true;
break;
}
else if (matrix[row*columns + column] > number)
column--;
else
row++;
}
}
return found;
}
int main()
{
int a[] = { 1,2,8,9,
2,4,9,12,
4,7,10,13,
6,8,11,15 };
bool found_7;
found_7 = find(a, 4, 4, 7);
return 0;
}
字符串:
c/c++中的字符串都以字符’\0’结尾,这样就能够很方便的知道字符串的尾部。
面试题5:替换空格
(省略)
链表
面试题6:从尾到头打印链表
可见专题:
https://blog.csdn.net/weixin_43319685/article/details/103779284
树
面试题7:重建二叉树
面试题8:二叉树的下一个节点
面试题9:用两个栈实现队列
2.4.1 递归和循环
面试题10:斐波那契数列
1、使用递归的方法进行解题,会导致低效率,不实用;
2、使用循环的方法进行解题,时间复杂度为O(n);
2.4.2 查找和排序
查找:顺序查找、二分查找(最常考察,主要针对有序数列)、哈希表查找和二叉排序查找;
排序:插入排序、冒泡排序、归并排序、快速排序;
关于排序的文章:
1、https://blog.csdn.net/weixin_41190227/article/details/86600821
2、https://blog.csdn.net/a3192048/article/details/80269862
1、平均复杂度为O(n2)的排序算法:(一般不会进行考查)
2、平均复杂度为O(nlog(n))的排序算法:(是考查的重点)
3、平均复杂度为O(n)的排序算法:
总结:至少需要掌握的算法有:
二分查找法、快速排序法。
面试题11:旋转数组的最小数字
解题思路:使用二分查找法,时间效率更高。
2.4.3 回溯法
面试题12:矩阵中的路径
在二维数组中寻找到制定的路径。
面试题13:机器人的运动范围
与面试题12类似
2.4.4 动态规划与贪心算法(最优解的问题)
面试题14:剪绳子
问题描述:有一根长度为n的绳子,将其剪为m段,求每段乘积的最大值。
2.4.5 位运算
位运算主要包括与、或、异或、左移和右移等运算。
左移n位,左边的n个位去掉,右边添加n个0;
右移n位,右边的n个位去掉,如果是正数,左边添加n个0;如果是负数,左边添加n个1;
面试题15:二进制中1的个数
请实现一个函数,输入一个整数,输出该数的二进制表示中1的个数。
常规思路:使用移位方法进行计算,移位比除法的效率高很多。下面的而代码没有考虑到负数的情况。
int coutOne(int n)
{
int cout = 0;
while (n != 0)
{
if (n & 1)
{
cout++;
}
n = n >> 1;
}
return cout;
}
1、高质量的代码就是需要考虑到所有的情况,是代码具有完美的鲁棒性;
2、具有更低的时间复杂度可空间复杂度;
面试题16:数值的整数次方
考察的是是否能考虑到所有的输入情况,例如负数次方,0的负数次方等情况。
面试题17:打印从1到最大的n位数
考察的是大数情况,使用字符串进行打印可以有效的解决大数的情况。也可以用递归的思路解决。
void PrintMax(int n)
{
if (n <= 0)
return ;
char *number = new char[n + 1];
number[n] = '\0';
for (int i = 0; i < 10; i++)
{
number[0] = i + '0';
PrintTomax(number, n, 0);
}
delete[]number;
}
void PrintTomax(char *number, int length, int index)
{
if (index == length - 1)
{
printNumber(number,length);
return;
}
for (int i = 0; i < 10; i++)
{
number[index + 1] = i + '0';
PrintTomax(number, length, index + 1);
}
}
void printNumber(char *number,int length)
{
bool isBeginning0 = true;
for (int i = 0; i < length; i++)
{
if (isBeginning0&&number[i] != '0')
isBeginning0 = false;
if (!isBeginning0)
cout << number[i];
}
cout << endl;
}
面试题18:删除链表的节点
题目一:在O(1)时间内删除链表的节点
删除节点的方式有两种,一种是知道删除节点的前一个节点,将前一个节点指向下一个节点;另一种方式是将删除节点的下一个节点的数据复制到删除节点,然后将删除节点的指针指向下下个节点以达到删除的目标。
为了代码的高质量,还需要考虑节点是否为尾节点、或者为头节点的情况。
题目二:删除链表中重复的节点
面试题19:正则表达式匹配
根据题目意思,使用递归的思想进行解题。
面试题20:表示数值的字符串
有一个函数判断字符串是否表示数值。
面试题21:调整数组顺序使奇数位于偶数前面
防御性编程,对函数的参数进行判断是否合规。
面试题22:链表中倒数第K个节点
解题思路:使用两个指针相隔k-1的距离一次进行遍历即可。
需要注意:1、该链表是否有k那么长;2、k值是否大于1;3、是否为空链表。
面试题23:链表中环的入口节点
解题思路:1、如何判断是否有环;2、判断环的数目;3、找环的入口节点。
面试题24:反转链表
将单向链表进行反转
面试题25:而合并两个排序的链表
将两个递增排序的列表合并成一个递增排序的列表。
面试题26:树的子结构
输入两棵二叉树A和B,判断B是否为A的子结构。
一般而言,关于树的题目更加困难,因为树的结构比较复杂。
第四章解决面试题的思路
面试题27:二叉树的镜像
问题:设计一个函数,输出一个二叉树的镜像。
面试题28:对称的二叉树
问题:设计一个函数,判断输入的二叉树是否为对称的二叉树。
解题思路:可以并通过前序遍历和前序遍历的对称遍历两种方式进行遍历,即通过中、左、右和中、右、左两种方式进行遍历。
面试题29:顺时针打印矩阵
问题:输入一个矩阵,由外到里顺时针进行打印
解题思路:一圈一圈进行打印。
面试题30:包含min函数的栈
要弹出栈的最小值,做法是申请一个辅助栈,用来存储最小值。
面试题31:栈的压入、弹出序列
题目:输入两个序列,一个表示栈的压入序列,判断第二个栈是否为弹出序
bool isPopOrder(const int *p_push, const int *p_pop,int length)
{
bool isPopOrder = false;
if (p_push == nullptr || p_pop == nullptr || length < 0)
return isPopOrder;
const int *push_next = p_push;
const int *pop_next = p_pop;
stack<int> m_push;
for (int i = 0; i < length; i++)
{
m_push.push(*(push_next + i));
while (m_push.size()&&m_push.top() == *pop_next)
{
m_push.pop();
++pop_next;
}
}
if (m_push.size() == 0 && pop_next - p_pop == length)
isPopOrder = true;
return isPopOrder;
}
int main()
{
int a[] = {1,2,3,4,5};
int b[] = { 4,5,3,2,1 };
int c[] = { 4,3,5,1,2 };
int i = isPopOrder(a, b, 5);
int j = isPopOrder(a, c, 5);
return 0;
}
面试题32:从上到下打印二叉树
题目一:二叉树的层序遍历,使用一个辅助容器-队列。首先打印根节点,并将根节点的两个子节点放入队列中,读取队列尾的数据,并将队列尾的子节点入队列,如此重复即可。
题目二:分行从上到下打印二叉树。需要可以记录每一层数量的变量。
题目三:之字形分层打印二叉树
面试题33:二叉搜索树的后序遍历序列
题目:输入一个数组,判断是否为二叉搜索树的后序遍历序列。
首先了解什么是搜索树,然后了解什么是后序遍历。
主要解题思路就是找到根节点,然后分成左右两子树,再进行递归判断。
面试题34:二叉树中和为某一值的路径
问题:输入二叉树和某一值,在二叉树中找到和为和该值的路径。
面试题35:复杂链表的复制
面试题36:二叉搜索树和双向链表
了解什么是搜索树、什么是双向链表。将单个树进行双向链表转换然后进行递归。
面试题37:序列化二叉树
题目:实现两个函数,分别进行序列化二叉树和反序列化二叉树。特别是反序列化的实现。
面试题38:字符串的序列
题目:给出一个字符串,打印出该字符串的所有组合可能。
void PrintPermutation(char* str, char *strBegin)
{
if (*strBegin == '\0')
cout << str << endl;
else
{
for (char* pMove = strBegin; *pMove != '\0'; ++pMove)
{
char temp = *pMove;
*pMove = *strBegin;
*strBegin = temp;
PrintPermutation(str, strBegin + 1);
temp = *pMove;
*pMove = *strBegin;
*strBegin = temp;
}
}
}
void PrintPermutation(char * str)
{
if (str == nullptr)
return;
PrintPermutation(str, str);
}
int main()
{
char a[] = "abcd";
PrintPermutation(a);
return 0;
}
面试题39:数组中出现次数超过一般的数字
用到了排序算法的分治法。
面试题40:最小的K个数
解法一:使用类似快排的方法进行解题。
解法二:使用基于红黑树的数据结构进行解题。
面试题41:数据流中的中位数
分析:和面试题40是一样的解题思路,就是先进行排序,再提取中位数。可以建立一个最大堆,使用最大堆排序和最小堆排序的方法。
面试题42:连续数组的最大和
题目:输入一个数组,其中有负数也有正数,求其子连续数组和最大的数组。
面试题43:1~n整数中1出现的次数
面试题44:数字序列中某一位的数字
面试题45:把数组排成最小的数
面试题46:把数字翻译成字符串
bool isAdd(char a, char b)
{
int data1 = a - '0';
int data2 = b - '0';
int data = data1 * 10 + data2;
if (data >= 10 && data <= 25)
return true;
else
return false;
}
int GetTranslation(const string &number)
{
int length = number.length();
int *counts = new int[length];
counts[length - 1] = 1;
int count = 0;
for (int i = length - 2; i >= 0; --i)
{
count = 0;
if (isAdd(number[i],number[i+1]))
{
if (i == length - 2)
count = 1 + counts[length - 1];
else if(i < length-2)
count = counts[i + 1] + counts[i + 2];
}
else
count = counts[i + 1];
counts[i] = count;
}
return counts[0];
}
int GetTranslation(int number)
{
if (number < 0)
return 0;
string numberToString = to_string(number);
return GetTranslation(numberToString);
}
int main()
{
int a = 1224;
int b = GetTranslation(a);
return 0;
}
面试题47:礼物的最大价值(最优解,迪杰斯特拉等等)
面试题48:最长不含重复字符的子字符串
bool isInStr(char a, string str)
{
int length = str.length();
for (int i = 0; i < length; ++i)
{
if (a == str[i])
return true;
}
return false;
}
string newString(const string &str, char a)
{
string newstring;
int length = str.length();
int sign = 0;
for (int i = 0; i < length; ++i)
{
if (sign)
newstring += str[i];
if (a == str[i])
sign = 1;
}
return newstring;
}
string LongestChar(const string &str)
{
int length = str.length();
if (length == 0)
return "wrong!!";
string result;
string temp;
int longest = 0;
for (int i = 0; i < length; i++)
{
if (!isInStr(str[i], temp))
{
temp += str[i];
if (temp.length() > result.length())
{
result = temp;
longest = result.length();
}
}
else
temp = newString(temp, str[i]);
}
return result;
}
int main()
{
string a = "abcddeeabcdef";
string result = LongestChar(a);
cout << result << endl;
return 0;
}
用空间换取时间
面试题49:丑数
只包含因子2,3,5的数
面试题50:第一次只出现一次的字符
使用哈希表
面试题51:数组中的逆序对
面试题52:两个链表的第一个公共节点
1、公共节点即是第一个两个链表相等的节点,可以使用两个栈作为辅助空间进行操作;
2、可以先遍历两个链表,得到两个链表的长度差,就可以方便的进行节点比较了。
1、学习的能力;
2、与人沟通的能力;
3、知识迁移的能力。
面试题53:再排序数组中查找数字
题目一:数字在排序数组中出现的次数。
题目二:0~n-1中缺失的数字。
题目三:数组中数值和下标相等的元素。
面试题54:二叉搜索树的第K大节点(需要进行实践一下)
面试题55:二叉树的深度(需要实践一下,二叉树的递归使用)
题目一:普通二叉树的深度
题目二:平衡二叉树,判断二叉树是否平衡
面试题56:数组中数字出现的次数(困难级别,需要进行实践)
题目一:数组中只出现一次的两个数,其他数字都出现了两次。
解题思路,如果只有一个数组出现了一次,将数组中的元素依次进行异或,最后得到的就是只出现一次的元素,因为出现两次的都被抵消了。
int GetFirst1(int resultEOR)
{
int index = 1;
while (index<8*sizeof(int))
{
if (resultEOR & 1)
return index;
resultEOR = resultEOR >> 1;
++index;
}
return index;
}
bool IsGrouo1(int number, int first1)
{
number = number >> (first1 - 1);
return (number & 1);
}
void FindNumberApperOnce(int *data, int length, int *num1, int *num2)
{
if (data == nullptr)
return;
int resultEOR = data[0];
for (int i = 1; i < length; ++i)
{
resultEOR ^= data[i];
}
unsigned int first1 = GetFirst1(resultEOR);
*num1 = 0;
*num2 = 0;
for (int i = 0; i < length; ++i)
{
if (IsGrouo1(data[i], first1))
*num1 ^= data[i];
else
*num2 ^= data[i];
}
cout << *num1 << *num2 << endl;
}
int main()
{
int a[] = {1,2,3,4,5,2,3,4,6,5};
int num1 = 0;
int num2 = 0;
FindNumberApperOnce(a, 10, &num1, &num2);
return 0;
}
题目二:数组中唯一只出现一次的数字,其他数字都出现了三次。
解题思路:将数组中的每一位元素按位进行相加,如果和的某一位能被3整除,则目标元素再该位上为零,否则为1。
面试题57:和为S的数字
题目1:在一个排序数组中寻找两个和为S的数。
题目2:寻找和为S的连续正数序列
面试题58:翻转字符串
题目1:翻转单词的顺序,但是单词不进行翻转。
思路:先翻转整个字符串,再翻转每个单词。
题目2:左旋转字符串
面试题59:队列的最大值(可以看一下优先队列)
题目一:滑动窗口的最大值
题目二:队列的最大值,在O(1)的时间复杂度下求一个队列的最大值。
可以使用辅助deque来进行操作。
面试题60:n个骰子的点数
面试题61:扑克牌中的顺子
面试题62:圆圈中最后剩下的数字
约瑟夫环的问题。
面试题63:股票的最大利润
面试题64:求1+2+3+…+n,不能使用乘除法和循环等常规方法。
面试题65:不能用加减乘除作加法
使用异或的方法进行模拟。
面试题66:构建乘积数组
使用两个辅助数组。