系列目录:
完整的代码都在这里
贪心算法(英语:greedy algorithm),又称贪婪算法,是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法
贪心算法在有最优子结构的问题中尤为有效。最优子结构的意思是局部最优解能决定全局最优解。简单地说,问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。
贪心算法与动态规划的不同在于它对每个子问题的解决方案都做出选择,不能回退。动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。
一般性步骤
- 确定最优子结构
- 设计递归算法
- 贪心的可行性,主要在区分使用动态规划还是贪心
- 递归算法转化为迭代算法
活动选择问题
背景简介
假设有一个场地,有很多活动需要用到它,我们要尽可能的安排多的活动。
我们有一张时间表,活动按结束时间已经排好序
Si:表示开始时间
Fi:表示结束时间
算法思路
依然是二分假设一个递归式 c[i,j]=c[i,k]+c[j,k]+1 c 表示活动的数量
已知集合Sij
假定Aij
是最优解,ak
是中间的一个值,那么我们需要去寻找Sik
(ai
结束之后ak
开始之前的那些活动)以及
Skj
(ak
结束之后和aj
开始之前的那些活动)中的最优集合。这时候如果存在一个集合Bik
它的活动数量是大于Aik
的,那么
和之前的假定Aij
是最优解就矛盾了。所以递归式就是成立的了。
这样就是之前动态规划的思路了。
我们应该尽可能选择早结束的活动,这样剩下的资源就更多,更能让其他活动分配。为什么这样的贪心是正确的?
条件假设
- Sk 是一个集合
- am 是最早结束的活动
- Ak 是最优解
- aj 是最优解中最早结束的活动
如果 am = aj ,那么就证明了 最早结束的活动一定在最优解内
如果 am != aj, 因为am一定是比aj早结束的所以,将am替换aj,am不会与Ak中的活动有交集,Ak依然是成立的。
所以最优解内,依然是一定可以包含am的。由此可知我们的贪心策略是成立的
算法过程
代码过程是很简单的,因为已经按照结束时间排序了。只要依次遍历,开始时间比上一次结束时间晚的就选择。
然后重新记录结束时间。
代码实现
function GreedyActivitySelector(s, f) {
let n = s.length;
let A = [0]; // 最终活动数组 存放活动编号
let end = f[0]; // 记录上次活动结束时间
for (let i = 1; i < n; i++) {
if (s[i] >= end) {
A.push(i);
end = f[i];
}
}
return A;
}
module.exports = GreedyActivitySelector;
简单测试用例(jest)
const GreedyActivitySelector = require("../chapter-16/GreedyActivitySelector");
const s = [1, 3, 0, 5, 3, 5, 6, 8, 8, 2, 12];
const f = [4, 5, 6, 7, 9, 9, 10, 11, 12, 14, 16];
test("活动选择问题", () => {
expect(GreedyActivitySelector(s, f)).toEqual([0, 3, 7, 10]);
});
哈夫曼编码
背景简介
哈夫曼编码可以有效地压缩数据,通常可以节约20%~90%的时间。
他是这样做的
- 统计每个字符出现的频率
- 频率高的翻译成越短的数字串
- 任意一个短的数字串 都不能是另一个长的前缀 (10,101 这样是不行的 因为10 是101的前面两位
如上表所示可以将 300000位的长度 压缩至224000位
算法思路
一个深度为k,节点个数为 2^k - 1 的二叉树为满二叉树,
其他特点
- 结点个数一定为奇数
- 第i层有2^(i-1) 个结点
- 有 2^(k-1) 个叶子
文件的最优编码方案总是对应一颗满二叉树?(不知道为啥 (ಥ_ಥ))
ps:为啥正确的这段跳过了 没有看 😃 看了也不一定看的懂:)
算法过程
算法使用一个以属性freq为关键字最小优先队列,已识别两个最低的频率的对象将其合并。
这里最小优先队列,直接使用之前的最大优先队列改造,详细的可以去看之前的1,2部分那一篇。
得到合并过后的哈弗曼树后递归遍历输出,就能获得每个key对应的code
整个构建过程如下图
代码实现
const format = (input) => {
return input.map((e) => {
e.priority = e.freq
return e
})
}
var MinPriorityQueue = require('../chapter-06/MinPriorityQueue')
// 整个函数用来构造 哈夫曼树
function HuffmanTree(c) {
let n = c.length;
let Q = new MinPriorityQueue(c);
for (let i = 0; i < n - 1; i++) {
let z = { key: '', freq: 0, priority: 0, }
// 这里取出最小的两个
let x = Q.extractMin()
let y = Q.extractMin()
// 进行相加得到新的节点 然后合并
z.priority = x.priority + y.priority
z.freq = x.freq + y.freq
z.key = z.freq
z.left = x
z.right = y
//这里改变了 原来数组形式的二叉树 变成对象形式的 方便向下查找
Q.insert(z)
}
return Q.queue[0]
}
// 这个函数 通过递归透传 code 来定义所有的编码
function PrintHuffman(Node) {
let codeMap = new Map() // 用来存放 key 对应的编码 code
let code = ''
const find = (node, codeMap, code) => {
let pass_code_left = code // 左子树的透传code
let pass_code_right = code // 右子树的透传code
let IS_NUMBER = /^[0-9]*$/
if (IS_NUMBER.test(node.key)) { //边界条件 当key不是字符的时候 结束递归
if (node.left) {
pass_code_left = pass_code_left + '0'
find(node.left, codeMap, pass_code_left)
}
if (node.right) {
pass_code_right = pass_code_right + '1'
find(node.right, codeMap, pass_code_right)
}
} else {
codeMap[node.key] = code
}
}
find(Node, codeMap, code)
return codeMap
}
function Huffman(input){
return PrintHuffman(HuffmanTree(format(input)))
}
module.exports = Huffman
简单测试用例(jest)
var Huffman = require('../chapter-16/Huffman')
var input = [
{ key: 'f', freq: 5 },
{ key: 'e', freq: 9 },
{ key: 'c', freq: 12 },
{ key: 'b', freq: 13 },
{ key: 'd', freq: 16 },
{ key: 'a', freq: 45 }
]
test('哈夫曼编码',()=>{
expect(Huffman(input).a).toBe('0')
expect(Huffman(input).c).toBe('100')
})