力扣Hot100系列18(Java)——[技巧]总结 (只出现一次的数字,多数元素,颜色分类,下一个排列,寻找重复数)

文章目录


前言

本文记录力扣Hot100里面关于"技巧"的五道题,包括常见解法和一些关键步骤理解,也有例子便于大家理解


一、只出现一次的数字

1.题目

给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。

示例 1 :

输入:nums = [2,2,1]

输出:1

示例 2 :

输入:nums = [4,1,2,1,2]

输出:4

示例 3 :

输入:nums = [1]

输出:1

2.代码

java 复制代码
class Solution {
    public int singleNumber(int[] nums) {
        // 初始化结果变量为0,因为0和任何数异或都等于这个数本身
        int single = 0;
        // 增强for循环,遍历数组中的每一个数字
        for (int num : nums) {
            // 核心操作:将当前结果与数组元素做异或运算(^= 是异或赋值运算符)
            single ^= num;
        }
        // 遍历结束后,single就是唯一出现一次的数字
        return single;
    }
}

3.理解

异或运算是二进制位运算,规则是:对应位相同则为0,不同则为1。比如 5 ^ 3:

  • 5 的二进制:101
  • 3 的二进制:011
  • 异或结果:110(即十进制6)

而代码能生效,全靠这3个关键特性:

  1. 0 异或任何数 = 任何数本身:a ^ 0 = a(比如 3 ^ 0 = 3)
  2. 任何数异或自身 = 0:a ^ a = 0(比如 5 ^ 5 = 0)
  3. 异或满足交换律和结合律:a ^ b ^ c = a ^ c ^ b = (a ^ a)

4.例子

例子:nums = [4,1,2,1,2]

代码执行流程:

  1. 初始化 single = 0
  2. 第一次遍历到数字4:
    • single = 0 ^ 4 = 4(特性1),single变为4;
  3. 第二次遍历到数字1:
    • single = 4 ^ 1 = 5(暂时保留,因为1还没遇到第二个),single变为5;
  4. 第三次遍历到数字2:
    • single = 5 ^ 2 = 7(继续保留,2也没遇到第二个),single变为7;
  5. 第四次遍历到数字1:
    • single = 7 ^ 1 = 6(此时1遇到第二个,抵消了之前的1,相当于4 ^ 2),single变为6;
  6. 第五次遍历到数字2:
    • single = 6 ^ 2 = 4(此时2遇到第二个,抵消了之前的2,只剩4),single变为4;
  7. 遍历结束,返回single的值4------这就是数组中只出现一次的数字。

简化理解(用交换律/结合律):

其实不用逐步算,直接把重复的数配对抵消:
4 ^ 1 ^ 2 ^ 1 ^ 2 = 4 ^ (1 ^ 1) ^ (2 ^ 2) = 4 ^ 0 ^ 0 = 4,结果完全一致。


二、多数元素

1.题目

给定一个大小为 n 的数组 nums ,返回其中的多数元素 。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的 ,并且给定的数组总是存在多数元素。

示例 1:

输入:nums = [3,2,3]

输出:3

示例 2:

输入:nums = [2,2,1,1,1,2,2]

输出:2

2.代码

java 复制代码
class Solution {
    public int majorityElement(int[] nums) {
        // 初始化计数器,用来记录当前候选元素的"票数"
        int count = 0;
        // 初始化结果变量,用来保存当前的候选元素
        int res = 0;
        
        // 遍历数组中的每一个数字
        for(int num : nums){
            // 条件1:如果当前票数为0,说明之前的候选元素被抵消完了,更换候选为当前数字
            if(count == 0){
                res = num;
            }
            
            // 条件2:如果当前数字等于候选元素,票数+1;否则票数-1
            if(num == res){
                count++;
            }else{
                count--;
            }
        }
        // 遍历结束后,res就是出现次数超过一半的多数元素
        return res;
    }
}

3.例子

例子:nums = [3,3,4,2,4,4,2,4,4]

初始化:count = 0res = 0

第1轮:遍历数字3

  • 条件1:count == 0 → 把候选res更新为3;
  • 条件2:当前数字3 == 候选3 → count 从0 → 1;
  • 此时状态:res=3count=1

第2轮:遍历数字3

  • 条件1:count≠0 → 不更换候选;
  • 条件2:当前数字3 == 候选3 → count 从1 → 2;
  • 此时状态:res=3count=2

第3轮:遍历数字4

  • 条件1:count≠0 → 不更换候选;
  • 条件2:当前数字4 ≠ 候选3 → count 从2 → 1;
  • 此时状态:res=3count=1

第4轮:遍历数字2

  • 条件1:count≠0 → 不更换候选;
  • 条件2:当前数字2 ≠ 候选3 → count 从1 → 0;
  • 此时状态:res=3count=0

第5轮:遍历数字4

  • 条件1:count == 0 → 把候选res更新为4;
  • 条件2:当前数字4 == 候选4 → count 从0 → 1;
  • 此时状态:res=4count=1

第6轮:遍历数字4

  • 条件1:count≠0 → 不更换候选;
  • 条件2:当前数字4 == 候选4 → count 从1 → 2;
  • 此时状态:res=4count=2

第7轮:遍历数字2

  • 条件1:count≠0 → 不更换候选;
  • 条件2:当前数字2 ≠ 候选4 → count 从2 → 1;
  • 此时状态:res=4count=1

第8轮:遍历数字4

  • 条件1:count≠0 → 不更换候选;
  • 条件2:当前数字4 == 候选4 → count 从1 → 2;
  • 此时状态:res=4count=2

第9轮:遍历数字4

  • 条件1:count≠0 → 不更换候选;
  • 条件2:当前数字4 == 候选4 → count 从2 → 3;
  • 此时状态:res=4count=3

遍历结束

返回res=4,正是数组中出现次数超过一半的多数元素。


三、颜色分类

1.题目

给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地 对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

必须在不使用库内置的 sort 函数的情况下解决这个问题。

示例 1:

输入:nums = [2,0,2,1,1,0]

输出:[0,0,1,1,2,2]

示例 2:

输入:nums = [2,0,1]

输出:[0,1,2]

2.代码

java 复制代码
class Solution {
    public void sortColors(int[] nums) {
        // 获取数组长度
        int n = nums.length;
        // p0:0的右边界(下一个0要放的位置),初始在0;p2:2的左边界(下一个2要放的位置),初始在最后一位
        int p0 = 0, p2 = n - 1;
        
        // i是遍历指针,注意终止条件是i <= p2(p2左边的元素还没处理完,p2右边全是2)
        for (int i = 0; i <= p2; ++i) {
            // 如果当前元素是2,就交换到p2位置,同时p2左移
            // 用while而非if:交换过来的元素可能还是2,需要继续交换直到当前i位置不是2
            while (i <= p2 && nums[i] == 2) {
                // 交换nums[i]和nums[p2]
                int temp = nums[i];
                nums[i] = nums[p2];
                nums[p2] = temp;
                // p2左移,因为该位置已经放好2了
                --p2;
            }
            
            // 处理完2之后,检查当前元素是否是0:如果是,交换到p0位置,p0右移
            if (nums[i] == 0) {
                int temp = nums[i];
                nums[i] = nums[p0];
                nums[p0] = temp;
                // p0右移,因为该位置已经放好0了
                ++p0;
            }
            
            // 注意:如果当前元素是1,无需处理,直接i++即可(循环会自动推进)
        }
    }
}

3.例子

例子:nums = [2,0,2,1,1,0]

初始状态:

  • 数组:[2,0,2,1,1,0]
  • n = 6p0 = 0p2 = 5i = 0

第1轮:i=0

  1. 检查nums[i] = 2,进入while循环(i<=p2成立):
    • 交换nums[0]nums[5] → 数组变为[0,0,2,1,1,2]
    • p2左移1 → p2 = 4
    • 再次检查nums[0] = 0(不是2),退出while循环;
  2. 检查nums[i] = 0
    • 交换nums[0]nums[p0=0](自己和自己交换,数组不变);
    • p0右移1 → p0 = 1
  3. i++i = 1
  • 当前状态:数组[0,0,2,1,1,2]p0=1p2=4i=1

第2轮:i=1

  1. 检查nums[i] = 0,不进入while(不是2);
  2. 检查nums[i] = 0
    • 交换nums[1]nums[p0=1](自己和自己交换,数组不变);
    • p0右移1 → p0 = 2
  3. i++i = 2
  • 当前状态:数组[0,0,2,1,1,2]p0=2p2=4i=2

第3轮:i=2

  1. 检查nums[i] = 2,进入while循环(i<=p2成立):
    • 交换nums[2]nums[4] → 数组变为[0,0,1,1,2,2]
    • p2左移1 → p2 = 3
    • 再次检查nums[2] = 1(不是2),退出while循环;
  2. 检查nums[i] = 1,无需处理;
  3. i++i = 3
  • 当前状态:数组[0,0,1,1,2,2]p0=2p2=3i=3

第4轮:i=3

  1. 检查nums[i] = 1,不进入while
  2. 检查nums[i] = 1,无需处理;
  3. i++i = 4
  • 当前状态:数组[0,0,1,1,2,2]p0=2p2=3i=4

循环终止

此时i=4p2=3i <= p2不成立,循环结束。

最终数组:[0,0,1,1,2,2](排序完成)。


四、下一个排列

1.题目

整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。

  • 例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1] 。
    整数数组的 下一个排列 是指其整数的下一个字典序更大的排列 。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列 。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
  • 例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。
  • 类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。
  • 而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。

给你一个整数数组 nums ,找出 nums 的下一个排列。

必须 原地 修改,只允许使用额外常数空间。

示例 1:

输入:nums = [1,2,3]

输出:[1,3,2]

示例 2:

输入:nums = [3,2,1]

输出:[1,2,3]

示例 3:

输入:nums = [1,1,5]

输出:[1,5,1]

2.代码

java 复制代码
class Solution {
    public void nextPermutation(int[] nums) {
        // 步骤1:从后往前找第一个 nums[i] < nums[i+1] 的位置 i
        int i = nums.length - 2; // 从倒数第二个元素开始(避免i+1越界)
        while (i >= 0 && nums[i] >= nums[i + 1]) { // 只要nums[i] >= nums[i+1],就继续往前找
            i--;
        }

        // 步骤2:如果找到i(说明不是最大排列),找j并交换
        if (i >= 0) { // i=-1时,说明数组是降序(最大排列),无需交换
            int j = nums.length - 1; // 从最后一个元素开始找
            while (j >= 0 && nums[i] >= nums[j]) { // 找第一个比nums[i]大的nums[j]
                j--;
            }
            swap(nums, i, j); // 交换i和j,保证i位置是"最小的更大值"
        }

        // 步骤3:反转i+1到末尾的元素(把降序改成升序,得到最小后续)
        reverse(nums, i + 1);
    }

    // 辅助方法:交换数组中两个位置的元素
    public void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

    // 辅助方法:反转数组中从start到末尾的元素
    public void reverse(int[] nums, int start) {
        int left = start, right = nums.length - 1;
        while (left < right) { // 双指针交换,直到相遇
            swap(nums, left, right);
            left++;
            right--;
        }
    }
}

3.例子

例子1:普通排列(nums = [1,3,2])

目标:找到[1,3,2]的下一个排列(结果是[2,1,3])。

初始状态:数组[1,3,2],长度n=3

步骤1:找升序拐点i

  • 初始化i = n-2 = 1(倒数第二个元素,索引1);
  • 检查nums[i] = 3nums[i+1] = 23 >= 2,满足循环条件,i--i=0
  • 检查nums[i] = 1nums[i+1] = 31 < 3,不满足循环条件,退出循环;
  • 最终i=0(找到拐点,说明不是最大排列)。

步骤2:找j并交换

  • 因为i>=0,初始化j = n-1 = 2(最后一个元素,索引2);
  • 检查nums[i] = 1nums[j] = 21 < 2,不满足循环条件(nums[i] >= nums[j]),退出循环;
  • 交换nums[i=0]nums[j=2]
    原数组[1,3,2] → 交换后[2,3,1]

步骤3:反转i+1到末尾

  • i+1 = 1,反转从索引1到末尾的元素([3,1]);
  • 反转过程:
    • left=1,right=2 → 交换nums[1]nums[2] → 数组变为[2,1,3]
    • left=2,right=1 → 不满足left < right,反转结束;
  • 最终数组:[2,1,3](正确的下一个排列)。

例子2:最大排列(nums = [3,2,1])

目标:当前是最大排列,返回最小排列([1,2,3])。

初始状态:数组[3,2,1],长度n=3

步骤1:找升序拐点i

  • 初始化i = n-2 = 1
  • 检查nums[1]=2 >= nums[2]=1i--i=0
  • 检查nums[0]=3 >= nums[1]=2i--i=-1
  • 退出循环,i=-1(说明是最大排列)。

步骤2:找j并交换

  • i=-1,跳过交换步骤。

步骤3:反转i+1到末尾

  • i+1 = 0,反转从索引0到末尾的整个数组([3,2,1]);
  • 反转过程:
    • left=0,right=2 → 交换nums[0]nums[2] → 数组变为[1,2,3]
    • left=1,right=1 → 不满足left < right,反转结束;
  • 最终数组:[1,2,3](最小排列,符合要求)。

例子3:更复杂的普通排列(nums = [1,5,4,3,2])

目标:找到下一个排列(结果是[2,1,3,4,5])。
步骤1:找i

  • i = 5-2 = 3nums[3]=3 >= nums[4]=2i=2
  • nums[2]=4 >= nums[3]=3i=1
  • nums[1]=5 >= nums[2]=4i=0
  • nums[0]=1 < nums[1]=5 → 退出,i=0

步骤2:找j并交换

  • j=4nums[0]=1 < nums[4]=2 → 退出循环;
  • 交换i=0j=4 → 数组变为[2,5,4,3,1]

步骤3:反转i+1到末尾

  • 反转索引1到4的元素([5,4,3,1])→ 反转后[1,3,4,5]
  • 最终数组:[2,1,3,4,5](正确的下一个排列)。

五、寻找重复数

1.题目

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数

假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。

你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。

示例 1:

输入:nums = [1,3,4,2,2]

输出:2

示例 2:

输入:nums = [3,1,3,4,2]

输出:3

示例 3 :

输入:nums = [3,3,3,3,3]

输出:3

2.代码

java 复制代码
class Solution {
    public int findDuplicate(int[] nums) {
        // 第一步:快慢指针找环内相遇点
        int slow = nums[0]; // 慢指针:初始在索引0,走1步(nums[0])
        int fast = nums[nums[0]]; // 快指针:初始在索引0,走2步(nums[nums[0]])
        
        // 循环直到快慢指针相遇(证明环存在)
        while (slow != fast) {
            slow = nums[slow]; // 慢指针每次走1步:当前值 → 下一个索引
            fast = nums[nums[fast]]; // 快指针每次走2步:当前值→下一个索引→再下一个索引
        }

        // 第二步:找环的入口(重复数)
        fast = 0; // 快指针重置到起点(索引0)
        // 两个指针以相同速度走,相遇点就是环的入口
        while (slow != fast) {
            slow = nums[slow]; // 慢指针从相遇点走
            fast = nums[fast]; // 快指针从起点走
        }

        return slow; // 相遇点就是重复数
    }
}

3.例子

例子:nums = [1,3,4,2,2]
第一步:快慢指针找环内相遇点

初始化:

  • 慢指针(slow):从0号格子出发跳1步,到 nums[0] = 1(停在1号格子);
  • 快指针(fast):从0号格子出发跳2步,先到1号再到 nums[1] = 3(停在3号格子)。

开始循环跳,直到相遇:

  1. 第一次跳:
    • 慢指针:从1号格子跳1步,到 nums[1] = 3(停在3号);
    • 快指针:从3号格子跳2步,先到2号再到 nums[2] = 4(停在4号);
    • 此时slow=3,fast=4,没相遇。
  2. 第二次跳:
    • 慢指针:从3号格子跳1步,到 nums[3] = 2(停在2号);
    • 快指针:从4号格子跳2步,先到2号再到 nums[2] = 4(停在4号);
    • 此时slow=2,fast=4,没相遇。
  3. 第三次跳:
    • 慢指针:从2号格子跳1步,到 nums[2] = 4(停在4号);
    • 快指针:从4号格子跳2步,先到2号再到 nums[2] = 4(停在4号);
    • 此时slow=4,fast=4,相遇了,退出第一步循环。

第二步:找环的入口(重复数)

重置快指针到起点(0号格子),现在快慢指针每次都只跳1步,直到相遇:

  1. 第一次跳:
    • 慢指针:从4号格子跳1步,到 nums[4] = 2(停在2号);
    • 快指针:从0号格子跳1步,到 nums[0] = 1(停在1号);
    • 此时slow=2,fast=1,没相遇。
  2. 第二次跳:
    • 慢指针:从2号格子跳1步,到 nums[2] = 4(停在4号);
    • 快指针:从1号格子跳1步,到 nums[1] = 3(停在3号);
    • 此时slow=4,fast=3,没相遇。
  3. 第三次跳:
    • 慢指针:从4号格子跳1步,到 nums[4] = 2(停在2号);
    • 快指针:从3号格子跳1步,到 nums[3] = 2(停在2号);
    • 此时slow=2,fast=2,相遇了!

最终返回slow=2,这就是数组中重复的数字。


如果本篇文章对您有帮助,可以点赞,收藏或评论哦!!!关注主包不迷路,让我们一起向前进步吧!!

相关推荐
mit6.8242 小时前
贪心构造+枚举子集+有向图判环
算法
_周游2 小时前
Java8 API文档搜索引擎_优化构建索引速度
java·服务器·搜索引擎·intellij-idea
孞㐑¥2 小时前
算法—字符串
开发语言·c++·经验分享·笔记·算法
北凉军2 小时前
IDEA中热部署插件JRebel激活失败404
java·ide·intellij-idea
乐观甜甜圈2 小时前
在Windows系统上hprof文件是否可以删除
java
鱼跃鹰飞2 小时前
Leetcode279:完全平方数
数据结构·算法·leetcode·面试
小龙报2 小时前
【数据结构与算法】单链表核心精讲:从概念到实战,吃透指针与动态内存操作
c语言·开发语言·数据结构·c++·人工智能·算法·链表
野犬寒鸦2 小时前
从零起步学习并发编程 || 第二章:多线程与死锁在项目中的应用示例
java·开发语言·数据库·后端·学习
张np2 小时前
java进阶-Zookeeper
java·zookeeper·java-zookeeper