【至少是其他数字两倍的最大数(两种实现)】和【青蛙跳台阶(递归和动态分配内存)】


前言

最近仔细想了想,如果写之前学过的简单知识并跟着复习无法静下心好好总结,很多细的知识点会漏掉,发挥不到很好的效果。因此我决定从现在所写练习中跟随练习查找自己的遗漏。而且最近的练习让我收获满满,学到的知识点也非常有帮助。


提示:以下是本篇文章正文

一、至少是其他数字两倍的最大数

题目:
给你一个整数数组 nums ,其中总是存在 唯一的 一个最大整数 。
请你找出数组中的最大元素并检查它是否 至少是数组中每个其他数字的两倍 。如果是,则返回 最大元素的下标 ,否则返回 - 1 。

1、优化前

代码如下:

#include <stdio.h>
int dominantIndex(int* nums, int numsSize)
{
	int i = 0;
	int max = nums[0];	//设最大值为第一个元素
	int ret = 0;
	for (i = 1; i < numsSize; i++)
	{
		if (nums[i] > max)
		{
			max = nums[i];
			ret = i;
		}
	}
	for (i = 0; i < numsSize; i++)	//比较最大值是否为其余元素的两倍
	{
		if (max < 2 * nums[i] && i != ret)
		{
			return -1;
		}
	}
	return ret;
}
int main()
{
	int arr[100] = { 0 };
	int n = 0;
	printf("请输入数组元素个数:");
	scanf("%d", &n);
	int i = 0;
	printf("请输入数组元素:");
	for (i = 0; i < n; i++)
	{
		scanf("%d", &arr[i]);
	}
	int ret = dominantIndex(arr, n);
	printf("最大元素下标为:%d\n", ret);
	return 0;
}

这道题难度较低,这是我第一次写完后的代码,但是对这段代码我做了几点总结:
(1)输入整数数组时,由于VS不支持变长数组的使用,所以我用了控制数组输入元素的方法。这几天的学习如整数数组输入,字符串输入,和分别求它们的元素个数,都是我没有完全掌握的内容。下面看这个表格:

arr[ ]整数数组字符串数组
输入1for循环控制输入元素个数gets(arr)(不推荐但最简单实用)
2初始化数组(在线OJ题不适用)fgets(会算上’\0’)
3while循环getchar()
求元素个数sizeof操作符strlen函数

(2)在这段代码中,我的方法是先找出数组中元素的最大值,再将最大值与其他值的二倍逐一比较,显然这是常规思路,而昨晚我刚好初次了解了数据结构中时间复杂度的概念,这里的时间复杂度就是O(n^2)。然后我就想,能不能优化时间复杂度,让两个循环并成一个循环。

2、优化后

代码如下(主函数同上):

int dominantIndex(int* nums, int numsSize)
{
	int i = 0;
	int max = nums[0];	//设最大值为第一个元素
	int secondmax = 0;  //设次大值
	int ret = 0;
	for (i = 1; i < numsSize; i++)
	{
		if (nums[i] > max)
		{
			secondmax = max;	//次大值更新为原最大值
			max = nums[i];
			ret = i;
		}
		else if (nums[i] > secondmax)
		{
			secondmax = nums[i];
		}
	}
	return max >= 2 * secondmax ? ret : -1;	//只要最大值大于次大值的两倍,那它一定大于其余所有值的两倍
}

对函数优化后,这道题的解题思路变成了再设一个次大值。如果有新的最大值,则将次大值赋值原最大值,最大值照常更新,如果有大于次大值的值,次大值还要赋为这个值。最后我们比较最大值和次大值的二倍,只要最大值大于次大值的两倍,那它一定大于其余所有值的两倍。这样,这段代码的时间复杂度就由O(n^2)减少到了O(n)。

二、青蛙跳台阶

题目:
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
数据范围:1≤n≤40。

1、递归

代码如下:

#include <stdio.h>
int jumpFloor(int number)
{
	if (number <= 2)
	{
		return number;
	}
	else
	{
		return jumpFloor(number - 1) + jumpFloor(number - 2);
	}
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = jumpFloor(n);
	printf("%d\n", ret);
	return 0;
}

这道题其实很早就写过了,用递归解决这种问题的时候很简单并且代码也很简洁,但是递归对于规模较大的问题时,会有以下几点问题:
(1)由于存在重叠子问题,会导致重复计算相同的台阶跳法数量,在数值较大时,重复计算的次数更多,导致效率低下。
(2)需要频繁地进行函数调用,降低了程序执行速度。
(3)使用函数调用栈来保存每一层递归的局部变量和返回地址,当问题规模较大时,可能会导致栈溢出。

这两天练习中,我频繁碰见程序使用动态分配内存malloc()函数,而我恰好没学习到这块知识,经过网上的查阅和总结,了解到这题可以使用动态分配内存或是动态规划的方法解决。算是我对动态分配内存的初步了解。

2、动态分配内存或动态规划

代码如下:

#include <stdio.h>
#include <stdlib.h>
int jumpFloor(int number)
{
	if (number <= 2)
	{
		return number;
	}
	int* dp = (int*)malloc((number + 1) * sizeof(int));
	dp[1] = 1;
	dp[2] = 2;
	int i = 0;
	for (i = 3; i <= number; i++)
	{
		dp[i] = dp[i - 1] + dp[i - 2];
	}
	int num = dp[number];
	free(dp);	//释放动态内存的分配
	return num;
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = jumpFloor(n);
	printf("%d\n", ret);
	return 0;
}

在这个示例中,我们使用 malloc() 动态分配了一个大小为 (n+1) * sizeof(int) 的整数数组 dp。在使用完数组 dp 后,我们使用 free() 函数释放了动态分配的内存。
这里附上malloc()函数的使用,声明如下:

void* malloc(size_t size);

malloc() 函数接受一个 size_t 类型的参数,size表示需要分配的内存大小(以字节为单位)。它返回一个指向分配内存起始位置的指针,或者在分配失败时返回 NULL。
1、malloc() 函数进行动态内存分配的基本步骤: 导入 <stdlib.h> 头文件,因为 malloc() 函数的定义在该头文件中。
2、定义一个指针变量来接收分配内存后的起始地址。
3、使用 malloc() 函数进行内存分配,并将所需的内存大小作为参数传递。
4、检查分配内存是否成功(即返回的指针是否为 NULL)。
5、 在使用完分配的内存后,使用 free() 函数释放该内存,避免内存泄漏。

这时候,我们就得了解使用动态分配内存和递归相比的好处:
(1)灵活性:动态分配内存使得程序可以根据需要在运行时分配内存,而不是在编译时固定分配。这使得程序可以动态地处理不确定大小的数据结构,提供更大的灵活性。
(2)节省内存:动态分配内存只会占用程序实际需要的内存空间,避免了静态分配可能导致的浪费。它可以根据程序运行时的需求动态地增加或减少内存的使用量。
(3)避免栈溢出:动态分配内存不依赖于栈的大小限制,因此可以处理大量数据或递归深度较深的情况,避免栈溢出的问题。

了解了动态分配内存后,我标题上的动态规划又是什么呢?看代码:

int jumpFloor(int number) 
{
    if (number <= 2) 
    {
        return number;
    }
    int dp[number + 1];
    dp[1] = 1;  // 第一级台阶的跳法数量为 1
    dp[2] = 2;  // 第二级台阶的跳法数量为 2
    for (int i = 3; i <= number; i++) 
    {
        dp[i] = dp[i - 1] + dp[i - 2];  // 跳到第 i 级台阶的跳法数量等于跳到第 i-1 级和跳到第 i-2 级的跳法数量之和
    }
    return dp[number];
}

看完这段代码,发现它和前面动态分配内存的方法不就差了一个malloc()函数的使用吗,这里用的是数组int dp[number+1],而这是一个变长数组,变长数组的使用是受编译器限制的,所以就要用到前面的动态分配内存来解决。
这时候我们来区分一下动态规划和动态分配内存:

1、动态规划是一种算法思想,用于解决具有最优子结构性质的问题,通过保存中间结果来避免重复计算,从而提高算法的效率。动态规划并不直接涉及内存分配。
2、动态分配内存是一种内存管理的概念,用于在程序运行时根据需要动态地分配和释放内存空间。它主要关注的是内存的分配和释放,以及解决一些与内存使用相关的问题。
虽然两者都包含了“动态”这个概念,但其所指的方面完全不同。动态规划是一种算法设计思想,而动态分配内存是一种内存管理机制。


总结

1、在至少是其他数字两倍的最大数这个问题的优化后中,让我们了解了新的思路,减少了时间复杂度。
2、在青蛙跳台阶这个问题中,了解到了动态分配内存和动态规划(主要讲一下动态内存分配),其实,递归和动态内存分配各有好处,总结如下:

动态分配内存的优点包括:
(1)灵活性:动态分配内存使得程序可以根据需要在运行时分配内存,而不是在编译时固定分配。这使得程序可以动态地处理不确定大小的数据结构,提供更大的灵活性。
(2)节省内存:动态分配内存只会占用程序实际需要的内存空间,避免了静态分配可能导致的浪费。它可以根据程序运行时的需求动态地增加或减少内存的使用量。
(3)避免栈溢出:动态分配内存不依赖于栈的大小限制,因此可以处理大量数据或递归深度较深的情况,避免栈溢出的问题。

递归的优点包括:
(1)简洁性:递归能够以一种简洁、直观的方式表达复杂的问题。通过将问题分解为相同的子问题,递归可以使代码更易于理解和编写。
(2)可读性:递归可以更好地体现问题的本质和解决方案的逻辑,提高代码的可读性。递归的实现通常比迭代更贴近问题的抽象描述。
(3)可维护性:递归使得代码结构更清晰,模块化更好,易于维护和调试。递归可以使问题解决过程更直观、自然,减少了大规模嵌套循环的使用。

然而,动态分配内存和递归也存在一些潜在的问题需要注意:
动态分配内存:可能导致内存泄漏或者内存碎片的问题,需要确保在适当的时候释放已分配的内存。同时,由于动态分配内存需要在运行时进行,其性能可能会受到一定的影响。
递归:在处理大规模问题或者递归深度较大时,可能导致性能下降和栈溢出的问题。过度依赖递归可能会增加空间复杂度和调用开销,需要谨慎设计递归算法。

因此,在使用动态分配内存和递归时,需要综合考虑问题的特点、实际需求和性能等因素,选择合适的方案来解决问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值