1、从字符串A中找到匹配字符串B的第一个子串的位置,以下的代码经测试比Plauger的stl(VC7)中的
::std::string::find要快30%左右
// 仿函数版本
template < class Character = char, class Size = int >
struct StringFinder
{
typedef Size size_t;
typedef Character char_t;
size_t operator()( const char_t* lpszSource, const char_t* lpszSearch )
{
// maybe the processing of the following line can delay to the caller
if ( ( NULL == lpszSource ) || ( NULL == lpszSearch ) ) return -1;
const char_t& cSource = *lpszSource;
const char_t& cSearch = *lpszSearch;
for ( ; ; )
{
if ( 0 == *lpszSearch )
{
return ( lpszSource - ( lpszSearch - &cSearch ) - &cSource );
}
if ( 0 == *lpszSource ) return -1;
if ( *lpszSource != *lpszSearch )
{
lpszSource -= lpszSearch - &cSearch - 1;
lpszSearch = &cSearch;
continue;
}
++ lpszSource;
++ lpszSearch;
}
return -1;
}
};
// 函数版本
template < class char_t, class size_t >
size_t StringFind( const char_t* lpszSource, const char_t* lpszSearch )
{
// maybe the processing of the following line can delay to the caller
if ( ( NULL == lpszSource ) || ( NULL == lpszSearch ) ) return -1;
const char_t& cSource = *lpszSource;
const char_t& cSearch = *lpszSearch;
for ( ; ; )
{
if ( 0 == *lpszSearch )
{
return ( lpszSource - ( lpszSearch - &cSearch ) - &cSource );
}
if ( 0 == *lpszSource ) return -1;
if ( *lpszSource != *lpszSearch )
{
lpszSource -= lpszSearch - &cSearch - 1;
lpszSearch = &cSearch;
continue;
}
++ lpszSource;
++ lpszSearch;
}
return -1;
}
2、字符串比较,同strcmp的功能,以下为仿函数版本。
struct StringCmp
{
int operator()( const char* lpszStr1, const char* lpszStr2 )
{
if ( NULL == lpszStr1 )
{
if ( NULL == lpszStr2 ) return 0;
return -1;
}
if ( NULL == lpszStr2 ) return 1;
for ( ; ( 0 != ( ( *lpszStr1 ) & ( *lpszStr2 ) ) ); ++ lpszStr1, ++ lpszStr2 )
{
if ( *lpszStr1 < *lpszStr2 ) return -1;
if ( *lpszStr1 > *lpszStr2 ) return 1;
}
if ( 0 != *lpszStr2 ) return -1;
if ( 0 != *lpszStr1 ) return 1;
return 0;
}
};
3、快速排序算法(做了优化处理的),当元素总数不超过给定的阈值(threshold)时,采用插入排序,如果这个
做笔试题的话,个人认为难度偏大,因为其中程序部分可能技巧性比较强,需要很细心。俺也是花了很长时间上机
调试,我认为可能冒泡、插入排序更适合笔试。做完此题后,我还参读了Plauger's stl(VC7)中::std::sort
的源码,不同的是,其阈值默认为32,并且在超过阈值时也作了优化,小于或等于则都用了插入排序,呵呵。。。
struct NullType;
template < class T, class Iterator = T*, class Pr = NullType, class Diff = __int64, Diff threshold = 9 >
struct Sorter;
template < class T, class Iterator, class Diff, Diff threshold >
struct Sorter< T, Iterator, NullType, Diff, threshold >// sort by operator <
{
typedef T type;
typedef T& reference;
typedef const T& const_reference;
typedef T* pointer;
typedef Iterator iterator;
void operator()( iterator begin, iterator end )// sort in field [begin, end)
{
QuickSort( begin, end );
}
void InsertionSort( iterator begin, iterator end )// sort in field [begin, end)
{
type swap_temp;
for ( iterator i = begin + 1; i < end; ++ i )
{
for ( iterator j = i; ( j > begin ) && ( *j < *( j - 1 ) ); -- j )
{
swap_temp = *j;
j[0] = j[-1];
j[-1] = swap_temp;
}
}
}
void QuickSort( iterator begin, iterator end )// sort in field [begin, end)
{
Diff diff = end - begin;
// optimize speed with InsertionSort if the number of elements is not more than threshold
if ( threshold >= diff )
{
InsertionSort( begin, end );
return;
}
// find pivot
iterator pivot = begin + ( diff >> 1 );
// swap pivot with the element before end
type swap_temp;
swap_temp = end[-1];
end[-1] = *pivot;
pivot[0] = swap_temp;
// partition
iterator right_first = Partition( begin, end - 2, end[-1] );
// restore pivot to primary position
if ( right_first < ( end - 1 ) )
{
swap_temp = right_first[0];
right_first[0] = end[-1];
end[-1] = swap_temp;
}
if ( ( right_first - begin ) > 2 )
QuickSort( begin, right_first );// sort in field [begin, right_first)
if ( ( end - right_first ) > 3 )
QuickSort( right_first + 1, end );// sort in field [right_first + 1, end )
}
private:
iterator Partition( iterator first, iterator last, const_reference pivot )
{
iterator last_temp = last;
type swap_temp;
for ( ; first < last; )
{
if ( *last < pivot )
{
if ( *first < pivot )
{
++ first;
continue;
}
swap_temp = *first;
first[0] = *last;
last[0] = swap_temp;
++ first;
-- last;
}
else
{
if ( *first < pivot )
{
++ first;
-- last;
continue;
}
-- last;
continue;
}
}
// calculate the first position of right part
for ( ; ( !( pivot < *last ) )&& ( last <= last_temp ); ++ last ) {}
return last;
}
};
template < class T, class Iterator, class Pr, class Diff, Diff threshold >
struct Sorter< T, Iterator, Pr, Diff, threshold >// sort by Pr
{
// Just change it1 < it 2 to Pr( it1, it2 )
};
4、判断字符串是否为回环串,类似于"abcdcba"(?),这个可能挺简单,但是好像很多大公司出的
笔试题似乎都很简单,不知其考察点在于什么。。。
template < class Character = char >
struct IsCircleString
{
typedef Character char_t;
bool operator()( const char_t* pStr )
{
const char_t* pLast = pStr;
for ( ; 0 != *pLast; ++ pLast ) {}
if ( ( ( pLast - pStr ) & 0x1 ) == 0 ) return false;//偶数即返回false
-- pLast;
for ( ; pStr < pLast; ++ pStr, -- pLast )
{
if ( *pStr != *pLast ) return false;
}
return true;
}
};
我没有记错的话是一道MSN的笔试题,网上无意中看到的,拿来做了一下。题目是这样的,给定一个字符串,一个这个字符串的子串,将第一个字符串反转,但保留子串的顺序不变。例如:
输入: 第一个字符串: "This is zhuxinquan's Chinese site: http://www.zhuxinquan.com/cn"
子串: "zhuxinquan"
输出: "nc/moc.zhuxinquan.www//:ptth :etis esenihC s'zhuxinquan si sihT"
一般的方法是先扫描一边第一个字符串,然后用stack把它反转,同时记录下子串出现的位置。然后再扫描一遍把记录下来的子串再用stack反转。我用的方法是用一遍扫描数组的方法。扫描中如果发现子串,就将子串倒过来压入堆栈。
最后再将堆栈里的字符弹出,这样子串又恢复了原来的顺序。源代码如下:
雅虎笔试题(字符串操作) 给定字符串A和B,输出A和B中的共有最大子串。
比如A="aocdfe" B="pmcdfa" 则输出"cdf"
#include
#include
#include
char *commanstring(char shortstring[], char longstring[])
{
int i, j;
char *substring=malloc(256);
if(strstr(longstring, shortstring)!=NULL) //如果……,那么返回shortstring
return shortstring;
for(i=strlen(shortstring)-1;i>0; i--) //否则,开始循环计算
{
for(j=0; j<=strlen(shortstring)-i; j++){
memcpy(substring, &shortstring[j], i);
substring[i]='\0';
if(strstr(longstring, substring)!=NULL)
return substring;
}
}
return NULL;
}
main()
{
char *str1=malloc(256);
char *str2=malloc(256);
char *comman=NULL;
gets(str1);
gets(str2);
if(strlen(str1)>strlen(str2)) //将短的字符串放前面
comman=commanstring(str2, str1);
else
comman=commanstring(str1, str2);
printf("the longest comman string is: %s\n", comman);
}
早一点的ms笔试:字符串倒序输出
#include
#include
using namespace std;
void convert(char *str,int len)
{
assert(str!=NULL);
char *p1=str;
char *p2=str+len-1;
while(p2>p1){
*p2^=*p1;
*p1^=*p2;
*p2^=*p1;
p1++;
--p2;
}
}
int main(){
char str[]="The quick brown fox jumps over a lazy dog";
cout << strcout << strchar *q=p;
while(*p!='\0'){
++p;
if(*p==' ' || *p=='\0'){
convert(q,p-q);
cout << str< q=p+1;
}
}
cout << str<
system("pause");
}
程序员面试题精选(07)-翻转句子中单词的顺序
题目:输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。句子中单词以空格符隔开。为简单起见,标点符号和普通字母一样处理。
例如输入“I am a student.”,则输出“student. a am I”。
分析:由于编写字符串相关代码能够反映程序员的编程能力和编程习惯,与字符串相关的问题一直是程序员笔试、面试题的热门题目。本题也曾多次受到包括微软在内的大量公司的青睐。
由于本题需要翻转句子,我们先颠倒句子中的所有字符。这时,不但翻转了句子中单词的顺序,而且单词内字符也被翻转了。我们再颠倒每个单词内的字符。由于单词内的字符被翻转两次,因此顺序仍然和输入时的顺序保持一致。
还是以上面的输入为例子。翻转“I am a student.”中所有字符得到“.tneduts a ma I”,再翻转每个单词中字符的顺序得到“students. a am I”,正是符合要求的输出。
参考代码:
///
// Reverse a string between two pointers
// Input: pBegin - the begin pointer in a string
// pEnd - the end pointer in a string
///
void Reverse(char *pBegin, char *pEnd)
{
if(pBegin == NULL || pEnd == NULL)
return;
while(pBegin < pEnd)
{
char temp = *pBegin;
*pBegin = *pEnd;
*pEnd = temp;
pBegin ++, pEnd --;
}
}
///
// Reverse the word order in a sentence, but maintain the character
// order inside a word
// Input: pData - the sentence to be reversed
///
char* ReverseSentence(char *pData)
{
if(pData == NULL)
return NULL;
char *pBegin = pData;
char *pEnd = pData;
while(*pEnd != '\0')
pEnd ++;
pEnd--;
// Reverse the whole sentence
Reverse(pBegin, pEnd);
// Reverse every word in the sentence
pBegin = pEnd = pData;
while(*pBegin != '\0')
{
if(*pBegin == ' ')
{
pBegin ++;
pEnd ++;
continue;
}
// A word is between with pBegin and pEnd, reverse it
else if(*pEnd == ' ' || *pEnd == '\0')
{
Reverse(pBegin, --pEnd);
pBegin = ++pEnd;
}
else
{
pEnd ++;
}
}
return pData;
}
程序员面试题精选(13)-第一个只出现一次的字符
题目:在一个字符串中找到第一个只出现一次的字符。如输入abaccdeff,则输出b。 分析:这道题是2006年google的一道笔试题。
看到这道题时,最直观的想法是从头开始扫描这个字符串中的每个字符。当访问到某字符时拿这个字符和后面的每个字符相比较,如果在后面没有发现重复的字符,则该字符就是只出现一次的字符。如果字符串有n个字符,每个字符可能与后面的O(n)个字符相比较,因此这种思路时间复杂度是O(n2)。我们试着去找一个更快的方法。
由于题目与字符出现的次数相关,我们是不是可以统计每个字符在该字符串中出现的次数?要达到这个目的,我们需要一个数据容器来存放每个字符的出现次数。在这个数据容器中可以根据字符来查找它出现的次数,也就是说这个容器的作用是把一个字符映射成一个数字。在常用的数据容器中,哈希表正是这个用途。
哈希表是一种比较复杂的数据结构。由于比较复杂,STL中没有实现哈希表,因此需要我们自己实现一个。但由于本题的特殊性,我们只需要一个非常简单的哈希表就能满足要求。由于字符(char)是一个长度为8的数据类型,因此总共有可能256 种可能。于是我们创建一个长度为256的数组,每个字母根据其ASCII码值作为数组的下标对应数组的对应项,而数组中存储的是每个字符对应的次数。这样我们就创建了一个大小为256,以字符ASCII码为键值的哈希表。
我们第一遍扫描这个数组时,每碰到一个字符,在哈希表中找到对应的项并把出现的次数增加一次。这样在进行第二次扫描时,就能直接从哈希表中得到每个字符出现的次数了。
参考代码如下:
///
// Find the first char which appears only once in a string
// Input: pString - the string
// Output: the first not repeating char if the string has, otherwise 0
///
char FirstNotRepeatingChar(char* pString)
{
// invalid input
if(!pString)
return 0;
// get a hash table, and initialize it
const int tableSize = 256;
unsigned int hashTable[tableSize];
for(unsigned int i = 0; i < tableSize; ++ i)
hashTable = 0;
// get the how many times each char appears in the string
char* pHashKey = pString;
while(*(pHashKey) != '\0')
hashTable[*(pHashKey++)] ++;
// find the first char which appears only once in a string
pHashKey = pString;
while(*pHashKey != '\0')
{
if(hashTable[*pHashKey] == 1)
return *pHashKey;
pHashKey++;
}
// if the string is empty
// or every char in the string appears at least twice
return 0;
}
程序员面试题精选(17)-把字符串转换成整数
题目:输入一个表示整数的字符串,把该字符串转换成整数并输出。例如输入字符串"345",则输出整数345。 分析:这道题尽管不是很难,学过C/C++语言一般都能实现基本功能,但不同程序员就这道题写出的代码有很大区别,可以说这道题能够很好地反应出程序员的思维和编程习惯,因此已经被包括微软在内的多家公司用作面试题。建议读者在往下看之前自己先编写代码,再比较自己写的代码和下面的参考代码有哪些不同。
首先我们分析如何完成基本功能,即如何把表示整数的字符串正确地转换成整数。还是以"345"作为例子。当我们扫描到字符串的第一个字符'3'时,我们不知道后面还有多少位,仅仅知道这是第一位,因此此时得到的数字是3。当扫描到第二个数字'4'时,此时我们已经知道前面已经一个3了,再在后面加上一个数字4,那前面的3相当于30,因此得到的数字是3*10+4=34。接着我们又扫描到字符'5',我们已经知道了'5'的前面已经有了34,由于后面要加上一个5,前面的34就相当于340了,因此得到的数字就是34*10+5=345。
分析到这里,我们不能得出一个转换的思路:每扫描到一个字符,我们把在之前得到的数字乘以10再加上当前字符表示的数字。这个思路用循环不难实现。
由于整数可能不仅仅之含有数字,还有可能以'+'或者'-'开头,表示整数的正负。因此我们需要把这个字符串的第一个字符做特殊处理。如果第一个字符是'+'号,则不需要做任何操作;如果第一个字符是'-'号,则表明这个整数是个负数,在最后的时候我们要把得到的数值变成负数。
接着我们试着处理非法输入。由于输入的是指针,在使用指针之前,我们要做的第一件是判断这个指针是不是为空。如果试着去访问空指针,将不可避免地导致程序崩溃。另外,输入的字符串中可能含有不是数字的字符。每当碰到这些非法的字符,我们就没有必要再继续转换。最后一个需要考虑的问题是溢出问题。由于输入的数字是以字符串的形式输入,因此有可能输入一个很大的数字转换之后会超过能够表示的最大的整数而溢出。
现在已经分析的差不多了,开始考虑编写代码。首先我们考虑如何声明这个函数。由于是把字符串转换成整数,很自然我们想到:
int StrToInt(const char* str);
这样声明看起来没有问题。但当输入的字符串是一个空指针或者含有非法的字符时,应该返回什么值呢?0怎么样?那怎么区分非法输入和字符串本身就是”0”这两种情况呢?
接下来我们考虑另外一种思路。我们可以返回一个布尔值来指示输入是否有效,而把转换后的整数放到参数列表中以引用或者指针的形式传入。于是我们就可以声明如下:
bool StrToInt(const char *str, int& num);
这种思路解决了前面的问题。但是这个函数的用户使用这个函数的时候会觉得不是很方便,因为他不能直接把得到的整数赋值给其他整形变脸,显得不够直观。
前面的第一种声明就很直观。如何在保证直观的前提下当碰到非法输入的时候通知用户呢?一种解决方案就是定义一个全局变量,每当碰到非法输入的时候,就标记该全局变量。用户在调用这个函数之后,就可以检验该全局变量来判断转换是不是成功。
下面我们写出完整的实现代码。参考代码:
enum Status {kValid = 0, kInvalid};
int g_nStatus = kValid;
///
// Convert a string into an integer
///
int StrToInt(const char* str)
{
g_nStatus = kInvalid;
long long num = 0;
if(str != NULL)
{
const char* digit = str;
// the first char in the string maybe '+' or '-'
bool minus = false;
if(*digit == '+')
digit ++;
else if(*digit == '-')
{
digit ++;
minus = true;
}
// the remaining chars in the string
while(*digit != '\0')
{
if(*digit >= '0' && *digit <= '9')
{
num = num * 10 + (*digit - '0');
// overflow
if(num > std::numeric_limits<int>::max())
{
num = 0;
break;
}
digit ++;
}
// if the char is not a digit, invalid input
else
{
num = 0;
break;
}
}
if(*digit == '\0')
{
g_nStatus = kValid;
if(minus)
num = 0 - num;
}
}
return static_cast<int>(num);
}
讨论:在参考代码中,我选用的是第一种声明方式。不过在面试时,我们可以选用任意一种声明方式进行实现。但当面试官问我们选择的理由时,我们要对两者的优缺点进行评价。第一种声明方式对用户而言非常直观,但使用了全局变量,不够优雅;而第二种思路是用返回值来表明输入是否合法,在很多API中都用这种方法,但该方法声明的函数使用起来不够直观。
最后值得一提的是,在C语言提供的库函数中,函数atoi能够把字符串转换整数。它的声明是int atoi(const char *str)。该函数就是用一个全局变量来标志输入是否合法的。
程序员面试题精选(20)-最长公共子串
题目:如果字符串一的所有字符按其在字符串中的顺序出现在另外一个字符串二中,则字符串一称之为字符串二的子串。注意,并不要求子串(字符串一)的字符必须连续出现在字符串二中。请编写一个函数,输入两个字符串,求它们的最长公共子串,并打印出最长公共子串。 例如:输入两个字符串BDCABA和ABCBDAB,字符串BCBA和BDAB都是是它们的最长公共子串,则输出它们的长度4,并打印任意一个子串。
分析:求最长公共子串(Longest Common Subsequence, LCS)是一道非常经典的动态规划题,因此一些重视算法的公司像MicroStrategy都把它当作面试题。
完整介绍动态规划将需要很长的篇幅,因此我不打算在此全面讨论动态规划相关的概念,只集中对LCS直接相关内容作讨论。如果对动态规划不是很熟悉,请参考相关算法书比如算法讨论。
先介绍LCS问题的性质:记Xm={x0, x1,…xm-1}和Yn={y0,y1,…,yn-1}为两个字符串,而Zk={z0,z1,…zk-1}是它们的LCS,则:
1. 如果xm-1=yn-1,那么zk-1=xm-1=yn-1,并且Zk-1是Xm-1和Yn-1的LCS;
2. 如果xm-1≠yn-1,那么当zk-1≠xm-1时Z是Xm-1和Y的LCS;
3. 如果xm-1≠yn-1,那么当zk-1≠yn-1时Z是Yn-1和X的LCS;
下面简单证明一下这些性质:
1. 如果zk-1≠xm-1,那么我们可以把xm-1(yn-1)加到Z中得到Z’,这样就得到X和Y的一个长度为k+1的公共子串Z’。这就与长度为k的Z是X和Y的LCS相矛盾了。因此一定有zk-1=xm-1=yn-1。
既然zk-1=xm-1=yn-1,那如果我们删除zk-1(xm-1、yn-1)得到的Zk-1,Xm-1和Yn-1,显然Zk-1是Xm-1和Yn-1的一个公共子串,现在我们证明Zk-1是Xm-1和Yn-1的LCS。用反证法不难证明。假设有Xm-1和Yn-1有一个长度超过k-1的公共子串W,那么我们把加到W中得到W’,那W’就是X和Y的公共子串,并且长度超过k,这就和已知条件相矛盾了。
2. 还是用反证法证明。假设Z不是Xm-1和Y的LCS,则存在一个长度超过k的W是Xm-1和Y的LCS,那W肯定也X和Y的公共子串,而已知条件中X和Y的公共子串的最大长度为k。矛盾。
3. 证明同2。
有了上面的性质,我们可以得出如下的思路:求两字符串Xm={x0, x1,…xm-1}和Yn={y0,y1,…,yn-1}的LCS,如果xm-1=yn-1,那么只需求得Xm-1和Yn-1的LCS,并在其后添加xm-1(yn-1)即可;如果xm-1≠yn-1,我们分别求得Xm-1和Y的LCS和Yn-1和X的LCS,并且这两个LCS中较长的一个为X和Y的LCS。
如果我们记字符串Xi和Yj的LCS的长度为c[i,j],我们可以递归地求c[i,j]:
/ 0 if i<0 or j<0
c[i,j]= c[i-1,j-1]+1 if i,j>=0 and xi=xj
\ max(c[i,j-1],c[i-1,j] if i,j>=0 and xi≠xj
上面的公式用递归函数不难求得。但从前面求Fibonacci第n项(本面试题系列第16题)的分析中我们知道直接递归会有很多重复计算,我们用从底向上循环求解的思路效率更高。
为了能够采用循环求解的思路,我们用一个矩阵(参考代码中的LCS_length)保存下来当前已经计算好了的c[i,j],当后面的计算需要这些数据时就可以直接从矩阵读取。另外,求取c[i,j]可以从c[i-1,j-1] 、c[i,j-1]或者c[i-1,j]三个方向计算得到,相当于在矩阵LCS_length中是从c[i-1,j-1],c[i,j-1]或者c[i-1,j]的某一个各自移动到c[i,j],因此在矩阵中有三种不同的移动方向:向左、向上和向左上方,其中只有向左上方移动时才表明找到LCS中的一个字符。于是我们需要用另外一个矩阵(参考代码中的LCS_direction)保存移动的方向。
参考代码如下:
#include "string.h"
// directions of LCS generation
enum decreaseDir {kInit = 0, kLeft, kUp, kLeftUp};
/
// Get the length of two strings' LCSs, and print one of the LCSs
// Input: pStr1 - the first string
// pStr2 - the second string
// Output: the length of two strings' LCSs
/
int LCS(char* pStr1, char* pStr2)
{
if(!pStr1 || !pStr2)
return 0;
size_t length1 = strlen(pStr1);
size_t length2 = strlen(pStr2);
if(!length1 || !length2)
return 0;
size_t i, j;
// initiate the length matrix
int **LCS_length;
LCS_length = (int**)(new int[length1]);
for(i = 0; i < length1; ++ i)
LCS_length = (int*)new int[length2];
for(i = 0; i < length1; ++ i)
for(j = 0; j < length2; ++ j)
LCS_length[j] = 0;
// initiate the direction matrix
int **LCS_direction;
LCS_direction = (int**)(new int[length1]);
for( i = 0; i < length1; ++ i)
LCS_direction = (int*)new int[length2];
for(i = 0; i < length1; ++ i)
for(j = 0; j < length2; ++ j)
LCS_direction[j] = kInit;
for(i = 0; i < length1; ++ i)
{
for(j = 0; j < length2; ++ j)
{
if(i == 0 || j == 0)
{
if(pStr1 == pStr2[j])
{
LCS_length[j] = 1;
LCS_direction[j] = kLeftUp;
}
else
LCS_length[j] = 0;
}
// a char of LCS is found,
// it comes from the left up entry in the direction matrix
else if(pStr1 == pStr2[j])
{
LCS_length[j] = LCS_length[i - 1][j - 1] + 1;
LCS_direction[j] = kLeftUp;
}
// it comes from the up entry in the direction matrix
else if(LCS_length[i - 1][j] > LCS_length[j - 1])
{
LCS_length[j] = LCS_length[i - 1][j];
LCS_direction[j] = kUp;
}
// it comes from the left entry in the direction matrix
else
{
LCS_length[j] = LCS_length[j - 1];
LCS_direction[j] = kLeft;
}
}
}
LCS_Print(LCS_direction, pStr1, pStr2, length1 - 1, length2 - 1);
return LCS_length[length1 - 1][length2 - 1];
}
void LCS_Print(int **LCS_direction,
char* pStr1, char* pStr2,
size_t row, size_t col)
{
if(pStr1 == NULL || pStr2 == NULL)
return;
size_t length1 = strlen(pStr1);
size_t length2 = strlen(pStr2);
if(length1 == 0 || length2 == 0 || !(row < length1 && col < length2))
return;
// kLeftUp implies a char in the LCS is found
if(LCS_direction[row][col] == kLeftUp)
{
if(row > 0 && col > 0)
LCS_Print(LCS_direction, pStr1, pStr2, row - 1, col - 1);
// print the char
printf("%c", pStr1[row]);
}
else if(LCS_direction[row][col] == kLeft)
{
// move to the left entry in the direction matrix
if(col > 0)
LCS_Print(LCS_direction, pStr1, pStr2, row, col - 1);
}
else if(LCS_direction[row][col] == kUp)
{
// move to the up entry in the direction matrix
if(row > 0)
LCS_Print(LCS_direction, pStr1, pStr2, row - 1, col);
}
}
扩展:如果题目改成求两个字符串的最长公共子字符串,应该怎么求?子字符串的定义和子串的定义类似,但要求是连续分布在其他字符串中。比如输入两个字符串BDCABA和ABCBDAB的最长公共字符串有BD和AB,它们的长度都是2。
程序员面试题精选(21)-左旋转字符串
题目:定义字符串的左旋转操作:把字符串前面的若干个字符移动到字符串的尾部。如把字符串abcdef左旋转2位得到字符串cdefab。请实现字符串左旋转的函数。要求时间对长度为n的字符串操作的复杂度为O(n),辅助内存为O(1)。
分析:如果不考虑时间和空间复杂度的限制,最简单的方法莫过于把这道题看成是把字符串分成前后两部分,通过旋转操作把这两个部分交换位置。于是我们可以新开辟一块长度为n+1的辅助空间,把原字符串后半部分拷贝到新空间的前半部分,在把原字符串的前半部分拷贝到新空间的后半部分。不难看出,这种思路的时间复杂度是O(n),需要的辅助空间也是O(n)。
接下来的一种思路可能要稍微麻烦一点。我们假设把字符串左旋转m位。于是我们先把第0个字符保存起来,把第m个字符放到第0个的位置,在把第2m个字符放到第m个的位置…依次类推,一直移动到最后一个可以移动字符,最后在把原来的第0个字符放到刚才移动的位置上。接着把第1个字符保存起来,把第m+1个元素移动到第1个位置…重复前面处理第0个字符的步骤,直到处理完前面的m个字符。
该思路还是比较容易理解,但当字符串的长度n不是m的整数倍的时候,写程序会有些麻烦,感兴趣的朋友可以自己试一下。由于下面还要介绍更好的方法,这种思路的代码我就不提供了。
我们还是把字符串看成有两段组成的,记位XY。左旋转相当于要把字符串XY变成YX。我们先在字符串上定义一种翻转的操作,就是翻转字符串中字符的先后顺序。把X翻转后记为XT。显然有(XT)T=X。
我们首先对X和Y两段分别进行翻转操作,这样就能得到XTYT。接着再对XTYT进行翻转操作,得到(XTYT)T=(YT)T(XT)T=YX。正好是我们期待的结果。
分析到这里我们再回到原来的题目。我们要做的仅仅是把字符串分成两段,第一段为前面m个字符,其余的字符分到第二段。再定义一个翻转字符串的函数,按照前面的步骤翻转三次就行了。时间复杂度和空间复杂度都合乎要求。
参考代码如下:
#include "string.h"
///
// Move the first n chars in a string to its end
///
char* LeftRotateString(char* pStr, unsigned 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; //pStr+nLength-1;
char* pSecondStart = pStr + n;// pStr+nLength;
char* pSecondEnd = pStr + nLength - 1;// pStr + n - 1
// reverse the first part of the string
ReverseString(pFirstStart, pFirstEnd);
// reverse the second part of the strint
ReverseString(pSecondStart, pSecondEnd);
// reverse the whole string
ReverseString(pFirstStart, pSecondEnd);
}
}
return pStr;
}
///
// Reverse the string between pStart and pEnd
///
void ReverseString(char* pStart, char* pEnd)
{
if(pStart == NULL || pEnd == NULL)
{
while(pStart <= pEnd)
{
char temp = *pStart;
*pStart = *pEnd;
*pEnd = temp;
pStart ++;
pEnd --;
}
}
}
程序员面试题精选100题(28)-字符串的排列 [折叠]
题目:输入一个字符串,打印出该字符串中字符的所有排列。例如输入字符串abc,则输出由字符a、b、c所能排列出来的所有字符串abc、acb、bac、bca、cab和cba。
分析:这是一道很好的考查对递归理解的编程题,因此在过去一年中频繁出现在各大公司的面试、笔试题中。
我们以三个字符abc为例来分析一下求字符串排列的过程。首先我们固定第一个字符a,求后面两个字符bc的排列。当两个字符bc的排列求好之后,我们把第一个字符a和后面的b交换,得到bac,接着我们固定第一个字符b,求后面两个字符ac的排列。现在是把c放到第一位置的时候了。记住前面我们已经把原先的第一个字符a和后面的b做了交换,为了保证这次c仍然是和原先处在第一位置的a交换,我们在拿c和第一个字符交换之前,先要把b和a交换回来。在交换b和a之后,再拿c和处在第一位置的a进行交换,得到cba。我们再次固定第一个字符c,求后面两个字符b、a的排列。
既然我们已经知道怎么求三个字符的排列,那么固定第一个字符之后求后面两个字符的排列,就是典型的递归思路了。
基于前面的分析,我们可以得到如下的参考代码:
void Permutation(char* pStr, char* pBegin);
/
// Get the permutation of a string,
// for example, input string abc, its permutation is
// abc acb bac bca cba cab
/
void Permutation(char* pStr)
{
Permutation(pStr, pStr);
}
/
// Print the permutation of a string,
// Input: pStr - input string
// pBegin - points to the begin char of string
// which we want to permutate in this recursion
/
void Permutation(char* pStr, char* pBegin)
{
if(!pStr || !pBegin)
return;
// if pBegin points to the end of string,
// this round of permutation is finished,
// print the permuted string
if(*pBegin == '\0')
{
printf("%s\n", pStr);
}
// otherwise, permute string
else
{
for(char* pCh = pBegin; *pCh != '\0'; ++ pCh)
{
// swap pCh and pBegin
char temp = *pCh;
*pCh = *pBegin;
*pBegin = temp;
Permutation(pStr, pBegin + 1);
// restore pCh and pBegin
temp = *pCh;
*pCh = *pBegin;
*pBegin = temp;
}
}
}
扩展1:如果不是求字符的所有排列,而是求字符的所有组合,应该怎么办呢?当输入的字符串中含有相同的字符串时,相同的字符交换位置是不同的排列,但是同一个组合。举个例子,如果输入aaa,那么它的排列是6个aaa,但对应的组合只有一个。
扩展2:输入一个含有8个数字的数组,判断有没有可能把这8个数字分别放到正方体的8个顶点上,使得正方体上三组相对的面上的4个顶点的和相等。
正在加载评论...
程序员面试题精选100题(29)-调整数组顺序使奇数位于偶数前面
[折叠]
题目:输入一个整数数组,调整数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。要求时间复杂度为O(n)。
分析:如果不考虑时间复杂度,最简单的思路应该是从头扫描这个数组,每碰到一个偶数时,拿出这个数字,并把位于这个数字后面的所有数字往前挪动一位。挪完之后在数组的末尾有一个空位,这时把该偶数放入这个空位。由于碰到一个偶数,需要移动O(n)个数字,因此总的时间复杂度是O(n2)。
要求的是把奇数放在数组的前半部分,偶数放在数组的后半部分,因此所有的奇数应该位于偶数的前面。也就是说我们在扫描这个数组的时候,如果发现有偶数出现在奇数的前面,我们可以交换他们的顺序,交换之后就符合要求了。
因此我们可以维护两个指针,第一个指针初始化为数组的第一个数字,它只向后移动;第二个指针初始化为数组的最后一个数字,它只向前移动。在两个指针相遇之前,第一个指针总是位于第二个指针的前面。如果第一个指针指向的数字是偶数而第二个指针指向的数字是奇数,我们就交换这两个数字。
基于这个思路,我们可以写出如下的代码:
void Reorder(int *pData, unsigned int length, bool (*func)(int));
bool isEven(int n);
/
// Devide an array of integers into two parts, odd in the first part,
// and even in the second part
// Input: pData - an array of integers
// length - the length of array
/
void ReorderOddEven(int *pData, unsigned int length)
{
if(pData == NULL || length == 0)
return;
Reorder(pData, length, isEven);
}
/
// Devide an array of integers into two parts, the intergers which
// satisfy func in the first part, otherwise in the second part
// Input: pData - an array of integers
// length - the length of array
// func - a function
/
void Reorder(int *pData, unsigned int length, bool (*func)(int))
{
if(pData == NULL || length == 0)
return;
int *pBegin = pData;
int *pEnd = pData + length - 1;
while(pBegin < pEnd)
{
// if *pBegin does not satisfy func, move forward
if(!func(*pBegin))
{
pBegin ++;
continue;
}
// if *pEnd does not satisfy func, move backward
if(func(*pEnd))
{
pEnd --;
continue;
}
// if *pBegin satisfy func while *pEnd does not,
// swap these integers
int temp = *pBegin;
*pBegin = *pEnd;
*pEnd = temp;
}
}
/
// Determine whether an integer is even or not
// Input: an integer
// otherwise return false
/
bool isEven(int n)
{
return (n & 1) == 0;
}
讨论:
上面的代码有三点值得提出来和大家讨论:
1.函数isEven判断一个数字是不是偶数并没有用%运算符而是用&。理由是通常情况下位运算符比%要快一些;
2.这道题有很多变种。这里要求是把奇数放在偶数的前面,如果把要求改成:把负数放在非负数的前面等,思路都是都一样的。
3.在函数Reorder中,用函数指针func指向的函数来判断一个数字是不是符合给定的条件,而不是用在代码直接判断(hard code)。这样的好处是把调整顺序的算法和调整的标准分开了(即解耦,decouple)。当调整的标准改变时,Reorder的代码不需要修改,只需要提供一个新的确定调整标准的函数即可,提高了代码的可维护性。例如要求把负数放在非负数的前面,我们不需要修改Reorder的代码,只需添加一个函数来判断整数是不是非负数。这样的思路在很多库中都有广泛的应用,比如在STL的很多算法函数中都有一个仿函数(functor)的参数(当然仿函数不是函数指针,但其思想是一样的)。如果在面试中能够想到这一层,无疑能给面试官留下很好的印象。
程序员面试题精选(49):最长递增子序列
题目描述:设L=1,a2,…,an>是n个不同的实数的序列,L的递增子序列是这样一个子序列Lin=K1,ak2,…,akm>,其中k12m且aK1k2km。求最大的m值
代码实现如下:
#include "stdio.h"
#include
template < class T >
int GetLISLen(T * arr, int n)
{
if (n return 0 ;
int iCurrMaxLen = 0 ;
int left, right, mid;
int * last = new int [n]();
last[ 0 ] = arr[ 0 ];
for ( int i = 1 ; i {
if (arr[i] >= last[iCurrMaxLen])
last[ ++ iCurrMaxLen] = arr[i];
else if (arr[i] last[ 0 ] = arr[i];
else
{
left = 0 ;
right = iCurrMaxLen;
while (left != right - 1 )
{
mid = (left + right) / 2 ;
(last[mid] <= arr[i]) ? (left = mid) : (right = mid);
}
last[right] = arr[i];
} // if
} // for
for ( i = 0 ; i {
printf( " %d " , last[i]);
if (i != iCurrMaxLen)
printf( " \x20 " );
else
printf( " \n " );
}
if (last)
{
delete [] last;
last = 0 ;
}
return iCurrMaxLen + 1 ;
}
int main()
{
int arr[11]={1,2,3,1,2,4,3,7,9,8,10};
GetLISLen(arr,11);
getchar();
return 0;
}
上述求解的最长递增序列不要求连续,下面给出求最长连续递增序列
template < class T >
int FindLongestConIncSubseq( const T * arr, int n, int * pos)
{
int start = 0 , end = 1 ;
int iMaxLen = 1 , iCurrLen = 1 ;
for (end = 1 ; end {
if (arr[end] >= arr[end - 1 ])
{
iCurrLen ++ ;
}
else
{
if (iCurrLen > iMaxLen)
{
iMaxLen = iCurrLen;
start = end - iMaxLen;
}
iCurrLen = 1 ;
}
} // for
if (iCurrLen > iMaxLen)
{
iMaxLen = iCurrLen;
start = end - iMaxLen;
}
* pos = start;
return iMaxLen;
}
程序员面试题精选(50):字符串原地压缩
题目描述:“eeeeeaaaff" 压缩为 "e5a3f2"
代码实现:
#include "stdio.h"
#include "stdlib.h"
void CompressStrInPlace(char* str)
{
char *dest=str;
if ((0 == str) || ('\0' == *str))
{
return ;
}
char* p = str + 1;
char temp[11];
int rep = 0;
char* sNum = 0;
while (*p != '\0')
{
if (*str != *p)
{
*(++str) = *p++;
}
else
{
while (*p == *str)
{
++rep;
++p;
}
if (rep < 10)
{
*(++str) = rep + '0'; // 30h
}
else
{
char* sNum = itoa(rep, temp, 10);
while ((*(++str) = *sNum++) != '\0')
/**//*null*/;
--str;
}
rep = 0;
}//if-else
}//while
*(++str) = '\0';
str=dest;
}
int main()
{
char str[100]="ssssssssssssssssssssssssssssllllllllluyutuuuuuuuu";
CompressStrInPlace(str);
printf("%s",str);
getchar();
return 0;
}
不过有个问题,就是只能对不含数字的字符串进行上述压缩
程序员面试题精选(51):按单词翻转句子
void ReverseString(char *s, int start, int end)
2 {
3 while (start 4 {
5 if (s[start] != s[end])
6 {
7 s[start] ^= s[end];
8 s[end] ^= s[start];
9 s[start] ^= s[end];
10 }
11
12 start++;
13 end--;
14 }
15 }
16
17 void ReverseByWords(char *s, int len, char seperator)
18 {
19 int start = 0, end = 0;
20
21 ReverseString(s, start, len - 1);
22
23 while (end 24 {
25 if (s[end] != seperator)
26 {
27 start = end;
28
29 while (end 30 end++;
31 end--;
32
33 ReverseString(s, start, end);
34 }
35
36 end++;
37
38 }//while
39 }
程序员面试题精选(52):字符串匹配实现(回溯与不回溯算法)
回溯:
const char * MyStrStr( const char * text, const char * pattern)
2 {
3 int i = 0 , j = 0 ;
4
5 while (pattern[i] && text[j])
6 {
7 if (pattern[i] == text[j])
8 {
9 ++ i;
10 ++ j;
11 }
12 else
13 {
14 i = 0 ;
15 j = j - i + 1 ;
16 }
17 } // while
18
19 if ( ! pattern[i])
20 {
21 return text + j - i;
22 }
23 else
24 {
25 return 0 ;
26 }
27 }
不回溯:
#include < cstring >
2 int BM( const char * text, const char * pattern)
3 {
4 int arrShiftTable[ 256 ];
5 int tLen = strlen(text);
6 int pLen = strlen(pattern);
7 int currPos, tReadPos, pReadPos;
8
9 // exclude NUL
10 for ( int i = 1 ; i < sizeof arrShiftTable / sizeof ( int ); i ++ )
11 {
12 arrShiftTable[i] = pLen;
13 }
14
15 // exclude the last char in pattern
16 for ( int i = 0 ; i < pLen - 1 ; i ++ )
17 {
18 arrShiftTable[pattern[i]] = pLen - 1 - i;
19 }
20
21 for (currPos = pLen - 1 ; currPos < tLen; /**/ /* null */ )
22 {
23 for (pReadPos = pLen - 1 , tReadPos = currPos;
24 pReadPos >= 0 && pattern[pReadPos] == text[tReadPos];
25 pReadPos -- , tReadPos -- )
26 /**/ /* null */ ;
27
28 if (pReadPos < 0 )
29 {
30 return tReadPos + 1 ;
31 }
32 else
33 {
34 currPos += arrShiftTable[text[currPos]];
35 }
36
37 } // outer for
38
39 return - 1 ;
40 }
已知strcpy函数的原型是:
char * strcpy(char * strDest,const char * strSrc);
1.不调用库函数,实现strcpy函数。
2.解释为什么要返回char *。
解说:
1.strcpy的实现代码
char * strcpy(char * strDest,const char * strSrc)
{
if ((strDest==NULL)||(strSrc==NULL)) //[1]
throw "Invalid argument(s)"; //[2]
char * strDestCopy=strDest; //[3]
while ((*strDest++=*strSrc++)!='\0'); //[4]
return strDestCopy;
}
错误的做法:
[1]
(A)不检查指针的有效性,说明答题者不注重代码的健壮性。
(B)检查指针的有效性时使用((!strDest)||(!strSrc))或(!(strDest&&strSrc)),说明答题者对C语言中类型的隐式转换没有深刻认识。在本例中char *转换为bool即是类型隐式转换,这种功能虽然灵活,但更多的是导致出错概率增大和维护成本升高。所以C++专门增加了bool、true、false三个关键字以提供更安全的条件表达式。
(C)检查指针的有效性时使用((strDest==0)||(strSrc==0)),说明答题者不知道使用常量的好处。直接使用字面常量(如本例中的0)会减少程序的可维护性。0虽然简单,但程序中可能出现很多处对指针的检查,万一出现笔误,编译器不能发现,生成的程序内含逻辑错误,很难排除。而使用NULL代替0,如果出现拼写错误,编译器就会检查出来。
[2]
(A)return new string("Invalid argument(s)");,说明答题者根本不知道返回值的用途,并且他对内存泄漏也没有警惕心。从函数中返回函数体内分配的内存是十分危险的做法,他把释放内存的义务抛给不知情的调用者,绝大多数情况下,调用者不会释放内存,这导致内存泄漏。
(B)return 0;,说明答题者没有掌握异常机制。调用者有可能忘记检查返回值,调用者还可能无法检查返回值(见后面的链式表达式)。妄想让返回值肩负返回正确值和异常值的双重功能,其结果往往是两种功能都失效。应该以抛出异常来代替返回值,这样可以减轻调用者的负担、使错误不会被忽略、增强程序的可维护性。
[3]
(A)忘记保存原始的strDest值,说明答题者逻辑思维不严密。
[4]
(A)循环写成while (*strDest++=*strSrc++);,同[1](B)。
(B)循环写成while (*strSrc!='\0') *strDest++=*strSrc++;,说明答题者对边界条件的检查不力。循环体结束后,strDest字符串的末尾没有正确地加上'\0'。
2.返回strDest的原始值使函数能够支持链式表达式,增加了函数的“附加值”。同样功能的函数,如果能合理地提高的可用性,自然就更加理想。
链式表达式的形式如:
int iLength=strlen(strcpy(strA,strB));
又如:
char * strA=strcpy(new char[10],strB);
返回strSrc的原始值是错误的。其一,源字符串肯定是已知的,返回它没有意义。其二,不能支持形如第二例的表达式。其三,为了保护源字符串,形参用const限定strSrc所指的内容,把const char *作为char *返回,类型不符,编译报错。
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/10697500/viewspace-545332/,如需转载,请注明出处,否则将追究法律责任。