1.链表的快排
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);
}