1.只出现一次的数字
思路分析1:使用哈希表存储每个数字和该数字出现的次数。遍历数组即可得到每个数字出现的次数,并更新哈希表,最后遍历哈希表,得到只出现一次的数字。
具体实现代码(详解版):
class Solution {
public:int singleNumber(vector<int>& nums) {unordered_map<int,int> mp;for(int num : nums) mp[num]++;for(const auto& pair : mp){if(pair.second == 1){return pair.first;}}return -1;}
};
- 时间复杂度:O(n)
- 空间复杂度:O(n)
思路分析2:脑筋急转弯之位运算(异或操作):这里不需要额外的哈希表,直接利用了异或的性质来完成运算。
- 任何数与 0 异或等于其本身:a ^ 0 = a
- 任何数与自己异或等于 0:a ^ a = 0
- 异或操作满足交换律和结合律:a ^ b ^ a = (a ^ a) ^ b = 0 ^ b = b
- 因此,对于数组中的所有元素,由于出现两次的元素会被抵消(变为 0),最终 res 中只剩下出现一次的元素。
具体实现代码(详解版):
class Solution {
public:int singleNumber(vector<int>& nums) {int res = 0;for(auto e : nums) res ^= e;//异或操作return res;}
};
- 时间复杂度:O(n)
- 空间复杂度:O(1)
2.多数元素
思路分析1:哈希表。
具体实现代码(详解版):
class Solution {
public:int majorityElement(vector<int>& nums) {int n = nums.size();unordered_map<int,int> mp;for(int num : nums) mp[num]++;for(const auto& pair : mp){if(pair.second > n / 2){return pair.first;}}return -1;}
};
- 时间复杂度:O(n)
- 空间复杂度:O(n)
思路分析2:Boyer-Moore 投票算法
- 核心思想:Boyer-Moore 投票算法的核心是**“相互抵消”的策略**:
为什么这样操作能找到多数元素?
假设 nums 中的多数元素为 M,它的出现次数超过了数组长度的一半(即大于 ⌊n/2⌋ 次),那么:在计数 count 的增减过程中,多数元素的出现次数无法被其他元素完全抵消掉。
换句话说,虽然其他元素可能抵消掉多数元素的一部分计数,但由于多数元素的数量大于数组中所有其他元素数量的总和,它最终会成为候选元素。
具体实现代码(详解版):
class Solution {
public:int majorityElement(vector<int>& nums) {int can = nums[0];//初始化候选者int cnt = 0;//计数器初始为0for(int i = 1; i < nums.size() ; i ++){if(nums[i] == cnt){//投一票cnt ++;}else{cnt --;//计数器减一,不等于候选者if(cnt == 0){//计数器为0,说明当前候选者不够格,淘汰can = nums[i];//更新候选者cnt = 1;//计数器归为1}}}return can;}
};
3. 颜色分类
思路分析1:不让调用sort,那就手搓快排!Acwing 排序
具体实现代码(详解版):
class Solution {
public:void quick_sort(vector<int>& nums,int l ,int r){if(l >= r) return;int i = l - 1, j = r + 1,x = nums[l + r >> 1];while(i < j){do i ++;while(nums[i] < x);do j --;while(nums[j] > x);if(i < j) swap(nums[i],nums[j]);}quick_sort(nums,l,j),quick_sort(nums,j + 1, r);}void sortColors(vector<int>& nums) {quick_sort(nums,0,nums.size() - 1);}
};
思路分析2:三指针(第一次见):问题实际上是经典的 荷兰国旗问题(Dutch National Flag Problem),可以使用 三指针法(Three-pointer technique) 进行解决。该方法的核心思想是通过三个指针将数组分为三部分:一部分是 0(红色),一部分是 1(白色),一部分是 2(蓝色)。
- 指针定义:
- low:指向当前区间的第一个 1 或 0,用于区分红色区域和白色区域。
- mid:当前扫描指针,扫描所有元素,负责区分白色区域和蓝色区域。
- high:指向当前区间的最后一个 1 或 2,用于区分蓝色区域和白色区域。
- 遍历数组
- 当 nums[mid] == 0 时,将 nums[mid] 和 nums[low] 交换,并增加 low 和 mid 指针。
- 当 nums[mid] == 1 时,直接增加 mid 指针。
- 当 nums[mid] == 2 时,将 nums[mid] 和 nums[high] 交换,并减少 high 指针。此时 mid 指针不增加,因为交换后的 nums[mid] 可能是 0 或 1,需要进一步处理。
- 终止条件:当 mid 超过 high 时,排序完成。
具体实现代码(详解版):
class Solution {
public:void sortColors(vector<int>& nums) {int low = 0, mid = 0, high = nums.size() - 1;// 当 mid 指针小于或等于 high 指针时,继续排序while (mid <= high) {if (nums[mid] == 0) {// 将 0 移到低位swap(nums[low], nums[mid]);low++;mid++;} else if (nums[mid] == 1) {// 1 已经在正确的位置,继续处理下一个mid++;} else {// 将 2 移到高位swap(nums[mid], nums[high]);high--;}}}
};
- 时间复杂度:O(n)
- 空间复杂度:O(1)
4.下一个排列
思路分析:先找到第一个降序对的位置,然后这就是我们需要修改替换的位置,用谁替换呢?当然是它右边第一个比它大的数字。这是为了确保我们选择一个 最小的 大于 nums[i] 的数字,这样交换后能保证下一个排列是字典序最小的。
- 从右向左查找第一个降序对:我们从右向左遍历数组,找到第一个满足 nums[i] < nums[i+1] 的位置 i。这个位置的元素是需要改变的元素,因为它决定了排序的次序。
为什么从右向左遍历?
如果我们从右向左找,意味着我们优先改变最后一个不符合递增的数字,这样保证我们修改的是最小的可能的地方,这样能生成下一个较小的、更大的排列。
- 如果我们遍历到头部都没有找到这样的 i,这意味着数组已经是降序排列的,也就是当前排列已经是最大的排列。此时,我们只需将整个数组反转,得到最小的排列(升序排列)。
说明整个数组是降序排列的,比如 [3, 2, 1]。此时,数组已经是最大的排列了。我们将整个数组反转,得到字典序最小的排列。
- 找到大于 nums[i] 的最小元素:如果找到了 i,接下来我们需要在 i+1 到数组末尾的部分中找到一个比 nums[i] 大的最小元素。假设该元素为 nums[j]。
为什么从右边查找 j?
为了确保我们选择一个 最小的 大于 nums[i] 的数字,这样交换后能保证下一个排列是字典序最小的。
- 交换 nums[i] 和 nums[j]:交换 nums[i] 和 nums[j],这时 nums[i] 会变成一个稍微大的元素,确保字典序的顺序。
- 反转 i+1 到数组末尾的部分:交换后,数组的右半部分并不一定是升序排列的,因此我们需要将它反转成升序,确保得到的排列是下一个字典序排列。
具体实现代码(详解版):
class Solution {
public:void nextPermutation(vector<int>& nums) {int n = nums.size();// 第一步:从右向左找到第一个降序对 nums[i] < nums[i + 1]int i = n - 2; // 从倒数第二个元素开始while (i >= 0 && nums[i] >= nums[i + 1]) {i--; // 如果 nums[i] >= nums[i + 1],继续向左移动}// 如果找到了降序对if (i >= 0) {// 第二步:从右边找第一个比 nums[i] 大的元素 nums[j]int j = n - 1;while (nums[j] <= nums[i]) {j--; // 找到第一个大于 nums[i] 的元素}// 第三步:交换 nums[i] 和 nums[j]swap(nums[i], nums[j]);}// 第四步:反转 nums[i+1] 到数组末尾的部分,确保得到的序列是升序reverse(nums.begin() + i + 1, nums.end());}
};
- 时间复杂度:O(n)
- 空间复杂度:O(1),所有操作都是在原数组上进行的,没有使用额外的空间。
5.寻找重复数
思路分析1:二分查找
- 利用数组的值的范围
- 数组中的元素范围是 [1, n-1],且总共有 n 个元素,因此数组中至少有一个元素重复。
- 二分查找
- 通过统计数组中小于等于某个值mid的元素个数来确定重复元素的位置
- 如果小于等等于mid的元素个数超过mid,说明重复的元素在[1,mid]内;
- 否则,重复的元素在[mid + 1,n - 1]区间。
具体实现代码(详解版):
class Solution {
public:int findDuplicate(vector<int>& nums) {int l = 1, r = nums.size() - 1; // 数字范围在 1 到 n-1 之间while (l < r) {int mid = l + (r - l) / 2; // 计算中间值// 统计小于等于 mid 的元素个数int count = 0;for (int num : nums) {if (num <= mid) {count++;}}// 如果小于等于 mid 的元素个数大于 mid,说明重复的元素在 [l, mid] 区间if (count > mid) {r = mid; // 缩小搜索范围} else {l = mid + 1; // 否则在 [mid + 1, r] 区间}}return l; // 最终 l 和 r 会指向重复元素}
};
- 时间复杂度:每次我们都通过二分法将搜索空间减半,并且需要遍历一次数组来统计小于等于 mid 的元素个数。因此时间复杂度为 O(n log n)。
- 空间复杂度:O(1)
思路分析2:快慢指针法
- 快慢指针方法的核心思想是利用 环形链表 的特性来检测重复元素。
数组与链表的映射:我们可以把数组视为一个链表,其中每个元素 nums[i] 表示指向索引 nums[i] 的下一节点。这样,数组的值实际上就成为了链表的节点值。
环的形成:由于数组中有重复元素,必然会形成一个环。例如,如果有重复的数字 x,那么在 x 所在的位置将会有两个指针指向该位置,这样形成了一个环。
快慢指针:慢指针(Tortoise)每次走一步,快指针(Hare)每次走两步;如果链表中存在环,快慢指针必定会相遇。如果它们相遇,那么相遇点就是环的入口,即重复元素所在的位置。
- 初始化:设置慢指针 slow 和快指针 fast,都从数组的第一个元素开始。
- 第一次相遇:快指针每次走两步,慢指针每次走一步。当它们相遇时,说明链表中存在环,且相遇点就是环中的一个位置
- 找到环的入口:将慢指针移动到数组的起始位置,而快指针保持在相遇点,然后两者每次都走一步。当它们再次相遇时,遇到的点就是环的入口,也就是重复的数字。
具体实现代码(详解版):
class Solution {
public:int findDuplicate(vector<int>& nums) {// 第一步:初始化慢指针和快指针int slow = nums[0], fast = nums[0];// 第二步:快慢指针在环中相遇do {slow = nums[slow]; // 慢指针每次走一步fast = nums[nums[fast]]; // 快指针每次走两步} while (slow != fast); // 直到慢指针和快指针相遇// 第三步:将慢指针移到数组起始位置slow = nums[0];// 第四步:慢指针和快指针每次走一步,直到它们再次相遇while (slow != fast) {slow = nums[slow]; // 慢指针每次走一步fast = nums[fast]; // 快指针每次走一步}// 返回重复的数字(即相遇点)return slow;}
};
- 时间复杂度:O(n)
- 空间复杂度:O(1)