https://leetcode.cn/problems/last-stone-weight/description/\
一、题目分析
有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出两块 最重的 石头,然后将它们一起粉碎。假设石头的重量分别为 x
和 y
,且 x <= y
。那么粉碎的可能结果如下:
- 如果
x == y
,那么两块石头都会被完全粉碎; - 如果
x != y
,那么重量为x
的石头将会完全粉碎,而重量为y
的石头新重量为y-x
。
最后,最多只会剩下一块石头。返回此石头的重量。如果没有石头剩下,就返回 0
。
二、示例分析
输入:[2,7,4,1,8,1] 输出:1 解释: 先选出 7 和 8,得到 1,所以数组转换为 [2,4,1,1,1], 再选出 2 和 4,得到 2,所以数组转换为 [2,1,1,1], 接着是 2 和 1,得到 1,所以数组转换为 [1,1,1], 最后选出 1 和 1,得到 0,最终数组转换为 [1],这就是最后剩下那块石头的重量。
提示:
1 <= stones.length <= 30
1 <= stones[i] <= 1000
通过示例,我们不难看出,今天这道题目的意思就是找出数组中最大和第二大的石头,进行判断,如果重量相同,就会完全粉碎。如果不同,那么就要计算二者之间的差值。(这里的提示会给我们后续优化代码提供帮助)
三、解题思路&代码实现
通过上述分析,我们了解了题目需求。以及要实现的功能,现在就来转换成代码吧!
首先第一步,我们要做的是要求出最大的和第二大的石头。也就是最值问题,但这里的最值问题会有些不大一样。我们还需要找出第二大的石头,这样怎么处理呢?很简单,我们只需把第一次求出的最大值置零就OK了。
int extractMax(int* stones, int stonesSize) {
// 初始化最大元素为数组的第一个元素
int max = stones[0];
// 遍历数组,找到最大元素
for (int i = 1; i < stonesSize; i++) {
// 如果当前元素大于已记录的最大元素,则更新最大元素
if (stones[i] > max)
max = stones[i];
}
// 遍历数组,找到最大元素在数组中的位置
for (int i = 0; i < stonesSize; i++) {
// 如果当前元素等于最大元素
if (stones[i] == max) {
// 将该元素置为0
stones[i] = 0;
// 找到最大元素并置为0后,退出循环
break;
}
}
// 返回提取的最大元素
return max;
}
之后,我么就要分析取得两块最大值后会有哪些情况呢?
第一种:(假设这里最大的为y,第二大的为x)我们的y==x;这样的话我们需要将两个石头的重量置零。(这里的第一种可以不用写,因为在取最大值的时候,我们已经把它们置零了)
第二种:当y!=x时,也就是y要比x大,那么这个时候我们就要计算二者之间的差值并且重新插入到数组当中。
void insert(int* stones, int stonesSize, int value) {
// 遍历数组
for (int i = 0; i < stonesSize; i++) {
// 如果当前位置的值为0
if (stones[i] == 0) {
// 将value插入到该位置
stones[i] = value;
// 插入完成,返回
return;
}
}
}
int lastStoneWeight(int* stones, int stonesSize) {
// 提取数组中的最大元素
int y = extractMax(stones, stonesSize);
// 再次提取数组中的最大元素
int x = extractMax(stones, stonesSize);
// 如果两次提取的最大元素不相等
if (x!= y)
// 将y - x插入到数组中第一个值为0的位置
insert(stones, stonesSize, y - x);
}
现在距离成功还差最后一步,大家想一下,这些操作肯定不会是只进行一次就会完成的吧?所以现在我们需要给它们加上循环。
int extractMax(int* stones, int stonesSize) {
// 初始化最大元素为数组的第一个元素
int max = stones[0];
// 遍历数组,找到最大元素
for (int i = 1; i < stonesSize; i++) {
// 如果当前元素大于已记录的最大元素,则更新最大元素
if (stones[i] > max) {
max = stones[i];
}
}
// 遍历数组,找到最大元素在数组中的位置
for (int i = 0; i < stonesSize; i++) {
// 如果当前元素等于最大元素
if (stones[i] == max) {
// 将该元素置为0
stones[i] = 0;
// 找到最大元素并置为0后,退出循环
break;
}
}
// 返回提取的最大元素
return max;
}
void insert(int* stones, int stonesSize, int value) {
// 遍历数组
for (int i = 0; i < stonesSize; i++) {
// 如果当前位置的值为0
if (stones[i] == 0) {
// 将value插入到该位置
stones[i] = value;
// 插入完成,返回
return;
}
}
}
int lastStoneWeight(int* stones, int stonesSize) {
// 持续循环,直到找到最后一块石头的重量
while (true) {
// 提取数组中的最大元素
int y = extractMax(stones, stonesSize);
// 再次提取数组中的最大元素
int x = extractMax(stones, stonesSize);
// 如果第二次提取的最大元素为0,说明只剩下一块石头,返回第一块石头的重量
if (x == 0) {
return y;
}
// 如果两次提取的最大元素不相等
if (x!= y) {
// 将y - x插入到数组中第一个值为0的位置
insert(stones, stonesSize, y - x);
}
}
// 理论上不会执行到这里,因为while循环中有返回语句(但还是推荐大家加上)
return 0;
}
至此,今天这道题目成功完成!
四、代码优化
这种方法虽然对小白会比较友好,但是这个算法的时间复杂度却是O(N^2)。这里有没有什么可以优化的方式呢?这就用到了在示例分析中给大家标红的提示中, 1 <= stones[i] <= 1000。
石头的重量只会在1-1000之间,这是我就非常希望大家还记得之前提到过的一种思想Hash表。
整体的思路就是,定义一个1001大小的数组,用来存储石头重量出现的次数。
优化后如下:
int extractMax(int* count) {
// 从最大可能值1000开始,递减遍历到0
for (int i = 1000; i >= 0; i--) {
// 如果当前值的计数大于0
if (count[i] > 0) {
// 将该值的计数减1
count[i]--;
// 返回当前值
return i;
}
}
// 如果没有找到非零元素,返回0
return 0;
}
// 函数功能:将一个值插入到计数数组中,增加对应值的计数
// 参数说明:
// count:指向整数数组的指针,该数组记录了不同值的出现次数
// value:要插入的值
void insert(int* count, int value) {
// 将对应值的计数加1
count[value]++;
}
int lastStoneWeight(int* stones, int stonesSize) {
// 初始化一个大小为1001的数组count,用于记录每个重量的石头数量,初始值都为0
int count[1001] = {0};
// 遍历石头重量数组,统计每种重量的石头出现的次数
for (int i = 0; i < stonesSize; i++) {
count[stones[i]]++;
}
// 持续循环,直到找到最后一块石头的重量
while (true) {
// 提取当前最大的非零重量
int y = extractMax(count);
// 再次提取当前最大的非零重量
int x = extractMax(count);
// 如果第二次提取的最大重量为0,说明只剩下一块石头,返回第一块石头的重量
if (x == 0) {
return y;
}
// 如果两次提取的最大重量不相等
if (x!= y) {
// 计算重量差值,并将差值插入到计数数组中
insert(count, y - x);
}
}
return 0;
}
这里为什么要初始化一个1001大小的数组是因为1 <= stones[i] <= 1000。
这里通过Hash表的思想,将代码的时间复杂度大致优化至O(N),但是还有个不好的地方,就是我们其实完全没必要每次求最都从数组的末尾端开始,我们可以定义一个变量用来存储上次最大值的下标位置,下一次开始就从这个位置开始即可。因为下一个数最大也就是和这一次的数相等。
int extractMax(int* count, int upperBound) {
// 从上限值开始递减遍历
for (int i = upperBound; i >= 0; i--) {
// 如果当前元素的计数大于0
if (count[i] > 0) {
// 将该元素的计数减1
count[i]--;
// 返回当前元素的值
return i;
}
}
// 遍历完未找到非零元素,返回0
return 0;
}
void insert(int* count, int value) {
// 将对应值的计数加1
count[value]++;
}
int lastStoneWeight(int* stones, int stonesSize) {
// 初始化一个大小为1001的数组,用于记录每个重量的石头数量,初始值都为0
int count[1001] = {0};
// 遍历石头重量数组,统计每种重量的石头出现次数
for (int i = 0; i < stonesSize; i++) {
count[stones[i]]++;
}
// 设定搜索的上限值为1000
int upperBound = 1000;
// 持续循环,直到找到最后一块石头的重量
while (true) {
// 提取当前最大的非零元素
int y = extractMax(count, upperBound);
// 再次提取当前最大的非零元素
int x = extractMax(count, upperBound);
// 更新上限值为当前最大的非零元素y
upperBound = y;
// 如果第二次提取的最大元素为0,说明只剩下一块石头,返回y
if (x == 0) {
return y;
}
// 如果两次提取的最大元素不相等
if (x!= y) {
// 计算差值并插入到计数数组中
insert(count, y - x);
}
}
return 0;
}
五、题目总结
希望通过今天的分享,大家对这道石头重量问题有了更深入的理解。从最初的暴力解法到利用哈希表优化,我们看到了不同思路和方法带来的巨大差异。在今后的学习和工作中,当面对类似问题时,希望大家能够灵活运用所学知识,不断探索更高效的解决方案。无论遇到什么困难,都不要放弃,相信自己一定能够找到最优解。感谢大家的阅读,期待下次与大家再次共同探索更多有趣的算法问题!谢谢大家!荆轲刺秦!!!