java 移动数字“0”算法

本文介绍了一种在不复制数组的前提下,将所有零元素移动到数组末尾同时保持非零元素相对顺序的算法实现。通过遍历和元素交换的方式,在O(n)的时间复杂度内完成操作。

题目: Given an array nums, write a function to move all 0’s to the end of it while maintaining the relative order of the non-zero elements.

For example, given nums = [0, 1, 0, 3, 12], after calling your function, nums should be [1, 3, 12, 0, 0].

Note:

You must do this in-place without making a copy of the array.
Minimize the total number of operations.

下面是我给出的算法:

public class Solution {
   public void moveZeroes(int[] nums) {
        for(int j=0;j<nums.length;j++){
            if(nums[j]==0){
                int temp = nums[j];
                int k = j;
                while(k<nums.length-1){
                    nums[k]=nums[k+1];
                    k++;
                }
                nums[nums.length-1] = -1;   //用数字-1补全数组,然后遍历数组将-1替换为0;
                j--;
            }
        }

        for(int i=0;i<nums.length;i++){
            if(nums[i]==-1){
                nums[i]=0;
            }
        }
   }
}
<think>根据用户的问题,用户想要了解Java实现2048游戏的向上移动算法。结合引用内容,特别是引用[2]中关于游戏规则的描述,我们可以知道向上移动算法需要处理方格向上移动、合并以及生成新方格等步骤。 在2048游戏中,向上移动意味着所有方格向上移动,如果相邻的方格数字相同则合并,合并后空格出现在下方。同时,每次移动后会在空白位置随机生成一个新的数字(2或4)。 我们将设计一个向上移动算法,该算法需要处理一个4x4的网格(通常用二维数组表示)。算法步骤可以分解为: 1. 遍历每一列(因为向上移动是按列处理的)。 2. 对于每一列,从第二行开始(因为第一行是最上面,不需要考虑更上面的合并),将非空的方格向上移动直到遇到边界或者另一个方格。 3. 检查当前方格和它上面的方格是否相同,如果相同则合并(上面的方格值翻倍,当前方格置为空,注意合并后该位置不能在同一轮再次合并)。 4. 移动后,在空白位置随机生成一个2或4(但注意,一次移动只生成一个)。 然而,在实际实现中,我们通常按列处理,并从上往下处理(这样合并后上面的方格不会再次被合并)。但是向上移动时,我们更倾向于从最上面开始往下检查,这样我们可以先处理上面的合并,然后下面的方格再依次填补上去。 具体步骤(以向上移动为例): 1. 对于每一列,从第二行开始(行索引为1)到第四行(行索引为3),将当前方格向上移动。 2. 具体移动:对于当前方格,如果它非空,则向上寻找直到遇到非空的方格或者边界。 - 如果遇到边界(即第一行),则直接移动到该位置(如果该位置为空)或者与第一行的方格比较。 - 如果遇到非空方格,则比较是否相等,相等则合并(注意:合并后,当前方格置空,合并后的位置不能再次合并,所以需要标记合并状态,或者按顺序处理避免重复合并)。 3. 实际上,更常见的做法是:对于每一列,将非空元素向上移动(压缩),然后再进行合并,合并后再压缩一次(因为合并后可能有空格)。 简化步骤(按列处理): a. 将当前列的非空方格向上压缩(即移除空格,使非空方格都排列在列的上方)。 b. 从上到下检查相邻的方格是否相同,如果相同则合并(上面的方格值翻倍,下面的方格置空,并记录合并的分数)。 c. 再次压缩该列(因为合并后可能有空格出现)。 注意:在合并操作中,同一对方格在一次移动中只能合并一次,所以上述步骤是安全的。 下面是一个具体的向上移动算法的实现思路(伪代码): ``` for (int j = 0; j < 4; j++) { // 遍历每一列 // 第一步:向上压缩(将非空元素移到上面) int[] temp = new int[4]; // 临时数组,用于存放压缩后的列(非空元素) int index = 0; for (int i = 0; i < 4; i++) { if (grid[i][j] != 0) { temp[index++] = grid[i][j]; } } // 第二步:合并相邻且相同的元素(从上到下) for (int i = 0; i < index-1; i++) { if (temp[i] == temp[i+1]) { temp[i] *= 2; // 合并后,后面的元素前移 for (int k = i+1; k < index-1; k++) { temp[k] = temp[k+1]; } // 最后一个位置置0,并且有效元素减少一个 temp[index-1] = 0; index--; // 元素个数减一 // 注意:合并后,后面的元素已经前移,所以跳过下一个元素(因为它已经移到当前合并的位置,而我们需要检查新的下一个) // 所以这里我们让i++,跳过下一个(因为下一个已经前移到了当前合并位置的下一个,而当前合并位置的下一个已经是原来下下一个了) // 但是,如果我们合并了第i和i+1个,那么原来的i+2变成了i+1,而我们已经处理过i+1(现在是原来的i+2)了吗?没有,所以我们可以不跳过,因为合并后我们继续检查当前位置(因为合并后当前位置的值可能和下一个再次合并?规则不允许,因为合并后当前位置的值是原来的两倍,而下一个是原来的下下一个,所以不会连续合并两次。但是为了避免连续合并,规则上是一次移动中每个方格只能合并一次,所以我们在合并后应该跳过下一个(即i++),因为下一个已经被移除了。 i++; // 跳过下一个,避免连续合并同一个格子两次 } } // 第三步:将临时数组放回原列(注意,临时数组可能后面有0,即空格) for (int i = 0; i < 4; i++) { if (i < index) { grid[i][j] = temp[i]; } else { grid[i][j] = 0; } } } ``` 但是,上述方法在合并时使用了一个临时数组,并且通过移动数组元素来合并,然后放回原数组。这种方法可行,但可能不是最高效的,不过对于4x4的网格来说足够。 另一种方法是在原数组上操作,使用两个指针,一个用于记录当前可以放置的位置,另一个用于遍历。但是考虑到合并需要相邻元素比较,我们分两步:先压缩,再合并,再压缩。 更常见的做法是: 1. 压缩:将当前列的非元素向上移动(覆盖掉元素)。 2. 合并:从上到下检查,如果当前元素和下一个元素相等,则合并(当前元素翻倍,下一个元素置,并记录分数),然后继续。 3. 再压缩:将合并后的列再次压缩(去掉合并产生的)。 这样,我们避免了使用临时数组,但需要两次遍历列来压缩。 下面是一个不使用临时数组的实现(伪代码): ``` for (int j = 0; j < 4; j++) { // 第一步:压缩列(向上移动元素) int writeIndex = 0; // 记录当前写入位置 for (int i = 0; i < 4; i++) { if (grid[i][j] != 0) { grid[writeIndex][j] = grid[i][j]; if (writeIndex != i) { grid[i][j] = 0; // 将原来的位置置空(移动后) } writeIndex++; } } // 第二步:合并相邻相同元素(从上到下) for (int i = 0; i < 3; i++) { // 从0到2(因为要检查i和i+1) if (grid[i][j] != 0 && grid[i][j] == grid[i+1][j]) { grid[i][j] *= 2; grid[i+1][j] = 0; // 合并后下一个置空 // 注意:合并后,分数增加(这里省略分数处理) } } // 第三步:再次压缩(因为合并后可能有空格) writeIndex = 0; for (int i = 0; i < 4; i++) { if (grid[i][j] != 0) { if (writeIndex != i) { grid[writeIndex][j] = grid[i][j]; grid[i][j] = 0; } writeIndex++; } } } ``` 注意:在第一次压缩后,该列的非元素已经连续排列在顶部,然后合并相邻的(从上到下),合并后可能中间出现空格(因为合并后置空了一个),所以需要再次压缩。 这种方法在每次移动中进行了两次压缩和一次合并遍历。对于4x4的网格,效率可以接受。 现在,我们根据上述第二种方法(不使用临时数组)来编写一个具体的向上移动的方法。注意,我们还需要在移动后生成一个新的数字,但生成新数字通常是在整个移动操作之后,并且要判断是否有方格移动(如果没有任何移动和合并,则不应该生成新数字)。因此,我们还需要在移动过程中记录是否有移动或合并发生。 下面是一个Java实现的示例代码(仅向上移动): ```java /** * 向上移动 * @return 返回是否发生了移动(包括合并) */ public boolean moveUp() { boolean moved = false; // 标记是否移动过 for (int j = 0; j < 4; j++) { // 第一次压缩:将非元素向上移动 int writeIndex = 0; for (int i = 0; i < 4; i++) { if (grid[i][j] != 0) { if (i != writeIndex) { grid[writeIndex][j] = grid[i][j]; grid[i][j] = 0; moved = true; // 发生了移动 } writeIndex++; } } // 合并相邻相同元素 for (int i = 0; i < 3; i++) { if (grid[i][j] != 0 && grid[i][j] == grid[i+1][j]) { grid[i][j] *= 2; grid[i+1][j] = 0; moved = true; // 发生了合并 // 注意:合并后,当前位置的值已经翻倍,下一个位置变为0,我们将在第二次压缩中处理 i++; // 跳过下一个,因为下一个已经被合并了(置0),避免重复合并(例如2,2,2,2合并成4,0,2,2,然后继续合并4和2?不对,但这里我们跳过下一个,即下一个已经是0了,所以不会合并,但跳过可以避免检查已经合并的) // 实际上,这里跳过下一个是因为我们已经处理了i和i+1,所以下一个位置是i+2,我们直接跳到i+1(即跳过i+1)然后循环会i++变成i+2,所以这里我们i++相当于跳到i+1,然后循环结束后i++变成i+2,所以跳过了i+1(已经被合并置0的位置) } } // 第二次压缩:合并后可能有空格,再次压缩 writeIndex = 0; for (int i = 0; i < 4; i++) { if (grid[i][j] != 0) { if (i != writeIndex) { grid[writeIndex][j] = grid[i][j]; grid[i][j] = 0; moved = true; // 发生了移动 } writeIndex++; } } } return moved; } ``` 注意:上述代码中,在合并循环中,我们使用了`i++`来跳过下一个位置,这是因为我们合并了`i`和`i+1`,那么`i+1`位置已经变为0,而`i+2`位置的值(如果有)不会和`i`位置的值相同(因为合并后`i`位置的值翻倍,而`i+2`位置还是原来的值),但是为了避免重复检查(即使不会合并),我们跳过下一个位置(即已经处理过的位置)。另外,在合并后,我们标记了`moved=true`。 另外,在第一次压缩后,如果发生了移动,我们标记`moved=true`,同样在第二次压缩后如果发生了移动也标记。合并时也标记。 但是,注意:有可能压缩并没有改变网格(比如已经排好序),但是合并发生了,所以合并标记也是必要的。 最后,这个方法返回是否发生了移动(包括合并),这样在游戏主循环中,如果返回true,则需要在空白位置生成一个新的数字。 以上是针对向上移动算法实现。其他方向的移动类似,只是遍历的方向不同。 希望这个回答能够帮助用户理解2048游戏中向上移动算法的实现。</think>### Java实现2048游戏的向上移动算法 在2048游戏中,向上移动算法的核心逻辑是处理网格中数字移动和合并。以下是完整的实现步骤和代码解析: #### 算法步骤 1. **遍历每列**:从第0列到第3列 2. **压缩空格**:将非数字向上移动(消除中间的空格) 3. **合并相同数字**:相邻相同数字合并(值翻倍) 4. **再次压缩**:合并后可能出现新空格,需再次压缩 5. **返回移动状态**:标记是否发生有效移动(用于判断是否需要生成新数字) #### Java代码实现 ```java public boolean moveUp() { boolean moved = false; // 标记是否发生有效移动 for (int col = 0; col < 4; col++) { // 第一步:压缩空格(非元素上移) int[] temp = new int[4]; int index = 0; for (int row = 0; row < 4; row++) { if (grid[row][col] != 0) { temp[index++] = grid[row][col]; } } // 第二步:合并相邻相同数字 for (int i = 0; i < index - 1; i++) { if (temp[i] == temp[i + 1]) { temp[i] *= 2; // 合并后值翻倍 temp[i + 1] = 0; // 被合并位置清 moved = true; // 标记发生有效移动 } } // 第三步:再次压缩(处理合并后的空格) int[] result = new int[4]; int pos = 0; for (int value : temp) { if (value != 0) { result[pos++] = value; } } // 第四步:更新网格并检查移动状态 for (int row = 0; row < 4; row++) { if (grid[row][col] != result[row]) { moved = true; // 检测到位置变化 } grid[row][col] = result[row]; } } return moved; } ``` #### 关键逻辑说明 1. **空格压缩** - 使用临时数组`temp`存储非元素 - 消除列中的空格:`[2, 0, 4, 2]` → `[2, 4, 2]` 2. **数字合并** - 仅合并相邻的相同数字:`[2, 2, 4]` → `[4, 0, 4]` - 合并后立即清被合并位置 - **注意**:每个数字单次移动中只能合并一次(避免级联合并) 3. **二次压缩** - 合并后重新排列:`[4, 0, 4]` → `[4, 4]` → 补为`[4, 4, 0, 0]` 4. **移动状态检测** - 比较移动前后网格变化 - 返回`true`时需在空白位置生成新数字(2或4) #### 算法复杂度 - 时间复杂度:$O(n^2)$(n=4,固定16次操作) - 空间复杂度:$O(n)$(使用临时数组) > 提示:其他方向移动可通过调整行列遍历顺序实现,向下移动只需反转遍历方向(从底部开始处理)[^2]。 --- ### 相关问题 1. **如何实现2048游戏的其他方向(下/左/右)移动?** *提示:调整行列遍历顺序和索引计算方式* 2. **移动后如何随机生成新数字?** *需考虑:空白位置选择策略(加权随机)、生成数字概率(90%为2,10%为4)* 3. **如何检测游戏结束条件?** *需同时满足:网格已满 + 无相邻相同数字 + 无有效移动* 4. **2048游戏有哪些性能优化方向?** *例如:使用位运算加速合并、预计算移动结果、状态缓存等* 5. **Java实现中如何设计计分系统?** *建议:在合并操作时累加分值(如合并后值=4时加4分)* > 引用说明:本文算法基于经典2048游戏规则实现[^2],具体优化可参考网格处理策略[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值