---- 贪心引擎×时空折叠术:两道让时间复杂度原地后空翻的微操实验
当跳跃游戏从青铜升级到王者段位,你的青蛙不再满足于笨拙的蹦跶——
它突然学会用动态规划给自己装上火箭推进器,在数组荷叶上轰出最骚走位;
而另一边,两个指针正在数据沙漠表演量子纠缠,以光年速度折叠有序数组,从时空裂缝里精准揪出中位数幽灵…
今天这两道题,将用贪心算法的涡轮增压系统和双指针的时空扭曲刀法,带你体验把算法玩成星际穿越的快感。
题目一:动态规划——跳跃游戏 II
题目描述
给定一个非负整数数组 nums
,每个元素 nums[i]
表示从位置 i
最多可以跳跃的步数。初始位于数组的第一个位置(nums[0]
),求到达最后一个位置的最少跳跃次数。题目保证可以到达最后一个位置。
示例: 输入:[2, 3, 1, 1, 4]
输出:2
(从下标 0
跳到 1
,再跳到 4
)
算法分析
-
动态规划解法:
- 状态定义:
dp[i]
表示到达位置i
的最小跳跃次数。 - 状态转移:对于每个
i
,遍历所有能跳到i
的位置j
(即j + nums[j] >= i
),并更新dp[i] = min(dp[i], dp[j] + 1)
。 - 复杂度:时间复杂度为 O(n²),空间复杂度为 O(n)
- 状态定义:
-
贪心解法:
- 核心思想:每次在当前可跳跃范围内选择能跳到最远的位置,从而最小化跳跃次数。
- 关键变量:
end
:当前跳跃的边界。maxPos
:当前能跳到的最远位置。steps
:跳跃次数。
- 过程:遍历数组,更新
maxPos
,当i == end
时进行一次跳跃,并更新end = maxPos
。 - 复杂度:时间复杂度为 O(n),空间复杂度为 O(1)
考察点
- 动态规划:状态定义与转移方程的构建。
- 贪心算法:局部最优选择(每次跳跃到最远可达位置)的证明与应用。
#include <stdio.h> // 标准输入输出库,提供printf等函数
#include <limits.h> // 定义整数类型极限值,如INT_MAX
/**
* @brief 计算到达数组末尾的最小跳跃次数(动态规划解法)
*
* @param nums 非负整数数组,每个元素表示该位置可跳跃的最大步数
* @param numsSize 数组的长度(元素个数)
* @return int 返回到达数组末尾所需的最小跳跃次数
*/
int jump(int* nums, int numsSize) {
/* 边界条件检查:当数组长度小于等于1时 */
if (numsSize <= 1) {
return 0; // 已经在末尾或空数组,不需要跳跃
}
/* 定义并初始化dp数组:dp[i]表示到达位置i的最小跳跃次数 */
int dp[numsSize]; // 创建动态规划数组
dp[0] = 0; // 初始化起始位置,跳跃次数为0
/* 初始化dp数组:将所有位置初始化为INT_MAX表示暂时不可达 */
for (int i = 1; i < numsSize; i++) {
dp[i] = INT_MAX; // 使用最大整数值表示初始状态不可达
}
/* 动态规划主循环:填充dp数组 */
for (int i = 1; i < numsSize; i++) { // 遍历每个目标位置i
for (int j = 0; j < i; j++) { // 检查所有可能到达i的起点j
/* 检查是否可以从位置j跳跃到位置i */
if (j + nums[j] >= i) { // j位置的最大跳跃距离是否足够到达i
/* 如果当前路径更优,则更新dp[i] */
if (dp[j] + 1 < dp[i]) { // 比较并更新最小跳跃次数
dp[i] = dp[j] + 1; // 更新为更小的跳跃次数
}
}
}
}
return dp[numsSize - 1]; // 返回最后一个位置的最小跳跃次数
}
/* 主函数:程序入口 */
int main() {
/* 定义并初始化测试用例数组 */
int nums[] = {3, 4, 1, 9, 14, 6, 2, 8, 8}; // 题目给定的输入数组
int numsSize = sizeof(nums) / sizeof(nums[0]); // 计算数组长度
/* 调用jump函数计算最小跳跃次数 */
int minJumps = jump(nums, numsSize); // 获取计算结果
/* 输出最终结果 */
printf("Minimum number of jumps to reach the end: %d\n", minJumps); // 打印最小跳跃次数
/* 额外验证:输出nums[2]的值(题目特殊要求) */
printf("Value of nums[2]: %d\n", nums[2]); // 打印数组第三个元素的值
return 0; // 程序正常退出,返回0
}
输出结果:
题目二:双指针找中位数
题目描述
给定两个已排序的数组 A
和 B
,长度分别为 m
和 n
,要求在不合并数组的情况下,使用双指针找到两个数组的中位数。
示例: 输入:A = [1, 3]
, B = [2]
输出:2.0
(合并后数组为 [1, 2, 3]
,中位数为 2
)
算法分析
-
双指针解法:
- 核心思想:模拟合并过程,通过双指针遍历两个数组,找到第
k
小的元素(k = (m + n) / 2
)。 - 关键变量:
i
和j
:分别指向A
和B
的当前元素。current
和previous
:记录当前和上一个遍历到的值,用于偶数长度时的中位数计算。
- 过程:依次比较
A[i]
和B[j]
,移动较小的指针,直到遍历到中位数位置。 - 复杂度:时间复杂度为 O(m + n),空间复杂度为 O(1)
- 核心思想:模拟合并过程,通过双指针遍历两个数组,找到第
#include <stdio.h> // 标准输入输出头文件,提供printf等函数
/**
* @brief 使用双指针法查找两个有序数组的中位数
* @param A 第一个有序数组(升序排列)
* @param m 数组A的元素个数
* @param B 第二个有序数组(升序排列)
* @param n 数组B的元素个数
* @return double 返回两个数组合并后的中位数(浮点型)
*/
double findMedianSortedArrays(int* A, int m, int* B, int n) {
/* 计算两个数组的总长度 */
int total = m + n;
/* 定义并初始化变量:
current - 当前遍历到的元素值
previous - 前一个遍历到的元素值(用于偶数长度计算) */
int current = 0, previous = 0;
/* 初始化双指针:
i - 指向数组A的当前位置(初始为0)
j - 指向数组B的当前位置(初始为0) */
int i = 0, j = 0;
/* 主循环:遍历到中位数位置(总长度的一半+1) */
for (int k = 0; k <= total / 2; k++) {
/* 保存前一个值(在current被覆盖前) */
previous = current;
/* 指针移动规则:
1. 如果A数组还有元素(i < m)且(B数组已遍历完或A当前元素较小)
2. 否则移动B数组的指针 */
if (i < m && (j >= n || A[i] <= B[j])) {
current = A[i]; // 取A的当前元素
i++; // A指针后移
} else {
current = B[j]; // 取B的当前元素
j++; // B指针后移
}
}
/* 根据总长度的奇偶性返回中位数 */
if (total % 2 == 0) {
/* 偶数长度:取中间两个数的平均值 */
return (previous + current) / 2.0; // 注意除以2.0保证浮点结果
} else {
/* 奇数长度:直接返回中间的数 */
return current;
}
}
/* 主函数:程序入口 */
int main() {
/* 打印功能说明 */
printf("Find the median of two sorted arrays using the two-pointer method:\n");
/* 定义并初始化测试用例数组 */
int A[] = {5, 6, 7, 8, 89}; // 第一个有序数组
int B[] = {10, 15, 16, 17, 19, 67, 89, 254, 698}; // 第二个有序数组
/* 计算数组长度:
sizeof(A)获取数组总字节数
sizeof(A[0])获取单个元素字节数
相除得到元素个数 */
int m = sizeof(A) / sizeof(A[0]);
int n = sizeof(B) / sizeof(B[0]);
/* 调用函数计算中位数 */
double median = findMedianSortedArrays(A, m, B, n);
/* 输出结果,保留1位小数 */
printf("Median: %.1f\n", median); // 预期输出16.5
/* 程序正常退出 */
return 0;
}
输出结果:
-
二分查找优化:
- 核心思想:利用数组有序性,通过二分查找快速定位中位数。
- 关键步骤:
- 确保
A
是较短的数组。 - 在
A
和B
中分别找分割点i
和j
,使得A[i-1] <= B[j]
且B[j-1] <= A[i]
。 - 根据分割点计算中位数。
- 确保
- 复杂度:时间复杂度为 O(log(min(m, n))),空间复杂度为 O(1)
考察点
- 双指针:有序数组的遍历与合并模拟。
- 数学性质:中位数的定义与奇偶长度处理。
- 二分查找:利用有序性优化查找效率。
#include <stdio.h> // 标准输入输出头文件,提供printf等函数
#include <limits.h> // 定义整数类型极限值,如INT_MIN和INT_MAX
/**
* @brief 返回两个整数中的较小值
* @param a 第一个整数
* @param b 第二个整数
* @return 较小的整数值
*/
int min(int a, int b) {
return a < b ? a : b; // 三元运算符实现最小值比较
}
/**
* @brief 返回两个整数中的较大值
* @param a 第一个整数
* @param b 第二个整数
* @return 较大的整数值
*/
int max(int a, int b) {
return a > b ? a : b; // 三元运算符实现最大值比较
}
/**
* @brief 使用二分查找法查找两个有序数组的中位数(优化版)
* @param A 第一个有序数组(需保证长度较短)
* @param m 数组A的长度
* @param B 第二个有序数组
* @param n 数组B的长度
* @return 中位数(double类型)
*/
double findMedianSortedArraysOptimized(int* A, int m, int* B, int n) {
/* 确保A是较短的数组:如果A比B长,交换A和B的位置 */
if (m > n) {
return findMedianSortedArraysOptimized(B, n, A, m);
}
/* 计算总长度和中位数的分割点:
total - 两个数组的总长度
half - 中位数的左分割点位置 */
int total = m + n;
int half = (total + 1) / 2; // +1确保奇数长度时正确
/* 初始化二分查找的边界:
left - 当前查找范围的左边界
right - 当前查找范围的右边界 */
int left = 0, right = m;
/* 开始二分查找循环 */
while (left <= right) {
/* 计算当前分割点:
i - 数组A的分割位置
j - 数组B的分割位置(由half-i决定) */
int i = (left + right) / 2; // A的分割点(中点)
int j = half - i; // B的分割点(保证左半部分总数为half)
/* 处理边界情况(当分割点在数组边界时):
使用INT_MIN/INT_MAX表示虚拟的无限小/大值 */
int A_left = (i == 0) ? INT_MIN : A[i - 1]; // A分割点左侧的值
int A_right = (i == m) ? INT_MAX : A[i]; // A分割点右侧的值
int B_left = (j == 0) ? INT_MIN : B[j - 1]; // B分割点左侧的值
int B_right = (j == n) ? INT_MAX : B[j]; // B分割点右侧的值
/* 检查分割条件是否满足:
1. A的左半部分最大值 <= B的右半部分最小值
2. B的左半部分最大值 <= A的右半部分最小值 */
if (A_left <= B_right && B_left <= A_right) {
/* 找到正确的分割点,根据总长度奇偶性返回中位数 */
if (total % 2 == 0) {
// 偶数长度:取左右两部分最大值的平均
return (max(A_left, B_left) + min(A_right, B_right)) / 2.0;
} else {
// 奇数长度:取左半部分的最大值
return max(A_left, B_left);
}
}
/* 调整二分查找范围 */
else if (A_left > B_right) {
// A的分割点太靠右,需要左移
right = i - 1;
} else {
// A的分割点太靠左,需要右移
left = i + 1;
}
}
/* 理论上不会执行到这里(题目保证输入有效) */
return 0.0;
}
/* 主函数:程序入口 */
int main() {
/* 打印功能说明 */
printf("Finding the median of two sorted arrays using binary search:\n");
/* 定义并初始化测试用例数组 */
int A[] = {4, 5, 6, 7, 8, 89}; // 第一个有序数组
int B[] = {10, 16, 17, 19, 89, 254}; // 第二个有序数组
/* 计算数组长度 */
int m = sizeof(A) / sizeof(A[0]); // 数组A的元素个数
int n = sizeof(B) / sizeof(B[0]); // 数组B的元素个数
/* 调用优化版的中位数查找函数 */
double median = findMedianSortedArraysOptimized(A, m, B, n);
/* 输出结果,保留1位小数 */
printf("Median: %.1f\n", median); // 预期输出13.0
/* 程序正常退出 */
return 0;
}
输出结果:
对比维度 | 跳跃游戏 II | 双指针找中位数 |
---|---|---|
问题类型 | 最优化问题(最小跳跃次数) | 查找问题(中位数) |
核心算法 | 动态规划或贪心 | 双指针或二分查找 |
时间复杂度 | O(n)(贪心)或 O(n²)(动态规划) | O(m + n)(双指针)或 O(log(min(m, n)))(二分) |
空间复杂度 | O(1)(贪心)或 O(n)(动态规划) | O(1) |
关键技巧 | 贪心的局部最优选择 | 双指针的同步移动或二分分割 |
难点 | 证明贪心选择的正确性 | 处理奇偶长度与边界条件 |
总结
-
算法选择:
- 跳跃游戏 II 更适合用贪心算法,因其能高效利用局部最优性。
- 双指针找中位数在数据规模较小时可用双指针,大规模时需用二分查找优化。
-
复杂度对比:
- 跳跃游戏 II 的贪心解法(O(n))优于动态规划(O(n²))。
- 双指针找中位数的二分解法(O(log(min(m, n))))显著优于双指针(O(m + n))。
-
思想共通性:
- 两者均通过减少无效计算(贪心的跳跃选择、二分的快速排除)来优化效率。
- 均需处理边界条件(如跳跃的终点、中位数的奇偶性)。
-
实际应用:
- 跳跃游戏 II 可用于路径规划或资源分配问题。
- 双指针找中位数是数据库查询和数据分析中的常见操作
现在你已解锁:
① 跳跃游戏的『火箭燃料配给学』(青蛙看了都想改行当航天员);
② 中位数狩猎的『时空折叠许可证』(连爱因斯坦都想偷看的操作手册)。
如果你的大脑像刚经历完星际跃迁的飞船引擎般发烫,请速在评论区输入『冷却液告急』;
如果觉得还能承受更高维的算法风暴,试试用反向贪心破解跳跃游戏,用量子指针同时定位三个中位数
博主:再次求波三连!
下期预告:请关注博主,每天上午会发布,如果你有什么问题,可以私信我也可以评论区留言