面试算法基础及编程 第四弹 (字符串、数值类、或其他常见相关)

版权声明:凡原创系列文章,均笔者的辛勤于中,如转载,请文章顶部注明来源。谢谢配合 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;
}

猜你喜欢

转载自blog.csdn.net/smilejiasmile/article/details/81814017