算法技巧理论基础

文章目录

  • 算法技巧理论基础
    • [1. 算法技巧概述](#1. 算法技巧概述)
      • [1.1 常见算法技巧](#1.1 常见算法技巧)
      • [1.2 技巧的特点](#1.2 技巧的特点)
    • [2. 位运算技巧](#2. 位运算技巧)
      • [2.1 异或运算的基本性质](#2.1 异或运算的基本性质)
      • [2.2 只出现一次的数字(136)](#2.2 只出现一次的数字(136))
      • [2.3 位运算的其他应用](#2.3 位运算的其他应用)
    • [3. 摩尔投票法](#3. 摩尔投票法)
      • [3.1 摩尔投票法的基本思想](#3.1 摩尔投票法的基本思想)
      • [3.2 多数元素(169)](#3.2 多数元素(169))
      • [3.3 摩尔投票法的扩展](#3.3 摩尔投票法的扩展)
    • [4. 三指针技巧(荷兰国旗问题)](#4. 三指针技巧(荷兰国旗问题))
      • [4.1 三指针的基本思想](#4.1 三指针的基本思想)
      • [4.2 颜色分类(75)](#4.2 颜色分类(75))
      • [4.3 三指针技巧的扩展](#4.3 三指针技巧的扩展)
    • [5. 排列问题技巧](#5. 排列问题技巧)
      • [5.1 下一个排列的基本思想](#5.1 下一个排列的基本思想)
      • [5.2 下一个排列(31)](#5.2 下一个排列(31))
      • [5.3 排列问题的扩展](#5.3 排列问题的扩展)
    • [6. 快慢指针技巧(Floyd判圈算法)](#6. 快慢指针技巧(Floyd判圈算法))
      • [6.1 快慢指针的基本思想](#6.1 快慢指针的基本思想)
      • [6.2 寻找重复数(287)](#6.2 寻找重复数(287))
      • [6.3 快慢指针的其他应用](#6.3 快慢指针的其他应用)
    • [7. 常见题型总结](#7. 常见题型总结)
      • [7.1 位运算类](#7.1 位运算类)
      • [7.2 投票算法类](#7.2 投票算法类)
      • [7.3 指针技巧类](#7.3 指针技巧类)
      • [7.4 排列问题类](#7.4 排列问题类)
    • [8. 技巧选择指南](#8. 技巧选择指南)
      • [8.1 何时使用位运算](#8.1 何时使用位运算)
      • [8.2 何时使用投票算法](#8.2 何时使用投票算法)
      • [8.3 何时使用指针技巧](#8.3 何时使用指针技巧)
      • [8.4 何时使用排列技巧](#8.4 何时使用排列技巧)
    • [9. 总结](#9. 总结)

算法技巧理论基础

1. 算法技巧概述

算法技巧是在解决特定类型问题时,使用的一些巧妙的方法和思路。这些技巧通常能够以较低的时间复杂度和空间复杂度解决问题,是算法竞赛和面试中的重要工具。

1.1 常见算法技巧

  • 位运算技巧:异或、与、或等位运算的巧妙应用
  • 双指针技巧:快慢指针、左右指针、三指针等
  • 数学技巧:摩尔投票法、排列组合等
  • 数组技巧:原地修改、交换元素等
  • 链表技巧:快慢指针找环、找中点等

1.2 技巧的特点

  • 高效性:通常时间复杂度较低,空间复杂度也较低
  • 巧妙性:需要一定的数学或逻辑思维
  • 适用性:适用于特定类型的问题
  • 简洁性:代码通常比较简洁

核心思想

复制代码
算法技巧的核心是:用巧妙的方法解决复杂的问题。

关键点:
1. 理解问题的本质
2. 找到合适的技巧
3. 掌握技巧的原理
4. 灵活应用技巧

2. 位运算技巧

2.1 异或运算的基本性质

异或运算(XOR):相同为0,不同为1。

真值表

复制代码
输入 A  输入 B  输出 A ⊕ B
0        0         0
0        1         1
1        0         1
1        1         0

基本性质

  • a ^ a = 0:任何数与自己异或结果为0
  • a ^ 0 = a:任何数与0异或结果为自己
  • a ^ b ^ a = (a ^ a) ^ b = 0 ^ b = b:交换律和结合律
  • a ^ b = b ^ a:交换律
  • (a ^ b) ^ c = a ^ (b ^ c):结合律

示例

复制代码
5 = 0101
0 = 0000
5 ^ 0 = 0101 = 5

5 = 0101
5 = 0101
5 ^ 5 = 0000 = 0

5 = 0101
3 = 0011
5 ^ 3 = 0110 = 6

2.2 只出现一次的数字(136)

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

核心思路

  • 利用异或运算的性质:a ^ a = 0a ^ 0 = a
  • 所有出现两次的数字异或后为0
  • 0与只出现一次的数字异或,结果就是该数字

完整过程示例

复制代码
数组:[4, 1, 2, 1, 2]

初始:res = 0

步骤1:res = 0 ^ 4 = 4
步骤2:res = 4 ^ 1 = 5
步骤3:res = 5 ^ 2 = 7
步骤4:res = 7 ^ 1 = 6
步骤5:res = 6 ^ 2 = 4

结果:4

验证:
4 ^ 1 ^ 2 ^ 1 ^ 2
= 4 ^ (1 ^ 1) ^ (2 ^ 2)
= 4 ^ 0 ^ 0
= 4

代码

cpp 复制代码
class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int res = 0;
        for (int x : nums) {
            res ^= x;
        }
        return res;
    }
};

关键点

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
  • 利用异或运算的交换律和结合律

2.3 位运算的其他应用

应用场景

  • 找只出现一次的数字
  • 找两个只出现一次的数字
  • 判断数字的奇偶性:n & 1
  • 快速计算2的幂:1 << n
  • 判断是否为2的幂:n & (n - 1) == 0

3. 摩尔投票法

3.1 摩尔投票法的基本思想

**摩尔投票法(Boyer-Moore Voting Algorithm)**是一种用于寻找数组中占多数的元素的算法。

核心思想

  • 维护一个候选元素和计数器
  • 遍历数组,如果当前元素等于候选元素,计数器+1;否则计数器-1
  • 当计数器为0时,更新候选元素
  • 最后剩下的候选元素就是多数元素

原理

复制代码
假设多数元素数量 = M
其他元素数量 = N
题目保证:M > N

每一次 count--,本质是:
一个多数元素 + 一个非多数元素 → 同时消失

由于 M > N,最后一定剩下多数元素

3.2 多数元素(169)

题目:给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊n/2⌋ 的元素。你可以假设数组是非空的,并且给定的数组总是存在多数元素。

完整过程示例

复制代码
数组:[3, 2, 3]

初始:candidate = 0, count = 0

步骤1:x = 3
  count == 0,更新 candidate = 3
  x == candidate,count = 1

步骤2:x = 2
  count != 0
  x != candidate,count = 0

步骤3:x = 3
  count == 0,更新 candidate = 3
  x == candidate,count = 1

结果:candidate = 3

验证:3出现2次,2出现1次,3是多数元素

更复杂示例

复制代码
数组:[2, 2, 1, 1, 1, 2, 2]

初始:candidate = 0, count = 0

步骤1:x = 2
  count == 0,更新 candidate = 2
  x == candidate,count = 1

步骤2:x = 2
  count != 0
  x == candidate,count = 2

步骤3:x = 1
  count != 0
  x != candidate,count = 1

步骤4:x = 1
  count != 0
  x != candidate,count = 0

步骤5:x = 1
  count == 0,更新 candidate = 1
  x == candidate,count = 1

步骤6:x = 2
  count != 0
  x != candidate,count = 0

步骤7:x = 2
  count == 0,更新 candidate = 2
  x == candidate,count = 1

结果:candidate = 2

验证:2出现4次,1出现3次,2是多数元素

代码

cpp 复制代码
class Solution {
public:
    int majorityElement(vector<int>& nums) {
        int candidate = 0;
        int count = 0;

        for (int x : nums) {
            if (count == 0) {
                candidate = x;
            }

            if (x == candidate) {
                count++;
            } else {
                count--;
            }
        }

        return candidate;
    }
};

关键点

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
  • 前提条件:多数元素必须存在且数量大于n/2

3.3 摩尔投票法的扩展

应用场景

  • 找出现次数大于n/2的元素
  • 找出现次数大于n/3的元素(需要两个候选元素)
  • 找出现次数大于n/k的元素(需要k-1个候选元素)

4. 三指针技巧(荷兰国旗问题)

4.1 三指针的基本思想

三指针技巧是用于将数组分成多个区域的方法,常用于排序和分类问题。

核心思想

  • 使用多个指针维护不同的区域边界
  • 通过交换元素来调整区域
  • 保持区域的性质不变

4.2 颜色分类(75)

题目:给定一个包含红色、白色和蓝色,共 n 个元素的数组 nums ,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

核心思路

  • 把数组划分成3个区域:[0区 | 1区 | 未处理 | 2区]
  • 使用三个指针:
    • left:0区右边界(不包含)
    • right:2区左边界(不包含)
    • i:当前扫描指针

区域含义

复制代码
[0, left-1]     → 全是 0
[left, i-1]     → 全是 1
[i, right]      → 未处理
[right+1, end]  → 全是 2

完整过程示例

复制代码
数组:[2, 0, 2, 1, 1, 0]

初始:left = 0, right = 5, i = 0
状态:[未处理: 2, 0, 2, 1, 1, 0]

步骤1:i=0, nums[0]=2
  2在右边,交换nums[0]和nums[5]
  数组:[0, 0, 2, 1, 1, 2]
  right = 4, i不变(因为交换后nums[0]可能是0或1,需要再检查)
  状态:[未处理: 0, 0, 2, 1, 1 | 2]

步骤2:i=0, nums[0]=0
  0在左边,交换nums[0]和nums[0](自己),left++
  数组:[0, 0, 2, 1, 1, 2]
  left = 1, i++
  状态:[0 | 未处理: 0, 2, 1, 1 | 2]

步骤3:i=1, nums[1]=0
  0在左边,交换nums[1]和nums[1](自己),left++
  数组:[0, 0, 2, 1, 1, 2]
  left = 2, i++
  状态:[0, 0 | 未处理: 2, 1, 1 | 2]

步骤4:i=2, nums[2]=2
  2在右边,交换nums[2]和nums[4]
  数组:[0, 0, 1, 1, 2, 2]
  right = 3, i不变
  状态:[0, 0 | 未处理: 1, 1 | 2, 2]

步骤5:i=2, nums[2]=1
  1在中间,i++
  数组:[0, 0, 1, 1, 2, 2]
  状态:[0, 0 | 1 | 未处理: 1 | 2, 2]

步骤6:i=3, nums[3]=1
  1在中间,i++
  数组:[0, 0, 1, 1, 2, 2]
  状态:[0, 0 | 1, 1 | 未处理: | 2, 2]

步骤7:i=4 > right=3,结束

结果:[0, 0, 1, 1, 2, 2]

代码

cpp 复制代码
class Solution {
public:
    void sortColors(vector<int>& nums) {
        int left = 0;          // 0 区右边界
        int right = nums.size() - 1;  // 2 区左边界
        int i = 0;             // 当前扫描指针

        while (i <= right) {
            if (nums[i] == 0) {
                swap(nums[i], nums[left]);
                left++;
                i++;
            } 
            else if (nums[i] == 1) {
                i++;
            } 
            else { // nums[i] == 2
                swap(nums[i], nums[right]);
                right--;
                // 注意:这里i不变,因为交换后nums[i]可能是0或1,需要再检查
            }
        }
    }
};

关键点

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
  • 注意:当nums[i] == 2时,交换后i不变,因为交换后的元素需要再检查
  • 含义始终保持:[0, left-1]全是0,[left, i-1]全是1,[i, right]未处理,[right+1, end]全是2

4.3 三指针技巧的扩展

应用场景

  • 颜色分类(荷兰国旗问题)
  • 将数组分成三部分
  • 快速排序的partition操作

5. 排列问题技巧

5.1 下一个排列的基本思想

下一个排列是字典序中当前排列的下一个排列。

核心思路

  1. 从后向前找到第一个升序对(nums[i] < nums[i+1])
  2. 在i的右边找到比nums[i]大的最小数nums[j]
  3. 交换nums[i]和nums[j]
  4. 反转i右边的数组(使其升序,保证字典序最小)

原理

  • 从后向前找升序对,找到可以增大的位置
  • 只变最小幅度保证字典序下一个
  • 反转保证右边升序(最小)

5.2 下一个排列(31)

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

完整过程示例

复制代码
数组:[1, 2, 3]

步骤1:从后向前找第一个升序对
  i = 1, nums[1] = 2, nums[2] = 3
  2 < 3,找到升序对,i = 1

步骤2:在i的右边找比nums[i]大的最小数
  j = 2, nums[2] = 3 > nums[1] = 2
  找到j = 2

步骤3:交换nums[i]和nums[j]
  数组:[1, 3, 2]

步骤4:反转i右边的数组
  i = 1,右边是[2],反转后还是[2]
  数组:[1, 3, 2]

结果:[1, 3, 2]

更复杂示例

复制代码
数组:[1, 2, 3, 5, 4]

步骤1:从后向前找第一个升序对
  i = 2, nums[2] = 3, nums[3] = 5
  3 < 5,找到升序对,i = 2

步骤2:在i的右边找比nums[i]大的最小数
  i的右边是[5, 4],nums[i] = 3
  5 > 3, 4 > 3,最小的是4
  找到j = 4, nums[4] = 4

步骤3:交换nums[i]和nums[j]
  数组:[1, 2, 4, 5, 3]

步骤4:反转i右边的数组
  i = 2,右边是[5, 3],反转后是[3, 5]
  数组:[1, 2, 4, 3, 5]

结果:[1, 2, 4, 3, 5]

代码

cpp 复制代码
class Solution {
public:
    void nextPermutation(vector<int>& nums) {
        int n = nums.size();
        int i = n - 2;

        // 1️找到第一个升序对
        while (i >= 0 && nums[i] >= nums[i + 1]) i--;

        if (i >= 0) {
            // 2️ 找到右边比 nums[i] 大的最小数
            int j = n - 1;
            while (nums[j] <= nums[i]) j--;
            swap(nums[i], nums[j]);
        }

        // 3️ 反转 i 右边的数组
        reverse(nums.begin() + i + 1, nums.end());
    }
};

关键点

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
  • 从后向前找突破口
  • 只变最小幅度保证字典序下一个
  • 反转保证右边升序(最小)

5.3 排列问题的扩展

应用场景

  • 下一个排列
  • 上一个排列
  • 全排列
  • 排列组合

6. 快慢指针技巧(Floyd判圈算法)

6.1 快慢指针的基本思想

**快慢指针(Floyd判圈算法)**是一种用于检测链表中是否存在环的算法,也可以用于找环的入口。

核心思想

  • 使用两个指针,一个快指针(每次走两步),一个慢指针(每次走一步)
  • 如果存在环,快指针和慢指针一定会相遇
  • 相遇后,将慢指针重置到起点,两个指针都每次走一步,再次相遇的点就是环的入口

原理

复制代码
设环的长度为C,环入口到起点的距离为L,相遇点到环入口的距离为X

慢指针走的距离:L + X
快指针走的距离:L + X + nC(n为快指针在环中多走的圈数)

由于快指针速度是慢指针的2倍:
2(L + X) = L + X + nC
L + X = nC
L = nC - X

这意味着:从起点到环入口的距离 = 从相遇点到环入口的距离(考虑环的长度)

6.2 寻找重复数(287)

题目:给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。

核心思路

  • 将数组看作链表,nums[i]表示下一个节点的索引
  • 由于有重复数字,链表中一定存在环
  • 使用快慢指针找环的入口,入口就是重复的数字

完整过程示例

复制代码
数组:[1, 3, 4, 2, 2]

将数组看作链表:
索引0 -> nums[0] = 1 -> 索引1
索引1 -> nums[1] = 3 -> 索引3
索引3 -> nums[3] = 2 -> 索引2
索引2 -> nums[2] = 4 -> 索引4
索引4 -> nums[4] = 2 -> 索引2(形成环)

链表:0 -> 1 -> 3 -> 2 -> 4 -> 2 -> 4 -> ...

步骤1:找相遇点
  初始:slow = nums[0] = 1, fast = nums[0] = 1
  
  第1步:
    slow = nums[1] = 3
    fast = nums[nums[1]] = nums[3] = 2
  
  第2步:
    slow = nums[3] = 2
    fast = nums[nums[2]] = nums[4] = 2
  
  相遇:slow = 2, fast = 2

步骤2:找环入口
  重置:slow = nums[0] = 1, fast = 2
  
  第1步:
    slow = nums[1] = 3
    fast = nums[2] = 4
  
  第2步:
    slow = nums[3] = 2
    fast = nums[4] = 2
  
  相遇:slow = 2, fast = 2

结果:重复数字是2

更直观示例

复制代码
数组:[3, 1, 3, 4, 2]

链表:0 -> 3 -> 4 -> 2 -> 3 -> 4 -> 2 -> ...

步骤1:找相遇点
  初始:slow = nums[0] = 3, fast = nums[0] = 3
  
  第1步:
    slow = nums[3] = 4
    fast = nums[nums[3]] = nums[4] = 2
  
  第2步:
    slow = nums[4] = 2
    fast = nums[nums[2]] = nums[3] = 4
  
  第3步:
    slow = nums[2] = 3
    fast = nums[nums[4]] = nums[2] = 3
  
  相遇:slow = 3, fast = 3

步骤2:找环入口
  重置:slow = nums[0] = 3, fast = 3
  
  第1步:
    slow = nums[3] = 4
    fast = nums[3] = 4
  
  相遇:slow = 4, fast = 4

  但这不是入口,继续:
  第2步:
    slow = nums[4] = 2
    fast = nums[4] = 2
  
  相遇:slow = 2, fast = 2

  继续:
  第3步:
    slow = nums[2] = 3
    fast = nums[2] = 3
  
  相遇:slow = 3, fast = 3

实际上,我们需要找到的是值,不是索引。
重复数字是3(因为nums[0] = 3,nums[2] = 3)

代码

cpp 复制代码
class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int slow = nums[0];
        int 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)
  • 将数组看作链表,利用快慢指针找环
  • 环的入口就是重复的数字

6.3 快慢指针的其他应用

应用场景

  • 检测链表中是否有环
  • 找链表中环的入口
  • 找链表的中点
  • 找链表的倒数第k个节点
  • 寻找重复数

7. 常见题型总结

7.1 位运算类

  1. 只出现一次的数字

    • 136.只出现一次的数字:利用异或运算的性质
  2. 位运算的其他应用

    • 判断数字的奇偶性
    • 快速计算2的幂
    • 判断是否为2的幂

7.2 投票算法类

  1. 多数元素

    • 169.多数元素:摩尔投票法找出现次数大于n/2的元素
  2. 扩展应用

    • 找出现次数大于n/3的元素
    • 找出现次数大于n/k的元素

7.3 指针技巧类

  1. 三指针问题

    • 75.颜色分类:荷兰国旗问题,三指针划分区域
  2. 快慢指针问题

    • 287.寻找重复数:快慢指针找环的入口

7.4 排列问题类

  1. 下一个排列

    • 31.下一个排列:字典序下一个排列
  2. 扩展应用

    • 上一个排列
    • 全排列

8. 技巧选择指南

8.1 何时使用位运算

使用场景

  • 需要找只出现一次的数字
  • 需要快速判断数字的某些性质
  • 需要节省空间

判断标准

  • 问题涉及数字的位操作
  • 需要O(1)空间复杂度
  • 可以利用位运算的性质

8.2 何时使用投票算法

使用场景

  • 需要找出现次数大于n/k的元素
  • 需要O(1)空间复杂度
  • 多数元素一定存在

判断标准

  • 问题涉及找多数元素
  • 空间复杂度要求O(1)
  • 多数元素存在且数量大于n/k

8.3 何时使用指针技巧

使用场景

  • 需要将数组分成多个区域
  • 需要找链表的某些节点
  • 需要检测环

判断标准

  • 问题涉及数组或链表的操作
  • 需要O(1)空间复杂度
  • 可以通过指针维护区域或状态

8.4 何时使用排列技巧

使用场景

  • 需要找下一个或上一个排列
  • 需要生成全排列
  • 需要处理排列组合问题

判断标准

  • 问题涉及排列
  • 需要按字典序处理
  • 需要找到特定的排列

9. 总结

算法技巧是解决特定类型问题的重要工具,掌握这些技巧可以大大提高解题效率。

核心要点

  1. 位运算:利用异或等位运算的性质,解决只出现一次的数字等问题
  2. 投票算法:利用计数器的增减,找出现次数大于n/k的元素
  3. 指针技巧:利用多个指针维护区域或状态,解决数组和链表问题
  4. 排列技巧:利用字典序的性质,找下一个或上一个排列

使用建议

  • 理解技巧的原理和适用场景
  • 掌握技巧的实现细节
  • 注意边界条件处理
  • 灵活应用技巧解决实际问题

常见题型总结

  • 位运算:只出现一次的数字、位操作问题
  • 投票算法:多数元素、出现次数大于n/k的元素
  • 指针技巧:颜色分类、寻找重复数、链表问题
  • 排列问题:下一个排列、全排列

学习路径

  1. 理解每个技巧的基本原理
  2. 掌握技巧的实现方法
  3. 通过例题加深理解
  4. 灵活应用技巧解决实际问题
相关推荐
咕咕嘎嘎10242 小时前
C++仿muduo库onethreadoneloop高并发服务器
服务器·网络·c++
少许极端2 小时前
算法奇妙屋(二十一)-两个数组或字符串的dp问题(动态规划)
算法·动态规划·两个数组或字符串的dp问题
草莓熊Lotso2 小时前
《算法闯关指南:递归,搜索与回溯算法--递归》--04. 两两交换链表中的结点 ,05.Pow(x,n)
数据结构·算法·链表
Bruce_kaizy2 小时前
c++图论——最短路之Johnson算法
开发语言·数据结构·c++·算法·图论
蒙奇D索大2 小时前
【数据结构】排序算法精讲 | 交换排序全解:交换思想、效率对比与实战代码剖析
数据结构·笔记·考研·算法·排序算法·改行学it
sin_hielo2 小时前
leetcode 1351
数据结构·算法·leetcode
睡醒了叭2 小时前
图像分割-传统算法-边缘分割
图像处理·opencv·算法·计算机视觉
AndrewHZ2 小时前
【图像处理基石】有哪些好用的图像去噪算法可以推荐一下么?
图像处理·深度学习·算法·计算机视觉·cv·噪声
Lion Long2 小时前
在 Windows 上快速搭建 VSCode 的 C++ 开发环境(基于 WSL)
linux·c++·windows·vscode·wsl