版权声明:凡原创系列文章,均笔者的辛勤于中,如转载,请文章顶部注明来源。谢谢配合 https://blog.csdn.net/smilejiasmile/article/details/81814017
// # -*- coding:utf-8 -*-
// # @Author: Mr.chen([email protected])
// # @Date: 2018-08-18 21:06:30
// 注:此为第四弹,主要讲解字符串、数值类、或其他常见相关面试笔试题,要求手写。
/*
1、替换字符串中的空格。题目:请实现一个函数,将一个字符串中的空格替换成“%20”。
例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy
思路:法一:从头到尾遍历字符串做替换,时间复杂度为O(n2),效率低
法二:从尾到头遍历字符串做替换,时间复杂度为O(n),效率高,下面用它实现
*/
// 指向字符数组的字符指针str,字符数组长度length
void replaceSpace(char *str,int length) {
// 边界检查1:判断字符数组是否为空
if(str==NULL)
return ;
// 遍历字符串,统计空格个数、替换前字符个数、替换后字符个数
int CountOfBlanks=0; // 空格个数
int Originallength=0;// 替换前字符个数
int len=0; // 替换后字符个数
for(int i=0;str[i]!='\0';++i)
{
Originallength++;
if(str[i]==' ')
++CountOfBlanks;
}
len =Originallength+2*CountOfBlanks;
// 边界检查2:判断字符数组是否越界
if(len+1>length)
return ;
// 替换空格
char*pStr1=str+Originallength;// 字符指针指向原始字符串的末尾
char*pStr2=str+len; // 字符指针指向替换后字符串的末尾
while(pStr1 != pStr2) // 替换结束的条件
{
if(*pStr1==' ')
{
*pStr2-- ='0';
*pStr2-- ='2';
*pStr2-- ='%';
}
else
{
*pStr2--=*pStr1;
}
--pStr1;
}
}
/*
2、位运算:二进制中 1 的个数。输入一个整数,输出该二进制中有多少个 1。经过分析(略),我们发
现把一个整数减去1,并且与原数进行与运算会把该整数最右边的一个 1 变成 0,那么一个整数的二
进制表示中会有多个 1,就可以进行多少次这样的操作。基于这种思想,我们可以写出以下代码。
*/
int numberOf1(int n)
{
int count = 0;
while(n)
{
++ count;
n = (n -1) & n;
}
return count;
} // 此题最为重要就是发现规律,在 Coding
/*
3、数值的整数次方。实现函数 double Power(double base,int exponent)
求 base 的 exponent 次方,不使用库函数,不考虑大树问题。思路:注意
边界情况,如果指数是 0或者负数,如果是负数,可以取绝对值,然后求倒数
,求倒数时注意,分母不能为 0,此外,我们可以采用如下这个方法(如下图)求
取指数,已经求过的没有必要再次计算。并且,该公式很容易使用递归来实现。
*/
double powerWithUnsignedExponent(double base,unsigned int exponent)
{
if(exponent == 0)
return 1;
if(exponent ==1)
return base;
double result = powerWithUnsignedExponent(base,exponent >> 1);
result *= result;
//判断是否是奇数
if(exponent & 0x1 == 1)
result *= base;
return result;
}// 在上面用 >> 右移运算符代替了除 2,用位与运算符代替了求余 %,判断是否是奇数
/*
4、调整数组顺序使奇数在前面,偶数在后面。暴力遍历循环,时间效率O(N2),一般方法
使用两个指针分别指向头和尾,如果有偶数在奇数的前面则交换他们。如果第一个指向
偶数,第二个指向奇数,则交换他们。如果,面试官还要我们注意扩展性(通用属性),
提高代码的可重复性,则可以有如下代码:
*/
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)
{
while(pBegin < pEnd && !func(*pBegin))
++ pBegin;
while(pBegin < pEnd && func(*pEnd))
-- pEnd;
if(pBegin < pEnd)
{
int temp = *pBegin;
*pBegin = *pEnd;
*pEnd = temp;
}
}
}
// 解耦和
bool isEven(int n)
{
return (n & 1) == 0;
}
// 调用时
void ReOrderOddEven(int* pData,unsigned int length)
{
ReOrder(pData,length,isEven);
}
/*
5、字符串的排列: 输入一个字符串,打印该字符串中的字符的所有的排列,
如例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符
串abc,acb,bac,bca,cab和cba。思路:对于该问题,一般不容易一下
想出解决方案。于是,我们应该尝试把复杂的问题分解成小的问题,如:
我们把一个字符串看成是两部分组成:第一部分是它的第一个字符,第
二个部分是后面所有的字符,如下图,我们使用两种不同的颜色背景来
来区分字符串的两部分。我们求整个字符串的排列,也可以看成两部分。
首先求所有可能出现在第一个位置的字符,即把第一个字符和后面的所有
字符交换,然后固定第一个字符,然后求后面所有字符的排列。这个时候,
仍把后面的字符分成两个部分,后面字符的第一个字符,和这个字符之后
所有的字符,让这个字符和它后面的字符依次交换。如下所示:我们可以
看出这是很典型的递归的思想。代码如下:
*/
void permutation(char* pStr)
{
if(NULL == pStr)
return;
permutation(pStr,pStr);
}
// pStr 指向整个字符串的第一个字符, pBegin 指向当前我们做排列操作的第一个字符。
// 在每一次递归的时候,我们从 pBegin 向后扫描一个字符,在交换完 pBegin 和 pChar
// 之后,我们对 pBegin 后面的字符串做递归的操作。
void permutation(char* pStr,char* pBegin)
{
if(*pBegin == '\0')
printf("%s\n",pStr);
else
{
for(char* pChar = pBegin; *pChar != '\0'; ++pChar)
{
// 交换
char* pTemp = *pChar;
*pChar = *pBegin;
*pBegin = *pTemp;
// 递归
permutation(pStr,pBegin+1);
// 回退
char* pTemp = *pChar;
*pChar = *pBegin;
*pBegin = *pTemp;
}
}
}
/*
6、数组中出现次数超过一半的数字。题目:数组中有一个数字出现的次数超过数组长度的一半,
请找出这个数字。例如,输入一个长度为 9 的数组 { 1,2,3,2,2,2,5,4,2 },由于数字 2
在数组中出现 5 次,超过数组的一半,因此输出 2。法一:先排序,再取中间数字,时间复杂
度 O(nlogn),不是最好的。法二:可借鉴快排的 partition 的思想,时间复杂度O(n)。法
三:下面代码使用的方法,充分利用数组的特点,进行降维,取最重要的特征,次数最大。思
路如下:由于该数字出现的次数超过数组的一半,也就是它出现的次数比起其他所有的数字出现
次数还要多。因此,我们可以考虑在遍历数组的时候,保存两个值,一个是数组中的一个值,一个
是该数字出现的次数。当我们遍历下一个数字的时候,如果下一个数字与之前保存的数字相同,
则次数加 1,如果不同,则次数减去 1。如果次数为 0,我们需要保存下一个数字,并且把次数置
为 1。由于要找的数字比起其他数字出现次数总和还多。故它一定是最后一次把次数置为 1,的
数字。此外,还应该检查一些异常输入,比如,输入数组是否为NULL (checkInvalidArray),
以及输入数组中,最高频率的数字出现次数,是否超过一半以上(checkMoreThanHalf)等,
代码如下:
*/
int moreThanHalfNumber(int* numbers,int length)
{
if(checkInvalidArray(numbers,length))
return 0;
int result = numbers[0];
int times = 1;
for(int i=1; i< length;++i)
{
if(times == 0)
{
result = numbers[i];
times = 1;
}
if(numbers[i] == result)
++ times;
else
-- times;
}
if (!checkMoreThanHalf(numbers,length,result))
return 0;
return result;
}
// 检查异常输入
bool g_inputInvalid = false;
bool checkInvalidArray(int* numbers,int length)
{
g_inputInvalid = false;
if(numbers == NULL || length <= 0)
g_inputInvalid = true;
return g_inputInvalid;
}
bool checkMoreThanHalf(int* numbers,int length,int number)
{
int times = 0;
for(int i=0; i < length; i++)
{
if(numbers[i] == number)
++ times;
}
bool isMoreThanHalf = true;
if( times * 2 <= length)
{
g_inputInvalid = true;
isMoreThanHalf = false;
}
return isMoreThanHalf;
}
/*
7、连续字数组的最大和,题目:输入一个整形数组,数组里面既有正数也有负数。数组中的
一个或连续多个整数组成一个字数组,求所有字数组的和的最大值,要求时间复杂度为O(n)。
例如输入:{1,-2,3,10,-4,7,2,-5}, 的和最大字数组为{3,10,-4,7,2,-5},该字数组的
和为 18。如果列举它所有的字数组,共有 n(n+1) /2 个字数组。计算出所有的字数组的和,
也需要O(n2)的时间。法一:我们可以尝试着分析数组的规律,当开始累加到出现负数,则说明
该抛弃前面所累加的和了,但是我们还需要存储上一次最大的和,如果下一次添加,比累加的大,
则需要更新最大和,下图是分析过程。此外,我们还需要考虑无效输入,比如输入的数组参数为
空指针,数组长度小于等于 0 等情况。此时,我们让函数返回什么数字,如果返回0,怎么区分是
最大和为0,还是无效输入,为此我们定义了一个全局变量来标记是否无效。代码如下:
*/
bool g_InvalidInput = false;
int FindGreatestSumOfSubArray(int* pData,int length)
{
if(NULL == pData || length <= 0)
{ // 区分输出是无效输入还是最大总和为 0
g_InvalidInput = true;
return 0;
}
g_InvalidInput = false;
int nCurSum = 0;
int nGreatestSum = 0x80000000;
for(int i=0; i<length; i++)
{
if(nCurSum <= 0)
nCurSum = pData[i];
else
nCurSum += pData[i];
// 更新最大值
if(nCurSum > nGreatestSum)
nGreatestSum = nCurSum;
}
return nGreatestSum;
}
/*
8、把数组排成最小的数:输入一个正整数数组,把数组中所有的数字拼成一个数,
打印能拼接出的所有数字中最小的一个。例如输入数组 [3,32,321] 能打印出
这 3 个数字能拼成的最小的数字为 321323。思路:法一:全排列求出数组中
所有数字的全排列,然后把每个全排列拼起来,求出拼出来的数字的最大值。
有 n!个组合,接下来看一种时间复杂度为O(nlogn)的算法。法二: 定义新的
排序规则,
* 如果两个数字m,n拼接成mn和nm,如果mn<nm,那么m应该排在n的前面,我们定义此时m小于n,如果mn=nm,我们定义m等于n。
* 可以考虑将数字转成字符串,一来防止数字拼接时的溢出,二来字符串的拼接和比较容易实现。
* 由于把数字 m和n 拼接起来得到mn和nm,它们的位数一定是相同的,因此比较它们的大小只需按照字符串大小的比较规则即可。
* 代码如下:
*/
const int g_maxNumberLength = 10;
char* g_strCombine1 = new char[g_maxNumberLength *2 +1];
char* g_strCombine2 = new char[g_maxNumberLength *2 +1];
void printMinNumber(int* numbers,int length)
{
if(NULL == numbers || length <= 0)
return;
char** strNumbers = (char**)(new int[length]);
for(int i=0; i<length; ++i)
{
strNumbers[i] = new char[g_maxNumberLength +1];
sprintf(strNumbers[i],"%d",numbers[i]);
}
qsort(strNumbers,length,sizeof(char*),compare);
// 打印出来
for(int i=0; i<length; ++i)
printf("%s",strNumbers[i]);
printf("\n");
// 释放内存
for(int i=0; i<length; ++i)
delete strNumbers[i];
delete strNumbers;
}
// 重载 qsort 的 compare 方法
int compare(const void* strNumber1,const void* strNumber2)
{
strcpy(g_strCombine1,*(const char**)strNumber1);
strcat(g_strCombine1,*(const char**)strNumber2);
strcpy(g_strCombine2,*(const char**)strNumber2);
strcat(g_strCombine2,*(const char**)strNumber1);
return strcmp(g_strCombine1,g_strCombine2);
}
/*
9、数字在排序数组中出现的次数。题目:统计一个数字在排序数组中出现的次数。
例如输入排序数组,{1,2,3,3,3,3,4,5} 和数字 3,由于 3 在这个数组中出现
了 4 次,因此输出 4。方法一:暴力遍历,时间复杂度为 O(n),法二:由于已经
排好序了,故相同的数必定在一起,分别找到该数第一次出现的地方和最后一次
出现的地方。分别使用二分查找算法,则总的时间复杂度,任然是 O(logn),我们
先来分析如何找到第一个 K,二分查找总是先拿数组的中间数字和 k 做比较,如果
中间数字比 K 大,则 k 只能出现在前半段,只需要去前半段查找即可,反之,去后半
段查找。如果中间数字与 k 相等呢。我们先来判断这个数字是不是第一个 k,如果
位于中间位置前一个不是K,则中间位置就是第一个k,如果中间位置的前一个位置也
是k,则说明第一个k,在数组的前半段,下一轮我们仍然需要在数组的前半段查找,
同理,查找最后一个k,最后就可以确定 k 的个数了。代码如下:
*/
// 找数组中的第一个 K
int GetFirstK(int* data,int length,int k,int start,int end)
{
if(start > end)
return -1;
int MiddleIndex = (start + end)% 2;
int MiddleData = data[MiddleIndex];
if(MiddleData == k)
{
if((MiddleIndex > 0 && data[MiddleIndex - 1] !=k) || MiddleIndex == 0)
return MinddleIndex;
else
end = cMiddleIndex - 1;
}
else if (MiddleData > k)
end = MiddleIndex - 1;
else
start = MinddleIndex + 1;
return GetFirstK(data,length,k,start,end);
}
// 找数组中的最后一个 K
int GetLastK(int* data,int length,int k,int start,int end)
{
if(start > end)
return -1;
int MiddleIndex = (start + end)% 2;
int MiddleData = data[MiddleIndex];
if(MiddleData == k)
{
if((MiddleIndex < length -1 && data[MiddleIndex + 1] !=k) || MiddleIndex == length -1)
return MinddleIndex;
else
start = MiddleIndex + 1;
}
else if (MiddleData < k)
start = MinddleIndex + 1;
else
end = MiddleIndex - 1;
return GetLastK(data,length,k,start,end);
}
// 得出 k 出现的次数
int GetNumberOfK(int* data,int length,int k)
{
int number = 0;
if(data != NULL && length >0)
{
int first = GetFirstK(data,length,k,0,length-1);
int last = GetLastK(data,length,k,0,length-1);
if(first > -1 && last > -1)
number = last - first + 1;
}
return number;
}
/*
10、数组中值出现一次的数字,题目:一个整数数组中除了两个数字外,其他数字均出现了两次,
写出程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度为O(1),
如果是出现一次的数字只有一个,则我们可以用异或去解决,如下,所示,如果有两个,我们
可以选择将原数组分成两个数组,让出现一次的两个数分别在两个数组之中,然后就可以使用
异或分别去解决了。
*/
// 次数出现一次的只有一个数
int FindAppearOnce(int arr[], int len)
{
int i = 0;
int ret = 0;
for(i = 0; i<len; i++)
{
ret = arr[i]^ret;
}
return ret;
}
// 次数出现一次的数字有两个
void FindAppearOnce(int arr[], int len, int* pn1, int* pn2)
{
int num = 0;//记录整组异或的结果,即两个一次出现的数异或的结果
int i = 0;
int k = 1;
for(i = 0; i<len; i++) //得出整组异或的结果,即两个一次出现的数异或的结果
{
num = num^arr[i];
}
while(num&1 != 1) //找出异或结果中从右边起第一个为1的bit位
{
k++;
num = num>>1;
}
for(i = 0; i<len ; i++)//将原数组分为两组,分别求出每组中出现一次的数字
{
int k_bit = (arr[i]>>(k-1)) & 1; //arr[i]第k位的值
if(k_bit == 1)
{
*pn1 = *pn1^arr[i];
}
else
{
*pn2 = *pn2^arr[i];
}
}
}
/*
11、不用加减乘除做加法。题目:写一个函数,求两个整数之和,要求在函数体中不能使用 加减乘除 四则运算符。
首先看十进制是如何做的: 5+7=12,三步走
第一步:相加各位的值,不算进位,得到2。
第二步:计算进位值,得到10. 如果这一步的进位值为0,那么第一步得到的值就是最终结果。
第三步:重复上述两步,只是相加的值变成上述两步的得到的结果2和10,得到12。
同样我们可以用三步走的方式计算二进制值相加: 5-101,7-111
第一步:相加各位的值,不算进位,得到010,二进制每位相加就相当于各位做异或操作,101^111。
第二步:计算进位值,得到1010,相当于各位做与操作得到101,再向左移一位得到1010,(101&111)<<1。
第三步重复上述两步, 各位相加 010^1010=1000,进位值为100=(010&1010)<<1。 继续重复上述两步:
1000^100 = 1100,进位值为0,跳出循环,1100为最终结果。
*/
int Add(int num1, int num2)
{
while(num2!=0)
{
int temp=num1^num2; // 各位相加的值
num2=(num1&num2)<<1; // 进位值
num1=temp;
}
return num1;
}