关于时间复杂度O(N)的思考

本文探讨了一道经典算法题“求连续子数组最大和”的高效解决方案,通过深入理解时间复杂度O(N)的要求,提出了一种新颖的解题思路,并给出了具体的实现代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一道算法题

在CSND旗下的庞果网上,看到一道名为“求连续子数组最大和”的算法题。其问题描述如下:

一个整数数组,输出其连续子数组最大和。如2, -3, 4, 5, -4, 6,输出11.要求时间复杂度为O(N).

在解决这道题的过程中,得到关于时间复杂度O(N)的思考。下面是整个解决问题的思考过程。

起初我的思路大致是:我要只遍历一遍整数数组,可以设置两个工作指针,分别从数组首尾出发,交替地向中间移动,在此过程中记录出现的最大和,并随着移动而修改最大和,直到两个指针相遇。在陷入控制指针移动并记录合适的相关数据的比较混乱的思维泥潭后,我开始关注题目中要求的时间复杂度O(N)的意义的思考。下面是想到的:

  1. 先确定复杂度要求,再确定思考方向。
  2. 对于O(n) 复杂度,应该对 O(n)本身要有深入思考,这样会对后面思考算法有极大的指导思路。 O(n)不是说所设计的算法只对数组遍历一边,而是可以遍历多遍,但遍数值要确定,是常量。这一点虽然早就知道,但是固有思维的偏见总是一看到O(N),就默认要只遍历一遍的思考方法。专门思考将时间负责度O(N)提出来审视思考,有助于彻底改变面对O(N)要求的算法的思考方式。
  3. 这样便可不用将思路集中在只想该如何求得最大和。就好比从河里捕鱼,不用眼睛只寻找鱼。可以观察整个河,思考与鱼有什么关系,可不可以先干点别的为捕鱼服务。发现其实河水是不需要的,没有就最好不过了。所以我可以先挖个坑把水放了,第二步再直接捡鱼。
  4. 回到此问题上,思考这些整数跟最终的连续子数组最大和有什么关系,期望得到的是一个最大和,而非由哪些数组成的最大和,想到相邻的正数(包括0)完全可以相加合并成一个正数,而对于相邻的负数,如果最大和包含它们,也必是因为它们两侧都有正数,并且一定包含两侧的正数,这样的话,相邻负数也完全可以相加合并成一个负数。
  • 第一遍遍历数组先把连续的同符号的数合并,用他们的和代替,对于我希望求的结果而言,他们没有必要单独存在。也许我在这步还没想到这样会为后面解题思路产生极大帮助,但至少它让数据变的简单许多。
  • 合并后,数据变的正数和负数交替出现,观察发现,数组首尾如果是负数,是完全可以删掉了,不影响计算。
  • 此时,数组首尾都为正数,且正数和负数交替出现,观察尝试发现一旦出现“正数+负数+正数”,我便可很容易确定其这3个数的连续最大和,同时并合并成一个正数(最大和,或者右边的正数),而且此数可以与后面两个数(一负一正)继续执行和前面一样的合并,并修改最大和,然后继续往后迭代计算。

综上,复杂度O(N)就变成了O(N+2+N),符合题意。将时间复杂度分解的过程也是将大问题分解成小问题解决,再合并的过程。


解题思路与程序设计之间的纽带

思考到这里,我本已经很满足了,觉得搞的差不多了。可当我开始实现其代码时,发现似乎还不是像上面分解的小问题这样清晰流畅。在写代码的过程中,我发现造成这个情况的原因是人脑构思出的解题思路与程序的清晰表达之间还需要纽带,我起初并没有注意它。那么这个纽带到底是什么?一个解题思路中的一个步骤其实都可以说对应一个考虑完备(应对全部可能的情景)的程序。而这样一个程序当作一个逻辑功能,用一个函数去完成其实还是比较困难的,写起来并不流畅。而如果一个函数只处理一种情景,写起来就容易对应人脑思维的步骤。而一般一个逻辑步骤对应的多种情景是由一个普通情景和一个或多个特殊情景组成。一个程序把处理情景和完成逻辑功能揉和到一块时就产生了我们经常感叹的想明白容易,写程序难。怎么自然地想到将其分离呢?还是对时间复杂度O(N)的思考。它可以分解为多个1或N的组合的本质意义是,每个分解的 1或N既可以是一个处理普通场景的步骤,也可以是处理一个特殊场景。纽带就是归纳每个分解步骤的场景。

下面将上面的解题思路结合场景归纳一次:

  1. 场景:全是负数,全是正数,正负数都有
  2. 步骤一:判断数组是否全部为负数,若是,返回最大值,即为最终要求结果。
  3. 场景:全是正数,正负数都有
  4. 步骤二:合并相邻同符号数
  5. 场景:只有一个正数,多个正负数交替出现
  6. 步骤三:如果数组首尾的数为负,则删掉。
  7. 场景:只有一个正数;多个正负数交替出现,且首尾都是正数。
  8. 步骤四:判断数组是否只有一个数,若是,返回此数,即为最终要求结果。
  9. 场景:多个正负数交替出现,且首尾都是正数,且个数是大于等于3。
  10. 步骤五:每3个数合并,再与后面两个数合并,重复迭代,知道合并结束,返回最大和。

场景会随着完成一个步骤而发生改变,需要不断更新,而每一个步骤的函数或代码块,都只需要考虑固定场景的输入,写起来简单很多。以下附上依照此完整思路的程序代码,代码量看上去依然不少,但程序语义清晰简洁,比代码量少更重要。

#include <iostream>
using namespace std;

//此函数负责合并数组符号相同且相邻的数
void first_merge(int a[], int& n)
{
	int i=0, length=n;
	n = 0;
	while((i+1)<length) 
	{
		if(a[i]*a[i+1]>=0)
		{
			a[i+1] += a[i];
		}
		else
		{
			a[n] = a[i];
			n += 1;
		}
		i += 1;
	}
	a[n] = a[i];
	n += 1;
}

//这里已经确保传入的数组不止一个元素了,函数写起来便可以不考虑单元素数组
void trim(int** a, int& n)
{
	int *arr = *a;
	if(arr[n-1] < 0)
	{
		n -= 1;
	}
	if(arr[0] < 0)
	{
		*a += 1;
		n -= 1;
	}
}

//按规则合并三个数,记录连续最大和,返回合并值
//参数一定是:n1为正数,n2为负数,n3为正数
int tri_nums_merge(int n1, int n2, int n3, int& maxsum)
{
	int sum = n1+n2+n3;
	maxsum = n1 > n3 ? n1:n3;
	maxsum = maxsum > sum ? maxsum:sum;
	int merge_value = n1+n2 > 0 ? sum:n3;
	return merge_value;
}

//求连续子数组最大和
int max_sum(int* a,int n)
{	
	int maxsum = a[0];
	//判断是否全为负数,若是,返回
	int i = 0;
	for(i=0; i<n; i++)
	{
		if(a[i]>0)
			break;
		else
			maxsum = maxsum < a[i] ? a[i]:maxsum;
	}
	if(i == n)
		return maxsum;

	first_merge(a, n);
	trim(&a, n);
	
	//此时如果数组只剩一个数,返回;否则按规则迭代合并
	if(n == 1)
	  return a[0];
	else
	{
		int n1 = a[0];
		maxsum = 0;
		for(int i=0; i<n-2; i+=2)
		{
		  n1 = tri_nums_merge(n1, a[i+1], a[i+2], maxsum);
		}
		return maxsum;
	}
}

int main()
{   
    int a1[3] = {-1, -2, -3};
	int a2[3] = {1, 2, 3};
	int a3[6] = {-2, -1, 3, -2, 3, -1};
	int a4[6] = {2, -3, 4, 5, -4, 6};
	cout << "a1:" << max_sum(a1, 3) << endl;
	cout << "a2:" << max_sum(a2, 3) << endl;
	cout << "a3:" << max_sum(a3, 6) << endl;
	cout << "a4:" << max_sum(a4, 6) << endl;

    return 0;  
} 


<think>好的,我现在要分析n=3的汉诺塔问题的时间复杂度。首先,我得先回忆一下汉诺塔问题的基本规则和递归解法。汉诺塔问题是将n个盘子从A柱移动到C柱,每次只能移动一个盘子,且任何时候大盘子不能放在小盘子上面。 用户提到n等于3的情况,我需要先理解在这种情况下具体的移动步骤,然后根据这些步骤来推导时间复杂度。不过时间复杂度通常是指算法相对于输入规模n的增长率,所以可能需要先了解一般情况下的时间复杂度,再具体到n=3的情况。 首先,汉诺塔问题的经典递归解法是将n-1个盘子从A经过C移动到B,然后将第n个盘子从A移动到C,最后将n-1个盘子从B经过A移动到C。这样,每一步递归都会分解成更小的子问题。时间复杂度可以用递推关系式来表示:T(n) = 2*T(n-1) + 1,其中T(1)=1。解这个递推式的话,应该是T(n) = 2^n -1,所以时间复杂度是O(2^n)。 不过用户现在具体问到n=3的情况,可能需要具体分析步骤次数。对于n=3,按照递归解法,移动次数应该是2^3 -1=7次。那时间复杂度是否就是O(7)即O(1)呢?或者说因为时间复杂度通常关注的是输入规模n的增长率,所以即使n=3,时间复杂度仍然按O(2^n)来表示? 这个时候可能需要澄清时间复杂度的概念。时间复杂度是描述算法执行时间随输入规模增长的趋势,所以即使对于特定的n=3,时间复杂度仍然是O(2^n),因为当n增大时,所需时间呈指数增长。但如果是单独讨论n=3的情况,可能直接说需要7次移动,但时间复杂度还是指数级的。 不过用户的问题可能希望得到更详细的解释,特别是在n=3时的具体步骤如何导致时间复杂度计算。可能需要将递归步骤展开,说明每个递归调用产生的移动次数,并最终得出总次数是2^n -1。 另外,用户可能混淆了具体的执行步骤数和大O表示法的时间复杂度。比如n=3时,步骤数是7次,但时间复杂度仍然表示为O(2^n),因为对于任意的n,步骤数都是2^n -1,所以时间复杂度是O(2^n)。这时候需要明确区分具体执行次数和时间复杂度的概念。 还需要确认汉诺塔问题的递归解法是否正确,是否存在其他解法可能影响时间复杂度,但通常汉诺塔问题的最优解就是递归方法,步骤数无法更少,因此时间复杂度确实为O(2^n)。 总结思考步骤: 1. 汉诺塔问题的递归解法步骤。 2. 推导递推公式并求解时间复杂度。 3. 针对n=3的具体情况计算步骤数。 4. 解释时间复杂度的概念,强调其与具体步骤数的区别。 5. 确认答案的正确性,确保没有遗漏关键点。</think>--- ### **汉诺塔问题分析(n=3)** #### **基本移动步骤** 当 $n=3$ 时,需要将 3 个盘子从 **A柱** 移动到 **C柱**,遵循以下步骤(使用递归思想): 1. 将 **前 2 个盘子** 从 A → B(借助 C) 2. 将 **第 3 个盘子** 从 A → C 3. 将 **前 2 个盘子** 从 B → C(借助 A) 具体移动序列为: ``` A→C, A→B, C→B, A→C, B→A, B→C, A→C ``` 共需 **7 次移动**。 --- ### **时间复杂度分析** #### **递推公式** 汉诺塔的递归算法时间复杂度满足递推关系: $$ T(n) = 2 \cdot T(n-1) + 1 $$ - **$T(n)$**:移动 $n$ 个盘子所需步骤数 - **$T(1) = 1$**:仅需 1 步移动单个盘子 #### **解递推公式** 展开递推式: $$ \begin{aligned} T(n) &= 2 \cdot T(n-1) + 1 \\ &= 2 \cdot [2 \cdot T(n-2) + 1] + 1 = 2^2 \cdot T(n-2) + 2 + 1 \\ &\ \ \vdots \\ &= 2^{n-1} \cdot T(1) + 2^{n-2} + \dots + 2 + 1 \\ &= 2^{n} - 1 \end{aligned} $$ #### **结论** - **步骤总数**:$T(n) = 2^n - 1$ - **时间复杂度**:$O(2^n)$(指数级复杂度) --- ### **n=3 的验证** 代入 $n=3$: $$ T(3) = 2^3 - 1 = 7 \ \text{次移动} $$ --- ### **关键说明** 1. **指数级增长的代价** 每增加一个盘子,步骤数翻倍。例如: - $n=3$ → 7 步 - $n=4$ → 15 步 - $n=10$ → 1023 步 2. **递归的局限性** 当 $n \geq 20$ 时,$2^{20} \approx 100$ 万步,实际运行时间会显著增加。 --- ### **优化思路** - **无更优时间复杂度解法**:汉诺塔问题的最少移动次数已被证明是 $2^n -1$,无法降低复杂度。 - **迭代替代递归**:可用栈模拟递归过程,但时间复杂度仍为 $O(2^n)$。 --- **总结**:对于 $n=3$ 的汉诺塔问题,需 **7 次移动**,时间复杂度为 $O(2^n)$,体现出递归算法在解决此类问题时的简洁性与效率局限性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值