【剑指offer】——优化时间和空间效率练习1

一、字符串的排列

1、题目要求
输入一个字符串,打印出该字符串中字符的所有排列。
【举个栗子】输入字符串abc,则打印出由字符串a、b、c所能排列出的所有字符串abc,acb,bac,bca,cab和cba。
2、题目分析
我们可以把字符看成两个部分,第一部分是他的第一个字符,第二部分是后面的所有字符。因此,我们要求整个字符串的排列,可以看成两步来完成。

  • 第一步求出所有可能出现在第一位置的字符,即把第一个字符和后面的所有字符进行交换;
  • 第二步固定第一个字符,求后面所有字符的排列。当求后面所有字符的排列的时候就是一个典型的递归过程。

有了以上的分析,我们就可以得出代码实现如下:

void swap(string& str, int i, int j)
{
	char temp = str[i];
	str[i] = str[j];
	str[j] = temp;
}
bool IsExist(vector<string>& result, string& str)
{
	auto it = result.begin();
	for (; it != result.end(); ++it)
	{
		if (*it == str)
		{
			return true;
		}
	}
	return false;
}
void permutationCore(string& str, int start, vector<string>& reslut)
{
	//到达了叶子结点
	if (start == str.length() - 1)
	{
		//去重功能
		if (!IsExist(reslut, str))
		{
			reslut.push_back(str);
		}
		return;
	}
	//start当前永远代表的是第一个元素,意味着i和start进行交换,就是以i作为开始
	for (int i = start; i < str.length(); i++)
	{
		swap(str, start, i);
		permutationCore(str, start + 1, reslut);//以i开头的所有的可能性全部处理并保存到了reslut中
		swap(str, start, i);
	}
}

vector<string> permutation(string str)
{
	vector<string> result;
	if (str.length() > 0)
	{
		permutationCore(str, 0, result);
		sort(result.begin(), result.end());
	}
	return result;
}
int main()
{
	string s = "abc";
	vector<string> result = permutation(s);
	for (int i = 0; i < result.size(); i++)
	{
		cout << result[i] << " ";
	}
}

二、连续子数组的最大和

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

示列1:
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
提示:
1 <= arr.length <= 10^5
-100 <= arr[i] <= 100

2、题目分析
方法一:
最直观的方法就是枚举数组的所有子数组,并求出他们的和。但是,仔细想一想这样做的时间复杂度,假设数组的长度是n,总共有n(n+1)/2个子数组,计算出所有子数组的和,最快也需要O(n^2)时间。

int maxSubArray(vector<int>& arr)
{
	int maxsum = 0X80000000;
	for (int i = 0; i < arr.size(); i++)
	{
		int sum = 0;
		for (int j = i; j < arr.size(); j++)
		{
			sum += arr[j];
			if (sum > maxsum)
				maxsum = sum;
		}
		
	}
	return maxsum;
}

方法二:举例分析数组的规律

  1. 挨个遍历数组中的每一个数。
    1.1如果遇到当前和cursum < 0 的情况,就抛弃前面累加的数组和,更新为当前数字
    1.2如果当前和cursum >= 0 的情况,就继续累加更新当前和。

  2. 如果cursum > maxsum 就更新maxsum的值

我们还是以一个具体的数组来分析该题的解法。设该数组为{1,-2,3,10,-4,7,2,-5}。我们可以像下表所示的这样来操作
在这里插入图片描述
注意:
对于无效的输入,比如数组为空,或者数组长度小于0的情况的处理,我们不能一味的返回0,因为也有可能存在最大子数组的和为0的情况,所以我们需要定义一个全局变量来区分。由此,我们就可以写出代码实现如下:

bool flag = false;
int maxSubArray(vector<int>& arr)
{
	if (arr.size() == 0)
	{
		flag = true;
		return 0;
	}

	int cursum = 0;
	int maxsum = 0x80000000;
	flag = false;

	for (int i = 0; i < arr.size(); i++)
	{
		if (cursum < 0)
			cursum = arr[i];
		else
			cursum += arr[i];
		if (cursum > maxsum)
			maxsum = cursum;
	}
	return maxsum;
}

方法三:应用动态规划法
其实上述代码我们还可以用动态规划的思想来分析。如果用函数f(i)表示以i个数字结尾的子数组的最大和,那么我们需要求出max[f(i)],其中0<= i<=n,则可以得出我们的递归公式f(i)如下:
在这里插入图片描述

int maxSubArray(vector<int>& nums) 
{
	vector<int> dp(nums.size(),0);
	dp[0]=nums[0];
	int max=INT_MIN;
	for(int i=1;i<nums.size();i++)
	{
		if(dp[i-1]>=0)
		{
			dp[i]=nums[i]+dp[i-1];//表示和上一个连续
		}
		else
			dp[i]=nums[i];//表示它为本身。
		max=max>dp[i]?max:dp[i];
	}
	return max;	
}

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

1、题目要求
输入一个整数n,求1~n这n个整数的十进制表示中1出现的次数,例如,输入12,1-12这些整数中包含1的数字有1、10、11和12,1一共出现了5次。
2、题目解析
方法一:时间效率不高的解法

int Numberof1(unsigned int n)
{
	int number = 0;
	while (n)
	{
		if (n % 10 == 1)
			number++;

		n = n / 10;
	}
	return number;
}
int NumberOf1Between1AndN(unsigned int n)
{
	int number = 0;

	for (unsigned int i = 1; i <= n; i++)
		number += Numberof1(i);
	return number;
}

方法二:从数字规律着手提高时间效率的解法

int  PowerBase10(unsigned int n)
{
	int result = 1;
	for (unsigned int i = 0; i < n; i++)
		result *= 10;
	return result;
}
int NumberOf1(const char* strN)
{
	if (!strN || *strN < '0' || *strN >'9' || *strN == '\0')
		return 0;

	int first = *strN - '0';
	unsigned int nlength = static_cast<unsigned int>(strlen(strN));

	if (nlength == 1 && first == 0)
		return 0;
	if (nlength == 1 && first > 0)
		return 1;

	int numFirstDigit = 0;
	if (first > 1)
		numFirstDigit = PowerBase10(nlength - 1);
	else if (first == 1)
		numFirstDigit = atoi(strN + 1) + 1;
	int numOtherDigits = first * (nlength - 1) * PowerBase10(nlength - 2);
	int numRecursive = NumberOf1(strN + 1);
	return numFirstDigit + numOtherDigits + numRecursive;

}
int NumberOf1Between1AndN(unsigned int n)
{
	if (n <= 0)
		return 0;

	char strN[50];
	sprintf(strN, "%d", n);
	return NumberOf1(strN);
}

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

1、题目大意
数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中,第五无是5,第13位是1等等。请写一个函数,求任意第n位对应的数字。
2、题目分析
这道题的求解还是和上一题类似,我们要找一个数字之间的相关规律才可以得到高效率的解法。我们还是以一个具体的例子来分析探究数字之间的相关规律。比如说序列的1001位是什么。其实我们可以把这个序列和对应的数字分成三段。
在这里插入图片描述
我们的1001对应在100-999的三位数里面,所以我们先将3位数之前的位数减去1001-180-10=811。要探究清楚第811位在三位数的哪一个位置,我们就用811/3=270…1。这就意味着第811位是从100开始的第270个数字即370中间的一位,也就是7.这样就完成的位数寻找的工作。那么将我们的逻辑过程对应到代码实现上又是怎样的呢?下面我们就来实现一下。
代码实现的关键函数就是得到m位的数字总共有多少个。就如上表中所展示的一样,2位数返回两位数的个数90。countOfIntegers的实现如下:

int countOfIntegers(int digits)
{
	if (digits = 1)
		return 10;
	int count = (int)std::pow(10, digits - 1);
	return 9 * count;
}

另外还有一个关键函数就是当我们知道要找的那一位数字位于某m位数之中后,就可以用digitAtIndex来找出那一位数字,其具体实现如下:

int beginNumber(int digits)
{
	if (digits == 1)
		return 0;
	return(int)std::pow(10, digits - 1);
}
int digitAtIndex(int index, int dights)
{
	int number = beginNumber(dights) + index / dights;
	int indexFromRight = dights - index % dights;
	for (int i = 1; i < indexFromRight; ++i)
		number /= 10;
	return number % 10;
}

上述代码有些许复杂,我们对其做一些简要的讲解。还是刚才那个例子,我们传入的index =811,digits=3.beginNumber函数主要完成的是得到m位数的第一个数字,由此就可以算出number=100+270=370.得到370过后,我们主要就是要得到其中的7这个数字。按照我们之前的经验要得到一个数字里面的某一个位数就要用除法和求余操作得到,那要除多少次才能求余呢?indexFromRight就是得到我们除法终止的次数。
有了上述主要函数,接下来就是我们最主要的digitAtdex函数的实现了

int digitAtdex(int index)
{
	if (index < 0)
		return -1;
	int digits = 1;
	while (true)
	{
		int numbers = countOfIntegers(digits);
		if (index < numbers * digits)
			return digitAtIndex(index, digits);

		index -= digits * numbers;
		digits++;
	}
	return -1;
}

五、把数组排成最小的数

1、题目要求
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出所有数字中最小的一个。例如,输入数组{3,32,32,321}.则打印出这三个数字能排成的最小数字321323
2、题目分析
一拿到这题我们最直接的解法可能就是先求出这个数组中所有数字的全排列,然后把每个排列拼起来,最后求出拼起来的数字的最小值。但是这种方法的时间复杂度太高了不适合求解。
接下来,我们换一种思路来求解一下,其实这道题就是想让我们确定一个排序规则,需要确定一个规则判断m和n哪个应该排在前面,而不是仅仅比较这两个数字的值哪个更大。根据排序规则,两个数字m和n能拼接成数字mn和nm。
在这里插入图片描述
在这里大小关系是我们所定义的,接下来最重要的就是我们如何去拼接数字了,即给出了数字m和n,如何得到数字mn和nm来比较他们的大小。直接比较他们的大小并不难,但是这样比较的前提就是他们必须是在int的表示范围内才行。这样拼接起来就是一个大数问题,解决大数问题的方法就是把数字转换成字符串。因为拼接起来的位数是相同的,所以可以按照字符串大小的比较规则就可以了。具体的代码实现如下:

const int g_MaxNumberLength = 10;

char* g_strCount1 = new char[g_MaxNumberLength * 2] + 1;
char* g_strCount2 = new char[g_MaxNumberLength * 2] + 1;

int  compare(const void* strNumber1, const void* strNumber2)
{
	strcpy(g_strCount1, *(const char**)strNumber1);
	strcat(g_strCount1, *(const char**)strNumber2);

	strcpy(g_strCount2, *(const char**)strNumber2);
	strcat(g_strCount2, *(const char**)strNumber1);

	return strcmp(g_strCount1, g_strCount2);
}
void printMinnumber(int* numbers, int length)
{
	if (numbers == nullptr || 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的时间复杂度相同,也就是O(logn).

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值