面试题

1.链表的快排

快排其实就是分而治之,局部有序,递归处理。
快排的处理过程就是 通过两个指针的从前到后的先后移动,不断将比中值小的值交换到前面,将比中值大的值交换到后面,形成两个子序列,再对两个子序列递归的做同样的处理。
这样来理解快排的话,就没有数据存储方式的不同,不论是list还是array,都一样。
其实像这种题, a**abc**edf,把 * 移到字母前面, 就是和快排一样的原理。


2.有一个N*N的棋盘, 上面放置了若干白色棋子,如果形成2*2  3*3 这种形状,则称白色棋子形成了正方形,寻找白色棋子所形成的最大正方形,给出其边长。

我的做法:
逐行扫描,对于每一行,寻找2个以上白色棋子连在一起的线段,每找到这样一条线段,记录白色棋子个数,第一个棋子所在行列数。扫描整个棋盘,将所有这样的线段找出来。
对其进行三级排序,一级关键字为白色棋子个数(降序),二级关键字为所在列数(升序),三级关键字为所在行数(升序)。
这样搜索一遍这个排好序的线段数组,按照如下方法:假设最大连续白色棋子个数为X,在这第一组中,在列数相同的一组中,记录行数连续递增的个数,可能有多个,取最大值(记为Y)。如果这个值Y>=X,则找到答案。如果Y< X, 记录这个数字,并继续下一组寻找,下一组中,记白色棋子连续个数为X1,用同样的方法找到Y,记为Y1,与已有Y进行比较,如果Y1>=Y && Y1 >= X1 则已找到答案,否则,如果Y1 > Y ,用Y1更新Y,继续之后的寻找。

这个做法,在最坏情况下,遍历N*N,N^2级别,线段个数为2/3N * 2/3N = 4/9N*N, N^2级别,排序N^2*log(N^2) =N^2*log(N)级别
最坏情况下,总时间复杂度 (N^2 + N^2*log(N))

3.

题目:在一个字符串中找到第一个只出现一次的字符。
如输入abaccdeff,则输出b。  
这道题是2006年google的一道笔试题。


这个是  

永久勘误:微软等面试100题答案V0.2版[第1-20题答案]


中的第17题

一种解法:

思路剖析:由于题目与字符出现的次数相关,我们可以统计每个字符在该字符串中出现的次数.
要达到这个目的,需要一个数据容器来存放每个字符的出现次数。

在这个数据容器中可以根据字符来查找它出现的次数,
也就是说这个容器的作用是把一个字符映射成一个数字。

在常用的数据容器中,哈希表正是这个用途。
由于本题的特殊性,我们只需要一个非常简单的哈希表就能满足要求。

由于字符(char)是一个长度为8的数据类型,因此总共有可能256 种可能。
于是我们创建一个长度为256的数组,每个字母根据其ASCII码值作为数组的下标对应数组的对应项,
而数组中存储的是每个字符对应的次数。

这样我们就创建了一个大小为256,以字符ASCII码为键值的哈希表。
我们第一遍扫描这个数组时,每碰到一个字符,在哈希表中找到对应的项并把出现的次数增加一次。
这样在进行第二次扫描时,就能直接从哈希表中得到每个字符出现的次数了。
//July、2010/10/20

#include <iostream.h>
#include <string.h>


char FirstNotRepeatingChar(char* pString)
{
      if(!pString)
            return 0;

      const int tableSize = 256;
      unsigned int hashTable[tableSize];
      for(unsigned int i = 0; i < tableSize; ++ i)
            hashTable[i] = 0;

      char* pHashKey = pString;
      while(*(pHashKey) != '/0')
            hashTable[*(pHashKey++)] ++;

      pHashKey = pString;
      while(*pHashKey != '/0')
      {
            if(hashTable[*pHashKey] == 1)
                  return *pHashKey;

            pHashKey++;
      }

      return *pHashKey;
}

int main()
{
    cout<<"请输入一串字符:"<<endl;
    char s[100];
    cin>>s;
    char* ps=s;
    cout<<FirstNotRepeatingChar(ps)<<endl;
    return 0;
}


//
请输入一串字符:
abaccdeff
b
Press any key to continue
///


第17题,我的想法:
用同样的hashtable, 扫描字符串"abcdacde", 对hashtable的意义做一下更改,0代表未出现,-1代表出现多次,正数代表只出现一次以及字符在字符串中出现的顺序。
扫描开始时维护一个从1开始的字符顺序变量 sq。
扫描字符ch  如果hashtable[ch] == 0, 则hashtable[ch] = sq; sq++;
如果hashtable[ch] != 0, 令hashtable[ch] = -1;
遍历处理完字符串后,只要再遍历一次hashtable,找到hashtable中最小的正数,即可找到第一个出现一次的字符。

这么做的话,时间复杂度没有根本性的提高,但是后面的一次遍历是常量级的,相当于只有一次遍历,减少一次遍历。
原来是N+N, 现在是N+C。

4.约瑟夫环的问题。

简单解法,链表和数组的方法。都比较直观,链表法最直观的,模拟了游戏过程。但是直接模拟游戏过程的方法复杂度比较高O(M*N)。查找这次退出的元素在上一次中的位置,可以推出一个关系,然后从最后一个人,只剩1一个开始,往前找。O(N)即可找到。

5.包含两种字符的最长子串。

有一个字符串,找出这个字符串中的只包含两种字符的子串的最大长度。如 aabbcccdddee,则输出6,不需要知道是哪两个字符。

暴力解法是找出所有的子串,就能找到只包含两种字符的子串的最大长度,复杂度是O(N^2),显然不是最优。

尝试有没有O(N*Log(N))的解法:

一般O(N^2)的都可以用分治法或者二分法,将复杂度降低到O(N*Log(N))甚至是O(N),但是这道题感觉不适合分治法。并不是最大符合条件子串就在左子串或者就在右子串,有可能分界点跨了这个最长子串,那就是两个子串递归回来之后,还需要继续在边界点附近寻找,而这种寻找相当于跟题目是一样的,相当于这样分析并没有将问题复杂度降低。

用两个指针遍历的解法:

两个指针,一前一后,查看两个指针区间内的字符数,如果字符数不超过2,前面的指针就一直向前移动,一直到区间内即将有三个字符,这时统计区间内字符数,作为最大长度的候选。然后向前移动后面的指针,一直移动到区间内只有一种字符,如此遍历字符串,直到前面的指针越界,返回保存的最大长度。这个思路的时间复杂度为O(N),空间复杂度为O(1)。

这是刚开始写下的漏洞百出的代码:

bool g_invalidInput = false;
int findMaxSubLen(char* str)
{
	int len = strlen(str);
	if (str == NULL || len < 2) {
		g_invalidInput = true; return 0;
	}
	int times[26] = {0}; // times[ch - 'a']
	char curCh[2] = {0};
	int i = 0, j = 0;
	curCh[0] = str[0];  times[curCh[0]] = 1;
	char lastch = 0;
	int maxlen = 0;
	while (i > len && j < len)
	{
		while(j < len && (lastch == curCh[0] || lastch == curCh[1]))
		{
			j++;
			if (lastch == curCh[0])  times[curCh[0]]++;
			if (lastch == curCh[1])  times[curCh[1]]++;
			lastch = str[j];
		}
		maxlen = max(maxlen, j - i);
		while(times[curCh[0]] & times[curCh[1]])
		{
			i++;
			if (str[i] == curCh[0])  times[curCh[0]]--;
			if (str[i] == curCh[1])  times[curCh[1]]--;
		}
	}
	return maxlen;
}
下面的是将这个思路整理好的代码

bool g_invalidInput = false;
int findMaxSubLen(char* str)
{
	if (str == NULL) {
		g_invalidInput = true; return 0;
	}
	//这里题意应该为字符种类不超过2个的子串的最大长度,种类为0或1都是合法输入
	int len = strlen(str);
	int times[255] = {0}; //
	char curCh[2] = {0};
	curCh[0] = str[0];  times[curCh[0]] = 1;
	int irear = 0, ifront = 1;
	
	int maxlen = 0;
	while (irear < len && ifront < len)
	{
		while(ifront < len)// && (lastch == curCh[0] || lastch == curCh[1]))
		{
			char chfront = str[ifront];
			if (chfront == curCh[0]){
				times[curCh[0]]++;
			}else if (times[curCh[1]] == 0 || chfront == curCh[1]){
				curCh[1] = chfront;
				times[curCh[1]]++;
			}else{
				break;
			}
			ifront++;
		}
		maxlen = max(maxlen, ifront - irear);
		while(times[curCh[0]] & times[curCh[1]])
		{
			times[str[irear]]--;
			irear++;
		}
		if (ifront < len){
			int indexCh = times[curCh[0]] == 0 ? 0 : 1;
			curCh[indexCh] = str[ifront];
			times[curCh[indexCh]]++;
		}
	}
	return maxlen;
}
其实这种做法将两个字符是什么都求出来了。感觉是不是有更快的做法。但是这个做法的时间复杂度已经是O(N)了,只是需要遍历2遍,是不是还存在只需要遍历一遍的做法呢。

6.棋盘问题,有N行棋盘,每一行有若干个棋子,不定数目,而且棋子没有顺序,可以理解成,每一行都有一堆棋子。两个人拿棋子,可以拿最少1个,最多K个,必须在同一行拿,拿到最后一个棋子的人输。假设两个拿棋子的人都足够聪明,能做出最有利于自己的选择。

先拿棋子的人是赢还是输,是否有定解,有定解的话,先拿棋子的人是先定赢还是定输。

分析:

对于第一个拿棋子的人,尝试所有可能的拿法,然后剩下的是一个子问题,将问题交给第二个拿的人去解决。在第一个拿棋子的人的第一次拿的所有的拿法中,查看第二个人的所有拿法,如果第二个人的所有拿法都是必赢的,那么第一个人就是必输的,如果第二个人有一个拿法是必输的,那么第一个人就是必赢的。递归的解决问题。递归的终止条件是将所有棋子都拿完了,这时这个人就赢了,因为另一个人拿了最后一个棋子。

基于这种思路不难写出最原始的暴力算法。

bool isEmpty(const vector<int> & vec)
{
	for (int i = 0; i < vec.size(); i++)
	{
		if(vec[i] != 0) return false;
	}
	return true;
}

bool isWinner(vector<int>& vec, int K)
{
	if (isEmpty(vec))  return true;

	for (int i = 0; i < vec.size(); i++)
	{
		for (int j = 1; j <= K; j++)
		{
			if (vec[i] >= j){
				vec[i] -= j;
				bool anotherWinner = isWinner(vec, K);
				vec[i] += j;
				if (!anotherWinner ){
					return true;
				}
			}
		}
	}
	return false;
}

接下来是怎样去优化。有几方面需要优化的:

1.由于递归,没有缓存中间结果而引起的大量重复计算。

2.N行棋子互相之间是没有顺序的,对于这种情况,第1行还剩5个棋子,第2行还剩6个棋子,和这种情况,第1行6个,第2行5个,这两种情况结果是一样的,其实是一种情况,不应该重复计算。

3.应该自底向上计算,而不是自顶向下,采用循环而不是递归,减少由于递归而造成的时间损失。

第一次优化:

遍历一遍数组得到最大值为M,即一行中棋子数目最多的。假设每行都是M,那所有的情况是一个全排列,一共有M^N种情况,不是每行都是M,也是在N次方这个数量级,如果要缓存中间结果,这个空间复杂度将会非常高。对于数组的每一种取值情况,都应该有一块空间存储其中间结果,需要能够一一对应。可以仔细想出一个优秀的hash函数,然后将中间结果存到hash表中,还要处理好冲突。我觉得这个空间复杂度太高,直接放hash表,虽然查找快,但是可能会浪费更多的空间,所以想到用二分查找树(或者红黑树),直接对应std::map, 不会用其他的空间,查找可以在lgN完成,查找树的key是什么呢,需要根据数组设计出一个hashcode,每一种数组的取值都会不相同,可以把数组看成是M进制数字的每一位,这样数组中任何一个元素的取值的不同都会导致hashcode的不同,而且数组中不同元素的取值是能够分别反映出来的,互不影响,没有冲突的存在。也就是说,查找树的key,是将数组遍历一遍,生成的M进制数字的十进制值,也就是生成的hashcode。这样在每次计算前,先算hashcode,到map中去找,如果找到直接返回,如果没有继续计算,并且在计算后将计算值insert到map中去。

这样就缓存了中间结果,减少了一部分运算。

typedef map<int, bool> MapRes;
long long getHashcode(vector<int>& vec, int M)
{
	long long sum = 0;
	for (int i = 0; i < vec.size(); i++)
	{
		sum *= M; sum += vec[i];
	}
	return sum;
}

bool isWinner(vector<int>& vec, int K, int M, MapRes& hashTable)
{
	long long hashcode = getHashcode(vec, M);
	if (0 == hashcode )  return true;

	MapRes::const_iterator it = hashTable.find(hashcode);
	if (it != hashTable.end()){
		return it->second;
	}

	for (int i = 0; i < vec.size(); i++)
	{
		for (int j = 1; j <= K; j++)
		{
			if (vec[i] >= j){
				vec[i] -= j;
				bool bOtherWin = isWinner(vec, K, M, hashTable);
				vec[i] += j;
				if (!bOtherWin){
					hashTable[hashcode] = true;
					return true;
				}
			}
		}
	}
	hashTable[hashcode] = false;
	return false;
}


bool isWinner(vector<int> vec, int K)
{
	int M = 0;
	for (int i = 0; i < vec.size(); i++)
	{
		if (vec[i] > M)
			M = vec[i];
	}
	
	MapRes hashTable;

	return isWinner(vec, K, M, hashTable);
}

第二次优化:

仍然有重复的计算,对于vec为{6,5,....}和vec为{5,6,......}这样的情况没有处理,虽然结果一样,但是两种情况的hashcode不一样,所以会分别计算一遍。但是如果数组一直是升序排好序的,计算hashcode时也是升序排好序的,那么计算出的hashcode就是一样的了。用这种思路的话,在递归调用之前,由于vec[i] 减掉了j,先将vec[i]插到合适的位置,使整个数组仍然是升序的(这个过程其实就是插入排序的Insert的过程),然后进行递归调用,再将vec[i]换回原先的位置,再将vec[i] 加上j。 其中将vec[i]插到合适的位置,不断的将vec[i]交换至当前的左侧,直到左侧的值小于等于vec[i],递归调用结束后,再将vec[i]不断交换至右侧,回复原状。

#include <vector>
#include <map>
#include <algorithm>
using namespace std;
typedef map<int, bool> MapRes;

long long getHashcode(vector<int>& vec, int M)
{
	long long sum = 0;
	for (int i = 0; i < vec.size(); i++)
	{
		sum *= M; sum += vec[i];
	}
	return sum;
}

int moveLeft(vector<int>& vec, int from)
{
	int i = from;
	int temp = vec[i];
	while(i > 0 && vec[i] < vec[i - 1])
	{
		vec[i] = vec[i-1];
		i--;
	}
	vec[i] = temp;
	return i;
}
void moveRight(vector<int>& vec, int from, int to)
{
	int i = from;
	int temp = vec[i];
	while(i < to)
	{
		vec[i] = vec[i+1];
	}
	vec[i] = temp;
}
bool isWinner(vector<int>& vec, int K, int M, MapRes& hashTable)
{
	long long hashcode = getHashcode(vec, M);
	if (0 == hashcode )  return true;

	MapRes::const_iterator it = hashTable.find(hashcode);
	if (it != hashTable.end()){
		return it->second;
	}

	for (int i = 0; i < vec.size(); i++)
	{
		for (int j = 1; j <= K; j++)
		{
			if (vec[i] >= j){
				vec[i] -= j;
				int inew = moveLeft(vec, i);   // make it sorted as ascending again
				bool bOtherWin = isWinner(vec, K, M, hashTable);
				moveRight(vec, inew, i);			//restore as it was
				vec[i] += j;
				if (!bOtherWin){
					hashTable[hashcode] = true;
					return true;
				}
			}
		}
	}
	hashTable[hashcode] = false;
	return false;
}


bool isWinner(vector<int> vec, int K)
{
	int M = 0;
	for (int i = 0; i < vec.size(); i++)
	{
		if (vec[i] > M)
			M = vec[i];
	}
	
	MapRes hashTable;

	sort(vec.begin(), vec.end());
	return isWinner(vec, K, M, hashTable);
}

自我感觉,第一次优化是没有问题的,解决了由于递归造成的重复计算。但是第二次优化,我不是很确定,虽然保证了每次得到的数组都是升序排列好的,对于vec为{6,5,....}和vec为{5,6,......}这样的情况,只计算一次,因为其hashcode一样,但是为了使数组有序,又增加了额外的O(N)的时间复杂度。
但是又可以这样想,在每次递归调用时,计算hashcode已经引入了O(N)的复杂度,为了保证排序,又引入了O(N)的复杂度,所以总的复杂度没有发生变化,而将所有的重复计算都消除掉了,所以感觉上这个优化还是可以的。

应该还有第三种优化,自底向上的循环计算,而不是自顶向下的递归调用。能力有限,日后再想。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值