【算法】常见基础算法

目录

前言:

一、双指针

二、滑动窗口

三、二分算法

四、前缀和

五、位运算

六、分治快排

[1. 三路划分铺垫](#1. 三路划分铺垫)

[2. 三路划分快排](#2. 三路划分快排)

[3. 快速选择算法](#3. 快速选择算法)

七、分治归并

快排和归并的区别:

八、链表

九、哈希表

十、栈

十一、字符串

十二、优先级队列(堆)

[十三、BFS && 队列](#十三、BFS && 队列)

[十四、BFS && FloodFill](#十四、BFS && FloodFill)

[十五、BFS && 最短路问题](#十五、BFS && 最短路问题)

十六、多源BFS问题

[十七、BFS && 拓扑排序](#十七、BFS && 拓扑排序)

[十八、递归 && 搜索 && 回溯](#十八、递归 && 搜索 && 回溯)


前言:

遇见一个问题,不妨先思考:

  1. 有没有暴力解法,如果有,再考虑如何在暴力解法上优化
  2. 要考虑特殊情况,比如是否为空、比如是否越界
  3. 正难则反
  4. 数据是否有序,如果有序,不妨考虑双指针和二分
  5. 数据是否具有单调性,如果有,不妨考虑滑动窗口
  6. 数据是否具有二段性,如果有,不妨考虑二分算法
  7. 如果要快速求出某一段连续区间的和,不妨考虑前缀和
  8. top k 问题,堆排序 or 快速选择算法

(笔者对算法的理解还很浅显,这些只是对笔者自己的提示,不构成建议。)

常见排序算法

常见排序算法的时间复杂度及其稳定性:

  1. 冒泡排序, O(N^2), 稳定的
  2. 插入排序, O(N^2), 稳定的
  3. 选择排序, O(N^2), 不稳定
  4. 希尔排序, O(N*logN), 不稳定
  5. 堆排序, O(N*logN), 不稳定
  6. 快速排序, O(N*logN), 不稳定
  7. 归并排序, O(N*logN), 稳定
  8. 计数排序, O(N+K), 稳定的(K 为数据范围)

各排序算法博客链接

冒泡排序

插入排序

选择排序

希尔排序

堆排序

快速排序

归并排序

计数排序

一、双指针

  • 双指针有多种,比如:同向(滑动窗口) 、异向、快慢
  • 当数据有序或者存在单调性的时候,不妨考虑双指针
  • 但双指针思想并不仅局限于有序时才能使用

例题:

283.移动零

解题思路:

代码:

cpp 复制代码
// 写法一
class Solution 
{
public:
    void moveZeroes(vector<int>& nums) 
    {
        int cur = 0; //遍历数组,左侧为已处理,右侧为未处理
        int des = -1; //非0元素的最后一个位置,左侧为非0元素,右侧为0

        // [非0,des] [des+1,cur-1] [cur, ]
        while(cur!=nums.size())
        {
            if(nums[cur]!=0)
            {
                ++des;
                swap(nums[cur],nums[des]);
            }
            ++cur;
        }
    }
};


// 写法二
class Solution 
{
public:
    void moveZeroes(vector<int>& nums) 
    {
        for(int cur = 0,dest = -1;cur < nums.size();cur++)
        {
            if(nums[cur])
            {
                swap(nums[++dest],nums[cur]);
            }
        }
    }
};

和为 s 的两个数

解题思路:

代码:

cpp 复制代码
class Solution
{
public:
    vector<int> twoSum(vector<int>& price, int target) 
    {
       int left = 0; 
       int right = price.size()-1;
       while(left < right)
       {
            if(price[left]+price[right]>target)
            {
                right--;
            }
            else if(price[left]+price[right]<target)
            {
                left++;
            }
            else
            return {price[left],price[right]};
       }
       // 不会走到这,这一行是为了避免编译器报错
       return {-1,-1};
    }
};

15.三数之和

解题思路:

代码:

cpp 复制代码
class Solution 
{
public:
    vector<vector<int>> threeSum(vector<int>& nums) 
    {
        sort(nums.begin(),nums.end());//先排序,方便使用双指针
        vector<vector<int>> ret;
        for(int i = 0; i < nums.size(); )
        {
            if(nums[i] > 0) break; // 如果nums[i]大于0,说明其后全为正数,不可能再找到两数之和为其相反数的数
            
            //[-1,0,1,2,-1,-4]
            int left = i+1; //(-1 [0,1,2,-1,-4]),left指向新区间首元素,将-1摘出来,后面元素作为新区间,在新区间里找两个和为1的数
            int right = nums.size()-1; 
            int target = -nums[i];//开始时为 -1 的相反数
            while(left < right)
            {
                //这一部分与查找总价格为目标值的两个商品思路大致相同 
                if(nums[left]+nums[right]>target)
                {
                    --right;
                }
                else if(nums[left]+nums[right]<target)
                {
                    ++left;
                }
                else
                {
                    //先插入,然后在新区间里继续缩小区间查找,保证不漏
                    ret.push_back({nums[left],nums[right],nums[i]});
                    left++,right--;

                    // 去重left和right,要注意避免越界
                    while(left < right && nums[left]==nums[left - 1])//新区间里,下个值相同就跳过
                    {
                        ++left;
                    }
                    while(left < right && nums[right]==nums[right + 1])//新区间里,下个值相同就跳过
                    {
                        --right;
                    }
                }
            }
            // 去重i
            i++;
            while(i < nums.size() - 1 && nums[i]==nums[i - 1])//如果下个nums[i]值和之前相同也跳过,也要注意避免越界
            {
                i++;
            }
        }
        return ret;
    }
};          

二、滑动窗口

滑动窗口是双指针的一种

满足单调性时,发现两个同向指针都可以做到不回退时,就可以用滑动窗口。

正确性:

利用单调性,规避了很多没有必要的枚举行为,正确性等同于暴力枚举,但时间复杂度一般只有O(n)。

基础步骤:

定义 left=0, right=0

进窗口,移动其中一个指针,比如移动 right 让数据进窗口。

判断,判断窗口内的数据是否满足条件,是否需要移动 left 让 left 旧数据出窗口。

④ 循环执行②和③,直到right 指针遍历完整个数组 / 字符串。

注:

其中还有一步是更新结果

但更新结果的时机是就题论题的,有的题是进窗口时更新结果,有的题是判断时更新结果,有的题是判断加出窗口结束后更新结果
注意:

解题步骤并不是最重要的,最重要的是如何能分析出某一题需要使用或可以使用滑动窗口

满足单调性时,发现两个同向指针都可以做到不回退时,就可以用滑动窗口。

例题:

1004.最大连续1的个数III

解题思想:找出最长的连续子数组,其中0的个数不超过k个。

cpp 复制代码
class Solution 
{
public:
    int longestOnes(vector<int>& nums, int k) 
    {
        int left = 0,right = 0,zero = 0;
        int ret = 0;
        while(right < nums.size())
        {
            if(nums[right]==0)
            {
                ++zero; // 计算窗口内0的数量 
            }
            while(zero > k) // 说明窗口内0的个数不符合要求
            {
                if(nums[left]==0)
                {
                    --zero;
                }
                ++left;
            } // 到这说明窗口内的数据符合要求

            ret = max(ret,right-left+1); // 更新结果
            ++right;
        }
        return ret;
    }
};

209.长度最小的子数组


三、二分算法

二分不一定需要有序,只要能找出一种规律使得其具有二段性,即能被分成两段,能淘汰其中一段,就能使用。

  1. 二分并非只能用在有序数组上,它的核心前提其实是二段性,而非 "有序" 本身。
  2. 所谓 "二段性",指的是:对于区间 [L, R],存在一个分界点 mid,使得区间可以被划分为两个部分,且满足 "前一部分都满足条件 A,后一部分都满足条件 B(A、B 互斥)"。
  3. 只要满足这个性质,不管数组是否完全有序,都可以通过判断 mid 的归属,直接淘汰一半区间,从而实现 O (log n) 的查找效率。

基础模板:

1. 基础模板代码

cpp 复制代码
// 朴素二分查找模板(适用于有序数组的精确查找)
while (left <= right) 
{
    // 写法1:向下取整的mid,等价于 (left + right) / 2
    int mid = left + (right - left) / 2; 

    if (/* 条件1:mid位置不满足目标,目标在右侧 */) 
    {
        left = mid + 1;
    } 
    else if (/* 条件2:mid位置不满足目标,目标在左侧 */) 
    {
        right = mid - 1;
    } 
    else 
    {
        // 找到目标值,返回结果
        return /* 结果值或索引 */;
    }
}

2. 向上取整 mid 写法

常用于避免死循环的场景:

cpp 复制代码
// 写法2:向上取整的mid,等价于 (left + right + 1) / 2
int mid = left + (right - left + 1) / 2; 

两种 mid 写法的区别(以区间 [0,3] 为例)

  • 向下取整:(0 + 3) / 2 = 1(取区间偏左的中间值)
  • 向上取整:(0 + 3 + 1) / 2 = 2(取区间偏右的中间值)

3. 关键细节说明

  • 循环条件 left <= right :表示闭区间 [left, right] 内还有元素未检查,需要继续循环。
  • 二段性 :模板的核心逻辑依赖区间的二段划分,通过 mid 的判断淘汰一半区间,实现 O(log n) 的效率。
  • 溢出问题 :用 left + (right - left) / 2 而非 (left + right) / 2,是为了避免 left + right 过大导致整数溢出。

进阶模板:

例题:

704. 二分查找 - 力扣(LeetCode)

34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

35. 搜索插入位置 - 力扣(LeetCode)

69. x 的平方根 - 力扣(LeetCode)

852. 山脉数组的峰顶索引 - 力扣(LeetCode)

162. 寻找峰值 - 力扣(LeetCode)


四、前缀和

作用:快速求出数组中某一段连续区间的和(一次前缀和时间复杂度可以达到O(1))

解题过程:

  1. 先预处理出一个前缀和数组或矩阵
  2. 根据题意使用前缀和数组或矩阵来解决问题

一维前缀和:

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main()
{
    //获取数据
    int n,m;
    cin >> n >> m;
    vector<int> arr(n+1);
    for(int i = 1;i <= n;i++) cin >> arr[i];

    //构建前缀和数组
    vector<long long> dp(n+1);
    for(int i = 1;i <= n;i++) dp[i] =  dp[i-1] + arr[i];

    //使用前缀和数组
    int l,r;
    while(m--)
    {
        cin >> l >> r;
        cout << dp[r] - dp[l-1] << endl;
    }

    return 0;
}

二维前缀和:

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main() 
{
    //获取数据
    int n = 0,m = 0;//行列
    int q = 0;// 查询次数
    cin >> n >> m >> q;
    vector<vector<int>> arr(n+1,vector<int>(m+1));// n+1行,m+1列
    for(int i = 1;i <= n;i++)
    {
        for(int j = 1;j <= m;j++)
        {
            cin >> arr[i][j];
        }
    }

    //构建前缀和矩阵
    vector<vector<long long>> dp(n+1,vector<long long>(m+1));// n+1行,m+1列,且要考虑防溢出
    for(int i = 1;i <= n;i++)
    {
        for(int j = 1;j <= m;j++)
        {
            dp[i][j] = dp[i-1][j] + dp[i][j-1] + arr[i][j] -dp[i-1][j-1];
        }
    }

    //使用前缀和矩阵
    int x1 = 0,y1 = 0,x2 = 0,y2 = 0;//坐标
    while(q--)
    {
        cin >> x1 >> y1 >> x2 >> y2;
        cout << dp[x2][y2] -dp[x1-1][y2] -dp[x2][y1-1] + dp[x1-1][y1-1] << endl;
    }

    return 0;
}

五、位运算

1. 基础位运算

  • << 左移:0100(4) << 1 = 1000(8)
  • >> 右移:0100(4) >> 1 = 0010(2)
  • ~ 取反:~0100 各位翻转(符号位一起变)
  • & 有 0 就是 0:0100 & 0011 = 0000
  • | 有 1 就是 1:0100 | 0011 = 0111
  • ^ 相同 0 不同 1:0100 ^ 0011 = 0111

2. 确定二进制第 x 位是 0 还是 1

  • 公式:(n >> x) & 1
  • 例子:n = 5(0101),看第 1 位(0101 >> 1) & 1 = 0010 & 1 = 0→ 第 1 位是 0

3. 把第 x 位改成 1

  • 公式:n = n | (1 << x)
  • 例子:n = 4(0100),把第 1 位变 10100 | (1<<1) = 0100 | 0010 = 0110(6)

4. 把第 x 位改成 0

  • 公式:n = n & (~(1 << x))
  • 例子:n = 6(0110),把第 1 位清 00110 & ~(0010) = 0110 & 1101 = 0100(4)

5. 位图思想(空间压缩)

例子:用 1 个 int 存 32 个状态

  • 上面三条操作都是为位图操作服务的
  • 哈希表:存 32 个布尔要 32字节
  • 位图:1 个 int(4字节)的 32 位分别记 0/1,省 8 倍空间

6. 提取最右侧的 1(lowbit)

  • 公式:n & -n
  • -n :对n的二进制数进行先取再反加1
  • -n的操作本质是将n的二进制数中最右侧的1的右侧部分全部变为相反
  • +12:00001100
  • -12:11110100
  • 例子:n = 12,00001100 & 11110100 = 00000100 → 十进制是 4,只保留最右边那个 1

7. 干掉最右侧的 1

  • 公式:n & (n-1)
  • n-1的操作本质上是将n二进制数的最右侧1(包含1本身)的右侧部分全变为相反
  • +12:00001100
  • (12-1):00001011
  • 例子:n = 12 ,00001100 & 00001011 = 1000(8)→ 最右边的 1 直接抹掉
  • 对应题:位 1 的个数、汉明距离

8. 位运算优先级

  • 原则:能加括号就加括号
  • 反例:a & b << c 容易算错正确:a & (b << c) 强制顺序

9. 异或 ^ 三大运算律

  1. a ^ 0 = a 例:5 ^ 0 = 5
  2. a ^ a = 0(消消乐)例:5 ^ 5 = 0
  3. a ^ b ^ c = a ^ (b ^ c)(结合律)例:1^2^3 = 1^(2^3)对应题:只出现一次的数字

题目链接:

判定字符是否唯一

cpp 复制代码
class Solution 
{
public:
    bool isUnique(string astr) 
    {
        if(astr.size() > 26) return false;//鸽巢原理

        int bitmap = 0;
        for(auto e : astr)
        {
            int tmp = e - 'a';// e 在位图中的位置

            //先判断e在位图中是否已经存在
            if(((bitmap >> tmp) & 1) == 1) 
            {
                return false;
            }
            //到此说明位图中e还未存在,将e放入位图中,即将该位置置1
            bitmap |= (1 << tmp);
        }
        return true;
    }
};

丢失的数字

两整数之和

只出现一次的数字II

消失的两个数字


六、分治快排

不了解基础快排的朋友可以先移步快速排序,基础快排那篇里未提及的三路划分会在这里做讲解。

这里会分三个部分引入

1. 三路划分铺垫

75.颜色分类

解题思路:

cpp 复制代码
class Solution 
{
public:
    void sortColors(vector<int>& nums) 
    {
        int n = nums.size();
        int left = -1, right = n, i = 0;

        while(i < right)
        {
            if(nums[i] == 0) swap(nums[++left], nums[i++]);
             
            else if(nums[i] == 1) i++;
         
            else swap(nums[--right], nums[i]); 
        }
    }
};

2. 三路划分快排

基础快排在遇到数据中有大量相同元素时,效率会降低,三路划分可以解决这种情况。

912.排序数组

解题思路:

与颜色分类核心步骤基本类似。

cpp 复制代码
class Solution
{
public:
    vector<int> sortArray(vector<int>& nums) 
    {
        srand(time(NULL)); // 种随机数种子
 
        qsort(nums, 0, nums.size() - 1);
        return nums;
    }

    //快排
    void qsort(vector<int>& nums, int l, int r)
    {
        // 递归出口
        if(l >= r)
        {
            return;
        } 

        // 将数组分成3块
        int key = getRandom(nums, l, r); 
        int i = l, left = l - 1, right = r + 1;
        // 核心操作
        while(i < right)
        {
            if(nums[i] < key) swap(nums[++left], nums[i++]);
   
            else if(nums[i] == key) i++;

            else swap(nums[--right], nums[i]);      
        }
        // 递归处理左右子区间,中间元素都相同,不用处理
        // [l, left] [left + 1, right - 1] [right, r]
        qsort(nums, l, left);
        qsort(nums, right, r);
    }
    
    // 随机选key
    int getRandom(vector<int>& nums, int left, int right)
    {
        int r = rand();
        return nums[r % (right - left + 1) + left];
    }
};

3. 快速选择算法

快速选择算法并未将数据排序,只是借助三路划分将数据分成三块,然后根据规则快速选择出某一部分数据。
215. 数组中的第K个最大元素 - 力扣(LeetCode)

解题思路:


七、分治归并

不了解归并排序的朋友可以先移步归并排序
912. 排序数组 - 力扣(LeetCode)

快排和归并的区别:

  1. 快排是选定key,将数组根据key分为两部分,之后对子数组不断根据key细分
    归并是算出mid,将数组均分为两部分,对左子数组排序,对右子数组排序,循环
  2. 快排类似二叉树的前根遍历,将原数组分块,将左子数组分块,最后将右子数组分块
    归并类似二叉树的后根遍历,对左子数组排序,对右子数组排序,最后合并左右子数组

八、链表

(一)链表常用技巧:

1. 画图

  • 核心原则:遇到链表问题,一定要画图

  • 作用:直观 + 形象 + 便于我们理解指针的指向变化和节点的连接关系

2. 引入虚拟"头"结点

  • 作用:

    • 便于处理边界情况(如头节点插入、删除头节点等需要特殊判断的场景)

    • 方便我们对链表进行统一操作,无需针对头节点做额外分支判断

  • 示意:引入一个不存储实际数据的 newHead 节点,让其 next 指向真正的第一个节点,形成 newHead → 1 → 2 → 3 → null 的结构

3. 不要吝啬空间,大胆去定义变量(比如给链表定义临时节点)

  • 在操作节点时,多定义几个指针变量(如 prevcurnext 等),避免在复杂操作中丢失节点引用

  • 原则:不要让链表断开

  • 说明(以双向链表节点插入为例):

    • 典型操作步骤(需要讲究顺序,容易出错导致链表断开):

      • prev->next->prev = cur(将原后继节点的前驱指向新节点)

      • cur->next = prev->next(新节点的后继指向原后继)

      • prev->next = cur(前驱节点的后继指向新节点)

      • cur->prev = prev(新节点的前驱指向前驱)

    • 但如果先定义一个next将必要的节点引用保存起来,再修改指针指向,就不必考虑这些

4. 快慢双指针

  • 应用场景:

    • 判环:快指针每次走两步,慢指针每次走一步,若相遇则存在环

    • 找链表中环的入口:相遇后,将一个指针重置到头部,两指针同速前进,再次相遇点即为环入口

    • 找链表中倒数第 n 个结点:快指针先走 n 步,然后快慢指针同速前进,快指针到达末尾时,慢指针即为目标节点


(二)链表中的常用操作

1. 创建一个新节点 new

  • 基础操作:申请节点内存并初始化数据域和指针域

2. 尾插

  • 操作:遍历链表找到最后一个节点(cur 指向尾节点,即 cur->next == null),将新节点链接到尾部

  • 示意:cur 指向尾节点,尾节点的 next 指向新节点,新节点的 next 指向 null

3. 头插(重点操作)

  • 操作:将新节点插入到链表头部(加了哨兵头结点后,这一步很容易)

  • 特殊应用:逆序链表

    • 逐个取出原链表节点,使用头插法插入到新链表中,即可完成链表逆序

例题:

2. 两数相加 - 力扣(LeetCode)

24. 两两交换链表中的节点 - 力扣(LeetCode)

143. 重排链表 - 力扣(LeetCode)

23. 合并 K 个升序链表 - 力扣(LeetCode)

25. K 个一组翻转链表 - 力扣(LeetCode)


九、哈希表

例题:

1. 两数之和 - 力扣(LeetCode)

49. 字母异位词分组 - 力扣(LeetCode)

面试题 01.02. 判定是否互为字符重排 - 力扣(LeetCode)

217. 存在重复元素 - 力扣(LeetCode)

219. 存在重复元素 II - 力扣(LeetCode)


十、栈

1047. 删除字符串中的所有相邻重复项 - 力扣(LeetCode)

844. 比较含退格的字符串 - 力扣(LeetCode)

227. 基本计算器 II - 力扣(LeetCode)

394. 字符串解码 - 力扣(LeetCode)

946. 验证栈序列 - 力扣(LeetCode)


十一、字符串

14. 最长公共前缀 - 力扣(LeetCode)

5. 最长回文子串 - 力扣(LeetCode)

67. 二进制求和 - 力扣(LeetCode)

43. 字符串相乘 - 力扣(LeetCode)


十二、优先级队列(堆)

C++ 中的 std::priority_queue 是一种容器适配器 ,提供优先队列(堆)的功能,默认是大顶堆 (元素降序排列,最大元素在队首)。它定义在 <queue> 头文件中,底层默认使用 std::vector 作为存储容器,也可指定 std::deque

特点:

  • 容器适配器 :不直接存储元素,而是封装其他容器(如 vector/deque)。
  • 堆结构 :底层通过堆算法(std::make_heapstd::push_heapstd::pop_heap)实现优先级管理。
  • 默认大顶堆:可通过自定义比较函数改为小顶堆或其他排序规则。
    常用接口(以 std::priority_queue<int> 为例)

1. 构造函数

构造方式 说明
priority_queue<T> 默认构造,大顶堆,底层用 vector<T>
priority_queue<T, Container> 指定底层容器(如 deque<T>)。
priority_queue<T, Container, Compare> 指定比较函数(如 greater<T> 实现小顶堆)。
priority_queue(InputIterator first, InputIterator last) 用迭代器范围初始化。

2. 成员函数

函数 说明 时间复杂度
push(const T& val) 插入元素 val 到队列(自动调整堆)。 O(log n)
emplace(Args&&... args) 原地构造元素并插入(C++11 起)。 O(log n)
pop() 移除队首(优先级最高)元素。 O(log n)
top() 返回队首元素的引用(不删除)。 O(1)
empty() 判断队列是否为空,返回 bool O(1)
size() 返回队列中元素个数。 O(1)
swap(priority_queue& other) 与另一个队列交换内容。 O(1)

1046.最后一块石头的重量

cpp 复制代码
class Solution 
{
public:
    int lastStoneWeight(vector<int>& stones) 
    {
        // 1.将所有石头全部放入大根堆
        priority_queue<int> pq;
        for(auto st : stones)
        {
            pq.push(st);
        }

        // 2.每次取堆顶数据碰撞,将碰撞后的结果再放入堆中
        while(pq.size() > 1)
        {
            int a = pq.top(); pq.pop();
            int b = pq.top(); pq.pop();
            if(a > b)// 由于是大根堆,a是大于等于b的
            {
                pq.push(a - b);
            } 
        }

        return pq.size() ? pq.top() : 0;
    }
};

703.数据流中的第K大元素

cpp 复制代码
class KthLargest 
{
public:
    priority_queue<int,vector<int>,greater<int>> _heap;// 小根堆
    int _k;// 小根堆的大小,主要是给add用

public:
    KthLargest(int k, vector<int>& nums) 
    {
        _k = k;
        for(auto e : nums)
        {
            _heap.push(e);
            if(_heap.size() > _k)
            {
                _heap.pop();
            }
        }
    }
    
    int add(int val) 
    {
        _heap.push(val);
        if(_heap.size() > _k)
        {
            _heap.pop();
        }
        return _heap.top();
    }
};

/**
 * Your KthLargest object will be instantiated and called as such:
 * KthLargest* obj = new KthLargest(k, nums);
 * int param_1 = obj->add(val);
 */

692. 前K个高频单词 - 力扣(LeetCode)

295. 数据流的中位数 - 力扣(LeetCode)


十三、BFS && 队列

429. N 叉树的层序遍历 - 力扣(LeetCode)

解题思路:

在BFS的过程中统计每一层的元素及其个数

103. 二叉树的锯齿形层序遍历 - 力扣(LeetCode)

662. 二叉树最大宽度 - 力扣(LeetCode)

515. 在每个树行中找最大值 - 力扣(LeetCode)


十四、BFS && FloodFill

题目基本特征:让寻找性质相同的联通块,执行某种操作。

题目:

图像渲染

cpp 复制代码
class Solution 
{
    typedef pair<int,int> PII;
public:
    vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int color) 
    {
        int value = image[sr][sc];
        if(value == color)//处理特殊情况
        {
            return image;
        }
        //矩阵长宽
        int m = image.size();
        int n = image[0].size();
        //坐标数组,方便快速遍历
        int dx[4] = {0,0,1,-1};
        int dy[4] = {1,-1,0,0};

        queue<PII> q;// 存符合条件需要改色的位置的坐标
        q.push({sr,sc});
        while(!q.empty())
        {
            //改色
            auto [a,b] = q.front();
            image[a][b] = color;
            q.pop();
            //遍历
            for(int i = 0;i < 4;i++)
            {
                int x = a + dx[i];
                int y = b + dy[i];
                if(x >= 0 && x < m && y >= 0 && y < n && image[x][y] == value)
                {
                    q.push({x,y});
                }
            }
        }
        return image;
    }
};

岛屿数量

cpp 复制代码
class Solution 
{
public:
    //坐标数组
    int dx[4] = {0,0,1,-1};
    int dy[4] = {1,-1,0,0};
    //bool类型的数组,用于标记岛屿是否被遍历过
    bool vis[301][301];
public:
    int numIslands(vector<vector<char>>& grid) 
    {
        int ret = 0;
        int m = grid.size();//矩阵高
        int n = grid[0].size();//矩阵宽

        for(int i = 0;i < m;i++)
        {
            for(int j = 0;j < n;j++)
            {
                if(grid[i][j]== '1' && !vis[i][j])
                {
                    ret++;
                    bfs(grid,i,j,m,n);
                }
            }
        }
        return ret;
    }

    void bfs(vector<vector<char>>& _grid,int i,int j,int m,int n)
    {
        queue<pair<int,int>> q;//存岛屿坐标
        q.push({i,j});
        while(!q.empty())
        {
            auto [a,b] = q.front();
            q.pop();
            for(int k = 0;k < 4;k++)
            {
                int x = a + dx[k];
                int y = b + dy[k];
                if(x >= 0 && x < m && y >= 0 && y < n && _grid[x][y] == '1' && !vis[x][y])
                {
                    q.push({x,y});
                    vis[x][y] = true;
                }
            }
        }
    }
};

岛屿的最大面积

被围绕的区域


十五、BFS && 最短路问题

例题:

1926. 迷宫中离入口最近的出口 - 力扣(LeetCode)

433. 最小基因变化 - 力扣(LeetCode)

127. 单词接龙 - 力扣(LeetCode)

675. 为高尔夫比赛砍树 - 力扣(LeetCode)


十六、多源BFS问题

例题:

542. 01 矩阵 - 力扣(LeetCode)

解题思路:

1020. 飞地的数量 - 力扣(LeetCode)

1765. 地图中的最高点 - 力扣(LeetCode)

1162. 地图分析 - 力扣(LeetCode)


十七、BFS && 拓扑排序

拓扑排序的前提条件:

图必须是有向无环图(DAG)。如果图中有环,则无法进行拓扑排序。

常见概念:

1.有向无环图(DAG图)

  • 有向:边是有方向的
  • 无环:选择一个起点出发,顺着箭头走,不能回到起点
  • 出度:有多少条边是从该节点出发(比如:1号的出度为2,2号的出度为1)
  • 入度:有多少条边指向该节点(比如:1号的入度为0,2号的入度为2)

2. AOV 网(顶点活动图)

AOV 网是一种用图来描述工程或项目活动之间依赖关系的模型:

  • 顶点(Vertex) :表示一个活动(或任务)。

  • 有向边(Edge) :表示活动之间的先后顺序依赖关系

例如:若存在边 A → B,则表示"活动 A 必须在活动 B 之前完成",即 B 依赖于 A。


3. 什么是拓扑排序?

拓扑排序是对有向无环图(DAG)的节点进行线性排序的一种算法。

  • 目的 :找到做事情的先后顺序 ,确保对于图中的每一条有向边 u → v,节点 u 在排序结果中总是位于节点 v 的前面,即事件 v 必须要在事件 u 之后才能执行。

  • 结果不唯一 :一个 DAG 可能存在多种合法的拓扑排序序列。

  • 比如:我想打手柄游戏,其步骤可以是:

    先连接手柄,再打开游戏,最后开始玩游戏;

    也可以是先打开游戏,再连接手柄,最后开始玩游戏

    但无论哪种顺序,想开始玩游戏,都必须先执行前面两个动作。

3.1 拓扑排序的思想

遵循"先完成没有前置依赖的任务"这一规则:

  1. 取出入度为 0 的节点,将其输出:这些节点没有前置依赖,可以立即执行。

  2. 删除与该节点相连的所有边:相当于"完成该任务后,解除后续任务的依赖"。

  3. 重复步骤 1 和 2 :直到图中没有节点,或者找不到入度为 0 的节点为止。

注意

  1. 如果在某一步发现图中还有节点,但不存在入度为 0 的节点 ,说明图中存在环,无法进行拓扑排序(因为环中每个点的入度至少为1,无法进行上面的第一步)
  2. 重要应用:判断有向图中是否存在环:如果拓扑排序能成功输出所有节点,则无环;反之则有环。

4. 如何实现拓扑排序

例题:

207. 课程表 - 力扣(LeetCode)

解题思路:

如何建图?

1. 如何存连接关系,即如何存边

  • 邻接矩阵 (二维数组)空间复杂度为 O(V²),当节点数多时非常浪费空间。

  • 邻接表 (链表/动态数组)空间复杂度为 O(V + E),只存实际存在的边,空间效率高。

结论:绝大多数情况下,直接使用「邻接表」即可,无需纠结稠密稀疏。

邻接表的两种代码实现方式:

方式 1:使用 vector<vector<Integer>>(有局限)

方式 2:使用 unordered_map<Integer, List<Integer>>(更万能一些)

2. 如何统计入度

  • 拓扑排序等算法需要知道每个节点有多少条边"进入"它(入度)。
  • 方法:使用一个 int 数组直接记录

210. 课程表 II - 力扣(LeetCode)

LCR 114. 火星词典 - 力扣(LeetCode)


十八、递归 && 搜索 && 回溯


感谢阅读,本文如有错漏之处,烦请斧正。

相关推荐
shylyly_2 小时前
内存函数的使用和实现
数据结构·算法
时空自由民.2 小时前
两轮平衡车控制系统
算法
吃好睡好便好2 小时前
Matlab中三种三维图的对比
开发语言·人工智能·学习·算法·matlab·信息可视化
157092511342 小时前
回溯算法基础分享
算法·深度优先
脆皮炸鸡7552 小时前
进程通信----命名管道
linux·经验分享·笔记·算法·学习方法
如竟没有火炬2 小时前
至少有K个重复字符的最长子串
开发语言·数据结构·python·算法·leetcode·动态规划
想带你从多云到转晴2 小时前
优选算法---双指针
java·算法
小O的算法实验室3 小时前
2026年IEEE TSMC,基于Q学习平衡全局与局部搜索的防空资源分配问题进化算法,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
谙弆悕博士3 小时前
快速学C语言——第17章:多文件编程与头文件规范
c语言·开发语言·算法·学习方法·头文件·多文件编程