6.剑指Offer --- 面试中的各项能力

Posted enlyhua

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了6.剑指Offer --- 面试中的各项能力相关的知识,希望对你有一定的参考价值。

第6章 面试中的各项能力 
6.1 面试官谈能力 
6.2 沟通能力和学习能力 
	1.沟通能力
	2.学习能力
		通过2种办法考察应聘者的学习能力:	
			1.询问最近看什么书或者做什么项目,从中学到了哪些技术。从而了解应聘者的学习愿望和学习能力。
			2.抛出一个新的概念,接下来观察应聘者能不能在较短的时间内理解这个新概念并解决相关问题。

	3.善于学习,沟通的人也善于提问
		学习能力怎么体现呢?面试官提出一个新概念,应聘者没听过,于是在他自己的思考理解的基础上提出进一步的问题。

6.3 知识迁移能力 
	根据已经掌握的知识,技术,能够迅速学习,理解新的技术并能运用到实际工作中去。

	考察知识迁移能力的一个方法是把经典的问题稍作变换。另一个方法是先问一个简单的问题,再追问一个难度更大的问题。


面试题38:数字在排序数组中出现的次数
	题目:统计一个数字在排序数组中出现的次数。例如输入排序数组{1,2,3,3,3,3,4,5}和数字3,由于3在这个数组中出现了4次,因此输出4.

//先查找排序数组中的第一个k
int GetFirstK(int* data, int length, int k, int start, int end)
{
	if (start > end) 
		return -1;

	int middleIndex = (start + end) / 2;
	int middleData = data[middleIndex];

	if (middleData == k) {
		//前一个不等于k的
		if ( (middleIndex > 0 && data[middleIndex - 1] != k) || middleIndex == 0  )
			return middleIndex;
		else {
			//k在前半段
			end = middleIndex - 1;
		}
	} else if (middleData > k) {
		end = middleIndex - 1;
	} else {
		start = middleIndex + 1;
	}

	return GetFirstK(data, length, k, start, end);
}

//查找最后一个k
int GetLastK(int* data, int length, int k, int start, int end)
{
	if (start > end)
		return -1;

	int middleIndex = (start + end) / 2;
	int middleData = data[middleIndex];

	if (middleIndex == k) {
		if ( (middleIndex < length - 1 && data[middleIndex + 1] != k) || middleIndex == length - 1 ) {
			return middleIndex;
		} else {
			start = middleIndex + 1;
		} 
	} else if (middleData < k) {
		start = middleIndex - 1;
	} else {
		end = middleIndex - 1;
	}

	return GetLastK(data, length, k, start, end);
}

//计算出k在数组中出现的次数
int GetNumberOfK(int* data, int length, int k)
{
	int number = 0;

	if (data != null && length > 0) {
		int first = GetFirstK(int* data, int length, int k, int start, int end);
		int last = GetLastK(int* data, int length, int k, int start, int end);

		if (first > -1 && last > -1) {
			number = last - first + 1;
		}
	} 

	return number;
}

	时间复杂度为 O(logn)。



面试题39:二叉树的深度
	题目一:输入一颗二叉树的根结点,求该数的深度。从根结点到叶结点依次经历的结点(含根,叶结点)形成树的一条路径,最长的路径的长度为树的深度。

struct BinaryTreeNode {
	int m_nValue;
	BinaryTreeNode* m_pLeft;
	BinaryTreeNode* m_pRight;
};
	

//如果一棵树只有一个结点,它的深度为1。如果根结点只有左子树而没有右子树,那么树的深度应该是其左子树的深度加1;同样如果根结点只有右子树而没有左子树,
那么树的深度应该是右子树的深度加1.如果既有左子树有又右子树,那么树的深度就是其左子树,右子树深度的较大值加1.
int TreeDepth(BinaryTreeNode* pRoot)
{
	if (pRoot == null)
		return 0;

	int nLeft = TreeDepth(pRoot->m_pLeft);
	int nRIght = TreeDepth(pRoot->m_pRight);

	return (nLeft > nRight) ? (nLeft + 1) : (nRight + 1);
}

	题目二:输入一颗二叉树的根结点,判断该树是不是平衡二叉树。如果某二叉树中任意结点的左右子树的深度相差不超过1,那么它就是一颗平衡二叉树。

解法一:需要重复遍历结点多次的解法,简单但不足以打动面试官

//在遍历树的每个结点的时候,调用函数 TreeDepth 得到它的左右子树的深度。
bool IsBalanced(BinaryTreeNode* pRoot)
{
	if (pRoot == null)
		return true;

	int left = TreeDepth(pRoot->m_pLeft);
	int right = TreeDepth(pRoot->m_pRight);
	int diff = left - right;

	if (diff > 1 || diff < -1) 
		return false;

	return IsBalanced(pRoot->m_pLeft) && IsBalanced(pRoot->m_pRight);
}
	
	缺点:每一个结点都会被重复遍历多次,时间效率不高。


解法二:每个结点只遍历一次的解法,正是面试官喜欢的
	
	如果我们用后序遍历的方式遍历二叉树的每一个结点,在遍历到一个结点之前我们就遍历了它的左右子树。只要在遍历每个结点的时候记录它的深度,我们就可以一边
遍历判断每个结点是不是平衡的。

bool IsBlanced(BinaryTreeNode* pRoot, int* pDepth)
{
	if (pRoot == null) {
		*pDepth = 0;
		return true;
	}

	int left, right;

	if (IsBalanced(pRoot->m_pLeft, &left) && IsBalanced(pRoot->m_pRight, &right)) {
		int diff = left - right;

		if (diff <= 1 && diff >= -1) {
			*pDepth = 1 + (left > right ? left : right);
			return true;
		}
	} 

	return false;
}

//我们只需要给上面的函数传入二叉树的根结点以及表示结点深度的整型变量即可。
bool IsBalanced(BinaryTreeNode* pRoot)
{
	int depth = 0;
	return IsBalanced(pRoot, &depth);
}
	
	上述代码中,我们用后序遍历的方式遍历整颗二叉树。在遍历某结点的左右子结点之后,我们可以根据它的左右子结点的深度判断它是不是平衡的,并得到当前结点
的深度。当最后遍历到树的根结点的时候,也就判断了整颗二叉树是不是平衡二叉树。


面试题40:数组只出现一次的数字
	题目:一个整形数组里除了2个数之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字,要求时间复杂度是O(n),空间复杂度是O(1)。

	任何一个数字异或它自己都等于0.也就是说,我们从头到尾一次异或数组中的每一个数字,那么最终的结果刚好是那个只出现一次的数字,因为那些成对出现两次的
数字全部在异或中消失了。
	
	我们还是从头到尾依次异或数组中的每一个数字,那么最终得到的结果是两个只出现一次的数字的异或结果。因为其他数字都出现了两次,在异或中全部消失了。由于
这两个数字肯定不一样,那么异或的结果肯定是不为0的,也就是说在这个结果数字的二进制表示中至少就有一位为1.我们在结果数字中找到第一个位1的位置,记为第n位。
现在我们以第n位是不是1位标准,把原数组中的数字分成两个子数组,第一个子数组中每个数字的第n位都是1,而第二个子数组中每个数字的第n位都是0。由于我们分组的
标准是数字中某一位是1还是0,那么出现了两次的数字肯定被分配到了同一个子数组。因为两个相同的数字的任意一位都是相同的,我们不可能把两个相同的数字分配到两个
子数组中去,于是我们已经把原数组分成了两个子数组,每个子数组包含一个只出现一次的数字,而其他数字都出现了两次。

void FindNumsAppearOnce(int data[], int length, int* num1, int* num2)
{
	if (data == null || length < 2)
		return;

	int resultExclusiveOR = 0;

	//对原数组异或
	for (int i = 0; i < length; i++) {
		resultExclusiveOR ^= data[i];
	}

	//找出第一个出现1的n位
	unsigned int indexOf1 = FindFirstBitIs1(resultExclusiveOR);


	//分组
	*num1 = *num2 = 0;
	for (int j = 0; j < length; j++) {
		if (IsBit1(data[j], indexOf1)) {
			*num1 ^= data[j];
		} else {
			*num2 ^= data[j];
		}
	}
}

//用来在整数 num 的二进制表示中找到最右边是1的位。
unsigned int FindFirstBitIs1(int num)
{
	int indexBit = 0;

	while ( (num & 1) == 0 && (indexBit < 8 * sizeof(int)) ) {
		num = num >> 1;
		indexBit++;
	}

	return indexBit;
}

//用来判断在num的二进制表示中从右边数起的 indexBit 位是不是1
bool IsBit1(int num, unsigned int indexBit)
{
	num = num >> indexBit;
	return (num & 1);
}



面试题41:和为的两个数字 VS 和为s的连续正数序列
	题目一:输入一个连续递增排序的数组的一个数字s,在数组中查找两个数字,使得它们的和正好是s。如果有多对数字的和等于s,输出任意一对即可。
例如输入数组 {1,2,4,7,11,15} 和数字15,由于 4+11=15,因此输出4和11.
	
	这个问题,很多人会立马想到O(n^2)的方法,也就是先在数组中固定一个数字,再依次判断数组中其余的n-1与它的和是不是等于s。

	定义两个指针,第一个指针指向数组的第一个(也就是最小的),第二个指针指向数组的最后一个(也就是最大的)。然后向中间移动。

bool FindNumbersWithSum(int data[], int length, int sum, int* num1, int* num2)
{
	bool found = false;

	if (length <= 1 || num1 == null || num2 == null)
		return found;

	int ahead = length - 1;
	int behind = 0;

	while (ahead > behind) {
		long long curSum = data[ahead] + data[behind];

		if (curSum == sum) {
			*num1 = data[behind];
			*num2 = data[ahead];
			found = true;
			break;
		} else if (curSum > sum) {
			ahead--;
		} else {
			behind++;
		}
	}

	return found;
}	
	
	时间复杂度为O(n)。


	题目二:输入一个正数,打印出所有和为s的连续正数序列(至少有两个数)。例如输入15,由于 1+2+3+4+5 = 4+5+6=7+8=15,所以结果打印出3个连续序列
1~5,4~6和7~8。


void FindContinuousSequence(int sum)
{
	if (sum < 3)
		return;

	int small = 1;
	int big = 2;

	//因为这个序列至少要两个数字,我们一直增加small到(1+s)/2为止
	int middle = (1 + sum) / 2;
	int curSum = small + big;

	while (small < middle) {
		if (curSum == sum) {
			PrintContinuousSequence(small, big);
		}

		while (curSum > sum && samll < middle) {
			curSum -= small;
			small++;

			if (curSum == sum) {
				PrintContinuousSequence(small, big);
			}
		}

		big++;
		cumSum += big;
	}
}

void PrintContinuousSequence(int small, int big)
{
	for (int i = small; i <= big; i++) {
		printf("%d ", i);
		printf("\\n");
	}
}


面试题42:翻转单词顺序 vs 左旋转字符串
	题目一:输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student"
则输出"student. a am I"。


//先实现一个函数以翻转字符串中的一段	
void Reverse(char* pBegin, char* pEnd)
{
	if (pBegin == null || pEnd == null) 
		return;

	while (pBegin < pEnd) {
		char temp = *pBegin;
		*pBegin = *pEnd;
		*pEnd = temp;

		pBegin++, pEnd--;
	}
}

//先翻转整个句子,再翻转句子中的每个单词
char* ReverseSentence(char* pData)
{
	if (pData == null) 
		return null;

	char* pBegin = pData;
	char* pEnd = pData;

	while (*pEnd != '\\0') {
		pEnd++;
	}

	pEnd++;

	//翻转整个句子
	Reverse(pBegin, pEnd);

	//翻转句子中的每个单词
	pBegin = pEnd = pData;
	while (*pBegin != '\\0') {

		//通过扫描空格来确定每个单词的起始和终止位置
		if (*pBegin == ' ') {
			pBegin++;
			pEnd++;
		} else if (*pEnd == ' ' || *pEnd == '\\0') {
			Reverse(pBegin, --pEnd);
			pBegin = ++pEnd;
		} else {
			pEnd++;
		}
	}

	return pData;
}
	

	面试题二:字符串的左旋转操作是把字符串前面的若干字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如输入字符串"abcdefg"和
数组2,该函数返回左旋转2位的结果"cdefgab"。
	

char* LeftRotateString(char* pStr, int n)
{
	if (pStr != null) {
		int nLength = static_cast<int>(strlen(pStr));

		if (nLength > 0 && n > 0 && n < nLength) {
			char* pFirstStart = pStr;
			char* pFirstEnd = pStr + n - 1;
			char* pSecondStart = pStr + n;
			char* pSecondEnd = pStr + nLength - 1;

			//翻转字符串前面的n个字符
			Reverse(pFirstStart, pFirstEnd);
			//翻转字符串后面部分
			Reverse(pSecondStart, pSecondEnd);
			//翻转整个字符串
			Reverse(pFirstStart, pSecondEnd);
		}
 	}

 	return pStr;
}


6.4 抽象建模能力 

面试题43:n个骰子的点数
	题目:把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。

	解法一:基于递归求骰子点数,时间效率不够高

int g_maxValue = 6;

void PrintProbability(int number)
{
	if (number < 1)
		return;

	int maxSum = number * g_maxValue;
	int* pProbalities = new int[maxSum - number + 1];

	for (int i = number; i <= maxSum; i++) {
		pProbalities[i - number] = 0;
	}

	Probability(number, pProbabilities);

	int total = pow((double)g_maxValue, number);
	for (int i = number; i <= maxSum; i++) {
		double ratio = (double)pProbabilities[i - number] / total;
		printf("%d: %e\\n", i, ratio);
	}

	delete[] pProbabilities;
}

void Probability(int number, int* pProbabilities)
{
	for (int i = 1; i <= g_maxValue; i++) {
		Probability(number, number, i, pProbabilities);
	}
}

void Probability(int original, int current, int sum, int* pProbabilities)
{
	if (current == 1) {
		pProbabilities[sum - original]++;
	} else {
		for (int i = 1; i <= g_maxValue; i++) {
			Probability(original, current - 1, i + sum, pProbabilities);
		}
	}
}


	解法二:基于循环求骰子点数,时间性能好

void PrintProbability(int number)
{
	if (number < 1)
		return;

	int* pProbabilities[2];
	pProbabilities[0] = new int[g_maxValue * number + 1];
	pProbabilities[1] = new int[g_maxValue * number + 1];

	for (int i = 0; i < g_maxValue * number + 1; i++) {
		pProbabilities[0][i] = 0;
		pProbabilities[1][i] = 0;
	}

	int flag = 0;
	for (int i = 0; i <= g_maxValue; i++) {
		pProbabilities[flag][i] = 1;
	}

	for (int k = 2; k <= number; k++) {
		for (int i = 0; i < k; i++) {
			pProbabilities[1 - flag][i] = 0;
		}

		for (int i = k; i <= g_maxValue * k; i++) {
			pProbabilities[1 - flag][i] = 0;
			for (int j = 1; j <= i && j <= g_maxValue; j++) {
				pProbabilities[1 - flag][i] += pProbabilities[flag][i - j];
			}
		}

		flag = 1 - flag;
	}

	double total = pow((double)g_maxValue, number);
	for (int i = number; i <= g_maxValue * number; i++) {
		doulbe ratio = (double)pProbabilities[flag][i] / total;
		printf("%d: %e\\n", ratio);
	}

	delete[] pProbabilities[0];
	delete[] pProbabilities[1];
}	



面试题44:扑克牌的顺子
	题目:从扑克牌中随机抽取5张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大小王可以看成任意数字。

	分析:
		怎么判断5个数字是不是连续的,最直观的方法是把数组排序。0可以当成任何数字,可以用0补满数组的空缺。如果排序后的数组不是连续的,只要我们有足够的
	0去补满,这个数组实际上还是连续的。如果数组中出现非0数字重复,则该数组不是连续的。

bool IsContinues(int* numbers, int length)
{
	if (numbers == null || length < 1)
		return;

	qsort(numbers, length, sizeof(int), compare);

	int numberOfZero = 0;
	int numberOfGap = 0;

	//统计数组中0的个数
	for (int i = 0; i < length && numbers[i] == 0; i++) {
		numberOfZero++;
	}

	//统计数组中的间隔数目
	int small = numberOfZero;
	int big = small + 1;
	while (big < length) {
		//两个数字相等,有对子,不可能是顺子
		if (numbers[small] == numbers[big]) {
			return false;
		}

		numberOfGap += numbers[big] - numbers[small] - 1;
		small = big;
		big++;
	}

	return (numberOfGap > numberOfZero) ? false : true;
}	

int compare(const void* arg1, const void* arg2)
{
	return *(int*)arg1 - *(int*)arg2;
}

面试题45:圆圈中最后剩下的数字
	题目:0,1 ...,n - 1 这n个数字排成一个圆圈,从数字0开始每次从这个圆圈中删除第m个数字。求出这个圆圈里剩下的最后一个数字。

	解法一:经典的解法,用环形链表模拟圆圈

int LastRemaining(unsigined int n, unsigned int m)
{
	if (n < 1 || m < 1) 
		return -1;

	unsigned int i = 0;

	list<int> numbers;

	for (i = 0; i < n; i++) {
		numbers.push_back(i);
	}

	list<int>::iterator current = numbers.begin();

	while (numbers.size() > 1) {
		for (int i = 1; i < m; i++) {
			current++;
			if (current == numbers.end()) {
				curent = numbers.begin();
			}
		}

		list<int>::iterator next = ++current;

		if (next == numbers.end()) {
			next = numbers.begin();
		}

		--current;
		numbers.erase(current);
		current = next;
	}

	return *(current);
}

	缺点:
		上述我们发现实际上需要在环形链表中重复遍历很多遍。这种方法删除一个数字需要m步运算,总共有n个数字,因此总的时间复杂度是O(m*n)。同时这种思路
	还需要一个辅助链表来模拟圆圈,空间复杂度为O(n)。


	解法二:创新的解法,拿offer不在话下

int LastRemaining(unsigned int n, unsigned int m)
{
	if (n < 1 || m < 1) 
		return -1;

	int last = 0;
	for (int i = 2; i <= n; i++) {
		last = (last + m) % i;
	}

	return last;
}

6.5 发散思维能力 

面试题46:求 1+2+ ... +n
	题目:求 1+2+ ... +n,要求不能使用 乘除法,for,while,if,else,switch,case 等关键字及条件判断语句(A?B:C)。

	解法一:利用构造函数求解

	循环只是让相同的代码执行n遍而已,我们完全可以不用for和while来达到这个效果。比如我们先定义一个类型,接着创建n个该类型的实例,那么这个类型的构造
函数将确定会被调用n次。我们可以将累加的代码放到构造函数里。

class Temp
{
	public:
		Temp() {++N; Sum += N;}

		static void Reset() { N = 0; Sum = 0; }
		static unsigned int GetSum() { return Sum; }

	private:
		static unsigned int N;
		static unsigned int Sum;
};

unsigned int Temp:N = 0;
unsigned int Temp:Sum = 0;

unsigned int Sum_Solution1(unsigned int n)
{
	Temp::Reset();

	Temp* a = new Temp[n];
	delete []a;
	a = null;

	return Temp::GetSum();
}

	
	解法二:利用虚函数求解

	我们同样可以用递归来做文章。既然不能在一个函数中判断是不是应该终止递归,那么我们不妨定义两个函数,一个函数充当递归函数的角色,另外一个函数处理终止
递归的情况,我们需要做的就是二选一。从二选一中,我们很自然的想到布尔变量,比如值为true(1)的时候调用第一个函数,值为false(0)的时候,调用第二个函数。
那现在的问题是如何把值变量n转换成布尔值。如果对n连续做两次反运算,即 !!n,那么非零的n转换为true,0转换为false。

class A;

A* Array[2];

class A
{
	public:
		virtual unsigned int Sum(unsigned int n)
		{
			return 0;
		}	
};

class B: public A
{
	public:
		virtual unsigned int Sum(unsigned int n)
		{
			return Array[!!n]->Sum(n-1) + n;
		}
};

int Sum_Solution2(int n)
{
	A a;
	B b;
	Array[0] = &a;
	Array[1] = &b;

	int value = Array[1]->Sum(n);

	return value;
}
	
	这种思路是利用虚函数来实现函数的选择。当n不为0的时候,调用函数 B::Sum;当n等于0的时候,调用函数A::Sum。


	解法三:利用函数指针求解

typedef unsigned int (*fun)(unsigned int);

unsigned int Solution3_Teminator(unsigned int n)
{
	return 0;
}

unsigned int Sum_Solution3(unsigned int n)
{
	static fun f[2] = { Solution3_Teminator, Sum_Solution3 };
	return n + f[!!n](n - 1);
}

	
	解法四:利用模板类型求解

template <unsigned int n> struct Sum_Solution4
{
	enum Value { N = Sum_Solution4<n - 1>::N + n };
};

template <> struct Sum_Solution4<1>
{
	enum Value { N = 1 };
}



面试题47:不用加减乘除做加法
	题目:编写一个函数,求两个整数之和,要求在函数体内不能使用+,-,*,/ 四则运算符号。

	第一步,不考虑进位对每一位相加,就是异或运算。
	第二步,考虑进位的情况。只有1加1会产生进位,我们可以想象成两个数先做与运算,然后再向左移动1位。
	第三步,把前面的两个数相加,重复前面两步,直到没有进位。

int Add(int num1, int num2)
{
	int sum, carry;

	do {
		sum = num1 ^ num2;
		carry = (num1 & num2) << 1;

		num1 = sum;
		num2 = carry;
	} while (num2 != 0);

	return num1;
}
	
	相关问题:
		不使用新的变量,交换两个变量的值。比如有两个变量a,b,我们希望交换它们的值。有两种不同的方法:
		1.基于加减法
			a = a + b;
			b = a - b;
			a = a - b;

		2.基于异或运算
			a = a^b
			b = a^b
			a = a^b



面试题48:不能被继承的类
	题目:用C++设计一个不能被继承的类。

	解法一:常规的解法,把构造函数设为私有

	在c++中子类的构造函数会自动调用父类的构造函数,子类的析构函数也会调用父类的析构函数。要想一个类不能被继承,我们只要把它的构造函数和析构函数设置
为私有函数。那么当一个类试图从它那继承的时候,必然会由于构造函数,析构函数而导致编译错误。
	另外通过定义公有的 静态函数来创建和释放类实例。

class SealedClass1
{
	public:
		static SealedClass1* GetInstance()
		{
			return new SealedClass1();
		}

		static void DeleteInstance(SealedClass1* pInstance)
		{
			delete pInstance;
		}

	private:
		SealedClass1() {}
		~SealedClass1() {}
};
	
	缺点:
		我们只能得到位于堆上的实例,而得不到位于栈上的实例。
	

	解法二:利用虚拟继承,能给面试官留下很好的印象
template <typename T> class MakeSealed
{
	frined T;

	private:
		MakeSealed() {};
		~MakeSealed() {};
};

class SealedClass2 : vitrual public MakeSealed<SealedClass2>
{
	public:
		SealedClass2() {};
		~SealedClass2() {};
};

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

以上是关于6.剑指Offer --- 面试中的各项能力的主要内容,如果未能解决你的问题,请参考以下文章

剑指Offer-知识迁移能力面试题59:滑动窗口的最大值

剑指 Offer

剑指Offer面试题九度OJ1384:二维数组中的查找

剑指Offer名企面试官精讲典型编程题pdf

剑指Offer名企面试官精讲典型编程题pdf

剑指Offer面试题:12 矩阵中的路径