程序员面试题精选 100 题(39)-颠倒栈
题目:用递归颠倒一个栈。例如输入栈{1, 2, 3, 4, 5},1 在栈顶。颠倒之后的栈为{5, 4, 3, 2, 1},5 处在 栈顶。
分析:乍一看到这道题目,第一反应是把栈里的所有元素逐一 pop 出来,放到一个数组里,然后在数 组里颠倒所有元素,最后把数组中的所有元素逐一 push 进入栈。这时栈也就颠倒过来了。颠倒一个数组是 一件很容易的事情。不过这种思路需要显示分配一个长度为 O(n)的数组,而且也没有充分利用递归的特性。
我们再来考虑怎么递归。
我们把栈{1, 2, 3, 4, 5}看成由两部分组成:
栈顶元素 1 和剩下的部分{2, 3, 4, 5}。
如果我们能把{2, 3, 4, 5}颠倒过来,变成{5, 4, 3, 2},然后在把原来的栈顶元素 1 放到底部,那么就整个栈就 颠倒过来了,变成{5, 4, 3, 2, 1}。
接下来我们需要考虑两件事情:一是如何把 {2, 3, 4, 5}颠倒过来变成{5, 4, 3, 2}。我们只要把{2, 3, 4, 5}
看成由两部分组成:栈顶元素 2 和剩下的部分{3, 4, 5}。我们只要把{3, 4, 5}先颠倒过来变成{5, 4, 3},然后再 把之前的栈顶元素 2 放到最底部,也就变成了{5, 4, 3, 2}。
至于怎么把{3, 4, 5}颠倒过来......很多读者可能都想到这就是递归。
也就是每一次试图颠倒一个栈的时 候,现在栈顶元素 pop 出来,再颠倒剩下的元素组成的栈,最后把之前的栈顶元素放到剩下元素组成的栈 的底部。递归结束的条件是剩下的栈已经空了。这种思路的代码如下:
// Reverse a stack recursively in three steps: // 1. Pop the top element // 2. Reverse the remaining stack // 3. Add the top element to the bottom of the remaining stack template<typename T> void ReverseStack(std::stack<T>& stack) { if(!stack.empty()) { T top = stack.top(); stack.pop(); ReverseStack(stack); AddToStackBottom(stack, top); } }
我 们 需 要考 虑 的 另 外 一 件 事情 是 如 何 把 一 个元 素 e 放 到 一 个栈 的 底 部 , 也 就 是如 何 实 现 AddToStackBottom。这件事情不难,只需要把栈里原有的元素逐一 pop 出来。当栈为空的时候,push 元素 e 进栈,此时它就位于栈的底部了。然后再把栈里原有的元素按照 pop 相反的顺序逐一 push 进栈。
注意到我们在 push 元素 e 之前,我们已经把栈里原有的所有元素都 pop 出来了,我们需要把它们保 存起来,以便之后能把他们再 push 回去。我们当然可以开辟一个数组来做,但这没有必要。由于我们可以 用递归来做这件事情,而递归本身就是一个栈结构。我们可以用递归的栈来保存这些元素。
基于如上分析,我们可以写出 AddToStackBottom 的代码:
// Add an element to the bottom of a stack: template<typename T> void AddToStackBottom(std::stack<T>& stack, T t) { if(stack.empty()) { stack.push(t); } else { T top = stack.top(); stack.pop(); AddToStackBottom(stack, t); stack.push(top); } }
程序员面试题精选 100 题(46)-对称子字符串的最大长度
题目:输入一个字符串,输出该字符串中对称的子字符串的最大长度。比如输入字 符串“google” ,由于该字符串里最长的对称子字符串是“goog” ,因此输出 4。
分析:可能很多人都写过判断一个字符串是不是对称的函数,这个题目可以看成是 该函数的加强版。
引子:判断字符串是否对称
要判断一个字符串是不是对称的,不是一件很难的事情。我们可以先得到字符串首 尾两个字符,判断是不是相等。如果不相等,那该字符串肯定不是对称的。否则我们接着判断里面的两个字符是不是相等,以此类推。基于这个思路,我们不难写出如下代码:
//////////////////////////////////////////////////////////////// // Whether a string between pBegin and pEnd is symmetrical? //////////////////////////////////////////////////////////////// bool IsSymmetrical(char* pBegin, char* pEnd) { if(pBegin == NULL || pEnd == NULL || pBegin > pEnd) return false; while(pBegin < pEnd) { if(*pBegin != *pEnd) return false; pBegin++; pEnd --; } return true; }
要判断一个字符串 pString 是不是对称的,我们只需要调用 IsSymmetrical(pString, &pString[strlen(pString) – 1])就可以了。
解法一:O (n^3 )的算法
现在我们试着来得到对称子字符串的最大长度。最直观的做法就是得到输入字符串的所有子字符串,并逐个判断是不是对称的。如果一个子字符串是对称的,我们就得到它的 长度。这样经过比较,就能得到最长的对称子字符串的长度了。于是,我们可以写出如下代 码:
//////////////////////////////////////////////////////////////// // Get the longest length of its all symmetrical substrings // Time needed is O(T^3) //////////////////////////////////////////////////////////////// int GetLongestSymmetricalLength_1(char* pString) { if(pString == NULL) return 0; int symmeticalLength = 1; char* pFirst = pString; int length = strlen(pString); while(pFirst < &pString[length - 1]) { char* pLast = pFirst + 1; while(pLast <= &pString[length - 1]) { if(IsSymmetrical(pFirst, pLast)) { int newLength = pLast - pFirst + 1; if(newLength > symmeticalLength) symmeticalLength = newLength; } pLast++; } pFirst++; } return symmeticalLength; }
我们来分析一下上述方法的时间效率。由于我们需要两重 while 循环,每重循环需要 O(n)的时间。另外,我们在循环中调用了 IsSymmetrical,每次调用也需要 O(n)的时 间。因此整个函数的时间效率是 O(n3 )。
通常 O(n 3)不会是一个高效的算法。如果我们仔细分析上述方法的比较过程,我们就能发现其中有很多重复的比较。假设我们需要判断一个子字符串具有 aAa 的形式(A 是 aAa 的子字符串,可能含有多个字符) 。我们先把 pFirst 指向最前面的字符 a,把 pLast 指向 最后面的字符 a,由于两个字符相同,我们在 IsSymtical 函数内部向后移动 pFirst,向前移动 pLast,以判断 A 是不是对称的。接下来若干步骤之后,由于 A 也是输入字符串的一个子字 符串,我们需要再一次判断它是不是对称的。也就是说,我们重复多次地在判断 A 是不是 对称的。
造成上述重复比较的根源在于 IsSymmetrical 的比较是从外向里进行的。在判断 aAa 是不是对称的时候,我们不知道 A 是不是对称的,因此需要花费 O(n)的时间来判断。下 次我们判断 A 是不是对称的时候,我们仍然需要 O(n)的时间。
解法二:O (n^2 )的算法
如果我们换一种思路,我们从里向外来判断。也就是我们先判断子字符串 A 是不是 对称的。如果 A 不是对称的,那么向该子字符串两端各延长一个字符得到的字符串肯定不 是对称的。如果 A 对称,那么我们只需要判断 A 两端延长的一个字符是不是相等的,如果 相等,则延长后的字符串是对称的。因此在知道 A 是否对称之后,只需要 O(1)的时间就 能知道 aAa 是不是对称的。
我们可以根据从里向外比较的思路写出如下代码:
//////////////////////////////////////////////////////////////// // Get the longest length of its all symmetrical substrings // Time needed is O(T^2) //////////////////////////////////////////////////////////////// int GetLongestSymmetricalLength_2(char* pString) { if(pString == NULL) return 0; int symmeticalLength = 1; char* pChar = pString; while(*pChar != '\0') { // Substrings with odd length char* pFirst = pChar - 1; char* pLast = pChar + 1; while(pFirst >= pString && *pLast != '\0' && *pFirst == *pLast) { pFirst--; pLast++; } int newLength = pLast - pFirst - 1; if(newLength > symmeticalLength) symmeticalLength = newLength; // Substrings with even length pFirst = pChar; pLast = pChar + 1; while(pFirst >= pString && *pLast != '\0' && *pFirst == *pLast) { pFirst--; pLast++; } newLength = pLast - pFirst - 1; if(newLength > symmeticalLength) symmeticalLength = newLength; pChar++; } return symmeticalLength; }
由于子字符串的长度可能是奇数也可能是偶数。长度是奇数的字符串是从只有一个 字符的中心向两端延长出来, 而长度为偶数的字符串是从一个有两个字符的中心向两端延长 出来。因此我们的代码要把这种情况都考虑进去。 在上述代码中,我们从字符串的每个字符串两端开始延长,如果当前的子字符串是 对称的,再判断延长之后的字符串是不是对称的。由于总共有 O(n)个字符,每个字符可 能延长 O(n)次,每次延长时只需要 O(1)就能判断出是不是对称的,因此整个函数的时 间效率是 O(n^2 ) 。
程序员面试题精选 100 题(61)-数对之差的最大值
题目:在数组中,数字减去它右边的数字得到一个数对之差。求所有数对之差的最大值。
例如在数组{2, 4, 1, 16, 7, 5, 11, 9} 中,数对之差的最大值是 11,是 16 减去 5 的结果。
分析:看到这个题目,很多人的第一反应是找到这个数组的最大值和最小值,然后觉得最大 值减去最小值就是最终的结果。 这种思路忽略了题目中很重要的一点: 数对之差是一个数字 减去它右边的数字。由于我们无法保证最大值一定位于数组的左边,因此这个思路不管用。
于是我们接下来可以想到让每一个数字逐个减去它右边的所有数字,并通过比较得到数对之差的最大值。 由于每个数字需要和它后面的 O(n)个数字作减法, 因此总的时间复杂 度是 O(n^2)。
解法一:分治法
通常蛮力法不会是最好的解法,我们想办法减少减法的次数。假设我们把数组分成 两个子数组, 我们其实没有必要拿左边的子数组中较小的数字去和右边的子数组中较大的数
字作减法。我们可以想象,数对之差的最大值只有可能是下面三种情况之一: (1)被减数和减数都在第一个子数组中,即第一个子数组中的数对之差的最大值; (2)被减数和减数都在第二个子数组中,即第二个子数组中数对之差的最大值; (3)被减数在第一个子数组中,是第一个子数组的最大值。减数在第二个子数组中,是第二个子数组的最小值。
这三个差值的最大者就是整个数组中数对之差的最大值。 在前面提到的三种情况中,得到第一个子数组的最大值和第二子数组的最小值不是 一件难事,但如何得到两个子数组中的数对之差的最大值?这其实是原始问题的子问题,
我 们可以递归地解决。下面是这种思路的参考代码:
int MaxDiff_Solution1(int numbers[], unsigned length) { if(numbers == NULL && length < 2) return 0; int max, min; return MaxDiffCore(numbers, numbers + length - 1, &max, &min); } int MaxDiffCore(int* start, int* end, int* max, int* min) { if(end == start) { *max = *min = *start; return 0x80000000; } int* middle = start + (end - start) / 2; int maxLeft, minLeft; int leftDiff = MaxDiffCore(start, middle, &maxLeft, &minLeft); int maxRight, minRight; int rightDiff = MaxDiffCore(middle + 1, end, &maxRight, &minRight); int crossDiff = maxLeft - minRight; *max = (maxLeft > maxRight) ? maxLeft : maxRight; *min = (minLeft < minRight) ? minLeft : minRight; int maxDiff = (leftDiff > rightDiff) ? leftDiff : rightDiff; maxDiff = (maxDiff > crossDiff) ? maxDiff : crossDiff; return maxDiff; }
在函数 MaxDiffCore 中,我们先得到第一个子数组中的最大的数对之差 leftDiff,再得到第二个子数组中的最大数对之差 rightDiff。接下来用第一个子数组的最大值减去第二个子数组的最小值得到 crossDiff 。这三者的最大值就是整个数组的最大数对之差。
解法二:转化成求解子数组的最大和问题
接下来再介绍一种比较巧妙的解法。如果输入一个长度为 n 的 数 组 numbers , 我 们 先 构 建 一 个 长 度 为 n-1 的 辅 助 数 组 diff , 并 且 diff[i] 等 于 numbers[i]-numbers[i+1](0<=i<n-1) 。如果我们从数组 diff 中的第 i 个数字一直累加到第 j 个 数字(j > i) ,也就是 diff[i] + diff[i+1] + ... + diff[j] = (numbers[i]-numbers[i+1]) + (numbers[i + 1]-numbers[i+2]) + ... + (numbers[j] – numbers[j + 1]) = numbers[i] – numbers[j + 1]。
分析到这里,我们发现原始数组中最大的数对之差(即 numbers[i] – numbers[j + 1])
其实是辅助数组 diff 中最大的连续子数组之和。我们在本系列的博客的第 3 篇《求子数组的 最大和》中已经详细讨论过这个问题的解决方法。基于这个思路,我们可以写出如下代码:
int MaxDiff_Solution2(int numbers[], unsigned length) { if(numbers == NULL && length < 2) return 0; int* diff = new int[length - 1]; for(int i = 1; i < length; ++i) diff[i - 1] = numbers[i - 1] - numbers[i]; int currentSum = 0; int greatestSum = 0x80000000; for(int i = 0; i < length - 1; ++i) { if(currentSum <= 0) currentSum = diff[i]; else currentSum += diff[i]; if(currentSum > greatestSum) greatestSum = currentSum; } delete[] diff; return greatestSum; }
解法三:动态规划法
既然我们可以把求最大的数对之差转换成求子数组的最大和,而子数组的最大和可 以通过动态规划求解, 那我们是不是可以通过动态规划直接求解呢?下面我们试着用动态规 划法直接求数对之差的最大值。
我们定义 diff[i]是以数组中第 i 个数字为减数的所有数对之差的最大值。也就是说对 于任意 h(h < i) ,diff[i]≥number[h]-number[i]。diff[i](0≤i<n)的最大值就是整个数组最大 的数对之差。
假设我们已经求得了 diff[i],我们该怎么求得 diff[i+1]呢?对于 diff[i],肯定存在一个 h(h < i) ,满足 number[h]减去 number[i]之差是最大的,也就是 number[h]应该是 number[i] 之前的所有数字的最大值。当我们求 diff[i+1]的时候,我们需要找到第 i+1 个数字之前的最大值。 i+1 个数字之前的最大值有两种可能:
这个最大值可能是第 i 个数字之前的最大值, 也有可能这个最大值就是第 i 个数字。第 i+1 个数字之前的最大值肯定是这两者的较大者。 我们只要拿第 i+1 个数字之前的最大值减去 number[i+1],就得到了 diff[i+1]。
int MaxDiff_Solution3(int numbers[], unsigned length) { if(numbers == NULL && length < 2) return 0; int max = numbers[0]; int maxDiff = max - numbers[1]; for(int i = 2; i < length; ++i) { if(numbers[i - 1] > max) max = numbers[i - 1]; int currentDiff = max - numbers[i]; if(currentDiff > maxDiff) maxDiff = currentDiff; } return maxDiff; }
在上述代码中,max 表示第 i 个数字之前的最大值,而 currentDiff 表示 diff[i] (0≤ i<n) ,diff[i]的最大值就是代码中 maxDiff。
解法小结
上述三种代码,虽然思路各不相同,但时间复杂度都是 O(n) 。我们也可以注意到第 一种方法是基于递归实现,而递归调用是有额外的时间、空间消耗的(比如在调用栈上分配 空间保存参数、临时变量等) 。第二种方法需要一个长度为 n-1 的辅助数组,因此其空间复 杂度是 O(n) 。第三种方法则没有额外的时间、空间开销,并且它的代码是最简洁的,因此这 是最值得推荐的一种解法。
题目:一个数组中有三个数字 a、b、c 只出现一次,其他数字都出现了两次。请找出三个只出现一次 的数字。
分析:在博客http://zhedahht.blog.163.com/blog/static/2541117420071128950682/中我们讨论了如何在一个数组中找出两个只出现一次的数字。在这道题中,如果我们能够找出一个只出现一次的数字,剩下两个只出现一次的数字就很容易找出来了。
如果我们把数组中所有数字都异或起来,那最终的结果(记为x)就是a、b、c三个数字的异或结果(x=a^b^c)。其他出现了两次的数字在异或运算中相互抵消了。
我们可以证明异或的结果x不可能是a、b、c三个互不相同的数字中的任何一个。我们用反证法证明。假设x等于a、b、c中的某一个。比如x等于a,也就是a=a^b^c。因此b^c等于0,即b等于c。这与a、b、c是三个互不相同的三个数相矛盾。
由于x与a、b、c都各不相同,因此x^a、x^b、x^c都不等于0。
我们定义一个函数f(n),它的结果是保留数字n的二进制表示中的最后一位1,而把其他所有位都变成0。比如十进制6表示成二进制是0110,因此f(6)的结果为2(二进制为0010)。f(x^a)、f(x^b)、f(x^c)的结果均不等于0。
接着我们考虑f(x^a)^f(x^b)^f(x^c)的结果。由于对于非0的n,f(n)的结果的二进制表示中只有一个数位是1,因此f(x^a)^f(x^b)^f(x^c)的结果肯定不为0。这是因为对于任意三个非零的数i、j、k,f(i)^f(j)的结果要么为0,要么结果的二进制结果中有两个1。不管是那种情况,f(i)^f(j)都不可能等于f(k),因为f(k)不等于0,并且结果的二进制中只有一位是1。
于是f(x^a)^f(x^b)^f(x^c)的结果的二进制中至少有一位是1。假设最后一位是1的位是第m位。那么x^a、x^b、x^c的结果中,有一个或者三个数字的第m位是1。
接下来我们证明x^a、x^b、x^c的三个结果第m位不可能都是1。还是用反证法证明。如果x^a、x^b、x^c的第m位都是1,那么a、b、c三个数字的第m位和x的第m位都相反,因此a、b、c三个数字的第m位相同。如果a、b、c三个数字的第m位都是0,x=a^b^c结果的第m位是0。由于x和a两个数字的第m位都是0,x^a结果的第m位应该是0。同理可以证明x^b、x^c第m位都是0。这与我们的假设矛盾。如果a、b、c三个数字的第m位都是1,x=a^b^c结果的第m位是1。由于x和a两个数字的第m位都是1,x^a结果的第m位应该是0。同理可以证明x^b、x^c第m位都是0。这还是与我们的假设矛盾。
因此x^a、x^b、x^c三个数字中,只有一个数字的第m位是1。于是我们找到了能够区分a、b、c三个数字的标准。这三个数字中,只有一个数字满足这个标准,而另外两个数字不满足。一旦这个满足标准数字找出来之后,另外两个数字也就可以找出来了。
这种思路的 C++代码如下:
void getThreeUnique(vector<int>& numbers, vector<int>& unique) { if(numbers.size() < 3) return; int xorResult = 0; vector<int>::iterator iter = numbers.begin(); for(; iter != numbers.end(); ++iter) xorResult ^= *iter; int flags = 0; for(iter = numbers.begin(); iter != numbers.end(); ++iter) flags ^= lastBitOf1(xorResult ^ *iter); flags = lastBitOf1(flags); // get the first unique number int first = 0; for(iter = numbers.begin(); iter != numbers.end(); ++iter) { if(lastBitOf1(*iter ^ xorResult) == flags) first ^= *iter; } unique.push_back(first); // move the first unique number to the end of array for(iter = numbers.begin(); iter != numbers.end(); ++iter) { if(*iter == first) { swap(*iter, *(numbers.end() - 1)); break; } } // get the second and third unique numbers getTwoUnique(numbers.begin(), numbers.end() - 1, unique); } int lastBitOf1(int number) { return number & ~(number - 1); } void getTwoUnique(vector<int>::iterator begin, vector<int>::iterator end, vector<int>& unique) { int xorResult = 0; for(vector<int>::iterator iter = begin; iter != end; ++iter) xorResult ^= *iter; int diff = lastBitOf1(xorResult); int first = 0; int second = 0; for(vector<int>::iterator iter = begin; iter != end; ++iter) { if(diff & *iter) first ^= *iter; else second ^= *iter; } unique.push_back(first); unique.push_back(second); }
上文中 getThreeUnique 从数组中找出三个只出现一次的数字,而 getTwoUnique 从数组中找出两个只 出现一次的数字。lastBitOf1 实现分析中的函数 f(n)的功能,它只保留数字 n 的二进制表示中的最后一 位 1,而把其他所有位都变成 0。
在函数 getThreeUnique 中,我们通过第一个 for 循环把 a、b、c 三个数字异或的结果保存到 xorResult 中, 接着在第二个 for 循环中求出 f(x^a)^f(x^b)^f(x^c)并保存到变量 flags 中。
在语句 flags=lastBitOf1(flags) 求出 f(x^a)^f(x^b)^f(x^c)结果的二进制中最后一位是 1 的位。并根据这一数位求出第一个只出现一次 的数字 first。接着把 first 交换到数组的最后,并在数组的前 n-1 个数字中求出另外两个只出现一次的 数字。