【学习笔记】剑指offer学习笔记

【学习笔记】剑指offer学习笔记

《剑指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 算法和数据操作

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
【学习笔记】剑指offer学习笔记_第1张图片
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:调整数组顺序使奇数位于偶数前面

3.4 代码的鲁棒性Robust

防御性编程,对函数的参数进行判断是否合规。
面试题22:链表中倒数第K个节点
解题思路:使用两个指针相隔k-1的距离一次进行遍历即可。
需要注意:1、该链表是否有k那么长;2、k值是否大于1;3、是否为空链表。

面试题23:链表中环的入口节点
解题思路:1、如何判断是否有环;2、判断环的数目;3、找环的入口节点。

面试题24:反转链表
将单向链表进行反转

面试题25:而合并两个排序的链表
将两个递增排序的列表合并成一个递增排序的列表。

面试题26:树的子结构
输入两棵二叉树A和B,判断B是否为A的子结构。
一般而言,关于树的题目更加困难,因为树的结构比较复杂。

第四章解决面试题的思路

4.2 画图让抽象问题形象化

面试题27:二叉树的镜像
问题:设计一个函数,输出一个二叉树的镜像。

面试题28:对称的二叉树
问题:设计一个函数,判断输入的二叉树是否为对称的二叉树。
解题思路:可以并通过前序遍历和前序遍历的对称遍历两种方式进行遍历,即通过中、左、右和中、右、左两种方式进行遍历。

面试题29:顺时针打印矩阵
问题:输入一个矩阵,由外到里顺时针进行打印
解题思路:一圈一圈进行打印。

4.3 举例让抽象问题具体化

面试题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:二叉树中和为某一值的路径
问题:输入二叉树和某一值,在二叉树中找到和为和该值的路径。

4.4 分解让复杂问题简单化

面试题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;
}

5.3 时间效率与空间效率的平衡

用空间换取时间

面试题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来进行操作。

6.4 抽象建模的能力

面试题60:n个骰子的点数

面试题61:扑克牌中的顺子

面试题62:圆圈中最后剩下的数字
约瑟夫环的问题。

面试题63:股票的最大利润

6.5 发散思维能力

面试题64:求1+2+3+…+n,不能使用乘除法和循环等常规方法。

面试题65:不能用加减乘除作加法
使用异或的方法进行模拟。

面试题66:构建乘积数组
使用两个辅助数组。

你可能感兴趣的:(编程技巧)