LeetCode 热题100之技巧关卡

1.只出现一次的数字

思路分析1:使用哈希表存储每个数字和该数字出现的次数。遍历数组即可得到每个数字出现的次数,并更新哈希表,最后遍历哈希表,得到只出现一次的数字。

具体实现代码(详解版):

cpp 复制代码
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 中只剩下出现一次的元素。

体实现代码(详解版):

cpp 复制代码
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:哈希表。

具体实现代码(详解版):

cpp 复制代码
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 投票算法的核心是**"相互抵消"的策略**:
    • 候选多数元素:算法假设存在一个候选多数元素,并通过不断的遍历和计数来验证这个候选多数元素的有效性。
    • 计数:算法维护一个计数器 count,用来跟踪候选元素在数组中出现的"净次数"。
    • 抵消策略:
      • 如果遇到的元素等于当前候选元素,则计数加 1。
      • 如果遇到的元素不等于当前候选元素,则计数减 1。
      • 当计数减到 0 时,认为当前候选元素不再可能是多数元素(因为支持它的数量被其他元素抵消了),因此换一个新的候选元素并重置计数为 1。

为什么这样操作能找到多数元素?

假设 nums 中的多数元素为 M,它的出现次数超过了数组长度的一半(即大于 ⌊n/2⌋ 次),那么:在计数 count 的增减过程中,多数元素的出现次数无法被其他元素完全抵消掉。

换句话说,虽然其他元素可能抵消掉多数元素的一部分计数,但由于多数元素的数量大于数组中所有其他元素数量的总和,它最终会成为候选元素。

具体实现代码(详解版):

cpp 复制代码
class Solution {
public:
    int majorityElement(vector<int>& nums) {
        int can = nums[0];//初始化候选者
        int cnt = 0;//计数器初始为0

        for(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 排序

具体实现代码(详解版):

cpp 复制代码
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 时,排序完成。

具体实现代码(详解版):

cpp 复制代码
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 到数组末尾的部分:交换后,数组的右半部分并不一定是升序排列的,因此我们需要将它反转成升序,确保得到的排列是下一个字典序排列。

具体实现代码(详解版):

cpp 复制代码
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]区间。

具体实现代码(详解版):

cpp 复制代码
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,都从数组的第一个元素开始。
  • 第一次相遇:快指针每次走两步,慢指针每次走一步。当它们相遇时,说明链表中存在环,且相遇点就是环中的一个位置
  • 找到环的入口:将慢指针移动到数组的起始位置,而快指针保持在相遇点,然后两者每次都走一步。当它们再次相遇时,遇到的点就是环的入口,也就是重复的数字。

具体实现代码(详解版):

cpp 复制代码
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)
相关推荐
云青山水林12 分钟前
比较各种排序方法的实现思想、优缺点和适用场合
数据结构·算法·排序算法
缸缸---15 分钟前
TOP K问题:利用堆排序找出数组中最小的k个数
数据结构·算法
初阳78525 分钟前
12. 日常算法
算法
Abelard_29 分钟前
LeetCode--排序算法(堆排序、归并排序、快速排序)
算法·leetcode·排序算法
姚先生9729 分钟前
LeetCode 349. 两个数组的交集 (C++实现)
c++·算法·leetcode
m0_7482404432 分钟前
使用 DBSCAN(基于密度的聚类算法) 对二维数据进行聚类分析
算法·数据挖掘·聚类
m0_5195231037 分钟前
算法练习——模拟题
数据结构·c++·学习·算法
robin_suli1 小时前
穷举vs暴搜vs深搜vs回溯vs剪枝系列一>组合
算法·剪枝·深度优先遍历·回溯
yuanbenshidiaos1 小时前
算法
算法
minstbe2 小时前
AI开发 - 算法基础 递归 的概念和入门(一) 递归算法的常见应用 PYTHON
算法