文章目录
- 最长公共子序列
- 算法原理
- 代码 [最常公共子序列.js](..\ab_code\ja_JavaScript_dataStructure\最常公共子序列.js)
- 01背包问题
- 算法原理
- 原理图
- 视频讲解
- 代码
- 输出效果
- 贪心算法
- 分配饼干问题
- 题目描述
- 代码
- 无重叠区间
- 题目
- 解题思路
- 算法原理
- 代码
- 递归和回溯
- 全排列问题
- 原理图一
- 原理图二
- 算法框架
- 代码
- 合并排序
- merge()原理图
- merge_sort()原理图
- 代码
- 自然合并排序_待填写
- 待完成代码1
最长公共子序列
算法原理
代码 最常公共子序列.js
var f = longCommonSubsequence("abcde", "ace");
console.log(f);
function longCommonSubsequence(text1, text2) {
let m = text1.length;
let n = text2.length;
let f = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0));
console.log(f);
for (i = 1; i <= m; i++) {
let a1 = text1[i - 1];
for (j = 1; j <= n; j++) {
let a2 = text2[j - 1];
if (a1 === a2) {
f[i][j] = f[i - 1][j - 1] + 1;
} else {
f[i][j] = Math.max(f[i - 1][j], f[i][j - 1]);
}
}
}
return f;
}
01背包问题
算法原理
原理图
视频讲解
代码
代码目录
01背包问题.html
- 前i -1个物品的组合价值
- 仍然是前i-1个物品的组合价值,准确的说去掉当前物品容量的前i-1个物品的组合价值
- 加上当前物品的价值
var dp = backPack(
[
[2, 3],
[3, 4],
[4, 5],
[5, 6],
],
8
);
console.log(dp);
function backPack(arr, capacity) {
let [weight, value] = arr[0];
// m表示背包中物品的数量
let m = arr.length;
let dp = new Array(m + 1).fill(0).map(() => new Array(capacity + 1).fill(0));
for (i = 1; i <= m; i++) {
let [weight, value] = arr[i - 1];
for (j = 1; j <= capacity; j++) {
if (weight > j) {
dp[i][j] = dp[i - 1][j];
} else {
// 不放当前物品,就是前n个物品的最佳组合
// 放当前物品,就是背包容量减去当前物品重量,即 j - weight
// 然后再加上物品的价值,即 + value
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight] + value);
}
}
}
return dp;
}
输出效果
贪心算法
分配饼干问题
题目描述
有一群孩子和一堆饼干,每个孩子有一个饥饿度,每个饼干都有一个大小。每个孩子只能吃
一个饼干,且只有饼干的大小不小于孩子的饥饿度时,这个孩子才能吃饱。求解最多有多少孩子
可以吃饱。
代码
贪心算法.js
- 当饼干的饱腹度大于小孩的饥饿度时,此时这个孩子吃饱了,于是孩子指针就得下移一位 child++;
- 当饼干的饱腹度不能满足孩子时,此时饼干不能满足,于是饼干指针就得往下移一位
let filledChildren = greedy([1, 3], [1, 2, 3]);
console.log(filledChildren);
/**
* 贪心的策略是:尽量使用 饱腹度最小 的饼干满足 饥饿度最小 的孩子
* 所以这里很显然的就会用到排序,然后从小到大的往上试
*/
function greedy(children, cookies) {
// 对数组进行从小到大的排序
children.sort((m, n) => m - n);
cookies.sort((m, n) => m - n);
let child = 0;
let cookie = 0;
while (child < children.length && cookie < cookies.length) {
// 当饼干的饱腹度大于小孩的饥饿度时,cookies[cookie] >= children[child]
// 此时这个孩子吃饱了
// 于是孩子指针就得下移一位 child++;
// 当饼干的饱腹度不能满足孩子时,此时饼干不能满足,
// 于是饼干指针就得往下移一位,cookies[cookie++]
if (cookies[cookie++] >= children[child]) {
child++;
}
}
return child;
}
无重叠区间
题目
给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意:
可以认为区间的终点总是大于它的起点。
区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
示例 1:
输入: [ [1,2], [2,3], [3,4], [1,3] ]
输出: 1
解释:
移除 [1,3] 后,剩下的区间没有重叠。
示例 2:
输入: [ [1,2], [1,2], [1,2] ]
输出: 2
解释:
你需要移除两个 [1,2] 来使剩下的区间没有重叠。
示例 3:
输入: [ [1,2], [2,3] ]
输出: 0
解释:
你不需要移除任何区间,因为它们已经是无重叠的了。
解题思路
本题解决一个很经典的贪心算法问题 Interval Scheduling(区间调度问题)。给你很多形如 [start, end] 的闭区间,请你设计一个算法,算出这些区间中最多有几个互不相交的区间。这个问题在生活中的应用广泛,比如你今天有好几个活动,每个活动都可以用区间 [start, end] 表示开始和结束的时间,请问你今天最多能参加几个活动呢?显然你一个人不能同时参加两个活动,所以说这个问题就是求这些时间区间的最大不相交子集。
解题思路分三步:
(1)从区间集合 intvs 中选择一个区间 x,这个 x 是在当前所有区间中结束最早的(end 最小)。
(2)把所有与 x 区间相交的区间从区间集合 intvs 中删除。
(3)重复步骤 1 和 2,直到 intvs 为空为止。之前选出的那些 x 就是最大不相交子集。
可以按每个区间的 end 数值升序排序:
算法原理
[注]图片来自公众号:labuladong
代码
贪心算法_无重叠区间.js
- 对这些区间[start,end],按照end进行升序排序
- 比较前后区间的end值,如果后面区间的start 小于前面区间的start值,说明区间重叠
let count = eraseOverlapIntervals([
[3, 4],
[2, 3],
[1, 2],
[1, 3],
]);
console.log(count);
function eraseOverlapIntervals(arr) {
// 对区间[start,end],按照end进行升序排序
arr.sort(([, n], [, y]) => {
return n - y;
});
let count = 0;
let [, base_end] = arr[0];
for (let i = 1; i < arr.length; i++) {
let [start, end] = arr[i];
// 后面区间的开始小于前面区间的结束,start < base_end
// 则区间重叠
if (start < base_end) {
count++;
} else {
base_end = end;
}
}
return count;
}
递归和回溯
全排列问题
原理图一
原理图二
算法框架
回溯算法框架。解决一个回溯问题,实际上就是一个决策树的遍历过程。你只需要思考 3 个问题:
1、路径:也就是已经做出的选择。
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件。
如果你不理解这三个词语的解释,没关系,我们后面会用「全排列」和「N 皇后问题」这两个经典的回溯算法问题来帮你理解这些词语是什么意思,现在你先留着印象。
代码方面,回溯算法的框架:
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」,特别简单。
代码
全排列.js
console.log(permute([1, 2, 3]));
function permute(nums) {
// 结果集
let res = [];
// 路径
let path = [];
// 已使用的数字
let used = [];
backtracking(nums, used);
function backtracking(nums, used) {
// 路径的长度等于数组的长度,遍历完成,退出
if (path.length === nums.length) {
res.push(Array.from(path));
return;
}
for (let i = 0; i < nums.length; i++) {
// 如果该数字已经被使用,则跳过
if (used[i]) continue;
path.push(nums[i]); // 当前元素推入path
used[i] = true; // 标志位设置为true,表示该元素以遍历
backtracking(nums, used);
path.pop();
used[i] = false;
}
}
return res;
}
合并排序
merge()原理图
- A1和A2比较,谁小,谁就写入A.
- 与此同时, 写入和被写入的下标加一移动
- 哨兵,最后用一个很大的数,用作哨兵,这样就是不用在去判断这两个数组的长度了.
merge_sort()原理图
代码
合并排序.js
function merge(Arr, start, mid, end) {
let Arr1 = Arr.slice(start, mid);
let Arr2 = Arr.slice(mid, end);
// 数组的会后一位,添加一个哨兵
Arr1.push(Number.MAX_SAFE_INTEGER);
Arr2.push(Number.MAX_SAFE_INTEGER);
// x 表示原数组的写入下标,所以起始值为 x =start
// 循环结束的条件,就是 写入下标小于这个end,即 x<end
// y 和 z 分别表示Arr1 和 Arr2 的下标
for (let x = start, y = 0, z = 0; x < end; x++) {
Arr[x] = Arr1[y] < Arr2[z] ? Arr1[y++] : Arr2[z++];
}
return Arr;
}
function merge_sort(Arr, start, end) {
// 递归终止条件
if (end - start < 2) return;
// 取中间值,向下取整
let mid = Math.ceil((start + end) / 2);
merge_sort(Arr, start, mid);
merge_sort(Arr, mid, end);
merge(Arr, start, mid, end);
}
let Arr = [2, 3, 52, 23, 2, 456, 74];
merge_sort(Arr, 0, Arr.length);
console.log(Arr);
自然合并排序_待填写
待完成代码1
/**
* 若数组a中元素为{4,8,3,7,1,5,6,2},则自然排好序的子数组段有{4,8},{3,7},{1,5,6},{2},
* 经一次合并后得到2个合并后的子数组段{3,4,7,8}和{1,2,5,6},
* 继续合并相邻排好序的子数组段,直至整个数组已排好序,最终结果{1,2,3,4,5,6,7,8}
*/
let Arr = [4, 8, 3, 7, 1, 5, 6, 2];
/
function merge_pass(Arr) {
let sortedArrOfStart = [0];
let sortedCount = 1;
let begin = Arr[0];
for (let i = 1; i < Arr.length; i++) {
if (begin > Arr[i]) {
sortedArrOfStart[sortedCount++] = i;
}
begin = Arr[i];
}
return sortedArrOfStart;
}