一、线性结构 {ignore}
数据结构和算法概述
- 什么是数据结构?
存储和运算是程序的两大基础功能,数据结构是专门研究数据存储的学科。
很多时候,我们无法仅使用简单的数字、字符串、布尔就能完整的描述数据,可能我们希望使用数组、对象、或它们组合而成的复合结构来对数据进行描述。这种复合的结构就是数据结构。
而在实际开发中,我们会发现很多场景中使用的数据结构有着相似的特征,于是,数据结构这门学科,就把这些相似的结构单独提取出来进行研究。
在这门学科中,常见的数据结构有:数组、链表、树、图等
- 什么是算法?
存储和运算是程序的两大基础功能,算法是专门研究运算过程的学科。
一个程序,很多时候都需要根据一种已知数据,通过计算,得到另一个未知数据,这个运算过程使用的方法,就是算法。
而在很多的场景中,它们使用的算法有一些共通的特点,于是把这些共通的算法抽象出来,就行了常见算法。
从一个更高的角度来对算法划分,常见的算法有:穷举法、分治法、贪心算法、动态规划
- 数据结构和算法有什么关系?
一个面向的是存储,一个面向的是运算,它们共同构成了计算机程序的两个重要部分。
有了相应的数据结构,免不了对这种数据结构的各种变化进行运算,所以,很多时候,某种数据结构都会自然而然的搭配不少算法。
- 数据结构和算法课程使用什么计算机语言?
数据结构和算法属于计算机基础课程,它们和具体的语言无关,用任何语言都可以实现。
本课程采用JavaScript语言。
线性结构
线性结构是数据结构中的一种分类,用于表示一系列的元素形成的有序集合。
常见的线性结构包括:数组、链表、栈、队列
数组
特别注意:这里所说的数组是数据结构中的数组,和JS中的数组不一样
数组是一整块连续的内存空间,它由固定数量的元素组成,数组具有以下基本特征:
- 整个数组占用的内存空间是连续的
- 数组中元素的数量是固定的(不可增加也不可减少),创建数组时就必须指定其长度
- 每个元素占用的内存大小是完全一样的
根据数组的基本特征,我们可以推导出数组具有以下特点:
- 通过下标寻找对应的元素效率极高,因此遍历速度快
- 无法添加和删除数据,虽然可以通过某种算法完成类似操作,但会增加额外的内存开销或时间开销
- 如果数组需要的空间很大,可能一时无法找到足够大的连续内存
JS中的数组
在ES6之前,JS没有真正意义的数组,所谓的Array,实际上底层实现是链表。
ES6之后,出现真正的数组(类型化数组),但是由于只能存储数字,因此功能有限
目前来讲,JS语言只具备不完善的数组(类型化数组)
链表
为弥补数组的缺陷而出现的一种数据结构,它具有以下基本特征:
- 每个元素除了存储数据,需要有额外的内存存储一个引用(地址),来指向下一个元素
- 每个元素占用的内存空间并不要求是连续的
- 往往使用链表的第一个节点(根节点)来代表整个链表
根据链表的基本特征,我们可以推导出它具有以下特点:
- 长度是可变的,随时可以增加和删除元素
- 插入和删除元素的效率极高
- 由于要存储下一个元素的地址,会增加额外的内存开销
- 通过下标查询链表中的某个节点,效率很低,因此链表的下标遍历效率低
手动用代码实现链表
实际上,很多语言本身已经实现了链表(如JS中的数组,底层就是用链表实现的),但链表作为一种基础的数据结构,通过手写代码实现链表,不仅可以锻炼程序思维和代码转换能力,对于后序的复杂数据结构的学习也是非常有帮助的。
因此,手写链表是学习数据结构和算法的一门基本功
手写一个链表结构,并完成一些链表的相关函数,要实现以下功能:
- 遍历打印
- 获取链表的长度
- 通过下标获取链表中的某个数据
- 通过下标设置链表中的某个数据
- 在链表某一个节点之后加入一个新节点
- 在链表末尾加入一个新节点
- 删除一个链表节点
- 链表倒序
/**
* 构造函数,表示链表的一个节点
*/
function Node(value) {
this.value = value; //节点的数据
this.next = null; //下一个节点的地址
}
/**
* 遍历一个链表,打印每个节点的数据
* @param root 链表的根节点
*/
function print(root) {
// var node = root;
// while (node) {
// //如果node有值,打印
// console.log(node.value);
// node = node.next;
// }
// 分治法
if (root) {
console.log(root.value); //打印自己
print(root.next);
}
}
/**
* 计算链表的长度
* @param {*} root
*/
function count(root) {
if (!root) return 0; //链表没有节点
return 1 + count(root.next); //1表示根节点占用一个数量
}
/**
* 得到链表某个下标的数据
* @param {*} root
* @param {*} index
*/
function getNode(root, index) {
/**
* 判断某个节点是否是我要查找的节点
* @param {*} node 表示某个节点
* @param {*} i 该节点是第几个节点
*/
function _getValue(node, i) {
if (!node) return null;
if (i === index) return node;
return _getValue(node.next, i + 1);
}
return _getValue(root, 0);
}
/**
* 设置链表某个位置的数据
*/
function setValue(root, index, value) {
function _setValue(node, i) {
if (!node) return;
if (i === index) {
node.value = value
}
else {
_setValue(node.next, i + 1);
}
}
_setValue(root, 0);
}
/**
* 在某个链表节点之后加入一个新节点
* @param node 在哪个节点之后加入
* @param newValue 新节点的数据
*/
function insertAfter(node, newValue) {
var newNode = new Node(newValue); //构建新节点
node.next = newNode;
newNode.next = node.next;
}
/**
* 在链表的末尾加入新节点
*/
function push(root, newValue) {
//判断root是不是最后一个节点
if (!root.next) {
//最后一个节点
var newNode = new Node(newValue);
root.next = newNode;
}
else {
push(root.next, newValue); //自己不是最后一个,看下一个
}
}
/**
* 根据给定的链表,和 给定的要删除的值,删除对应节点
* @param {*} root
* @param {*} nodeValue
*/
function removeNode(root, nodeValue) {
if (!root || !root.next) return; //无法删除的情况
if (root.next.value === nodeValue) {
//下一个节点就是要找的节点
root.next = root.next.next;
}
else {
//下一个节点还不是
removeNode(root.next, nodeValue);
}
}
/**
* 给定一个链表,返回一个倒序后的根节点
* @param {*} root
*/
function reverse(root) {
if (!root || !root.next) return root;
if (!root.next.next) {
var temp = root.next; //保存返回的节点
//有两个节点的链表
root.next.next = root;
root.next = null;
return temp;
}
else {
var temp = reverse(root.next); //后续节点倒序
root.next.next = root;
root.next = null;
return temp;
}
}
var a = new Node("a");
var b = new Node("b");
var c = new Node("c");
a.next = b;
b.next = c;
// insertAfter(b, "d");
var temp = reverse(a);
print(temp);
二、排序和查找 {ignore}
排序算法
排序算法没有优劣之分,在不同的场景中,不同的排序算法执行效率不同。
- 选择排序 Selection Sort
一次选择排序,可以将某个区间的最小值排列到该区域的第一位,具体的方式是:
- 找出该区域的最小值
- 将该值与该区域第一个值交换
- 对下一个区域重复上述过程,直到排序完成
- 冒泡排序 Bubble Sort
一次冒泡排序,可以将某个区域序列的最大值排序到该区域的最后一位,具体的方式是:
- 将第1位和第2位比较,如果前者比后者大则交换
- 将第2位和第3位比较,如果前者比后者大则交换
- 依次类推,直到比较到该区域的最后两位
- 重复上述过程,直到序列排序完成
- 插入排序 Insertion Sort
将序列分为两个部分,一部分是有序的,一部分是无序的,现在要做的是,就是不断的从无序的部分取出数据,加入到有序的部分,直到整个排序完成
例如:序列[5, 7, 2, 3, 6]
- 分为有序的序列和无序的序列 (5) (7 2 3 6)
- 不断的扩充有序序列 (5 7) (2 3 6)
- 不断的扩充有序序列 (2 5 7) (3 6)
- 不断的扩充有序序列 (2 3 5 7) (6)
- 不断的扩充有序序列 (2 3 5 6 7)
- 排序完成
- 快速排序 Quick Sort
选择一个数(比如序列的最后一位)作为基准数,将整个序列排序成两部分,一部分比该数小,另一部分比该数大,基准数在中间,然后对剩余的序列做同样的事情,直到排序完成
例如:序列[5, 7, 2, 3, 6, 4]
- 选择4作为基准数,排序成为:(3, 2) 4 (7, 6, 5)
- 对于3,2, 继续使用该方式排序,得到: (2, 3) 4 (7,6,5)
- 对于7,6,5,继续使用该方式排序,得到: (2, 3) 4 (5,6,7)
- 排序完成
查询算法
- 顺序查找 Inorder Search
即普通的遍历,属于算法的穷举法,没啥好解释的
- 二分查找 Binary Search
如果一个序列是一个排序好的序列,则使用二分查找可以极大的缩短查找时间
具体的做法是:
查找该序列中间未知的数据
- 相等,找到
- 要找的数据较大,则对后续部分的数据做同样的步骤
- 要找的数据较小,则对前面部分的数据做同样的步骤
- 插值查找 Interpolation Search
插值查找是对二分查找的进一步改进
如果序列不仅是一个排序好的序列,而且序列的步长大致相同,使用插值查找会更快的找到目标。
插值查找基于如下假设:下标之间的距离比和数据之间的距离比大致相同,即:
(目标下标-最小下标) / (最大下标 - 最小下标) ≈ (目标值 - 最小值) / (最大值 - 最小值)
因此可以算出大致的下标落点:
目标下标 ≈ (目标值 - 最小值) / (最大值 - 最小值) * (最大下标 - 最小下标) + 最小下标
这样就可以计算出大致的下标落点,后续的比较和二分查找一样。
// sort.js
/**
* 交换数组中指定的位置
* @param {*} arr
* @param {*} i1
* @param {*} i2
*/
function swap(arr, i1, i2) {
var temp = arr[i1]
arr[i1] = arr[i2];
arr[i2] = temp;
}
/**
* 选择排序
* @param {*} arr
*/
function selectionSort(arr) {
for (var i = 0; i < arr.length - 1; i++) {
//搞定 i ~ arr.length-1 区间
//从该区间中找出最小值,和 第 i 位交换
var min = arr[i]; //定义一个变量,为该区间的第一个数
var index = i; //最小值所在的位置
for (var j = i + 1; j < arr.length; j++) {
if (arr[j] < min) {
min = arr[j];
index = j; //重新记录最小值的位置
}
}
//最小值已经找出
//交换第i位和第index位
swap(arr, i, index);
}
}
/**
* 冒泡排序
* @param {*} arr
*/
function bubbleSort(arr) {
for (var i = 0; i < arr.length - 1; i++) {
//需要经过arr.length-1次的冒泡
//i:0 循环:0~arr.length-1-i
//i:1 循环:0~arr.length-1-i
//i:2 循环: 0~arr.length-1-i
for (var j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
}
}
}
}
/**
* 插入排序
* @param {*} arr
*/
function insertionSort(arr) {
for (var i = 1; i < arr.length; i++) {
if (arr[i] < arr[i - 1]) {
//将第i位的值加入到前面有序队列的正确位置
var temp = arr[i];
for (var j = i; j >= 0; j--) {
if (j > 0 && arr[j - 1] > temp) {
arr[j] = arr[j - 1];
}
else {
arr[j] = temp;
break;
}
}
}
}
}
/**
* 快速排序
* @param {*} arr
*/
function quickSort(arr) {
/**
* 对数组的某个区域进行一次快速排序
* @param {*} arr
* @param {*} start 区域的起始下标
* @param {*} end 区域的结束下标
*/
function _quickSort(arr, start, end) {
if (start >= end || start > arr.length - 1) return;
var low = start, high = end;
var key = arr[end]; //基准值
while (low < high) {
while (low < high && arr[low] <= key) low++;
arr[high] = arr[low];
while (low < high && arr[high] >= key) high--;
arr[low] = arr[high];
}
//low === high
arr[low] = key;
_quickSort(arr, start, low - 1);
_quickSort(arr, low + 1, end);
}
_quickSort(arr, 0, arr.length - 1);
}
var arr = [5, 3, 1, 6, 7, 4];
console.log(arr)
quickSort(arr)
console.log(arr);
//search.js
var num = 0;
/**
* 查找一个数组中目标值是否存在(顺序查找)
* @param {*} arr
* @param {*} target
*/
function inOrderSearch(arr, target) {
for (var i = 0; i < arr.length; i++) {
num++;
if (arr[i] === target) {
return true;
}
}
return false;
}
function binarySearch(arr, target) {
if (arr.length === 0 || target < arr[0] || target > arr[arr.length - 1]) return false;
var minIndex = 0; //最小下标
var maxIndex = arr.length - 1; //最大下标
while (minIndex <= maxIndex) {
num++;
var mid = Math.floor((minIndex + maxIndex) / 2);//中间下标
if (arr[mid] === target) {
return true;
}
else if (arr[mid] > target) {
maxIndex = mid - 1;
}
else {
minIndex = mid + 1;
}
}
return false;
}
function interpolationSearch(arr, target) {
if (arr.length === 0 || target < arr[0] || target > arr[arr.length - 1]) return false;
var minIndex = 0; //最小下标
var maxIndex = arr.length - 1; //最大下标
while (minIndex <= maxIndex) {
num++;
var mid = (target - arr[minIndex]) / (arr[maxIndex] - arr[minIndex]) * (maxIndex - minIndex) + minIndex;
if (arr[mid] === target) {
return true;
}
else if (arr[mid] > target) {
maxIndex = mid - 1;
}
else {
minIndex = mid + 1;
}
}
return false;
}
var arr = new Array(100000);
for (var i = 0; i < arr.length; i++) {
arr[i] = i + 1;
}
var result = interpolationSearch(arr, 100000)
console.log(result, num);
三、树形结构 {ignore}
树
树是一个类似于链表的二维结构,每个节点可以指向0个或多个其他节点
树具有以下特点:
- 单根:如果一个节点A指向了另一个节点B,仅能通过A直接找到B节点,不可能通过其他节点直接找到B节点
- 无环:节点的指向不能形成环
树的术语:
- 结点的度:某个节点的度 = 该节点子节点的数量
- 树的度:一棵树中,最大的节点的度为该树的度
- 结点的层:从根开始定义起,根为第1层,根的子结点为第2层,以此类推;
- 树的高度或深度:树中结点的最大层次
- 叶子节点:度为0的结点称为叶结点;
- 分支节点:非叶子节点
- 子节点、父节点:相对概念,如果A节点有一个子节点B,则A是B的父节点,B是A的子节点
- 兄弟节点:如果两个节点有同一个父节点,则它们互为兄弟节点
- 祖先节点:某个节点的祖先节点,是从树的根到该节点本身经过的所有节点
- 后代节点:如果A是B的祖先节点,B则是A的后代节点
树的代码表示法:
function Node(value){
this.value = value;
this.children = [];
}
二叉树
如果一颗树的度为2,则该树是二叉树
二叉树可以用下面的代码表示
function Node(value){
this.value = value;
this.left = null;
this.right = null;
}
二叉树的相关算法
编写各种函数,实现下面的功能
- 对二叉树遍历打印
- 前(先)序遍历 DLR
- 中序遍历 LDR
- 后序遍历 LRD
- 根据前序遍历和中序遍历结果,得到一颗二叉树
- 计算树的深度
- 查询二叉树
- 深度优先 Depth First Search
- 广度优先 Breadth First Search
- 比较两棵二叉树,得到比较的结果
function Node(value) {
this.value = value;
this.left = null;
this.right = null;
}
/**
* 前序遍历
* @param {*} root
*/
function DLR(root) {
if (!root) return;// 没有节点
console.log(root.value);
DLR(root.left);
DLR(root.right);
}
/**
* 中序遍历
* @param {*} root
*/
function LDR(root) {
if (!root) return;// 没有节点
LDR(root.left);
console.log(root.value);
LDR(root.right);
}
/**
* 后序遍历
* @param {*} root
*/
function LRD(root) {
if (!root) return;// 没有节点
LRD(root.left);
LRD(root.right);
console.log(root.value);
}
/**
* 根据前序遍历,和 中序遍历,得到一棵树的根节点
* @param {*} dlr
* @param {*} ldr
*/
function getTree(dlr, ldr) {
dlr = dlr.split("");
ldr = ldr.split("");
if (dlr.length !== ldr.length) throw new Error("无效的遍历值");
if (dlr.length === 0) return null;
var rootValue = dlr[0]; //取出根节点的值
var root = new Node(rootValue);
var index = ldr.indexOf(rootValue); //找到根节点的值在中序遍历中的位置
var leftLDR = ldr.slice(0, index).join(""); //左边的中序遍历结果
var leftDLR = dlr.slice(1, leftLDR.length + 1).join(""); //左边的前序遍历结果
root.left = getTree(leftDLR, leftLDR);
var rightLDR = ldr.slice(index + 1).join(""); //右边的中序遍历结果
var rightDLR = dlr.slice(leftLDR.length + 1).join(""); //右边的前序遍历结果
root.right = getTree(rightDLR, rightLDR);
return root;
}
/**
* 得到一棵树的深度
* @param {*} root
*/
function getDeep(root) {
if (!root) return 0;
var left = getDeep(root.left);
var right = getDeep(root.right);
return Math.max(left, right) + 1;
}
var root = getTree("abcde", "cbdae")
console.log(root)
console.log(getDeep(root));
// var a = new Node("a");
// var b = new Node("b");
// var c = new Node("c");
// var d = new Node("d");
// var e = new Node("e");
// var f = new Node("f");
// a.left = b;
// a.right = e;
// b.left = c;
// b.right = d;
// e.left = f;
// LRD(a);
四、图结构 {ignore}
概念
图结构中,一个结点可以链接到任意结点,所有结点链接而成的结构,即为图结构
图结构中的链接可以是有向的,也可以是无向的(双向链接),本文仅讨论双向链接
树结构是一种特殊的图结构
图结构没有根,可以有环,但是在一个图结构中,不能存在两个或以上的孤立结点
可以使用图中任何一个结点表示整个图结构
图结构是一种常见的数据结构,例如网络爬虫抓取的网页就是一种典型的图结构
图结构的代码可表示为:
function Node(value){
this.value = value;
this.neighbors = [];
}
相关算法
- 查询算法
和树结构一样,图结构的查询也可以分为深度优先(Depth First Search)和广度优先(Breadth First Search)查询
- 最小生成树算法
如果一个图中结点连接而成的边具备某种数值,需要将这些边进行精简,生成一个连接全节点同时总边长最小的树结构,该树称之为最小生成树
实现最小生成树可以使用Prim算法,从任意一个点出发,连接到该点最短的点,组成一个部落,然后继续连接到该部落最短的点,直到把所有点连接完成
function Node(value) {
this.value = value;
this.neighbors = [];
}
/**
* 深度优先遍历
* @param {*} node
* @param {*} targetValue
* @param finded 已经找过的结点
*/
function deepFirstSearch(node, targetValue, finded) {
//如果finded数组中包含了node,说明,当前结点已经被看过了,直接返回
if (finded.includes(node)) return false;
if (node.value === targetValue) return true;
finded.push(node); //将当前结点加入到已找过的结点
//自己不是要找到,看相邻结点
for (var i = 0; i < node.neighbors.length; i++) {
if (deepFirstSearch(node.neighbors[i], targetValue, finded)) {
//在其中一个相邻结点的深搜过程中找到了
return true;
}
}
return false; //所有相邻的结点的深搜过程中都找不到
}
/**
* 图的广搜
* @param {*} nodes 要找的某一群结点,该数组中的结点都是没有找过的
* @param {*} targetValue
* @param finded 已经找过的结点
*/
function breadthFirstSearch(nodes, targetValue, finded) {
if (nodes.length === 0) return false;
var nexts = [];
for (var i = 0; i < nodes.length; i++) {
if (nodes[i].value === targetValue) {
return true;
}
//说明该结点已找过
finded.push(nodes[i]);
//直接将该结点的相邻结点加入到数组nexts
for (var j = 0; j < nodes[i].neighbors.length; j++) {
var n = nodes[i].neighbors[j]; //第j个邻居
if (!nexts.includes(n)) {
nexts.push(n);
}
}
}
//重新对nexts进行处理
for (var i = 0; i < nexts.length; i++) {
if (finded.includes(nexts[i])) {
nexts.splice(i, 1);
i--;
}
}
console.log(nexts);
return breadthFirstSearch(nexts, targetValue, finded);
}
var a = new Node("a");
var b = new Node("b");
var c = new Node("c");
var d = new Node("d");
var e = new Node("e");
a.neighbors.push(b, c, e);
b.neighbors.push(a, c, d);
c.neighbors.push(a, b);
e.neighbors.push(a, e);
d.neighbors.push(b, e);
var result = breadthFirstSearch([c], "e", []);
console.log(result);
/**
* 使用Prim算法根据点的集合,和边的集合,链接结点
* @param {*} nodes
* @param {*} sides
*/
function Prim(nodes, sides) {
//先做一个验证
if (nodes.length !== sides.length || nodes.length <= 1) return;
var horde = [nodes[0]]; //已连接的点形成的部落
while (horde.length < nodes.length) {
//添加一个点到部落horde
//找到一个到这个部落最短的点
var result = { //求的最短的点
dis: Infinity, //距离默认无穷大
to: null, //连接到部落的哪个点,部落的点
from: null //从哪个点连接到部落,新的点
}
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i]; //一个点一个点拿出来比较
if (horde.includes(node)) {
//部落中已经有这个点了
continue; //进行下一次循环
}
//这个点没有在部落中
var info = getMinDistance(node); //得到该点到部落的距离信息
if (info.dis < result.dis) {
result.dis = info.dis;
result.to = info.connect;
result.from = node;
}
}
//将点和部落中对应的点进行连接
result.to.neighbors.push(result.from);
result.from.neighbors.push(result.to);
//将该点加入到部落中
horde.push(result.from);
}
/**
* 查找指定的结点到当前部落最短的距离和要连接的点
* @param {*} node
*/
function getMinDistance(node) {
var result = { //求的结果
dis: Infinity, //距离默认无穷大
connect: null //连接到部落的哪个点
}
for (var i = 0; i < horde.length; i++) {
var target = horde[i]; //部落中的某个结点
var dis = getDistance(node, target);
if (dis < result.dis) {
result.dis = dis;
result.connect = target;
}
}
return result;
}
/**
* 得到两个点的距离
* @param {*} node1
* @param {*} node2
*/
function getDistance(node1, node2) {
var i1 = nodes.indexOf(node1); //第一个点的下标
var i2 = nodes.indexOf(node2);//第二个点的下标
return sides[i1][i2];
}
}
//点的集合
var nodes = [a, b, c, d, e];
//边的集合
var sides = [
[0, 8, 3, Infinity, 4],//a点到其他结点的距离
[8, 0, 4, 10, Infinity], //b到其他点的距离
[3, 4, 0, 3, Infinity], //c到其他点的距离
[Infinity, 10, 3, 0, 6], //d到其他点的距离
[4, Infinity, Infinity, 6, 0], //e到其他点的距离
];
Prim(nodes, sides);
console.log(a)
五、贪心算法和动态规划 {ignore}
贪心算法
当遇到一个求解全局最优解问题时,如果可以将全局问题切分为小的局部问题,并寻求局部最优解,同时可以证明局部最优解累计的结果就是全局最优解,则可以使用贪心算法
面试题:找零问题
示例:假设你有一间小店,需要找给客户46分钱的硬币,你的货柜里只有面额为25分、10分、5分、1分的硬币,如何找零才能保证数额正确并且硬币数最小
动态规划
分治法有一个问题,就是容易重复计算已经计算过的值,使用动态规划,可以讲每一次分治时算出的值记录下来,防止重复计算,从而提高效率。
面试题1:青蛙跳台阶问题
有N级台阶,一只青蛙每次可以跳1级或两级,一共有多少种跳法可以跳完台阶?
面试题2:最长公共子序列问题(LCS)
有的时候,我们需要比较两个字符串的相似程度,通常就是比较两个字符串有多少相同的公共子序列
例如有两个字符串
- 邓哥特有的贵族气质吸引了很多女孩
- 邓哥喜欢吃秋葵和香菜,但是他的女朋友们不喜欢
以上两个字符串的最长公共子序列为:邓哥的女
/**
* 贪心算法:找零问题
* @param {*} total 找零总数
* @param {*} deno 面额
*/
function exchange(total, deno) {
var result = []; //找零结果
while (total > 0) {
//还要找
var max = -1; //最大可用面额
for (var i = 0; i < deno.length; i++) {
var d = deno[i]; //当前面额
if (d > max && d <= total) {
max = d;
}
}
result.push(max); //找钱结果
total -= max;
}
return result;
}
var result = exchange(41, [25, 20, 10, 5, 1])
console.log(result);
-------------------------------------------------------
function jumpTemp(n) {
if (n === 1) return 1;
if (n === 2) return 2;
return jumpTemp(n - 1) + jumpTemp(n - 2);
}
/**
* 青蛙跳n级台阶一共有多少种跳法
* @param {*} n
*/
function jump(n) {
var table = []; //用一个数组记录已经跳过的台阶结果
function _jump(n) {
if (table[n]) return table[n]; //已经算过了,不用再算了
//没有算过
var newRecord; //用于记录这一次运算的结果
if (n === 1) newRecord = 1;
else if (n === 2) newRecord = 2;
else {
newRecord = _jump(n - 1) + _jump(n - 2);
}
table[n] = newRecord;
return newRecord;
}
var result = _jump(n);
console.log(table);
return result;
}
/**
*
* @param {*} str1
* @param {*} str2
*/
function LCS(str1, str2) {
var table = [];
function _LCS(str1, str2) {
//判断目前的输入值是否有对应的计算结果(是不是已经存过了)
for (var i = 0; i < table.length; i++) {
if (table[i].str1 === str1 && table[i].str2 === str2) {
return table[i].result;
}
}
//没有存储结果
var newResult; //用于计算最终计算的结果
if (!str1 || !str2) newResult = "";// 其中有一个字符串没东西
else if (str1[0] === str2[0]) {
//开头相同
newResult = str1[0] + _LCS(str1.substr(1), str2.substr(1));
}
else {
var s1 = _LCS(str1, str2.substr(1));
var s2 = _LCS(str1.substr(1), str2);
if (s1.length < s2.length) {
newResult = s2;
}
else {
newResult = s1;
}
}
table.push({
str1: str1,
str2: str2,
result: newResult
})
return newResult;
}
var result = _LCS(str1, str2);
console.log(table)
return result;
}
var result = LCS("邓哥特有的贵族气质吸引了很多女孩", "邓哥喜欢吃秋葵和香菜,但是他的女朋友们不喜欢");
console.log(result);
----------------------------------思考题-------------------------------------
/**
*
* @param {*} total 找零总数 41
* @param {*} denos 数组,记录面额 [25, 20, 5, 1]
* @returns 数组,得到最优解 例如:[20, 20, 1],如果无解,返回false
*/
function 找零问题(total, denos) {
// 如果 denos数组长度为0,无解
// 把denos第一个面额拿出来,看看该面额是否和total相等,如果相等,直接放到最优解中
// 第一个面额比total大,这个面额不能找零,重新看剩下面额(分治)
// 第一个面额比total小,两种情况
// 1. 如果我找了这个面额的最优解
// 2. 我没有找这个面额的最优解
// 根据上面两种情况看哪种情况最优,取最优的
}