【算法突破】【C++】 第一篇 数组

大纲

  • 基础知识
    • vector对象的定义方式
    • vector主要成员函数
    • sort()排序及比较运算符的重载
  • 数组的存取操作
    • 整体建表法
    • 元素移动法
    • 区间划分法(也叫双指针法、滑动窗口法、分治法等)
    • 计数排序法
    • 元素交换法
  • 有序数组
    • 二路归并法(分治算法之一)
    • 三路归并法
    • 多路归并法

一、数组基础知识

1.1 常用vector对象的定义方式

cpp 复制代码
vector<int> v1;                             // 定义元素为int类型的向量v1
vector<int> v2(10);                         // 指定向量v2的初始大小为10个int元素
vector<double> v3(10, 1.25);                // 指定v3的10个初始元素的初始值均为1.25
vector<vector<int>> v4;                     // 定义一个元素类型为向量的向量v4
vector<vector<int>> v5(3,vector<int>(5,0)); // 定义一个初始化为3行5列且元素为0的二维向量v5

1.2 vector的主要成员函数

  • capacity():返回向量容器所能容纳的元素个数。
    • 注意,此处容量是指vector容器中最多存放的元素个数。而长度表示实际容纳的元素个数即size()的返回值。
  • resize(n):调整vector容器的大小,使其恰好能容纳n个元素,增加部分用默认值填充。
  • empty():判断vector容器是否为空。
  • size():返回vector的长度。
  • i\]:返回vector容器中下表为i的元素。

  • back():返回vector容器中的尾元素。
  • push_back():在vector容器的尾部添加一个元素。
  • insert(pos, e):在向量vector的pos位置处插入一个元素e。
  • erase():删除vector中某个迭代器或者迭代器区间指定的元素。
  • clear():删除vector容器中的所有元素。
    • 注意:size变为0,但capacity保持不变。
  • begin()/end():用于正向遍历,返回vector中首元素的位置/尾元素的后一个位置。
  • rbegin()/rend():用于反向遍历,返回vector中尾元素的位置/首元素的前一个位置。

1.3 sort()排序

C++的STL提供了sort()排序算法

  • 内置数据类型的排序
    • sort()默认以less<T>小于关系函数作为关系比较函数实现递增排序。
    • greater<T>大于关系函数实现递减排序。

sort()函数使用案例:

cpp 复制代码
vector< int > v = {2,1,5,4,3};
sort(v.begin(), v.end(), less<int>);     // 指定递增排序
sort(v.begin(), v.end());                // 不用指定less<int>也会默认递增排序
sort(v.begin(), v.end(), greater<int>);  // 指定递减排序

【记忆】

<大,即小less than大,从小到大,递增排序;

>小,即大greater than小,从大到小,递减排序。

  • 自定义数据类型的排序
    • 在声明结构体类型或者类中重载"<"或者">"运算符,以实现按指定成员递增或递减排序。
    • 在声明结构体类型或者类中重载"( )"运算符,以实现按指定成员递增或递减 排序。
      • 该方法本质其实还是用大小运算符比较。

1.4 案例1:重载"<"或者">"运算符,实现按指定成员递增或递减排序:

  • 示例代码:
cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Person {
    string name;
    int age;
  
    // 重载小于运算符<,用于升序排序
    bool operator<(const Person& other) const {
      return age < other.age;
    }
  
    // 重载大于运算法>,用于降序排序
    bool operator>(const Person& other) const {
      return age > other.age;
    }
};

int main(){
    vector<Person> people = {
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35}
    };
    // 按age进行升序排列
    sort(people.begin(),people.end());
    for (const auto &p: people) {
      cout << p.name << "(" << p.age << ")" << endl;
    }
    std::cout << "--------------------------\n";
    // 按age进行升序排列
    sort(people.begin(),people.end(), greater());
    for (const auto &p: people) {
      cout << p.name << "(" << p.age << ")" << endl;
    }
}
  • 示例代码输出:

    Bob (25)
    Alice (30)
    Charlie (35)

    Charlie (35)
    Alice (30)
    Bob (25)

1.5 案例2:重载()运算符,实现按指定成员递增或递减排序:

  • 示例代码
CPP 复制代码
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

struct Person {
    string name;
    int age;
};

// 自定义比较器:升序排序(ascending order)
struct CompareByAgeAsc {
    bool operator()(const Person& a, const Person& b) const {
      return a.age < b.age;
    }
};

// 自定义比较器:降序排序(descending order)
struct CompareByAgeDesc {
    bool operator()(const Person& a, const Person& b) const {
      return a.age > b.age;
    }
};

int main() {
    vector<Person> people = {
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35}
    };
  
    // 升序排序
    sort(people.begin(), people.end(), CompareByAgeAsc());
    for (const auto& p : people)
      cout << p.name << " (" << p.age << ")\n";
  
     cout << "--------------------------\n";
  
    // 降序排序
    sort(people.begin(), people.end(), CompareByAgeDesc());
    for (const auto& p : people)
      cout << p.name << " (" << p.age << ")\n";
  
    return 0;
}

注意⚠️:和比较运算符的重载不一样的是,()运算符的比较器是一个独立的结构体或类

  • 示例代码输出:

    Bob (25)
    Alice (30)
    Charlie (35)

    Charlie (35)
    Alice (30)
    Bob (25)

1.6 案例2的另一种写法:Lambda表达式

cpp 复制代码
std::sort(people.begin(), people.end(), [](const Person& a, const Person& b) {
  return a.age < b.age;
});

Lambda表达式语法解析

cpp 复制代码
[](const Person& a, const Person& b) { return a.age < b.age; }

// []表示捕获列表:告诉 lambda 要"捕获"哪些外部变量。这里为空,表示不捕获任何变量。
// (const Person& a, const Person& b) 参数列表:和普通函数一样,接收两个 Person 引用用于比较。
// { ... } 函数体:具体的比较逻辑。
// return a.age < b.age; 如果 a 的年龄小于 b,则 a 排在 b 前面 → 升序。
  • Lambda表达式使用示例:
cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm> // sort 所需
#include <string>

using namespace std;
// 定义Person结构体(不需要重载任何操作符)
struct Person {
    string name;
    int age;
};

int main() {
    // 创建一个包含多个Person的vector
    vector<Person> people = {
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35},
        {"Diana", 28}
    };
    cout << "排序前:\n";
    for (const auto& p : people) {
      cout << p.name << " (" << p.age << ")\n";
    }
    // 使用Lambda表达式按age升序排序
    sort(people.begin(), people.end(), [](const Person& a, const Person& b) {
      return a.age < b.age; // 如果a应该排在b前面,返回true,即小的在前大的在后,升序排列
    });
    cout << "\n按年龄升序排序后:\n";
    for (const auto& p : people) {
      cout << p.name << " (" << p.age << ")\n";
    }
    // 再次排序:按年龄降序
    sort(people.begin(), people.end(), [](const Person& a, const Person& b) {
      return a.age > b.age; // 降序:a年龄更大时排前面
    });
    cout << "\n按年龄降序排序后:\n";
    for (const auto& p : people) {
      cout << p.name << " (" << p.age << ")\n";
    }
    // 按姓名字母顺序排序(字符串天然支持 <)
    sort(people.begin(), people.end(), [](const Person& a, const Person& b) {
      return a.name < b.name;
    });
    cout << "\n按姓名升序排序后:\n";
    for (const auto& p : people) {
      cout << p.name << " (" << p.age << ")\n";
    }
    return 0;
}
  • Lambda表达式使用示例输出结果:
cmd 复制代码
排序前:
Alice (30)
Bob (25)
Charlie (35)
Diana (28)

按年龄升序排序后:
Bob (25)
Diana (28)
Alice (30)
Charlie (35)

按年龄降序排序后:
Charlie (35)
Alice (30)
Diana (28)
Bob (25)

按姓名升序排序后:
Alice (30)
Bob (25)
Charlie (35)
Diana (28)

二、数组的存取操作

2.1 整体建表法

整体建表法是数据结构中的一种表初始化方法,指一次性将一组数据元素按照特定逻辑(如顺序或链式)组织成一个完整的表结构。它通常用于初始化线性表(如顺序表、单链表等),通过遍历输入数据,逐个插入到表中,最终形成一个完整的数据结构。 C++中,整体建表法常用于创建顺序表(数组实现)或链表(动态节点连接),其核心思想是根据输入数据的顺序,构建一个逻辑连续的线性表结构。

案例:LeetCode 27 移除元素

给你一个数组nums 和一个值val,你需要原地 移除所有数值等于val的元素。元素的顺序可能发生改变。然后返回nums 中与val 不同的元素的数量 。 假设nums 中不等于val的元素数量为k ,要通过此题,您需要执行以下操作:更改nums 数组,使nums 的前k 个元素包含 等于val 的元素。nums的其余元素和nums的大小并不重要。返回k

具体示例和限制详见原题页面,此处不再赘述。

使用整体建表法解题思路:直接从头开始判断、并原地替换元素。由于不关心nums在第k个元素后具体有什么变化,因此在目标达成时将k直接返回即可。

cpp 复制代码
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
      int k = 0;
      for (int i =0; i < nums.size(); i++) {
        if (nums[i] != val) {
          nums[k] = nums[i];
          k++;
        }
        // 当nums[i] == val,直接跳过
      }
      return k;
    }
 };

2.2 元素移动法

元素移动法个人理解就是在原有数组内对元素进行移动的方法。移动元素的本质还是将元素插入到某个位置。有点抽象,不要太在意什么移动法还是整体建表法,稍微有点逻辑思维都会找到自己的方法。

案例:依然以该题作为案例进行说明。LeetCode 27 移除元素

使用元素移动法解题思路:用i从0开始遍历a,用k(k≥0)累计到当前为止要删除的元素的个数(初始值为0):

  • 若a[i] != val,说明a[i]是要保留的元素,将a[i]前移动k个位置重新插入a中(k=0时原地移动一次);
  • 若a[i] == val,说明a[i]是要删除的元素,不移动a[i],并且执行k++;
  • 最后返回结果数组长度n-k即可。
cpp 复制代码
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
      int k = 0;
      for (int i =0; i < nums.size(); i++) {
          if (nums[i] == val) { // nums[i] 需要删掉
              k++;
          } else {
              nums[i - k] = nums[i];
          }
      }
      return nums.size() - k;
    }
};

2.3 区间划分法(也叫双指针法、滑动窗口法、分治法等)

区间划分法也叫双指针法、滑动窗口法、分治法等,依据具体场景而定。在数组问题中,"区间划分"指的是将数组划分为若干个逻辑子区间,通过维护这些区间的边界(通常用指针或索引表示),在一次遍历中高效地完成搜索、排序、去重、求和等操作。

这种方法的核心思想是:

  • 避免暴力枚举(O(n²) 或更高)。
  • 利用数组的有序性或特定性质,通过移动区间边界来缩小搜索空间。
  • 将复杂问题分解为子问题,逐个击破。

这里以双指针为例。形象比喻:用你的指头和笔,作为两个指针在指元素,然后你就明白了什么是双指针!

不废话,上代码:

cpp 复制代码
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        // 使用双指针解法
        // 1、定义指针pen,用于记录"笔"的位置
        // 2、定义指针finger,用于记录"指头"的位置
        // 当"指头"找到目标值时,用"笔"改掉那块数据,用指头一直找到最后,当"笔"改了几个数字,那就返回几个。
        int pen = 0 ;
        for (int finger = 0; finger < nums.size(); finger++) {
            if (nums[finger] != val) {
                nums[pen] = nums[finger];
                pen++;
            }
        }
        return pen;
    }
 };

2.4 计数排序法

计数排序法(Counting Sort)是一种非比较型排序算法,适用于整数且范围较小的数据。它通过统计每个元素的出现次数,直接确定元素在有序序列中的位置,时间复杂度为O(n+k)其中n是元素数量k是数据范围(最大值与最小值的差值)。

核心步骤

  1. 找出极值:确定数组中的最大值max和最小值min。
  2. 统计频率:创建计数数组count,统计每个元素出现的次数。
  3. 计算前缀和:将count转换为前缀和数组,count[i] 表示小于等于i + min的元素个数。
  4. 反向填充:从后向前遍历原数组,根据count确定元素在结果数组中的位置,并更新count。

2.5 元素交换法

过于简单不再赘述。可以利用临时变量法,也可以直接使用std::swap()方法。

std::swap是C++标准库提供的通用交换函数,用于交换两个同类型对象的值,定义在头文件中。

cpp 复制代码
std::swap(vector[index1], vector[index2]); // 这里不一定非得是vector,也可以是其他容器或元素。

案例:

cpp 复制代码
#include 
#include 
using namespace std;

int main() {
    vector v = {10, 20, 30, 40};
    // 交换索引为 0 和 3 的元素
    swap(v[0], v[3]);
    cout << "交换后的vector: ";
    for (int num : v) cout << num << " ";
    cout << endl;
    return 0;
}

输出结果:

复制代码
交换后的vector: 40 20 30 10

leetcode案例:189. 轮转数组

三、有序数组

3.1 二路归并法(分治算法之一)

给定两个递增 有序数组a和b,将所有元素归并到数组c中,并且要求c中的元素也是递增有序的 。二路归并法(Two-way Merge Sort)是一种经典的分治算法,主要用于对数组或列表进行高效排序。它是归并排序(Merge Sort)的核心操作。

cpp 复制代码
vector<int> merge2(vector<int> &a, vector<int> &b) {
   int m = a.size(), n = b.size();
   vector<int> c(m + n);
   int i = 0, j = 0;
   while(i < m && j < n) {
     // 哪个小就归并哪个
     if (a[i] < b[j]) {
       c.push_back(a[i]); // 归并a[i]
       i++;
     } else {
       c.push_back(b[j]); // 归并b[j]
       j++;
     }
   }
   // 剩余的元素归并
   while(i < m) {
     c.push_back(a[i]); // 归并a[i..m-1]
     i++;
   }
   // 剩余的元素归并
   while(j < n) {
     c.push_back(b[j]); // 归并b[j..n-1]
     j++;
   }
   return c;
 }

求差集

二路归并法的另一种用法:求差集

a-b=c表示a-b的(差集)结果包含所有属于a但不属于b的元素。定两个递增有序数组a和b表示两个集合,每个集合中的元素不重复,求a-b的结果(差集),用递增有序数组c表示,同时要求c中的元素不重复。

例如a=[1,2,5,8],b=[1,3,4,5,8,10],则c=[2]。

思路:

  • 使用两个指针i(指向a)和j(指向b),初始为 0。
  • 当i<a.size()时且j<b.size()时:
  • 当b归并完成,a没有归并完成,则将a中剩余的元素归并到c中。
  • 当a归并完成,b还没归并完成,直接结束,不需要处理b剩余部分。
  • 当将元素插入c时,检查是否与c的最后一个元素相等,避免重复添加
cpp 复制代码
vector<int> difference(vector<int> a, vector<int> b) {
   int m = a.size();
   int n = b.size();
   vector<int> c;
   int i = 0, j = 0; // 两个指针
   while (i < m && j < n) {
     if (a[i] < b[j]) {
       c.push_back(a[i]);
       i++;
     } else if (a[i] > b[j]) {
       j++;
     } else { // a[i] == b[j] 直接跳过
       i++;
       j++;
     }
   }
   while (i < m) {
     c.push_back(a[i]);
     i++;
   }
   return c;
 }

归并结果除重

使用二路归并法的过程中,当生成归并元素x时需要与结果数组c的末尾元素比较,只有不相等时将其添加到c中,避免重复添加。

使用方法:

cpp 复制代码
if(c.empty() || a[i] != c.back()) {
  c.push_back(a[i]);
 }

3.2 多路归并法

多路归并法时一种将多个已排列的序列合并成一个有序序列的方法。核心思想:每次从多个序列的头部选择最小的元素,逐步合并到结果中。

3.2.1 多路归并法案例

  • 归并段0:[1, 3, 5, 7]
  • 归并段1:[2, 4, 6, 8]
  • 归并段2:[0, 9, 10, 11]
  • 则k路归并算法的二维数组为:a = [[1, 3, 5, 7], [2, 4, 6, 8], [0, 9, 10, 11]]。
cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

const int INF = 0x3f3f3f3f; // 正无穷大
int min_k(vector<int> &x) { // 返回x中最小元素的段号(这个段号就是说有多少个数组,这些数组被编了号,每个数组都有自己的号,这个号就叫段号)
    int mini = 0; // 初始化最小段号为0
    for (int i = 1; i < x.size(); ++i) { // 为什么这里i要从1开始?如果i从0开始,会导致x[0]与自己比较
        if (x[i] < x[mini]) {
          mini = i;  // 更新最小段号
        }
    }
    if (x[mini] == INF) {
        return -1; // 如果最小段号对应的元素是正无穷大,则返回-1 为什么?
    } else {
        return mini; // 否则返回最小段号
    }
}
vector<int> merge_k(vector<vector<int>> &a) { // k路归并算法
    int k = a.size(); // 获取数组的个数(需要归并的数组)
    vector<int> p(k); // 创建一个长度为k的向量p,用于记录每个数组的当前位置
    vector<int> x(k); // 创建一个长度为k的向量x,用于存储每个数组当前位置对应的元素
    for (int i = 0; i < k; ++i) { // 初始化向量p和x
        p[i] = 0;
        x[i] = a[i][0]; // 将向量x的第i个元素初始化为对应数组的当前位置对应的元素
    }
    vector<int> res;
    while (true) {
        int mini = min_k(x); // 找到最小元素所在的归并段号
        if (mini == -1) { // 如果最小元素是正无穷大,则说明所有元素已经处理完毕
            break;
        }
        res.push_back(x[mini]); // 将最小元素加入结果向量中,也可写为res.push_back(x[mini]);
        p[mini]++; // 更新最小元素所在的归并段号的当前位置,如归并段2的最小位置为0,则更新它的最小位置为1,即"9"元素的位置1到p中。
        if (p[mini] >= a[mini].size()) { // 如果最小元素所在的归并段号的当前位置已经超出了该归并段号的数组长度,则将最小元素更新为正无穷大
            x[mini] = INF;
        } else { // 否则,将最小元素更新为最小元素所在的归并段号的当前位置对应的元素
            x[mini] = a[mini][p[mini]]; // 这里还是以归并段2为例,最小的元素更新为了9,这时候数组(向量)x从[1,2,0]更新为[1,2,9]
        }
    }
    return res;
}
/*
\* 归并段0:[1, 3, 5, 7]
\* 归并段1:[2, 4, 6, 8]
\* 归并段2:[0, 9, 10, 11]
\* 则 a = [1, 3, 5, 7], [2, 4, 6, 8], [0, 9, 10, 11]]。
\* */
int main() {
    vector<vector<int>> a = {{1, 3, 5, 7}, {2, 4, 6, 8}, {0, 9, 10, 11}};
    vector<int> res = merge_k(a);
    for (int re : res) {
        cout << re << " ";
    }
}

合并结果:

复制代码
0 1 2 3 4 5 6 7 8 9 10 11

多路归并法练习

合并K个升序链表(LeetCode 23)

查找和最小的K对数字(LeetCode 373)

有序矩阵中第K小的元素(LeetCode 378)

相关推荐
ULTRA??37 分钟前
ROS Action 完整示例(AI辅助):客户端发目标 + 服务器接参数(lambda 替代 boost::bind)
c++·python
45288655上山打老虎1 小时前
【智能指针】
开发语言·c++·算法
稚辉君.MCA_P8_Java1 小时前
Gemini永久会员 go数组中最大异或值
数据结构·后端·算法·golang·哈希算法
水饺编程1 小时前
第3章,[标签 Win32] :WM_CREATE 消息的产生
c语言·c++·windows·visual studio
Michelle80231 小时前
机器学习实战操作手册
人工智能·算法·机器学习
HaiLang_IT1 小时前
【目标检测】基于卷积神经网络的轨道部件(扣件、轨枕、钢轨)缺陷检测算法研究
算法·目标检测·cnn
草莓熊Lotso1 小时前
《算法闯关指南:优选算法--前缀和》--31.连续数组,32.矩阵区域和
c++·线性代数·算法·矩阵
程序喵大人1 小时前
CMake入门教程
开发语言·c++·cmake·cmake入门
csuzhucong1 小时前
斜转魔方、斜转扭曲魔方
前端·c++·算法