文章目录
- [一 基础知识](#一 基础知识)
-
- [1 结构体struct 与类class](#1 结构体struct 与类class)
- [2 函数与参数传递、指针与引用](#2 函数与参数传递、指针与引用)
- [3 C++ STL](#3 C++ STL)
-
- [3.1 vector](#3.1 vector)
- [3.2 set](#3.2 set)
- [3.3 map](#3.3 map)
- [3.4 pair](#3.4 pair)
- [3.5 algorithm](#3.5 algorithm)
- [二 经典问题](#二 经典问题)
-
- [1 排序、散列(hash)、二分查找](#1 排序、散列(hash)、二分查找)
-
- [1.1 插入排序](#1.1 插入排序)
- [1.2 冒泡排序](#1.2 冒泡排序)
- [1.3 选择排序](#1.3 选择排序)
- [1.4 希尔排序](#1.4 希尔排序)
- [1.5 归并排序](#1.5 归并排序)
-
- [1.5.1 基本概念](#1.5.1 基本概念)
- [1.6 快速排序](#1.6 快速排序)
- [1.7 堆排序](#1.7 堆排序)
-
- [1.7.1 基本概念](#1.7.1 基本概念)
- [1.7.2 核心数据结构 堆(Heap)](#1.7.2 核心数据结构 堆(Heap))
- [1.7.3 算法原理](#1.7.3 算法原理)
- [1.7.4 详细步骤演示](#1.7.4 详细步骤演示)
- [1.8 桶排序](#1.8 桶排序)
-
- [1.8.1 核心思想](#1.8.1 核心思想)
- [1.8.2 C++实现实例](#1.8.2 C++实现实例)
- [1.9 基数排序(Radix Sort)](#1.9 基数排序(Radix Sort))
-
- [1.9.1 核心思想](#1.9.1 核心思想)
- [1.9.2 LSD 基数排序算法步骤](#1.9.2 LSD 基数排序算法步骤)
- [2 双指针(Two Pointers)](#2 双指针(Two Pointers))
-
- [2.1 核心思想](#2.1 核心思想)
- [2.2 常见模式分类](#2.2 常见模式分类)
- [2.3 经典应用场景与代码示例](#2.3 经典应用场景与代码示例)
- [2.4 综合应用与复杂场景](#2.4 综合应用与复杂场景)
- [2.5 算法模板总结](#2.5 算法模板总结)
- [2.6 常见陷阱与技巧](#2.6 常见陷阱与技巧)
一 基础知识
1 结构体struct 与类class
- struct 和 class 中都可以声明或定义属性和方法
- 默认权限不同(struct public/ class private)
- 是否可用于声明模板(struct 不可以)
2 函数与参数传递、指针与引用
- 数组作为参数时,第一维不需要填写长度,第二维需要;
- 应用并没有对变量地址取副本,而是对原变量取了一个别名,对引用变量得操作即对原变量的操作;
- 应用并不是取地址的意思,常量不可以使用引用。
cpp
void swap(int* &p1, int* &p2)
// 指针变量的引用 int* p1 = &a;
// p1 是指针变量, &a 是地址常量(无符号整数)
// swap(p1, p2) 正确
// swap(a, b) 错误
3 C++ STL
3.1 vector
3.2 set
- 特点:内部有序(自动递增)、元素不重复
3.3 map
- 特点:映射,以键从小到大排序,键和值唯一
- 常用于对字符串进行排序或判断字符串是否已经出现过
3.4 pair
- 特点:二元结构体,方便比较(先比较第一个,再比较第二个)
3.5 algorithm
cpp
max(x, max(y, z));
abs(x);
fabs(x);
swap(x, y);
reverse(it, it2); //数组元素反转
fill(a, a+5, 123);
二 经典问题
排序,将一组数按照从小到大排列
1 排序、散列(hash)、二分查找
1.1 插入排序
从第2个元素开始,将该元素和它前面额元素做大小比较,将该元素插入到合适的位置
1.2 冒泡排序
每一次比较相邻的两个元素,如果前面的数大于后面的数,就将两个数交换,第一轮先找到最大的,并放在最后,下一轮不需要找最后一个元素,直到完成整个数组的排序。
1.3 选择排序
第一轮从所有数中找到最小的,和第一个元素交换位置,下一轮从第二个元素开始,将剩下的所有元素中最小的和第二个元素交换位置,直到最后两个元素。
1.4 希尔排序
- 希尔排序是插入排序的一种高效改进版本,也称为缩小增量排序。它通过将原始数组分割成多个子序列分别进行插入排序,然后逐步缩小增量,最终完成整个数组的排序。
- 算法原理:
- 选择增量序列: 确定一个递减的增量序列
- 按增量分组: 根据当前增量将数组分成若干子序列
- 插入排序: 对每个子序列进行插入排序
- 减小增量: 重复上述过程,直至增量为1
cpp
void shellSort(vector<int>& arr) {
int n = arr.size();
// 使用希尔增量序列:n/2, n/4, n/8, ..., 1
for (int gap = n / 2; gap > 0; gap /= 2) {
// 对每个子数组进行插入排序
for (int i = gap; i < n; i++) {
int temp = arr[i];
int j;
// 对子数组进行插入排序
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
arr[j] = arr[j - gap];
}
arr[j] = temp;
}
}
}
1.5 归并排序
1.5.1 基本概念
- 归并排序(Merge Sort)是一种分治算法。核心思想是将大问题分解成小问题,解决小问题后再合并结果。
- 主要特点:
- 稳定排序:相等元素的相对顺序保持不变
- 时间复杂度:始终为O(nlogn)
- 空间复杂度:O(n) 需要额外的存储空间
- 适合场景:大数据排序、链表排序、外部排序
- 算法原理:
- 分解: 将数组递归地分成两半,直到每个子数组只有一个元素
- 解决: 单个元素的数据自然是有序的
- 合并: 将两个有序数组合并成一个有序数组。
cpp
#include <iostream>
#include <vector>
using namespace std;
// 合并两个有序数组
void merge(vector<int>& arr, int left, int mid, int right) {
int n1 = mid - left + 1; // 左半部分长度
int n2 = right - mid; // 右半部分长度
// 创建临时数组
vector<int> leftArr(n1);
vector<int> rightArr(n2);
// 拷贝数据到临时数组
for (int i = 0; i < n1; i++)
leftArr[i] = arr[left + i];
for (int j = 0; j < n2; j++)
rightArr[j] = arr[mid + 1 + j];
// 合并临时数组到原数组
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (leftArr[i] <= rightArr[j]) {
arr[k] = leftArr[i];
i++;
} else {
arr[k] = rightArr[j];
j++;
}
k++;
}
// 拷贝剩余元素
while (i < n1) {
arr[k] = leftArr[i];
i++;
k++;
}
while (j < n2) {
arr[k] = rightArr[j];
j++;
k++;
}
}
// 递归归并排序
void mergeSort(vector<int>& arr, int left, int right) {
if (left >= right) return; // 递归终止条件
int mid = left + (right - left) / 2; // 防止溢出
// 递归分解
mergeSort(arr, left, mid); // 排序左半部分
mergeSort(arr, mid + 1, right); // 排序右半部分
// 合并结果
merge(arr, left, mid, right);
}
// 封装函数
void mergeSort(vector<int>& arr) {
if (arr.size() <= 1) return;
mergeSort(arr, 0, arr.size() - 1);
}
// 测试
int main() {
vector<int> arr = {12, 11, 13, 5, 6, 7, 9, 2, 8};
cout << "原始数组: ";
for (int num : arr) cout << num << " ";
cout << endl;
mergeSort(arr);
cout << "排序后数组: ";
for (int num : arr) cout << num << " ";
cout << endl;
return 0;
}
- 适合使用归并排序的情况
- 需要稳定排序: 保持相等元素的相对顺序
- 大数据排序: 时间复杂稳定度为O(nlogn)
- 外部排序: 数据太大无法全部装入内存
- 链表排序: 不需要额外空间合并
- 并行计算: 容易实现并行化
- 核心原理
- 合并过程就像两个人在两个队列中挑选按:
- 总是比较两个数组当前的"最小元素"
- 选择更小的那个放入结果
- 移动指针继续比较
- 合并过程就像两个人在两个队列中挑选按:
1.6 快速排序
- 实际上就是选取一个pivot,将比pivot小的元素放在它的左侧,比它大的元素放在它的右侧;然后对左右侧子序列递归地进行上述过程。
- 适合使用快速排序
- 通用排序: 大多数情况下最快
- 内存受限: 原地排序,空间复杂度低
- 大数据集: 平均性能优秀
- 随机数据: 对随机数据性能极佳
- 不适合的情况
- 需要稳定排序: 快速排序不稳定
- 几乎有序的数据: 可能退化为O(n2)
- 下面例子得partition函数中得i、j还用到了双指针中的快慢指针概念
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// Lomuto分区方案
int partition(vector<int>& arr, int low, int high) {
int pivot = arr[high]; // 选择最后一个元素作为基准
int i = low - 1; // 小于基准元素的边界
for (int j = low; j < high; j++) {
// 如果当前元素小于或等于基准
if (arr[j] <= pivot) {
i++;
swap(arr[i], arr[j]);
}
}
// 将基准放到正确位置
swap(arr[i + 1], arr[high]);
return i + 1; // 返回基准的最终位置
}
// 快速排序主函数
void quickSort(vector<int>& arr, int low, int high) {
if (low < high) {
// pi是分区索引,arr[pi]现在在正确位置
int pi = partition(arr, low, high);
// 递归排序分区前后的元素
quickSort(arr, low, pi - 1); // 排序左半部分
quickSort(arr, pi + 1, high); // 排序右半部分
}
}
// 包装函数
void quickSort(vector<int>& arr) {
if (arr.size() <= 1) return;
quickSort(arr, 0, arr.size() - 1);
}
// 测试
int main() {
vector<int> arr = {10, 7, 8, 9, 1, 5, 3, 6, 2, 4};
cout << "原始数组: ";
for (int num : arr) cout << num << " ";
cout << endl;
quickSort(arr);
cout << "排序后数组: ";
for (int num : arr) cout << num << " ";
cout << endl;
return 0;
}
1.7 堆排序
1.7.1 基本概念
- 堆排序(Heap Sort)是一种基于二叉堆数据结构的比较排序算法。它结合了插入排序和归并排序的有点,具有原地排序和时间复杂度稳定的特点。
- 主要特点
- 时间复杂度:O(nlogn) 最好、最坏、平均都是
- 空间复杂度:O(1)原地排序
- 不稳定排序:相等元素的相对顺序可能改变
- 不需要递归:适合大数据排序
1.7.2 核心数据结构 堆(Heap)
- 堆是一种完全二叉树,有两种类型:
-
大堆顶 Max Heap
-
父节点的值 >= 子节点的值
-
根节点是最大值
-
用于升序排序
100 / \19 36
/ \ /
17 3 25
-
-
小堆顶 Min Heap
- 父节点的值 <= 子节点的值
- 根节点是最小值
- 用于降序排序
1.7.3 算法原理
堆排序分为两个主要阶段
- 建堆(Build Heap)
- 将无序数组构成一个大顶堆(小顶堆)
- 排序(Sort)
- 将堆顶(最大值,如果是升序)与最后一个元素交换
- 堆大小减一,对新的堆顶进行"堆化"(heapify)
- 重复上述过程直到堆大小为1
1.7.4 详细步骤演示
以数组[4, 10, 3, 5, 1]为例
- 步骤1 构建大顶堆
cpp
原始数组:[4, 10, 3, 5, 1]
表示为完全二叉树:
4
/ \
10 3
/ \
5 1
建堆过程:
1. 最后一个非叶子节点是10(索引1)
2. 堆化:10 > 5 且 10 > 1,不需要交换
4
/ \
10 3
/ \
5 1
3. 处理节点4(索引0)
- 比较:4 < 10(左子节点)
- 交换4和10:
10
/ \
4 3
/ \
5 1
4. 递归堆化子树(节点4)
- 比较:4 < 5(左子节点)
- 交换4和5:
10
/ \
5 3
/ \
4 1
最终大顶堆:[10, 5, 3, 4, 1]
- 步骤2 排序阶段
cpp
堆数组:[10, 5, 3, 4, 1] 堆大小=5
第1轮:
- 交换堆顶(10)和末尾(1):[1, 5, 3, 4, 10]
- 堆化根节点(1):
1 < 5,交换1和5:[5, 1, 3, 4, 10]
1 < 4,交换1和4:[5, 4, 3, 1, 10]
堆大小减1:4
第2轮:
- 交换堆顶(5)和末尾(1):[1, 4, 3, 5, 10]
- 堆化根节点(1):
1 < 4,交换1和4:[4, 1, 3, 5, 10]
堆大小减1:3
第3轮:
- 交换堆顶(4)和末尾(3):[3, 1, 4, 5, 10]
- 堆化根节点(3):
3 > 1 且 3 > ? (右子节点不存在)
堆大小减1:2
第4轮:
- 交换堆顶(3)和末尾(1):[1, 3, 4, 5, 10]
- 堆大小减1:1
最终排序结果:[1, 3, 4, 5, 10]
1.8 桶排序
- **桶排序(Bucket Sort)是一种分布式排序算法,它将待排序元素分配到有限数量的"桶"中,然后对每个桶单独排序,最后按顺序合并所有桶。
1.8.1 核心思想
- 桶排序基于这样一个假设:输入数据均匀分布在一个区间内。它将这个范围划分为若干个大小相等的子区间(桶),然后将元素分配到对应的桶中。
- 基本步骤:
- 分桶: 创建空桶,将元素分配到对应的桶中。
- 桶内排序: 对每个非空桶内的元素进行排序
- 合并: 按顺序将各个桶中的元素合并起来
1.8.2 C++实现实例
- 处理[0,1)之间的小数
cpp
#include <iostream>
#include <vector>
#include <algorithm>
// 桶排序函数,假设输入在[0,1)范围内
void bucketSort(std::vector<float>& arr) {
int n = arr.size();
// 1. 创建n个空桶
std::vector<std::vector<float>> buckets(n);
// 2. 将元素分配到桶中
for (int i = 0; i < n; i++) {
int bucketIndex = n * arr[i]; // 计算桶索引
buckets[bucketIndex].push_back(arr[i]);
}
// 3. 对每个桶排序(使用插入排序或其他稳定排序)
for (int i = 0; i < n; i++) {
std::sort(buckets[i].begin(), buckets[i].end());
}
// 4. 合并桶
int index = 0;
for (int i = 0; i < n; i++) {
for (float num : buckets[i]) {
arr[index++] = num;
}
}
}
// 测试
int main() {
std::vector<float> arr = {0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12, 0.23, 0.68};
std::cout << "排序前: ";
for (float num : arr) std::cout << num << " ";
std::cout << std::endl;
bucketSort(arr);
std::cout << "排序后: ";
for (float num : arr) std::cout << num << " ";
std::cout << std::endl;
return 0;
}
1.9 基数排序(Radix Sort)
- 基数排序是一种非比较型的整数排序算法,它按照数字的每一位(从低位到高位或从高位到低位)进行排序。
1.9.1 核心思想
- 基本原理:
- 按位排序:从最低位(LSD)或最高位(MSD)开始,对每一位进行排序
- 稳定性:每一轮的排序必须是稳定的(保持相对元素的相对顺序)
- 多趟排序:需要多趟排序,每趟处理一位数字
- 主要方法:
- LSD(Least Significant Digit) : 从最低位开始排序
- MSD(Most Significant Dight) :从最高位开始排序(类似字典序)
1.9.2 LSD 基数排序算法步骤
原始数组:[170, 45, 75, 90, 802, 24, 2, 66]
第1趟(个位):
桶0: 170, 90
桶2: 802, 2
桶4: 24
桶5: 45, 75
桶6: 66
合并:170, 90, 802, 2, 24, 45, 75, 66
第2趟(十位):
桶0: 802, 2
桶2: 24
桶4: 45
桶6: 66
桶7: 170, 75
桶9: 90
合并:802, 2, 24, 45, 66, 170, 75, 90
第3趟(百位):
桶0: 2, 24, 45, 66, 75, 90
桶1: 170
桶8: 802
合并:2, 24, 45, 66, 75, 90, 170, 802
最终排序完成!
2 双指针(Two Pointers)
- 双指针是一种在数组、链表或字符串中通过使用两个指针协同工作来解决问题的算法技巧
2.1 核心思想
- 使用两个指针(索引)以不同的速度、方向或条件在数据结构中移动,从而:
- 减少时间复杂度(常从O(n2)降低到O(n))
- 减少空间复杂度(常从O(n)降低到O(1))
- 简化问题逻辑
2.2 常见模式分类
- 同向双指针(快慢指针)
- 对撞指针(相向指针)
- 滑动窗口
2.3 经典应用场景与代码示例
- 同向双指针(快慢指针)
- 移除有序数组中的重复元素
- 移除元素(指定值)
- 判断链表是否有环
- 找到链表的中间节点
- 对撞指针
- 两数之和(有序数组)
- 三数之和
cpp
// LeetCode 15: 三数之和
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end()); // 先排序
for (int i = 0; i < nums.size(); i++) {
// 跳过重复元素
if (i > 0 && nums[i] == nums[i - 1]) continue;
int target = -nums[i];
int left = i + 1, right = nums.size() - 1;
while (left < right) {
int sum = nums[left] + nums[right];
if (sum == target) {
result.push_back({nums[i], nums[left], nums[right]});
// 跳过重复元素
while (left < right && nums[left] == nums[left + 1]) left++;
while (left < right && nums[right] == nums[right - 1]) right--;
left++;
right--;
} else if (sum < target) {
left++;
} else {
right--;
}
}
}
return result;
}
3. 验证回文串
cpp
// LeetCode 125: 验证回文串
bool isPalindrome(string s) {
int left = 0, right = s.length() - 1;
while (left < right) {
// 跳过非字母数字字符
while (left < right && !isalnum(s[left])) left++;
while (left < right && !isalnum(s[right])) right--;
// 比较(忽略大小写)
if (tolower(s[left]) != tolower(s[right])) {
return false;
}
left++;
right--;
}
return true;
}
4. 盛最多水的容器
cpp
// LeetCode 11: 盛最多水的容器
int maxArea(vector<int>& height) {
int left = 0, right = height.size() - 1;
int maxWater = 0;
while (left < right) {
int width = right - left;
int h = min(height[left], height[right]);
maxWater = max(maxWater, width * h);
// 移动较矮的那边(因为高度受限于较矮的)
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return maxWater;
}
// 示例:
// 输入: height = [1,8,6,2,5,4,8,3,7]
// 输出: 49
// 解释: 第2根(8)和第9根(7)之间:宽度7,高度7,面积49
- 滑动窗口
- 无重复字符的最长子串
cpp
// LeetCode 3: 无重复字符的最长子串
int lengthOfLongestSubstring(string s) {
unordered_set<char> window;
int left = 0, maxLen = 0;
for (int right = 0; right < s.length(); right++) {
// 如果字符重复,移动左指针直到不重复
while (window.count(s[right])) {
window.erase(s[left]);
left++;
}
window.insert(s[right]);
maxLen = max(maxLen, right - left + 1);
}
return maxLen;
}
2.4 综合应用与复杂场景
- 合并两个有序数组
- 接雨水
cpp
// LeetCode 42: 接雨水
int trap(vector<int>& height) {
if (height.empty()) return 0;
int left = 0, right = height.size() - 1;
int leftMax = 0, rightMax = 0;
int water = 0;
while (left < right) {
if (height[left] < height[right]) {
if (height[left] >= leftMax) {
leftMax = height[left];
} else {
water += leftMax - height[left];
}
left++;
} else {
if (height[right] >= rightMax) {
rightMax = height[right];
} else {
water += rightMax - height[right];
}
right--;
}
}
return water;
}
- 颜色分类(芬兰国旗问题)
cpp
// LeetCode 75: 颜色分类
void sortColors(vector<int>& nums) {
int low = 0, mid = 0, high = nums.size() - 1;
// 三指针法
while (mid <= high) {
if (nums[mid] == 0) {
swap(nums[low], nums[mid]);
low++;
mid++;
} else if (nums[mid] == 1) {
mid++;
} else { // nums[mid] == 2
swap(nums[mid], nums[high]);
high--;
// 注意:这里不移动mid,因为交换过来的元素还未检查
}
}
}
2.5 算法模板总结
- 快慢指针(数组去重)
cpp
int slow = 0;
for (int fast = 0; fast < n; fast++) {
if (满足条件) {
nums[slow] = nums[fast];
slow++;
}
}
return slow; // 新长度
- 对撞指针(有序数组)
cpp
int left = 0, right = n - 1;
while (left < right) {
if (条件满足) {
// 处理结果
left++;
right--;
} else if (条件1) {
left++;
} else {
right--;
}
}
- 滑动窗口(子串问题)
cpp
int left = 0, right = 0;
while (right < n) {
// 扩大窗口
window.add(s[right]);
right++;
while (窗口需要收缩) {
// 收缩窗口
window.remove(s[left]);
left++;
}
}
2.6 常见陷阱与技巧
- 指针移动条件
cpp
// 错误示例:容易造成死循环
while (left < right) {
if (条件) {
left++; // 可能永远不移动right
}
}
// 正确:确保至少有一个指针移动
while (left < right) {
if (条件1) {
left++;
} else if (条件2) {
right--;
} else {
// 至少移动一个
left++;
}
}
- 边界条件处理
cpp
// 空数组/链表
if (nums.empty()) return 0;
// 单个元素
if (nums.size() == 1) return 1;
// 链表判空
if (!head || !head->next) return nullptr;
- 去重技巧
cpp
// 在结果集中去重
result.push_back({a, b, c});
while (left < right && nums[left] == nums[left + 1]) left++;
while (left < right && nums[right] == nums[right - 1]) right--;
left++;
right--;
- 双指针是面试和算法竞赛中的高频考点,掌握这三种模式及其变体,能解决大量数组和链表相关的问题。关键是多练习,培养指针移动的直觉。