六大排序:冒泡、插入、选择、快速、归并、希尔
排序算法
冒泡排序
/*
冒泡排序
从数组最左端开始向右遍历,依次比较相邻元素大小,如果“左元素 > 右元素”就交换二者。
遍历完成后,最大的元素会被移动到数组的最右端。
1.首先,对n个元素执行“冒泡”,将数组的最大元素交换至正确位置。
2.接下来,对剩余n-1个元素执行“冒泡”,将第二大元素交换至正确位置
3.以此类推,经过n-1轮“冒泡”后,前n-1大的元素都被交换至正确位置。
4.仅剩的一个元素必定是最小元素,无须排序,因此数组排序完成
*/
void bubbleSort(std::vector<int>& arr) {
int n = arr.size();
for (int i = 0; i < n - 1; ++i) {
bool is_swap = false;
for (int j = 0; j < n - 1 - i; ++j) {
if (arr[j] > arr[j + 1]) {
std::swap(arr[j], arr[j + 1]);
is_swap = true;
}
}
if (!is_swap) break;
}
}
插入排序
/*
插入排序
在未排序区间选择一个基准元素,
将该元素与其左侧已排序区间的元素逐一比较大小,并将该元素插入到正确的位置。
1.初始状态下,数组的第 1 个元素已完成排序。
2.选取数组的第 2 个元素作为 base ,将其插入到正确位置后,数组的前 2 个元素已排序。
3.选取第 3 个元素作为 base ,将其插入到正确位置后,数组的前 3 个元素已排序。
4.以此类推,在最后一轮中,选取最后一个元素作为 base ,将其插入到正确位置后,所有元素均已排序。
*/
void insertionSort(std::vector<int>& arr) {
int n = arr.size();
for(int i = 1; i < n; ++i) {
for (int j = i; j > 0 && arr[j] < arr[j - 1]; --j) {
std::swap(arr[j], arr[j - 1]);
}
}
}
选择排序
/*
选择排序
开启一个循环,每轮从未排序区间选择最小的元素,将其放到已排序区间的末尾。
1.初始状态下,所有元素未排序,即未排序(索引)区间为[0,n-1].
2.选取区间[0,n-1]中的最小元素,将其与索引0处的元素交换。完成后,数组前 1 个元素已排序。
3.选取区间[1,n-1]中的最小元素,将其与索引1处的元素交换。完成后,数组前 2 个元素已排序。
4.以此类推。经过n-1轮选择与交换后,数组前n-1个元素已排序。
5.仅剩的一个元素必定是最大元素,无须排序,因此数组排序完成。
*/
void selectionSort(std::vector<int>& arr) {
int n = arr.size();
for (int i = 0; i < n - 1; ++i) {
int mid = i;
for (int j = i + 1; j < n; ++j) {
if (arr[j] < arr[mid]) {
mid = j;
}
}
std::swap(arr[i], arr[mid]);
}
}
快速排序
/* 哨兵划分 */
int partition(vector<int> &nums, int left, int right) {
// 以 nums[left] 为基准数
int i = left, j = right;
while (i < j) {
while (i < j && nums[j] >= nums[left])
j--; // 从右向左找首个小于基准数的元素
while (i < j && nums[i] <= nums[left])
i++; // 从左向右找首个大于基准数的元素
swap(nums, i, j); // 交换这两个元素
}
swap(nums, i, left); // 将基准数交换至两子数组的分界线
return i; // 返回基准数的索引
}
// 选择数组中的某个元素作为“基准数”,
// 将所有小于基准数的元素移到其左侧,而大于基准数的元素移到其右侧。
/*
1.选取数组最左端元素作为基准数,初始化两个指针 i 和 j 分别指向数组的两端。
2.设置一个循环,在每轮中使用 i(j)分别寻找第一个比基准数大(小)的元素,然后交换这两个元素。
3.循环执行步骤 2. ,直到 i 和 j 相遇时停止,最后将基准数交换至两个子数组的分界线。
*/
/* 快速排序 */
void quickSort(vector<int> &nums, int left, int right) {
// 子数组长度为 1 时终止递归
if (left >= right)
return;
// 哨兵划分
int pivot = partition(nums, left, right);
// 递归左子数组、右子数组
quickSort(nums, left, pivot - 1);
quickSort(nums, pivot + 1, right);
}
归并排序
* 合并左子数组和右子数组 */
void merge(vector<int> &nums, int left, int mid, int right) {
// 左子数组区间为 [left, mid], 右子数组区间为 [mid+1, right]
// 创建一个临时数组 tmp ,用于存放合并后的结果
vector<int> tmp(right - left + 1);
// 初始化左子数组和右子数组的起始索引
int i = left, j = mid + 1, k = 0;
// 当左右子数组都还有元素时,进行比较并将较小的元素复制到临时数组中
while (i <= mid && j <= right) {
if (nums[i] <= nums[j])
tmp[k++] = nums[i++];
else
tmp[k++] = nums[j++];
}
// 将左子数组和右子数组的剩余元素复制到临时数组中
while (i <= mid) {
tmp[k++] = nums[i++];
}
while (j <= right) {
tmp[k++] = nums[j++];
}
// 将临时数组 tmp 中的元素复制回原数组 nums 的对应区间
for (k = 0; k < tmp.size(); k++) {
nums[left + k] = tmp[k];
}
}
/* 归并排序 */
void mergeSort(vector<int> &nums, int left, int right) {
// 终止条件
if (left >= right)
return; // 当子数组长度为 1 时终止递归
// 划分阶段
int mid = left + (right - left) / 2; // 计算中点
mergeSort(nums, left, mid); // 递归左子数组
mergeSort(nums, mid + 1, right); // 递归右子数组
// 合并阶段
merge(nums, left, mid, right);
}
希尔排序
// 希尔排序
void shellSort(std::vector<int>& nums) {
for (int gap = nums.size() / 2; gap > 0; gap /= 2) {
for (int i = gap; i < nums.size(); ++i) {
for (int j = i; j - gap >= 0 && nums[j - gap] > nums[j]; j -= gap) {
std::swap(nums[j - gap], nums[j]);
}
}
}
}
五大算法
搜索
二分查找
是一种基于分治策略的高效搜索算法。它利用数据的有序性,每轮缩小一半搜索范围,直至找到目标元素或搜索区间为空为止。
优点:
1.二分查找的时间效率高。在大数据量下,对数阶的时间复杂度具有显著优势;
2.二分查找无须额外空间。
局限:
1.二分查找仅适用于有序数据;
2.二分查找仅适用于数组;
3.小数据量下,线性查找性能更佳。
Question 1 :给定一个长度为 n 的数组 nums ,元素按从小到大的顺序排列且不重复。请查找并返回元素 target 在该数组中的索引。若数组不包含该元素,则返回-1。
/*
二分查找(双闭区间)
时间复杂度为O(logn),空间复杂度为O(1)
*/
int binarySearch(vector<int> &nums, int target) {
// 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素
int i = 0, j = nums.size() - 1;
// 循环,当搜索区间为空时跳出(当 i > j 时为空)
while (i <= j) {
int m = i + (j - i) / 2; // 计算中点索引 m
if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中
i = m + 1;
else if (nums[m] > target) // 此情况说明 target 在区间 [i, m-1] 中
j = m - 1;
else // 找到目标元素,返回其索引
return m;
}
// 未找到目标元素,返回 -1
return -1;
}
/* 二分查找(左闭右开区间) */
int binarySearchLCRO(vector<int> &nums, int target) {
// 初始化左闭右开区间 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1
int i = 0, j = nums.size();
// 循环,当搜索区间为空时跳出(当 i = j 时为空)
while (i < j) {
int m = i + (j - i) / 2; // 计算中点索引 m
if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j) 中
i = m + 1;
else if (nums[m] > target) // 此情况说明 target 在区间 [i, m) 中
j = m; //***不同之处***
else // 找到目标元素,返回其索引
return m;
}
// 未找到目标元素,返回 -1
return -1;
}
Question 2 :给定一个长度为 n 的有序数组 nums 和一个元素 target ,数组不存在重复元素。现将 target 插入数组 nums 中,并保持其有序性。若数组中已存在元素 target ,则插入到其左方。请返回插入后 target 在数组中的索引。
/* 二分查找插入点(无重复元素) */
int binarySearchInsertionSimple(vector<int> &nums, int target) {
int i = 0, j = nums.size() - 1; // 初始化双闭区间 [0, n-1]
while (i <= j) {
int m = i + (j - i) / 2; // 计算中点索引 m
if (nums[m] < target) {
i = m + 1; // target 在区间 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1; // target 在区间 [i, m-1] 中
} else {
return m; // 找到 target ,返回插入点 m
}
}
// 未找到 target ,返回插入点 i
return i;
}
/* 二分查找插入点(存在重复元素) */
int binarySearchInsertion(vector<int> &nums, int target) {
int i = 0, j = nums.size() - 1; // 初始化双闭区间 [0, n-1]
while (i <= j) {
int m = i + (j - i) / 2; // 计算中点索引 m
if (nums[m] < target) {
i = m + 1; // target 在区间 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1; // target 在区间 [i, m-1] 中
} else {
j = m - 1; // 首个小于 target 的元素在区间 [i, m-1] 中 ***不同之处***
}
}
// 返回插入点 i
return i;
}
Question 3:给定一个长度为 n 的有序数组 nums ,其中可能包含重复元素。请返回数组中最左一个元素 target 的索引。若数组中不包含该元素,则返回 -1。
/* 二分查找最左一个 target */
int binarySearchLeftEdge(vector<int> &nums, int target) {
// 等价于查找 target 的插入点
int i = binarySearchInsertion(nums, target);
// 未找到 target ,返回 -1
if (i == nums.size() || nums[i] != target) {
return -1;
}
// 找到 target ,返回索引 i
return i;
}
/* 二分查找最右一个 target */
int binarySearchRightEdge(vector<int> &nums, int target) {
// 转化为查找最左一个 target + 1
int i = binarySearchInsertion(nums, target + 1);
// j 指向最右一个 target ,i 指向首个大于 target 的元素
int j = i - 1;
// 未找到 target ,返回 -1
if (j == -1 || nums[j] != target) {
return -1;
}
// 找到 target ,返回索引 j
return j;
}
哈希优化策略
Question 1:给定一个整数数组 nums 和一个目标元素 target ,请在数组中搜索“和”为 target 的两个元素,并返回它们的数组索引。返回任意一个解即可。
/* 方法一:暴力枚举(以时间换空间) 此方法的时间复杂度为O(n*n),空间复杂度为O(1),在大数据量下非常耗时。*/
vector<int> twoSumBruteForce(vector<int> &nums, int target) {
int size = nums.size();
// 两层循环,时间复杂度为 O(n^2)
for (int i = 0; i < size - 1; i++) {
for (int j = i + 1; j < size; j++) {
if (nums[i] + nums[j] == target)
return {i, j};
}
}
return {};
}
/* 方法二:辅助哈希表(以空间换时间) 此方法的时间复杂度为O(n),空间复杂度为O(n) */
vector<int> twoSumHashTable(vector<int> &nums, int target) {
int size = nums.size();
// 辅助哈希表,空间复杂度为 O(n)
unordered_map<int, int> dic;
// 单层循环,时间复杂度为 O(n)
for (int i = 0; i < size; i++) {
if (dic.find(target - nums[i]) != dic.end()) {
return {dic[target - nums[i]], i};
}
dic.emplace(nums[i], i);
}
return {};
}
搜索算法总结
搜索算法可根据实现思路分为以下两类:
1.暴力搜索:通过遍历数据结构来定位目标元素,例如数组、链表、树和图的遍历等。
优点:简单且通用性好,无须对数据做预处理和借助额外的数据结构。
局限:此类算法的时间复杂度为O(n),其中 n 为元素数量,因此在数据量较大的情况下性能较差。
2.自适应搜索:利用数据组织结构或数据包含的先验信息,实现高效元素查找,例如二分查找、哈希查找和二叉搜索树查找等。
“二分查找”利用数据的有序性实现高效查找,仅适用于数组。
“哈希查找”利用哈希表将搜索数据和目标数据建立为键值对映射,从而实现查询操作。
“树查找”在特定的树结构(例如二叉搜索树)中,基于比较节点值来快速排除节点,从而定位目标元素。
优点:效率高,时间复杂度可达到O(logn)甚至O(1)。
局限:使用这些算法往往需要对数据进行预处理,例如,二分查找需要预先对数组进行排序,哈希查找和树查找都需要借助额外的数据结构,维护这些数据结构也需要额外的时间和空间开销。
如何选择搜索方法
查找算法效率对比:
线性搜索适用于小型或频繁更新的数据;二分查找适用于大型、排序的数据;哈希查找适用于对查询效率要求较高且无须范围查询的数据;树查找适用于需要维护顺序和支持范围查询的大型动态数据。
注:用哈希查找替换线性查找是一种常用的优化运行时间的策略,可将时间复杂度从O(n)降至 O(1)。
分治
全称分而治之,是一种非常重要且常见的算法策略。分治通常基于递归实现,包括“分”和“治”两个步骤
1.分(划分阶段):递归地将原问题分解为两个或多个子问题,直至到达最小子问题时终止。
2.治(合并阶段):从已知解的最小子问题开始,从底至顶地将子问题的解进行合并,从而构建出原问题的解。
如何判断分治问题
一个问题是否适合使用分治解决,通常可以参考以下几个判断依据:
1.问题可以分解:原问题可以分解成规模更小、类似的子问题,以及能够以相同方式递归地进行划分。
2.子问题是独立的:子问题之间没有重叠,互不依赖,可以独立解决。
3.子问题的解可以合并:原问题的解通过合并子问题的解得来。
应用
归并排序是分治策略的典型应用,其递归地将数组划分为等长的两个子数组,直到只剩一个元素时开始逐层合并,从而完成排序。
时间复杂度为O(logn)的搜索算法通常是基于分治策略实现的。
//基于分治实现二分查找
/* 二分查找:问题 f(i, j) */
int dfs(vector<int> &nums, int target, int i, int j) {
// 若区间为空,代表无目标元素,则返回 -1
if (i > j) {
return -1;
}
// 计算中点索引 m
int m = (i + j) / 2;
if (nums[m] < target) {
// 递归子问题 f(m+1, j)
return dfs(nums, target, m + 1, j);
} else if (nums[m] > target) {
// 递归子问题 f(i, m-1)
return dfs(nums, target, i, m - 1);
} else {
// 找到目标元素,返回其索引
return m;
}
}
/* 二分查找 */
int binarySearch(vector<int> &nums, int target) {
int n = nums.size();
// 求解问题 f(0, n-1)
return dfs(nums, target, 0, n - 1);
}
// 给定一棵二叉树的前序遍历 preorder 和中序遍历 inorder ,请从中构建二叉树,返回二叉树的根节点。
/* 构建二叉树:分治 */
TreeNode *dfs(vector<int> &preorder, unordered_map<int, int> &inorderMap, int i, int l, int r) {
// 子树区间为空时终止
if (r - l < 0)
return NULL;
// 初始化根节点
TreeNode *root = new TreeNode(preorder[i]);
// 查询 m ,从而划分左右子树
int m = inorderMap[preorder[i]];
// 子问题:构建左子树
root->left = dfs(preorder, inorderMap, i + 1, l, m - 1);
// 子问题:构建右子树
root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// 返回根节点
return root;
}
/* 构建二叉树 */
TreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {
// 初始化哈希表,存储 inorder 元素到索引的映射
unordered_map<int, int> inorderMap;
for (int i = 0; i < inorder.size(); i++) {
inorderMap[inorder[i]] = i;
}
TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorder.size() - 1);
return root;
}
小结
·分治是一种常见的算法设计策略,包括分(划分)和治(合并)两个阶段,通常基于递归实现。
·判断是否是分治算法问题的依据包括:问题能否分解、子问题是否独立、子问题能否合并。
·相较于暴力搜索,自适应搜索效率更高。时间复杂度为O(logn)的搜索算法通常是基于分治策略实现的。
回溯
回溯算法(backtracking algorithm)是一种通过穷举来解决问题的方法,它的核心思想是从一个初始状态出发,暴力搜索所有可能的解决方案,当遇到正确的解则将其记录,直到找到解或者尝试了所有可能的选择都无法找到解为止。之所以称之为回溯算法,是因为该算法在搜索解空间时会采用“尝试”与“回退”的策略。当算法在搜索过程中遇到某个状态无法继续前进或无法得到满足条件的解时,它会撤销上一步的选择,退回到之前的状态,并尝试其他可能的选择。
回溯的“尝试、回退、剪枝”的主体框架:
/* 回溯算法框架 */
void backtrack(State *state, vector<Choice *> &choices, vector<State *> &res) {
// 判断是否为解
if (isSolution(state)) {
// 记录解
recordSolution(state, res);
// 不再继续搜索
return;
}
// 遍历所有选择
for (Choice choice : choices) {
// 剪枝:判断选择是否合法
if (isValid(state, choice)) {
// 尝试:做出选择,更新状态
makeChoice(state, choice);
backtrack(state, choices, res);
// 回退:撤销选择,恢复到之前的状态
undoChoice(state, choice);
}
}
}
优点:回溯算法本质上是一种深度优先搜索算法,它尝试所有可能的解决方案直到找到满足条件的解。这种方法的优点在于能够找到所有可能的解决方案,而且在合理的剪枝操作下,具有很高的效率。
局限性:在处理大规模或者复杂问题时,回溯算法的运行效率可能难以接受
动态规划
动态规划(dynamic programming)是一个重要的算法范式,它将一个问题分解为一系列更小的子问题,并通过存储子问题的解来避免重复计算,从而大幅提升时间效率。动态规划是一种“从底至顶”的方法:从最小子问题的解开始,迭代地构建更大子问题的解,直至得到原问题的解。
DP问题特性:
·分治算法递归地将原问题划分为多个相互独立的子问题,直至最小子问题,并在回溯中合并子问题的解,最终得到原问题的解。
·动态规划也对问题进行递归分解,但与分治算法的主要区别是,动态规划中的子问题是相互依赖的,在分解过程中会出现许多重叠子问题。
·回溯算法在尝试和回退中穷举所有可能的解,并通过剪枝避免不必要的搜索分支。原问题的解由一系列决策步骤构成,我们可以将每个决策步骤之前的子序列看作一个子问题。
最优子结构的含义:原问题的最优解是从子问题的最优解构建得来的。
无后效性是动态规划能够有效解决问题的重要特性之一,其定义为:给定一个确定的状态,它的未来发展只与当前状态有关,而与过去经历的所有状态无关。
DP解题思路:动态规划的解题流程会因问题的性质和难度而有所不同,但通常遵循以下步骤:描述决策,定义状态,建立dp表,推导状态转移方程,确定边界条件等。
例题:给定一个 n x m 的二维网格 grid ,网格中的每个单元格包含一个非负整数,表示该单元格的代价。机器人以左上角单元格为起始点,每次只能向下或者向右移动一步,直至到达右下角单元格。请返回从左上角到右下角的最小路径和。
// 第一步:思考每轮的决策,定义状态,从而得到dp表
本题的每一轮的决策就是从当前格子向下或向右走一步。设当前格子的行列索引为 [i,j],则向下或向右走一步后,索引变为 [i+1,j] 或
[i,j+1]。因此,状态应包含行索引和列索引两个变量,记为 [i,j]。
状态[i,j]对应的子问题为:从起始点 [0,0] 走到 [i,j] 的最小路径和,解记为dp[i,j]。
// 第二步:找出最优子结构,进而推导出状态转移方程
对于状态 [i,j],它只能从上边格子 [i-1,j] 和左边格子 [i,j-1] 转移而来。因此最优子结构为:到达 [i,j] 的最小路径和由 [i,j-1] 的最小路径和与 [i-1,j] 的最小路径和中较小的那一个决定。
根据以上分析,可推出状态转移方程: dp[i,j] = min(dp[i-1,j],dp[i,j-1]) + grid[i,j]
// 第三步:确定边界条件和状态转移顺序
在本题中,处在首行的状态只能从其左边的状态得来,处在首列的状态只能从其上边的状态得来,因此首行 i=0 和首列 j=0 是边界条件。
/* 最小路径和:动态规划 */
int minPathSumDP(vector<vector<int>> &grid) {\
// 初始化grid的行列数
int n = grid.size(), m = grid[0].size();
// 初始化 dp 表
vector<vector<int>> dp(n, vector<int>(m));
dp[0][0] = grid[0][0];
// 状态转移:首行
for (int j = 1; j < m; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
// 状态转移:首列
for (int i = 1; i < n; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
// 状态转移:其余行和列
for (int i = 1; i < n; i++) {
for (int j = 1; j < m; j++) {
dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];
}
}
return dp[n - 1][m - 1];
}
/* 最小路径和:空间优化后的动态规划 */
int minPathSumDPComp(vector<vector<int>> &grid) {
int n = grid.size(), m = grid[0].size();
// 初始化 dp 表
vector<int> dp(m);
// 状态转移:首行
dp[0] = grid[0][0];
for (int j = 1; j < m; j++) {
dp[j] = dp[j - 1] + grid[0][j];
}
// 状态转移:其余行
for (int i = 1; i < n; i++) {
// 状态转移:首列
dp[0] = dp[0] + grid[i][0];
// 状态转移:其余列
for (int j = 1; j < m; j++) {
dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];
}
}
return dp[m - 1];
}
贪心
贪心算法(greedy algorithm)是一种常见的解决优化问题的算法,其基本思想是在问题的每个决策阶段,都选择当前看起来最优的选择,即贪心地做出局部最优的决策,以期获得全局最优解。
贪心算法和动态规划都常用于解决优化问题。它们之间存在一些相似之处,比如都依赖最优子结构性质,但工作原理不同。
·动态规划会根据之前阶段的所有决策来考虑当前决策,并使用过去子问题的解来构建当前子问题的解。
·贪心算法不会考虑过去的决策,而是一路向前地进行贪心选择,不断缩小问题范围,直至问题被解决。
贪心算法特性:
·贪心选择性质:只有当局部最优选择始终可以导致全局最优解时,贪心算法才能保证得到最优解。
·最优子结构:原问题的最优解包含子问题的最优解。
贪心算法解题步骤:
1.问题分析:梳理与理解问题特性,包括状态定义、优化目标和约束条件等。这一步在回溯和动态规划中都有涉及。
2.确定贪心策略:确定如何在每一步中做出贪心选择。这个策略能够在每一步减小问题的规模,并最终解决整个问题。
3.正确性证明:通常需要证明问题具有贪心选择性质和最优子结构。这个步骤可能需要用到数学证明,例如归纳法或反证法等。
例题:给定 n 个物品,第 i 个物品的重量为 wgt[i-1]、价值为 val[i-1],和一个容量为 cap 的背包。每个物品只能选择一次,但可以选择物品的一部分,价值根据选择的重量比例计算,问在限定背包容量下背包中物品的最大价值。
// 1. 贪心策略确定
最大化背包内物品总价值,本质上是最大化单位重量下的物品价值。
1.将物品按照单位价值从高到低进行排序。
2.遍历所有物品,每轮贪心地选择单位价值最高的物品。
3.若剩余背包容量不足,则使用当前物品的一部分填满背包。
/* 物品 */
class Item {
public:
int w; // 物品重量
int v; // 物品价值
Item(int w, int v) : w(w), v(v) {
}
};
/* 分数背包:贪心 */
double fractionalKnapsack(vector<int> &wgt, vector<int> &val, int cap) {
// 创建物品列表,包含两个属性:重量、价值
vector<Item> items;
for (int i = 0; i < wgt.size(); i++) {
items.push_back(Item(wgt[i], val[i]));
}
// 按照单位价值 item.v / item.w 从高到低进行排序
sort(items.begin(), items.end(), [](Item &a, Item &b) { return (double)a.v / a.w > (double)b.v / b.w; });
// 循环贪心选择
double res = 0;
for (auto &item : items) {
if (item.w <= cap) {
// 若剩余容量充足,则将当前物品整个装进背包
res += item.v;
cap -= item.w;
} else {
// 若剩余容量不足,则将当前物品的一部分装进背包
res += (double)item.v / item.w * cap;
// 已无剩余容量,因此跳出循环
break;
}
}
return res;
}
补充知识点:
双指针:
双指针方法在处理数组和链表的问题时非常有效,常见的应用场景包括:
左右指针:解决有序数组中的问题,如两数之和、三数之和等。
快慢指针:检测链表中的环、找链表中的中间节点等。
滑动窗口:解决字符串或数组中的子串问题,如最长无重复子串等。