常见问题
- 什么是数据结构?
用于表示数据关系的结构
常见的数据结构有:数组、链表、树、图等 - 什么是算法?
根据已知数据得到结果数据的计算方法
常见的算法有:穷举法、分治法、贪心算法、动态规划
- 数据结构和算法有什么关系?
数据结构关心的是如何使用合适的结构存储数据
算法关心的是计算过程
有了相应的数据结构,免不了对这种数据结构的各种变化进行运算,所以,很多时候,某种数据结构都会自然而然的搭配不少算法。 - 数据结构和算法课程使用什么计算机语言?
数据结构和算法属于计算机基础课程,它们和具体的语言无关,用任何语言都可以实现。
本课程采用JavaScript语言。
- 配置要求
| 最低配置 | 推荐配置 | |
|---|---|---|
| 知识 | 变量、判断、循环、数组、函数、 构造函数、值类型和引用类型 | 除最低配置外,加上递归、作用域链、 执行上下文、执行栈 |
| 代码量 | 500行 | 3000行 |
| 练习量 | 3个效果 | 10个效果 |
递归知识回顾
递归(recursion)是函数式编程思想的产物,它使用数学函数的思想进行运算,只要在数学逻辑上是合理的,即代码中的函数一定合理
使用递归时,无须深究其运行过程!
斐波拉契数列的特点是:第1位和第2位固定为1,后面的位,其数字等于前两位之和,比如:
[1, 1, 2, 3, 5, 8, 13, 21, ......]
求斐波拉契数列第n位的值,n>0
如果使用函数f(n)来表示斐波拉契数列第n位的值,通过数学分析,可以轻松得到:
以上等式考虑到了所有情况,并且在数学逻辑上是合理的,因此,可以轻松书写到代码中:
f(1) = 1
f(2) = 1
f(n) = f(n-1) + f(n-2)
// 求斐波拉契数列第n位的值
function f(n){
if(n === 1 || n === 2){
return 1;
}
return f(n-1) + f(n-2);
}
1. 线性结构
线性结构是数据结构中的一种分类,用于表示一系列的元素形成的有序集合。
常见的线性结构包括:数组、链表、栈、队列
数组
特别注意:这里所说的数组是数据结构中的数组,和JS中的数组不一样
数组是一整块连续的内存空间,它由固定数量的元素组成,数组具有以下基本特征:
- 整个数组占用的内存空间是连续的
- 数组中元素的数量是固定的(不可增加也不可减少),创建数组时就必须指定其长度
- 每个元素占用的内存大小是完全一样的
根据数组的基本特征,我们可以推导出数组具有以下特点:
- 通过下标寻找对应的元素效率极高,因此遍历速度快
- 无法添加和删除数据,虽然可以通过某种算法完成类似操作,但会增加额外的内存开销或时间开销
- 如果数组需要的空间很大,可能一时无法找到足够大的连续内存
JS中的数组
在ES6之前,JS没有真正意义的数组,所谓的Array,实际上是一个对象。
ES6之后,出现真正的数组(类型化数组),但是由于只能存储数字,因此功能有限
目前来讲,JS语言只具备不完善的数组(类型化数组)
链表
为弥补数组的缺陷而出现的一种数据结构,它具有以下基本特征:
- 每个元素除了存储数据,需要有额外的内存存储一个引用(地址),来指向下一个元素
- 每个元素占用的内存空间并不要求是连续的
- 往往使用链表的第一个节点(根节点)来代表整个链表
根据链表的基本特征,我们可以推导出它具有以下特点:
- 长度是可变的,随时可以增加和删除元素
- 插入和删除元素的效率极高
- 由于要存储下一个元素的地址,会增加额外的内存开销
- 通过下标查询链表中的某个节点,效率很低,因此链表的下标遍历效率低
手动用代码实现链表
实际上,很多语言本身已经实现了链表,但链表作为一种基础的数据结构,通过手写代码实现链表,不仅可以锻炼程序思维和代码转换能力,对于后序的复杂数据结构的学习也是非常有帮助的。
因此,手写链表是学习数据结构和算法的一门基本功
手写一个链表结构,并完成一些链表的相关函数,要实现以下功能:
function Node(value) {
this.value = value;
this.next = null;
}
var a = new Node("a");
var b = new Node("b");
var c = new Node("c");
var d = new Node("d");
a.next = b;
b.next = c;
c.next = d;
- 遍历打印
function print(node){
var n = node
while(n) {
console.log(n.value)
n = n.next
}
}
// 打印整个链表
function print(node) {
// 打印自己
if (!node) {
return; //没有节点,无法打印
}
console.log(node.value);
// 打印下一个
print(node.next);
}
- 获取链表的长度
// 获取链表的长度
function getLength(node) {
if (!node) {
return 0; // 没有节点,长度为0
}
return 1 + getLength(node.next);
}
- 通过下标获取链表中的某个数据
// 通过下标获取链表中的某个数据
function getValue(node, index) {
// 根据当前节点和当前下标,得到对应的值
function _getValue(curNode, curIndex) {
if (!curNode) {
return null; // 没有找到
} else if (curIndex !== index) {
return _getValue(curNode.next, curIndex + 1);
} else {
// 下标相等
return curNode.value;
}
}
return _getValue(node, 0);
}
- 通过下标设置链表中的某个数据
// 通过下标设置链表中的某个数据
function setValue(node, index, value) {
// 设置当前节点的值
function _setValue(curNode, curIndex) {
if (!curNode) {
return; // 节点都没啦,停止
} else if (curIndex === index) {
// 找到了对应的节点
curNode.value = value;
} else {
_setValue(curNode.next, curIndex + 1);
}
}
_setValue(node, 0);
}
- 在链表末尾加入一个新节点
// 在链表末尾加入一个新节点
function add(node, newValue) {
if (node.next) {
// 当前节点还不是最后一个
add(node.next, newValue);
} else {
// 当前节点已经是最后一个
var newNode = new Node(newValue);
node.next = newNode;
}
}
- 在链表某一个节点之后加入一个新节点
// 在链表某一个节点之后加入一个新节点
function insert(node, insertValue, newValue) {
if (node.value === insertValue) {
// 当前节点之后要加入新节点
var newNode = new Node(newValue);
newNode.next = node.next;
node.next = newNode;
} else if (!node.next) {
// 当前节点也不是,同时已经到了末尾
var newNode = new Node(newValue);
node.next = newNode;
} else {
// 当前节点不是,同时又没有到末尾,直接看下一个
insert(node.next, insertValue, newValue);
}
}
- 删除一个链表节点
// 删除指定值的节点,不考虑链表中有重复的值,返回删除之后的链表的首节点
function remove(node, value) {
// 被删除值的前一个节点指向被删除值的后一个节点 node->next
// 处理特殊情况,只有1个节点
if(!node) { // 无节点
return null
} else if (node.value === value) {
// 删除头
return node.next
} else if(node.next && node.next.value === value) {
node.next = node.next.next
return node
} else {
node.next = remove(node.next, value)
return node
}
}
2. 排序和查找
排序算法
已知条件
var nums = [3, 8, 7, 9, 6, 5];
// 交换数组两个下标的值,很多排序算法都需要用
function swap(nums, i, j) {
var temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
- 选择排序 Selection Sort
// 选择排序
function selectionSort(nums) {
for (var i = 0; i < nums.length - 1; i++) {
// 在 i ~ nums.length - 1 范围内找到最小值所在的下标
var min = Infinity;
var index;
for (var j = i; j < nums.length; j++) {
if (nums[j] < min) {
min = nums[j];
index = j;
}
}
// 现在,找到了最小值的位置,保存到了变量index中
// 将index 和 i 交换
swap(nums, i, index);
}
}
// 我写的选择排序
function myselectionSort(nums){
for(var i = 0; i < nums.length-1; i++) {
// 初始化当前元素为此轮最小的
var minValue = nums[i],
minIndex = i;
for(var j = i; j < nums.length; j++) {
if (nums[j] < minValue) {
minValue = nums[j]
minIndex = j
}
}
if (minIndex !== i) {
swap(nums, i, minIndex)
}
}
return nums
}
- 冒泡排序 Bubble Sort
// 冒泡排序
function bubbleSort(nums) {
for (var i = 0; i < nums.length - 1; i++) {
// 依次看 0 ~ nums.length - 2 - i 范围内的数据,只要它比后面的大,就交换
for (var j = 0; j <= nums.length - 2 - i; j++) {
if (nums[j] > nums[j + 1]) {
swap(nums, j, j + 1);
}
}
}
return nums
}
function myBubbleSort(nums){
// 外循环共 nums.length-1次,因为每轮确定一个最大值(或最小值),只剩下一个的时候不需要了
for(var i = 0; i < nums.length - 1; i++){
// 每次内循环的次数为 nums.length-1-i
// 注意,最大的值在最后面,所以每次都得从头开始
for(var j = 0; j < nums.length-1-i; j++) {
if (nums[j] > nums[j+1]) {
swap(nums, j, j+1)
}
}
}
return nums
}
- 快速排序 Quick Sort
选择一个数(比如序列的最后一位)作为基准数,将整个序列排序成两部分,一部分比该数小,另一部分比该数大,基准数在中间,然后对剩余的序列做同样的事情,直到排序完成
问题被转换为了:如何对数组的某个区域进行快速排序
- 选择4作为基准数,排序成为:(3, 2) 4 (7, 6, 5)
- 对于3,2, 继续使用该方式排序,得到: (2, 3) 4 (7,6,5)
- 对于7,6,5,继续使用该方式排序,得到: (2, 3) 4 (5,6,7)
- 排序完成
// 快速排序
function quickSort(nums) {
// 在指定的下标范围内,做这种骚操作(以一个数为基础,小的靠左,大的靠右)
function _quickSort(start, end) {
if (start >= end || start < 0 || end > nums.length - 1) {
// 范围有问题
return;
}
var low = start, // 低位游标
high = end, // 高位游标
key = nums[end]; // 基准值
while (low < high) {
//1. 低位向高位移动
while (low < high && nums[low] <= key) {
low++;
}
nums[high] = nums[low]; //把当前不合理的数字扔到高位去
//2. 高位向低位移动
while (low < high && nums[high] >= key) {
high--;
}
nums[low] = nums[high];
}
// 高位低位重叠
nums[low] = key;
// 对左边的范围重来一次
_quickSort(start, low - 1);
// 对右边的范围重来一次
_quickSort(low + 1, end);
}
// _quickSort(0, nums.length - 1);
}
function myQuickSort(nums){
function _quickSort(start, end) {
if(start >= end || start < 0 || end > nums.length-1) {
return
}
var low = start,
high=end,
key = nums[end];
// key是用于比较的枢纽
while(low < high) {
//1. 低位向高位移动
while(low < high && nums[low] <= key){
low++;
}
nums[high] = nums[low]
//2. 高位向低位移动
while (low < high && nums[high] >= key) {
high--;
}
nums[low] = nums[high]
}
// 高低重叠
nums[low] = key
_quickSort(start, low-1)
_quickSort(low+1, end)
}
_quickSort(0, nums.length-1)
}
查询算法
- 顺序查找 Inorder Search
即普通的遍历,属于算法的穷举法,没啥好解释的
var nums = [5, 6, 7, 9, 9];
// 顺序查找
function inorderSearch(nums, target) {
for (var i = 0; i < nums.length; i++) {
if (nums[i] === target) {
// 找到了
return true;
}
}
return false;
}
- 二分查找 Binary Search
如果一个序列是一个排序好的序列,则使用二分查找可以极大的缩短查找时间
具体的做法是:
查找该序列中间未知的数据
- 相等,找到
- 要找的数据较大,则对后续部分的数据做同样的步骤
- 要找的数据较小,则对前面部分的数据做同样的步骤
var nums = [5, 6, 7, 9, 9];
// 针对排序好的数组,二分查找
function binarySearch(nums, target) {
var minIndex = 0, // 小的下标
maxIndex = nums.length - 1; // 大的下标
while (minIndex <= maxIndex) {
var mid = Math.floor((minIndex + maxIndex) / 2); // 中间下标
if (nums[mid] === target) {
return true; // 找到了
} else if (nums[mid] > target) {
maxIndex = mid - 1;
} else {
minIndex = mid + 1;
}
}
return false;
}
- 插值查找 Interpolation Search
插值查找是对二分查找的进一步改进
如果序列不仅是一个排序好的序列,而且序列的步长大致相同,使用插值查找会更快的找到目标。
插值查找基于如下假设:下标之间的距离比和数据之间的距离比大致相同
于是有
从而有
(target - a) / (g - a) ≈ (mid - minIndex) / (maxIndex - minIndex)
mid ≈ (target - a) / (g - a) * (maxIndex - minIndex) + minIndex
var nums = [5, 6, 7, 9, 9];
// 针对排序好的数组,插值查找
function interpolationSearch(nums, target) {
var minIndex = 0, // 小的下标
maxIndex = nums.length - 1; // 大的下标
while (minIndex < maxIndex) {
var mid = Math.floor(
((target - nums[minIndex]) / (nums[maxIndex] - nums[minIndex])) *
(maxIndex - minIndex) +
minIndex
);
// 防止左右横跳
if (mid < minIndex || mid > maxIndex) {
return false;
}
if (nums[mid] === target) {
return true; // 找到了
} else if (nums[mid] > target) {
maxIndex = mid - 1;
} else {
minIndex = mid + 1;
}
}
return false;
}
3. 树形结构
树
树是一个类似于链表的二维结构,每个节点可以指向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 = []; // 指向其他节点的数组
}
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.children.push(b, c);
b.children.push(d, e, f);
二叉树
如果一颗树的度为2,则该树是二叉树
二叉树可以用下面的代码表示
function Node(value){
this.value = value;
this.left = null;
this.right = null;
}
二叉树的相关算法
编写各种函数,实现下面的功能
- 对二叉树遍历打印
1.1 前(先)序遍历 DLR
// 前序遍历
function DLR(node) {
if (!node) {
return;
}
console.log(node.value); // 先自己
DLR(node.left);
DLR(node.right);
}
1.2 中序遍历 LDR
// 中序遍历
function LDR(node) {
if (!node) {
return;
}
LDR(node.left);
console.log(node.value);
LDR(node.right);
}
1.3 后序遍历 LRD
// 后序遍历
function LRD(node) {
if (!node) {
return;
}
LRD(node.left);
LRD(node.right);
console.log(node.value);
}
- 根据前序遍历和中序遍历结果,得到一颗二叉树
// 根据前序和中序遍历结果,得到一颗二叉树
function getTree(dlr, ldr) {
if (dlr.length === 0 && ldr.length === 0) {
// 没有前序和中序,没有节点
return null;
}
var rootValue = dlr[0]; // 通过前序遍历的第一个字符,得到根节点的value值
var root = new Node(rootValue); // 创建根节点
var rootIndex = ldr.indexOf(rootValue); // 根节点在中序遍历中的位置
var leftLDR = ldr.substring(0, rootIndex); // 左边的中序遍历
var leftDLR = dlr.substr(1, leftLDR.length); // 左边的前序
var rightLDR = ldr.substr(rootIndex + 1); // 右边的中序
var rightDLR = dlr.substr(leftDLR.length + 1); // 右边的前序
root.left = getTree(leftDLR, leftLDR);
root.right = getTree(rightDLR, rightLDR);
return root;
}
- 计算树的深度
// 计算树的深度
function getDeep(node) {
if (!node) {
return 0;
}
return 1 + Math.max(getDeep(node.left), getDeep(node.right));
}
-
查询二叉树
4.1 深度优先 Depth First Search
// 深度优先搜索
function deepSearch(node, target) {
if (!node) {
return false;
}
console.log(node.value);
if (node.value === target) {
//自己就是
return tru e; // 找到了
}
return deepSearch(node.left, target) || deepSearch(node.right, target);
}
4.2 广度优先 Breadth First Search
// 广度优先搜索
function breadthSearch(node, target) {
function _breadthSearch(nodes) {
if (nodes.length === 0) {
// 数组没东西
return false;
}
var nextLayer = []; // 下一层的节点
for (var i = 0; i < nodes.length; i++) {
console.log(nodes[i].value);
if (nodes[i].value === target) {
return true;
}
if (nodes[i].left) {
nextLayer.push(nodes[i].left);
}
if (nodes[i].right) {
nextLayer.push(nodes[i].right);
}
}
return _breadthSearch(nextLayer);
}
return _breadthSearch([node]);
}
- 比较两棵二叉树,得到它们的差异
// 对比两个树的差异,返回差异结果(数组)
function diff(node1, node2) {
var result = [];
if (!node1 && !node2) {
return result; // 两个节点都没有值,不可能有差异,返回空数组即可
}
if (!node1 && node2) {
result.push({ type: "新增", from: null, to: node2 });
return result; // 这种情况不需要继续往后比较了
}
if (node1 && !node2) {
result.push({ type: "删除", from: node1, to: null });
return result; // 这种情况不需要继续往后比较了
}
if (node1.value !== node2.value) {
result.push({ type: "修改", from: node1, to: node2 }); // 加入「修改」的变化
// 不能停止
}
var leftDiff = diff(node1.left, node2.left); // 左树的差异
var rightDiff = diff(node1.right, node2.right); // 右树的差异
// return [...result, ...leftDiff, ...rightDiff]
return result.concat(leftDiff).concat(rightDiff); // 合并差异
}
var node1 = getTree("ABDGECF", "GDBEAFC");
var node2 = getTree("AKDECFT", "DKEAFCT");
var result = diff(node1, node2);
console.log(result);
4. 图结构
图的概念
图结构中,一个结点可以链接到任意结点,所有结点链接而成的结构,即为图结构
图结构中的链接可以是有向的,也可以是无向的(双向链接),本文仅讨论双向链接
树结构是一种特殊的图结构
图结构没有根,可以有环,但是在一个图结构中,不能存在两个或以上的孤立结点
可以使用图中任何一个结点表示整个图结构
图结构是一种常见的数据结构,例如网络爬虫抓取的网页就是一种典型的图结构
图结构的代码可表示为:
function Node(value){
this.value = value;
this.neighbors = [];
}
相关算法
查询算法
- 和树结构一样,图结构的查询也可以分为深度优先(Depth First Search)和广度优先(Breadth First Search)查询
深度优先
function Node(value) {
this.value = value;
this.neighbors = [];
}
// 图搜:深度优先
function deepSearch(node, target) {
var found = []; // 已经找过的节点
function _deepSearch(node) {
// 该函数内部无论进行多少次递归,使用的都是同一个found变量
if (found.includes(node)) {
// 如果该节点已经找过
return false;
}
if (node.value === target) {
return true; // 找到了
}
found.push(node); // 这个节点不是,已经找过了
for (var i = 0; i < node.neighbors.length; i++) {
var nextNode = node.neighbors[i];
if (_deepSearch(nextNode)) {
// 通过邻居找到了
return true;
}
}
return false; // 所有的邻居都不是
}
return _deepSearch(node);
}
广度优先
// 图搜:广度优先
function breadthSearch(node, target) {
var found = []; // 已经找过的
// 必须保证:nodes参数中的节点都是没有找过的
function _breadthSearch(nodes) {
if (nodes.length === 0) {
// 这一层已经没有任何节点了
return false;
}
var next = []; // 下一层的节点数组
for (var i = 0; i < nodes.length; i++) {
var n = nodes[i]; //当前找的节点是n
if (n.value === target) {
return true;
}
found.push(n); //这个节点不是,加入到已找节点中
// n不是,得把n的邻居加入下一层的节点数组中
for (var j = 0; j < n.neighbors.length; j++) {
if (!next.includes(n.neighbors[j])) {
// 该判断是为了保证next数组中没有重复的
next.push(n.neighbors[j]); // 把n的这个邻居加入到下一层
}
}
}
// 这一层已经看完了,found已经更新了
// 此时,再来去掉next中已经找过的节点
for (var i = 0; i < next.length; i++) {
if (found.includes(next[i])) {
next.splice(i, 1);
i--;
}
}
return _breadthSearch(next);
}
return _breadthSearch([node]);
}
最小生成树算法
- 如果一个图中结点连接而成的边具备某种数值,需要将这些边进行精简,生成一个连接全节点同时总边长最小的树结构,该树称之为最小生成树
实现最小生成树可以使用Prim算法,从任意一个点出发,连接到该点最短的点,组成一个部落,然后继续连接到该部落最短的点,直到把所有点连接完成
var nodes = [
new Node("a"),
new Node("b"),
new Node("c"),
new Node("d"),
new Node("e"),
]; // 节点的数组
var sides = [
[0, 8, 3, Infinity, 4],
[8, 0, 4, 10, Infinity],
[3, 4, 0, 3, Infinity],
[Infinity, 10, 3, 0, 6],
[4, Infinity, Infinity, 6, 0],
];
// 根据边的情况,将nodes中的节点连起来
function Prim(nodes, sides) {
if (nodes.length <= 1) {
return;
}
var hord = [nodes[0]]; // 部落(部落中的每一个村庄是连通的),初始只有一个村庄
while (hord.length < nodes.length) {
// 说明还有村庄没有加入到部落
// 继续向部落中添加村庄
connectToHord();
}
// 从非部落节点中,找到一个最短距离的节点,连接到部落
function connectToHord() {
var result = {
from: null,
to: null,
dis: Infinity,
};
for (var i = 0; i < nodes.length; i++) {
var n = nodes[i];
if (!hord.includes(n)) {
// 不在部落中的节点
var info = getMinDistance(n); // 看一下这个点到部落的情况
if (info.dis < result.dis) {
// 发现了有更短的节点
result = info;
}
}
}
// result一定是最优的点
result.from.neighbors.push(result.to);
result.to.neighbors.push(result.from);
// 加入部落
hord.push(result.from);
}
// 得到node到部落最小的距离,以及连接到部落的哪个点
function getMinDistance(node) {
var result = {
from: node,
to: null,
dis: Infinity,
};
for (var i = 0; i < hord.length; i++) {
var dis = getDistance(node, hord[i]);
if (dis < result.dis) {
// 有更小的距离
result.to = hord[i];
result.dis = dis;
}
}
return result;
}
// 得到两个节点之间的距离
function getDistance(node1, node2) {
if (node1 === node2) {
// 不能得到自己到自己的距离
throw new Error("不能得到自己到自己的距离");
}
var row = nodes.indexOf(node1);
var col = nodes.indexOf(node2);
return sides[row][col];
}
}
Prim(nodes, sides);
5. 贪心算法和动态规划
贪心算法
面试题:找零问题
示例:假设你有一间小店,需要找给客户46分钱的硬币,你的货柜里只有面额为25分、10分、5分、1分的硬币,如何找零才能保证数额正确并且硬币数最小
贪心算法:当遇到一个求解全局最优解问题时,如果可以将全局问题切分为小的局部问题,并寻求局部最优解,局部最优解累计的结果即全局最优解
某些情况下,局部最优解累计的结果并不一定是全局最优解
// 找零问题
// total:找零总数
// denos:目前的面额
function exchange(total, denos) {
var result = []; // 存放找零结果的数组
while (total > 0) {
// 还有钱需要找,得继续找零
// 找出面额中最大的能找的面额
var max = -Infinity;
for (var i = 0; i < denos.length; i++) {
if (denos[i] > max && denos[i] <= total) {
max = denos[i];
}
}
result.push(max); // 结果加入该面额
total -= max; // 找零总数减少
}
return result;
}
var result = exchange(76, [50, 35, 10, 5, 1]);
console.log(result);
// 输出
[50, 10, 10, 5, 1]
现实生活中没有这种面额
但是此时最优解为:[35, 35, 5, 1]
动态规划
动态规划:一个求全局最优解的问题,把它分解为多个重复子问题,对重复子问题求解,同时缓存那些重复子过程
面试题1:青蛙跳台阶问题
有N级台阶,一只青蛙每次可以跳1级或两级,一共有多少种跳法可以跳完台阶?
方法1
var count = 0;
function jump(n) {
var cache = []; // 缓存n对应的结果 cache[n] = 结果
function _jump(n) {
if (n === 1) {
return 1;
}
if (n === 2) {
return 2;
}
//看缓存中有没有记录
if (cache[n]) {
return cache[n]; // 有缓存,直接返回
}
count++; // 测试,计算次数+1
console.log("当前在正运算的n的值:", n);
var result = _jump(n - 1) + _jump(n - 2);
cache[n] = result; // 把结果缓存起来,以供将来使用
return result;
}
var r = _jump(n);
console.log(cache);
return r;
}
方法2
function jump(n) {
first = 1
second = 2
third = 0
if (n === 1) {
return first;
}
if (n === 2) {
return second;
}
for(var i = 0; i < n-2; i++) {
third = second + first
first = second
second = third
}
return third
}
面试题2:最长公共子序列问题(LCS)
有的时候,我们需要比较两个字符串的相似程度,通常就是比较两个字符串有多少相同的公共子序列
公共子序列不要求连续
例如有两个字符串
- LCS算法可以很好的计算出两篇论文的相似度
- 利用LCS,可以计算出搜索关键字与搜索结果的相似度
以上两个字符串的最长公共子序列为:LCS可以计算出的相似度
// 得到两个字符串的最长公共子序列
function LCS(str1, str2) {
var cache = {}; // i-j: 结果
// i: 指向第一个字符串中的某个位置
// j: 指向第二个字符串中的某个位置
function _lcs(i, j) {
// 看这两个位置如何处理
if (i >= str1.length || j >= str2.length) {
// i或j已经超出了范围
return "";
}
var cacheKey = i + "-" + j; //缓存的属性名字
if (cache[cacheKey]) {
// 有缓存
return cache[cacheKey];
}
var result; // 计算结果
if (str1[i] === str2[j]) {
// 当前这个字符,一定进入最长公共子序列
result = str1[i] + _lcs(i + 1, j + 1);
} else {
// i不动
var lcs1 = _lcs(i, j + 1);
// j不动
var lcs2 = _lcs(i + 1, j);
result = lcs1.length > lcs2.length ? lcs1 : lcs2;
}
//缓存结果
cache[cacheKey] = result;
return result;
}
return _lcs(0, 0);
}
var result = LCS(
"LCS算法可以很好的计算出两篇论文的相似度",
"利用LCS,可以计算出搜索关键字与搜索结果的相似度"
);
console.log(result);