声明:
这篇博客的某些题目和答案成果源自于July和何海涛的博客,网址:点击打开链接 点击打开链接
本人只是针对自己情况,把感兴趣的题目都罗列出来;针对其他的题目(不是来自上面两位),写出了自己的算法,仅供自己慢慢学习和品味。如有问题,请在博客下面留言。
11 题目:在一个字符串中找到第一个只出现一次的字符。如输入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[i] = 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;
}
12 题目:写一个函数,求两个整数的之和,要求在函数体内不得使用+、-、×、÷。
分析:这又是一道考察发散思维的很有意思的题目。当我们习以为常的东西被限制使用的时候,如何突破常规去思考,就是解决这个问题的关键所在。
看到的这个题目,我的第一反应是傻眼了,四则运算都不能用,那还能用什么啊?可是问题总是要解决的,只能打开思路去思考各种可能性。首先我们可以分析人们是如何做十进制的加法的,比如是如何得出5+17=22这个结果的。实际上,我们可以分成三步的:第一步只做各位相加不进位,此时相加的结果是12(个位数5和7相加不要进位是2,十位数0和1相加结果是1);第二步做进位,5+7中有进位,进位的值是10;第三步把前面两个结果加起来,12+10的结果是22,刚好5+17=22。
前面我们就在想,求两数之和四则运算都不能用,那还能用什么啊?对呀,还能用什么呢?对数字做运算,除了四则运算之外,也就只剩下位运算了。位运算是针对二进制的,我们也就以二进制再来分析一下前面的三步走策略对二进制是不是也管用。
5的二进制是101,17的二进制10001。还是试着把计算分成三步:第一步各位相加但不计进位,得到的结果是10100(最后一位两个数都是1,相加的结果是二进制的10。这一步不计进位,因此结果仍然是0);第二步记下进位。在这个例子中只在最后一位相加时产生一个进位,结果是二进制的10;第三步把前两步的结果相加,得到的结果是10110,正好是22。由此可见三步走的策略对二进制也是管用的。
接下来我们试着把二进制上的加法用位运算来替代。第一步不考虑进位,对每一位相加。0加0与1加1的结果都0,0加1与1加0的结果都是1。我们可以注意到,这和异或的结果是一样的。对异或而言,0和0、1和1异或的结果是0,而0和1、1和0的异或结果是1。接着考虑第二步进位,对0加0、0加1、1加0而言,都不会产生进位,只有1加1时,会向前产生一个进位。此时我们可以想象成是两个数先做位与运算,然后再向左移动一位。只有两个数都是1的时候,位与得到的结果是1,其余都是0。第三步把前两个步骤的结果相加。如果我们定义一个函数AddWithoutArithmetic,第三步就相当于输入前两步骤的结果来递归调用自己。
有了这些分析之后,就不难写出如下的代码了:
int AddWithoutArithmetic(int num1, int num2)
{
if(num2 == 0)
return num1;
int sum = num1 ^ num2;
int carry = (num1 & num2) << 1;
return AddWithoutArithmetic(sum, carry);
}
13 题目:输入一个链表的头结点,反转该链表,并返回反转后链表的头结点。链表结点定义如下:
struct ListNode
{
int m_nKey;
ListNode* m_pNext;
};
为了正确地反转一个链表,需要调整指针的指向。与指针操作相关代码总是容易出错的,因此最好在动手写程序之前作全面的分析。在面试的时候不急于动手而是一开始做仔细的分析和设计,将会给面试官留下很好的印象,因为在实际的软件开发中,设计的时间总是比写代码的时间长。与其很快地写出一段漏洞百出的代码,远不如用较多的时间写出一段健壮的代码。
为了将调整指针这个复杂的过程分析清楚,我们可以借助图形来直观地分析。假设下图中l、m和n是三个相邻的结点:
a?b?…?l mànà…
假设经过若干操作,我们已经把结点l之前的指针调整完毕,这些结点的m_pNext指针都指向前面一个结点。现在我们遍历到结点m。当然,我们需要把调整结点的m_pNext指针让它指向结点l。但注意一旦调整了指针的指向,链表就断开了,如下图所示:
a?b?…l?m nà…
因为已经没有指针指向结点n,我们没有办法再遍历到结点n了。因此为了避免链表断开,我们需要在调整m的m_pNext之前要把n保存下来。
接下来我们试着找到反转后链表的头结点。不难分析出反转后链表的头结点是原始链表的尾位结点。什么结点是尾结点?就是m_pNext为空指针的结点。
基于上述分析,我们不难写出如下代码:
///
// Reverse a list iteratively
// Input: pHead - the head of the original list
// Output: the head of the reversed head
///
ListNode* ReverseIteratively(ListNode* pHead)
{
ListNode* pReversedHead = NULL;
ListNode* pNode = pHead;
ListNode* pPrev = NULL;
while(pNode != NULL)
{
// get the next node, and save it at pNext
ListNode* pNext = pNode->m_pNext;
// if the next node is null, the currect is the end of original
// list, and it's the head of the reversed list
if(pNext == NULL)
pReversedHead = pNode;
// reverse the linkage between nodes
pNode->m_pNext = pPrev;
// move forward on the the list
pPrev = pNode;
pNode = pNext;
}
return pReversedHead;
}
14 题目:一个台阶总共有n级,如果一次可以跳1级,也可以跳2级。求总共有多少总跳法,并分析算法的时间复杂度。
分析:这道题最近经常出现,包括MicroStrategy等比较重视算法的公司都曾先后选用过个这道题作为面试题或者笔试题。
首先我们考虑最简单的情况。如果只有1级台阶,那显然只有一种跳法。如果有2级台阶,那就有两种跳的方法了:一种是分两次跳,每次跳1级;另外一种就是一次跳2级。
现在我们再来讨论一般情况。我们把n级台阶时的跳法看成是n的函数,记为f(n)。当n>2时,第一次跳的时候就有两种不同的选择:一是第一次只跳1级,此时跳法数目等于后面剩下的n-1级台阶的跳法数目,即为f(n-1);另外一种选择是第一次跳2级,此时跳法数目等于后面剩下的n-2级台阶的跳法数目,即为f(n-2)。因此n级台阶时的不同跳法的总数f(n)=f(n-1)+(f-2)。
我们把上面的分析用一个公式总结如下:
/ 1 n=1
f(n)= 2 n=2
\ f(n-1)+(f-2) n>2
分析到这里,相信很多人都能看出这就是我们熟悉的Fibonacci序列。
15 题目:输入一个正数n,输出所有和为n连续正数序列。
例如输入15,由于1+2+3+4+5=4+5+6=7+8=15,所以输出3个连续序列1-5、4-6和7-8。
分析:这是网易的一道面试题。
这道题和本面试题系列的第10题有些类似。我们用两个数small和big分别表示序列的最小值和最大值。首先把small初始化为1,big初始化为2。如果从small到big的序列的和大于n的话,我们向右移动small,相当于从序列中去掉较小的数字。如果从small到big的序列的和小于n的话,我们向右移动big,相当于向序列中添加big的下一个数字。一直到small等于(1+n)/2,因为序列至少要有两个数字。
基于这个思路,我们可以写出如下代码:
void PrintContinuousSequence(int small, int big);
/
// Find continuous sequence, whose sum is n
/
void FindContinuousSequence(int n)
{
if(n < 3)
return;
int small = 1;
int big = 2;
int middle = (1 + n) / 2;
int sum = small + big;
while(small < middle)
{
// we are lucky and find the sequence
if(sum == n)
PrintContinuousSequence(small, big);
// if the current sum is greater than n,
// move small forward
while(sum > n)
{
sum -= small;
small ++;
// we are lucky and find the sequence
if(sum == n)
PrintContinuousSequence(small, big);
}
// move big forward
big ++;
sum += big;
}
}
/
// Print continuous sequence between small and big
/
void PrintContinuousSequence(int small, int big)
{
for(int i = small; i <= big; ++ i)
printf("%d ", i);
printf("\n");
}
16 题目:输入一个链表的头结点,从尾到头反过来输出每个结点的值。
链表结点定义如下:
struct ListNode
{
int m_nKey;
ListNode* m_pNext;
};
分析:这是一道很有意思的面试题。该题以及它的变体经常出现在各大公司的面试、笔试题中。
看到这道题后,第一反应是从头到尾输出比较简单。于是很自然地想到把链表中链接结点的指针反转过来,改变链表的方向。然后就可以从头到尾输出了。反转链表的算法详见本人面试题精选系列的第19题,在此不再细述。但该方法需要额外的操作,应该还有更好的方法。
接下来的想法是从头到尾遍历链表,每经过一个结点的时候,把该结点放到一个栈中。当遍历完整个链表后,再从栈顶开始输出结点的值,此时输出的结点的顺序已经反转过来了。该方法需要维护一个额外的栈,实现起来比较麻烦。
既然想到了栈来实现这个函数,而递归本质上就是一个栈结构。于是很自然的又想到了用递归来实现。要实现反过来输出链表,我们每访问到一个结点的时候,先递归输出它后面的结点,再输出该结点自身,这样链表的输出结果就反过来了。
基于这样的思路,不难写出如下代码:
///
// Print a list from end to beginning
// Input: pListHead - the head of list
///
void PrintListReversely(ListNode* pListHead)
{
if(pListHead != NULL)
{
// Print the next node first
if (pListHead->m_pNext != NULL)
{
PrintListReversely(pListHead->m_pNext);
}
// Print this node
printf("%d", pListHead->m_nKey);
}
}
扩展:该题还有两个常见的变体:
1. 从尾到头输出一个字符串;
2. 定义一个函数求字符串的长度,要求该函数体内不能声明任何变量。
struct ListNode
{
int m_nKey;
ListNode* m_pNext;
};
函数的声明如下:
void DeleteNode(ListNode* pListHead, ListNode* pToBeDeleted);
分析:这是一道广为流传的Google面试题,能有效考察我们的编程基本功,还能考察我们的反应速度,更重要的是,还能考察我们对时间复杂度的理解。
在链表中删除一个结点,最常规的做法是从链表的头结点开始,顺序查找要删除的结点,找到之后再删除。由于需要顺序查找,时间复杂度自然就是O(n) 了。
我们之所以需要从头结点开始查找要删除的结点,是因为我们需要得到要删除的结点的前面一个结点。我们试着换一种思路。我们可以从给定的结点得到它的下一个结点。这个时候我们实际删除的是它的下一个结点,由于我们已经得到实际删除的结点的前面一个结点,因此完全是可以实现的。当然,在删除之前,我们需要需要把给定的结点的下一个结点的数据拷贝到给定的结点中。此时,时间复杂度为O(1)。
上面的思路还有一个问题:如果删除的结点位于链表的尾部,没有下一个结点,怎么办?我们仍然从链表的头结点开始,顺便遍历得到给定结点的前序结点,并完成删除操作。这个时候时间复杂度是O(n)。
那题目要求我们需要在O(1)时间完成删除操作,我们的算法是不是不符合要求?实际上,假设链表总共有n个结点,我们的算法在n-1总情况下时间复杂度是O(1),只有当给定的结点处于链表末尾的时候,时间复杂度为O(n)。那么平均时间复杂度[(n-1)*O(1)+O(n)]/n,仍然为O(1)。
基于前面的分析,我们不难写出下面的代码。
参考代码:
///
// Delete a node in a list
// Input: pListHead - the head of list
// pToBeDeleted - the node to be deleted
///
void DeleteNode(ListNode* pListHead, ListNode* pToBeDeleted)
{
if(!pListHead || !pToBeDeleted)
return;
// if pToBeDeleted is not the last node in the list
if(pToBeDeleted->m_pNext != NULL)
{
// copy data from the node next to pToBeDeleted
ListNode* pNext = pToBeDeleted->m_pNext;
pToBeDeleted->m_nKey = pNext->m_nKey;
pToBeDeleted->m_pNext = pNext->m_pNext;
// delete the node next to the pToBeDeleted
delete pNext;
pNext = NULL;
}
// if pToBeDeleted is the last node in the list
else
{
// get the node prior to pToBeDeleted
ListNode* pNode = pListHead;
while(pNode->m_pNext != pToBeDeleted)
{
pNode = pNode->m_pNext;
}
// deleted pToBeDeleted
pNode->m_pNext = NULL;
delete pToBeDeleted;
pToBeDeleted = NULL;
}
}
值得注意的是,为了让代码看起来简洁一些,上面的代码基于两个假设:(1)给定的结点的确在链表中;(2)给定的要删除的结点不是链表的头结点。不考虑第一个假设对代码的鲁棒性是有影响的。至于第二个假设,当整个列表只有一个结点时,代码会有问题。但这个假设不算很过分,因为在有些链表的实现中,会创建一个虚拟的链表头,并不是一个实际的链表结点。这样要删除的结点就不可能是链表的头结点了。当然,在面试中,我们可以把这些假设和面试官交流。这样,面试官还是会觉得我们考虑问题很周到的。
18 题目:一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
分析:这是一道很新颖的关于位运算的面试题。
首先我们考虑这个问题的一个简单版本:一个数组里除了一个数字之外,其他的数字都出现了两次。请写程序找出这个只出现一次的数字。
这个题目的突破口在哪里?题目为什么要强调有一个数字出现一次,其他的出现两次?我们想到了异或运算的性质:任何一个数字异或它自己都等于0。也就是说,如果我们从头到尾依次异或数组中的每一个数字,那么最终的结果刚好是那个只出现依次的数字,因为那些出现两次的数字全部在异或中抵消掉了。
有了上面简单问题的解决方案之后,我们回到原始的问题。如果能够把原数组分为两个子数组。在每个子数组中,包含一个只出现一次的数字,而其他数字都出现两次。如果能够这样拆分原数组,按照前面的办法就是分别求出这两个只出现一次的数字了。
我们还是从头到尾依次异或数组中的每一个数字,那么最终得到的结果就是两个只出现一次的数字的异或结果。因为其他数字都出现了两次,在异或中全部抵消掉了。由于这两个数字肯定不一样,那么这个异或结果肯定不为0,也就是说在这个结果数字的二进制表示中至少就有一位为1。我们在结果数字中找到第一个为1的位的位置,记为第N位。现在我们以第N位是不是1为标准把原数组中的数字分成两个子数组,第一个子数组中每个数字的第N位都为1,而第二个子数组的每个数字的第N位都为0。
现在我们已经把原数组分成了两个子数组,每个子数组都包含一个只出现一次的数字,而其他数字都出现了两次。因此到此为止,所有的问题我们都已经解决。
基于上述思路,我们不难写出如下代码:
///
// Find two numbers which only appear once in an array
// Input: data - an array contains two number appearing exactly once,
// while others appearing exactly twice
// length - the length of data
// Output: num1 - the first number appearing once in data
// num2 - the second number appearing once in data
///
void FindNumsAppearOnce(int data[], int length, int &num1, int &num2)
{
if (length < 2)
return;
// get num1 ^ num2
int resultExclusiveOR = 0;
for (int i = 0; i < length; ++ i)
resultExclusiveOR ^= data[i];
// get index of the first bit, which is 1 in resultExclusiveOR
unsigned int indexOf1 = FindFirstBitIs1(resultExclusiveOR);
num1 = num2 = 0;
for (int j = 0; j < length; ++ j)
{
// divide the numbers in data into two groups,
// the indexOf1 bit of numbers in the first group is 1,
// while in the second group is 0
if(IsBit1(data[j], indexOf1))
num1 ^= data[j];
else
num2 ^= data[j];
}
}
///
// Find the index of first bit which is 1 in num (assuming not 0)
///
unsigned int FindFirstBitIs1(int num)
{
int indexBit = 0;
while (((num & 1) == 0) && (indexBit < 32))
{
num = num >> 1;
++ indexBit;
}
return indexBit;
}
///
// Is the indexBit bit of num 1?
///
bool IsBit1(int num, unsigned int indexBit)
{
num = num >> indexBit;
return (num & 1);
}
19 题目:我们把只包含因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含因子7。习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第1500个丑数。
分析:这是一道在网络上广为流传的面试题,据说google曾经采用过这道题。
所谓一个数m是另一个数n的因子,是指n能被m整除,也就是n % m == 0。根据丑数的定义,丑数只能被2、3和5整除。也就是说如果一个数如果它能被2整除,我们把它连续除以2;如果能被3整除,就连续除以3;如果能被5整除,就除以连续5。如果最后我们得到的是1,那么这个数就是丑数,否则不是。
基于前面的分析,我们可以写出如下的函数来判断一个数是不是丑数:
bool IsUgly(int number)
{
while(number % 2 == 0)
number /= 2;
while(number % 3 == 0)
number /= 3;
while(number % 5 == 0)
number /= 5;
return (number == 1) ? true : false;
}
接下来,我们只需要按顺序判断每一个整数是不是丑数,即:
int GetUglyNumber_Solution1(int index)
{
if(index <= 0)
return 0;
int number = 0;
int uglyFound = 0;
while(uglyFound < index)
{
++number;
if(IsUgly(number))
{
++uglyFound;
}
}
return number;
}
我们只需要在函数GetUglyNumber_Solution1中传入参数1500,就能得到第1500个丑数。该算法非常直观,代码也非常简洁,但最大的问题我们每个整数都需要计算。即使一个数字不是丑数,我们还是需要对它做求余数和除法操作。因此该算法的时间效率不是很高。
接下来我们换一种思路来分析这个问题,试图只计算丑数,而不在非丑数的整数上花费时间。根据丑数的定义,丑数应该是另一个丑数乘以2、3或者5的结果(1除外)。因此我们可以创建一个数组,里面的数字是排好序的丑数。里面的每一个丑数是前面的丑数乘以2、3或者5得到的。
这种思路的关键在于怎样确保数组里面的丑数是排好序的。我们假设数组中已经有若干个丑数,排好序后存在数组中。我们把现有的最大丑数记做M。现在我们来生成下一个丑数,该丑数肯定是前面某一个丑数乘以2、3或者5的结果。我们首先考虑把已有的每个丑数乘以2。在乘以2的时候,能得到若干个结果小于或等于M的。由于我们是按照顺序生成的,小于或者等于M肯定已经在数组中了,我们不需再次考虑;我们还会得到若干个大于M的结果,但我们只需要第一个大于M的结果,因为我们希望丑数是按从小到大顺序生成的,其他更大的结果我们以后再说。我们把得到的第一个乘以2后大于M的结果,记为M2。同样我们把已有的每一个丑数乘以3和5,能得到第一个大于M的结果M3和M5。那么下一个丑数应该是M2、M3和M5三个数的最小者。
前面我们分析的时候,提到把已有的每个丑数分别都乘以2、3和5,事实上是不需要的,因为已有的丑数是按顺序存在数组中的。对乘以2而言,肯定存在某一个丑数T2,排在它之前的每一个丑数乘以2得到的结果都会小于已有最大的丑数,在它之后的每一个丑数乘以2得到的结果都会太大。我们只需要记下这个丑数的位置,同时每次生成新的丑数的时候,去更新这个T2。对乘以3和5而言,存在着同样的T3和T5。
有了这些分析,我们不难写出如下的代码:
int GetUglyNumber_Solution2(int index)
{
if(index <= 0)
return 0;
int *pUglyNumbers = new int[index];
pUglyNumbers[0] = 1;
int nextUglyIndex = 1;
int *pMultiply2 = pUglyNumbers;
int *pMultiply3 = pUglyNumbers;
int *pMultiply5 = pUglyNumbers;
while(nextUglyIndex < index)
{
int min = Min(*pMultiply2 * 2, *pMultiply3 * 3, *pMultiply5 * 5);
pUglyNumbers[nextUglyIndex] = min;
while(*pMultiply2 * 2 <= pUglyNumbers[nextUglyIndex])
++pMultiply2;
while(*pMultiply3 * 3 <= pUglyNumbers[nextUglyIndex])
++pMultiply3;
while(*pMultiply5 * 5 <= pUglyNumbers[nextUglyIndex])
++pMultiply5;
++nextUglyIndex;
}
int ugly = pUglyNumbers[nextUglyIndex - 1];
delete[] pUglyNumbers;
return ugly;
}
int Min(int number1, int number2, int number3)
{
int min = (number1 < number2) ? number1 : number2;
min = (min < number3) ? min : number3;
return min;
}
和第一种思路相比,这种算法不需要在非丑数的整数上做任何计算,因此时间复杂度要低很多。感兴趣的读者可以分别统计两个函数GetUglyNumber_Solution1(1500)和GetUglyNumber_Solution2(1500)的运行时间。当然我们也要指出,第二种算法由于要保存已经生成的丑数,因此需要一个数组,从而需要额外的内存。第一种算法是没有这样的内存开销的。
20 题目:用递归颠倒一个栈。例如输入栈{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);
}
}