《剑指Offer》题四十一~题五十

四十一、数据流中的中位数

题目:如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。

提示:数据是从一个数据流中读出来的,因此数据的数目随着时间的变化而增加,即如果用一个容器来保存从流中读出来的数据,则当有新的数据从流中读出来时,这些数据就插入数据容器。

分析:可以定义该数据容器的数据结构包括数组、链表、二叉搜索树、AVL树及最大堆和最小堆。

 

四十二、连续子数组的最大和

题目:输入一个整型数组,数组里有正数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)。

提示:看到这道题,很多人都能想到最直观的方法,即枚举数组的所有子数组并求出它们的和。然而,这并非最优解。

分析:我们可以试着从头到尾逐个累加示例数组中的每个数字。初始化和为0。例如,输入的数组为{1, -2, 3, 10, -4, 7, 2, -5}。第一步加上第一个数字1,此时和为1;第二步加上数字-2,和就变成了-1,我们发现,由于-2是一个负数,因此累加-2之后得到的和比原来的和还要小,因此,我们要把之前得到的和1保存下来,因为它有可能是最大的子数组的和;第三步加上数字3,得到的和是2,比3本身还小,即从第一个数字开始的子数组的和会小于从第三个数字开始的子数组的和,因此我们不用考虑从第一个数字开始的子数组,之前累加的和也被抛弃,我们从第三个数字重新开始累加,此时得到的和是3;第四步加10,得到的和为13;第五步加上-4,和为9,我们发现,由于-4是一个负数,因此累加-4之后得到的和比原来的和还要小。因此,我们要把之前得到的和13保存下来,因为它有可能是最大的子数组的和;第六步加上数字7,得到的和为16,此时的和比之前最大的和13还要大,把最大的子数组的和由13更新为16;第七步加上2,累加得到的和为18,同时更新最大子数组的和;第八步加上最后一个数字-5,由于得到的和为13,小于此前最大的和18。因此,最终最大的子数组的和为18,对应的子数组是{3, 10, -4, 7, 2}。

解法:

int greatestSum_of_subArray(int *pData, int length)
{
	if(pData == nullptr || length <= 0)		return -1;
	int curSum = 0;							// 当前累加的子数组和 
	int greatestSum = 0x80000000;			// 最大的子数组和 
	for(int i = 0; i < length; ++i) {
		if(curSum <= 0)
			curSum = pData[i];
		else
			curSum += pData[i];
		if(curSum > greatestSum)
			greatestSum = curSum;
	}
	return greatestSum;
}

小结:该方法需要应聘者仔细地分析累加子数组地和的过程,从而找到解题的规律。但如果应聘者熟练掌握了动态规划算法,那么它就能轻松地找到解题方案。

   

四十三、1~n整数中1出现的次数

题目:输入一个整数n,求1~n这n个整数的十进制表示中1出现的次数。例如,输入12,1~12这些整数中包含1的数字有1、10、11和12,1一共出现了5次。

效率不高的解法:

int numOfOne_betweenOneAndN(int n)
{
	int num = 0;
	for(int i = 1; i <= n; ++i)
		num += num_of_one(i);
	return num;
}

int num_of_one(int n)
{
	int num = 0;
	while(n) {
		if(n % 10 == 1)	num++;
		n = n / 10;
	}
	return num;
}

分析:此题考查应聘者做优化的激情和能力,上面这个解法大部分应聘者都能想到。当面试官提示还有更快的方法之后,应聘者千万不要轻易放弃尝试。虽然想到时间复杂度为O(logn)的方法不容易,但应聘者要展示自己追求更快算法的激情,多尝试不同的方法,不能轻易说自己想不出来并且放弃努力。 

 

四十四、数字序列中某一位的数字

题目:数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中,第5位(从0开始计数)是5,第13位是1,第19位是4,等等。请写一个函数,求任意第n位对应的数字。

提示:和上题一样,本题有最直观的方法,但其不是最快的方法,所以需要想出更好的算法。 

 

四十五、把数组排成最小的数

题目:输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的那个。例如,输入数组{3, 32, 321},则打印出这3个数字能排成的最小数字321323。

分析:两个数字m和n能拼接成数字mn和nm。如果mn<nm,则定义m小于n;如果nm<mn,则定义n小于m;如果nm=mn,则定义n等于m。此外,虽然m和n都在int型能表达的范围内,但nm和mn用int型表示就有可能溢出了,所以这还是一个隐形的大数问题。 

 

四十六、把数字翻译成字符串

题目:给定一个数字,我们按照如下规则把它翻译为字符串:0翻译成"a",1翻译成"b",……,11翻译成"l",……,25翻译成"z"。一个数字可能有多个翻译。例如,12258有5种不同的翻译,分别是"bccfi"、"bwfi"、"bczi"、"mcfi"和"mzi"。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。

提示:我们有两种不同的选择来翻译第一位数字1。第一种选择是数字1单独翻译成"b",后面剩下数字2258;第二种选择是1和紧挨着的2一起翻译成“m”,后面剩下数字258。即当最开始的一个或者两个数字被翻译成一个字符之后,我们接着翻译后面剩下的数字。显然,我们可以写一个递归函数来计算翻译的数目。

用递归的思路分析:定义函数f(i)表示从第 i 位数字开始的不同翻译的数目,那么f(i) = f(i+1) + g(i, i+1) × f(i+2)。当第 i 位和第 i+1 位两位数字拼接起来的数字在10~25的范围内时,函数g(i, i+1)的值为1;否则为0。

递归解法:

int get_translation_count(int number)
{
	if(number < 0)	return 0;
	string numStr = to_string(number);
	return get_translation(numStr);
}

int get_translation(const string &numStr)
{
	int length = numStr.length();
	if(length == 0)	return 0;
	if(length == 1)	return 1;
	if(length == 2) {
		if((numStr[0] == '1') || (numStr[0] == '2' && numStr[1] >= '0' && numStr[1] <= '5')) 
			return 2;
		else 
			return 1;
	}
	if(length > 2) {
		string str1 = numStr.substr(1);
		string str2 = numStr.substr(2);
		if((numStr[0] == '1') || (numStr[0] == '2' && numStr[1] >= '0' && numStr[1] <= '5'))
			return get_translation(str1) + get_translation(str2);
		else 
			return get_translation(str1);
	}
}

分析:尽管可以这么写,但由于存在重复的子问题,故递归并不是解决这个问题的最佳方法。递归从最大的问题开始自上而下解决问题,我们也可以从最小的子问题开始自下而上解决问题,这样就可以消除重复的子问题。即我们从数字的末尾开始,然后从右到左翻译并计算不同翻译的数目。

循环解法:

int GetTranslationCount(int number)
{
    if(number < 0)
        return 0;

    string numberInString = to_string(number);
    return GetTranslationCount(numberInString);
}

int GetTranslationCount(const string& number)
{
    int length = number.length();
    int* counts = new int[length];
    int count = 0;

    for(int i = length - 1; i >= 0; --i)
    {
        count = 0;
         if(i < length - 1)
               count = counts[i + 1];
         else
               count = 1;

        if(i < length - 1)
        {
            int digit1 = number[i] - '0';
            int digit2 = number[i + 1] - '0';
            int converted = digit1 * 10 + digit2;
            if(converted >= 10 && converted <= 25)
            {
                if(i < length - 2)
                    count += counts[i + 2];
                else
                    count += 1;
            }
        }

        counts[i] = count;
    }

    count = counts[0];
    delete[] counts;

    return count;
}

小结:如果应聘者只是把递归分析转换为递归代码,则应聘者不一定能够通过这道题的面试。面试官期待应聘者能够用基于循环的代码来避免不必要的重复计算。  

  

四十七、礼物的最大值

题目:在一个mxn的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格,直到到达棋盘的右下角。给定一个棋盘及其上面的礼物,请计算你最多能拿到多少价值的礼物?

提示:可以用动态规划解决此题。我们可以先用递归的思路来分析问题,而编写基于循环的代码。

用递归的思路分析:定义一个函数f(i, j)表示到达坐标为(i, j)的格子时能拿到的礼物总和的最大值。由题可知,我们有两种可能的路径到达坐标为(i, j)的格子:通过格子(i-1, j)或者(i, j-1)。所以f(i, j) = max(f(i-1, j), f(i, j-1)) + gift[i, j],gift[i, j]表示坐标为(i, j)的格子里礼物的价值。

 

 

四十八、最长不含重复字符的子字符串

题目:请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。假设字符串中只包含'a'~'z'的字符。例如,在字符串"arabcacfr"中,最长的不含重复字符的子字符串是"acfr",长度为4。

提示:可以用动态规划解决此题。如果使用蛮力法(时间为O(n3)),就是找出字符串的所有子字符串,然后判断每个子字符串中是否包含重复的字符。

分析:定义函数f(i)表示以第 i 个字符为结尾的不包含重复字符的子字符串的最长长度。我们从左到右逐一扫描字符串中的每个字符。当我们计算以第 i 个字符为结尾的不包含重复字符的子字符串的最长长度f(i)时,我们已经知道f(i-1)了。

 

 

四十九、丑数

题目:我们把只包含因子2、3和5的数称作丑数。求按从小到大的顺序的第1500个丑数。例如,6、8都是丑数,但14不是,因为它包含因子7。习惯上我们把1当作第一个丑数。

提示:影响此题效率的是方法的选择,①计算所有的数;②只计算丑数。其中后一种方法需要创建数组保存已经找到的丑数,是一种用空间换时间的解法。

 

五十、第一个只出现一次的字符

题目一:字符串中第一个只出现一次的字符。在字符串中找出第一个只出现一次的字符。如输入"abaccdeff",则输出'b'。

分析:可以使用一个容器统计每个字符在该字符串中出现的次数,这个容器的作用是把一个字符映射成一个数字。这个容器可以是哈希表,即定义哈希表的键值(key)是字符,而值(value)是该字符出现的次数。

哈希表解法:

char firstNotRepeatChar(char *pStr)
{
	if (pStr == nullptr)	return '\0';
	unsigned int hashTable[256];
	memset(hashNum, 0, 256);
	char *pCur = pStr;
	while(*pCur != '\0') {
		hashTable[*pCur]++;
		pCur++;
	}
	pCur = pStr;
	while(*pCur != '\0') {
		if(hashTable[*pCur] == 1) 
			return *pCur;
		pCur++;
	}
	return '\0';
}

小结:哈希表是一种比较复杂的数据结构,STL中的map和unordered_map实现了哈希表的功能,我们可以直接拿过来用。由于本题只需要一个非常简单的哈希表就能满足要求,所以我们可以实现一个简单的哈希表。  

 

题目二:字符流中第一个只出现一次的字符。请实现一个函数,用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是'g';当从该字符流中读出前6个字符"google"时,第一个只出现一次的字符是'l'。

提示:本题仍可用哈希表来完成。字符只能一个接着一个从字符流中读出来。

分析:定义一个哈希表来保存字符在字符流中的位置,哈希表的键值为字符的ASCII码,哈希表的值为字符对应的位置。 

解法:

class CharStatistics
{
public:
    CharStatistics() : index(0)
    {
        for(int i = 0; i < 256; ++i)
            occurrence[i] = -1;
    }

    void Insert(char ch)
    {
        if(occurrence[ch] == -1)
            occurrence[ch] = index;
        else if(occurrence[ch] >= 0)
            occurrence[ch] = -2;

        index++;
    }

    char FirstAppearingOnce()
    {
        char ch = '\0';
        int minIndex = numeric_limits<int>::max();
        for(int i = 0; i < 256; ++i)
        {
            if(occurrence[i] >= 0 && occurrence[i] < minIndex)
            {
                ch = (char) i;
                minIndex = occurrence[i];
            }
        }

        return ch;
    }

private:
    // occurrence[i]: A character with ASCII value i;
    // occurrence[i] = -1: The character has not found;
    // occurrence[i] = -2: The character has been found for mutlple times
    // occurrence[i] >= 0: The character has been found only once
    int occurrence[256];
    int index;
};

  

posted @ 2018-08-28 23:27  GGBeng  阅读(210)  评论(0编辑  收藏  举报