面试题40:数组中只出现一次的数字
题目:一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写出程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
分析:我们还是从头到尾依次异或数组中的每一个数字,那么最终得到的结果就是两个只出现一次的数字的异或结果。因为其他数字都出现了两次,在异或中全部抵消了。由于这两个数字肯定不一样,那么异或的结果肯定不为0,也就是说在这个结果数字的二进制表示中至少就有一位为1。我们在结果数字中找到第一个为1的位的位置,记为第n位。现在我们以第n位是不是1位标准把原数组中的数字分成两个子数组,第一个子数组中每个数字的第n位都是1,而第二个子数组中每个数字的第n位都是0。由于分组的标准是数字中的某一位是1还是0,那么出现了两次的数组肯定被分配到同一组。因为两个相同的数字的任意一位都是相同的,我们不可能把两个相同的数字分配到两个子数组中去,于是我们已经把原数组分成两个子数组,每个子数组都包含一个只出现一次的数字,而其他数字都出现了两次。我们已经知道如何在数组中找出唯一一个只出现一次数字,因此到此为止所有的问题都已经解决了。
void FindNumsAppearOnce(int data[],int length,int* num1,int num2)
{
if(data == NULL || length < 2)
return;
int resultExclusiveOR = 0;
for(int i = 0;i < length; ++ i)
resultExclusiveOR ^=data[i];
unsigned int indexOf1 = FindFirstBitIs1(resultExclusiveOR);
*num1 = *num2 = 0;
for(int j =0 ;j<length; ++j)
{
if(IsBit1(data[i],indexOf1))
*num1 ^= data[j];
else
*num2 ^=data[j];
}
}
unsigned int FindFirstBitIs1(int num)
{
int indexBit = 0;
while(((num & 1) == 0) && (indexBit <8*sizeof(int)))
{
num = num >> 1;
++ indexBit;
}
return indexBit;
}
bool IsBit1(int num,unsigned int indexBit)
{
num = num >> indexBit;
return (num & 1);
}
在上述代码中,FindFirstBitIs1用来在整数num的二进制表示中找到最右边是1的位,IsBit1的作用是判断在num的二进制表示中从右边数起的indexBit位是不是1。
面试题41:和为s的两个数字VS和为s的连续正数序列
题目一:输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,输出任意一对即可。
bool FindNumbersWithSum(int data[],int length, int sum,int* num1,int* num2)
{
bool found = false;
if(length < 1 || num1 == NULL || num2 == NULL)
return found;
int ahead = length - 1;
int behind = 0;
while(ahead > begind)
{
long long curSum = data[ahead] + data[behind];
if(curSum == sum)
{
*num1 = data[behind];
*num2 = data[ahead];
found = true;
break;
}
else if(curSum > sum)
ahead --;
else
behind ++;
}
return found;
}
在上述代码中,ahead为较小的数字的下标,behind为较大的数字的下标。由于数组是有序的,因此较小数字一定位于较大数字的前面,这就是while循环继续的条件是ahead>behind的原因。代码中只有一个while循环从两端向中间扫描数组,因此这种算法的时间复杂度是O(n)。
题目二:输入一个正数s,打印出所有和为s的连续正数序列(至少含有两个数)。例如输入15,由于1+2+3+4+5=4+5+6=7+8=15,所以至少打印出3个连续序列1~5、4~6和7~8。
分析:我们考虑用两个数small和big分别表示序列的最小值和最大值。首先把small初始化为1,big初始化为2。如果从small到big的序列的和大于s,我们可以从序列中去掉较小的值,也就是增大small的值。如果从small到big的序列和小于s,我们可以增大big,让这个序列包含更多的数字。因为这个序列至少要有两个数字,我们一直增加small到(1+s)/2为止。
void FindContinuousSequence(int sum)
{
if(sum < 3)
return;
int small = 1;
int big = 2;
int middle = (1 + sum) / 2;
int curSum =small + big;
while(small < middle)
{
if(curSum == sum)
PrintContinuousSequence(small,big);
while(curSum > sum && small < middle)
{
curSum -= small;
small ++;
if(curSum == sum)
PrintContinuousSequence(small,big);
}
big ++;
curSum += big;
}
}
void PrintContinuousSequence(int small,int big)
{
for(int i = small; i <= big;++ i)
printf("%d ",i);
printf("\n");
}
面试题42:翻转单词顺序VS左旋转字符串
题目一:输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串“I am a student.”,则输出“student. a am I”。
void Reverse(char *pBegin,char *pEnd)
{
if(pBegin == NULL || pEnd == NULL)
return;
while(pBegin < pEnd)
{
char temp = *pBegin;
*pBegin = *pEnd;
*pEnd = temp;
pBegin++,pEnd--;
}
}
char* ReverseSentence(char *pData)
{
if(pData == NULL)
return NULL;
char *pBegin = pData;
char *pEnd = pData;
while(*pEnd != '\0')
pEnd++;
pEnd--;
//翻转整个句子
Reverse(pBegin,pEnd);
//翻转句子中的每个单词
pBegin = pEnd =pData;
while(*pBegin != '\0')
{
if(*pBegin == ' ')
{
if(*pBegin == ' ')
{
pBegin++;
pEnd++;
}
else if(*pEnd == ' ' || *pEnd == '\0')
{
Reverse(pBegin,--pEnd);
pBegin = ++pEnd;
}
else
{
pEnd ++;
}
}
return pData;
}
}
在英语句子中,单词被空格符号分隔,因此我们可以通过扫描空格来确定每个单词的起始和终止位置。在上述代码的翻转每个单词阶段,指针pBegin指向单词的第一个字符,而pEnd指向单词的最后一个字符。
题目二:字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如输入字符串“abcdefg”和数字2,该函数将返回左旋转2位得到的结果“cdefgab”。
char* LeftRotateString(char* pStr,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;
char* pSecondStart = pStr + n;
char* pSecondend =pStr+nLength-1;
//翻转字符串的前n个字符
Reverse(pFirstStart,pFirstend);
//翻转字符串的后面部分
Reverse(pSecondStart,pSecondend);
//翻转整个字符串
Reverse(pFirstStart,pSecondend);
}
}
return pStr;
}
参考资料《剑指Offer》