在现代Web开发中,JavaScript 是不可或缺的编程语言。掌握其核心功能和原理对于开发者至关重要。本文通过手写实现JavaScript的一些关键功能和算法,帮助读者深入理解其工作原理,提升编程技能。无论你是初学者还是有经验的开发者,都能从中受益
8、斐波那契数列
方法一:普通递归
代码优美逻辑清晰。但是有重复计算的问题,如:当n为5的时候要计算fibonacci(4) + fibonacci(3),当n为4的要计算fibonacci(3) + fibonacci(2) ,这时fibonacci(3)就是重复计算了。运行 fibonacci(50) 会出现浏览器假死现象,毕竟递归需要堆栈,数字过大内存不够。
function fibonacci(n) {
if (n == 1 || n == 2) {
return 1
};
return fibonacci(n - 2) + fibonacci(n - 1);
}
fibonacci(30)
方法二:改进递归-把前两位数字做成参数避免重复计算
function fibonacci(n) {
function fib(n, v1, v2) {
if (n == 1)
return v1;
if (n == 2)
return v2;
else
return fib(n - 1, v2, v1 + v2)
}
return fib(n, 1, 1)
}
fibonacci(30)
方法三:改进递归-利用闭包特性把运算结果存储在数组里,避免重复计算
var fibonacci = function () {
let memo = [0, 1];
let fib = function (n) {
if (memo[n] == undefined) {
memo[n] = fib(n - 2) + fib(n - 1)
}
return memo[n]
}
return fib;
}()
fibonacci(30)
方法三1:改进递归-摘出存储计算结果的功能函数
var memoizer = function (func) {
let memo = [];
return function (n) {
if (memo[n] == undefined) {
memo[n] = func(n)
}
return memo[n]
}
};
var fibonacci=memoizer(function(n){
if (n == 1 || n == 2) {
return 1
};
return fibonacci(n - 2) + fibonacci(n - 1);
})
fibonacci(30)
循环
方法一:普通for循环
function fibonacci(n) {
var n1 = 1, n2 = 1, sum;
for (let i = 2; i < n; i++) {
sum = n1 + n2
n1 = n2
n2 = sum
}
return sum
}
fibonacci(30)
方法二:for循环+解构赋值
var fibonacci = function (n) {
let n1 = 1; n2 = 1;
for (let i = 2; i < n; i++) {
[n1, n2] = [n2, n1 + n2]
}
return n2
}
fibonacci(30)
9、汉诺塔问题
经典的汉诺塔问题,将x塔座上n个从大到小的盘子移动到z塔座上,要求大盘子不能放在小盘子上面,可以借助y塔座,问最多需要多少次。
分析:当n=1时,可以直接将盘子从x塔座移动到z塔座; 当n>1时,要想把第n个盘子从x塔座移动到z塔座,则需要借助y塔座,即先将1~n-1盘子从x塔座移动到y塔座。然后再把第n个盘子从x塔座移动到z塔座,最后再把y塔座上1到n-1的盘子移动到z塔座上,就完成了。
那么如何将1到n-1的盘子从x移动到y,思路同上,即递归。
let c=0;
let move=function(n,x,y,z){
let director=function(x,z,n){
c=c+1;
console.log("第"+ c +"次移动,将第"+n+"个盘子从"+x+"移动到"+z);
}
if(n==1){
director(x,z,1);
}else{
move(n-1,x,z,y);
director(x,z,n);
move(n-1,y,x,z);
}
}
move(3,"x","y","z");
10、合并两个 有序数组
这道题是我在腾讯面试的时候被问到的,当时的回答实在难以令人满意。这道题本来也不难,然后我就一步步尝试性地回答推进,首先,可以直接用数组方法concat(),当合并后数组并不关心大小排序时。接下来是,考虑合并后数组有序,这也是不难实现的,下面贴代码。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>合并两个有序数组</title>
<!-- 经常用到的数组操作方法说明:slice和splice方法参数必须是数字,不能是字母或代数式 。
slice方法并不修改原数组,如果想删除数组中的一段元素,应该使用splice方法 -->
</head>
<body>
<script type="text/javascript">
//方法一:一种鸡肋方法,合并两个数组,然后调用sort方法
/*function merge(nums1, nums2) {
for(var i=0;i<nums2.length;i++){
nums1.push(nums2[i])
}
nums1.sort(function(a,b){//排序参数设置,实现从小到大排序
return a-b;
});
return nums1;
};*/
//方法二:
function mergeArray(arr1,arr2){
var ind1=0; //标记arr1的对比元素的初始索引值
var ind2=0; //标记arr2的对比元素的初始索引值
var arr=[]; //作为输出的新数组
while(ind1<arr1.length && ind2<arr2.length){
//当arr1和arr2元素均未全部存入arr中,则从数组第一个元素开始进行比较,将较小的元素存入arr中
if(arr1[ind1]<=arr2[ind2]){
arr.push(arr1.slice(ind1)[0]); //若arr1元素小于arr2元素,则将arr1的元素存入arr中
ind1++;//已将元素push到输出数组中,将数组arr1的index指向移动到下一个
}else{
arr.push(arr2.slice(ind2)[0]);
ind2++;
}
}
//当不满足上述while条件(ind1<arr1.length && ind2<arr2.length)时,就直接将剩余数组元素拼接在输出数组arr后面
return arr.concat((ind1<arr1.length)?arr1.slice(ind1):arr2.slice(ind2));//这个地方也可以分开写
}
var nums1=[1,2,3];
var nums2=[2,5,6];
//var arr=merge(nums1,nums2);
//console.log(arr);
console.log(mergeArray(nums1,nums2));//[1, 2, 2, 3, 5, 6]
</script>
</body>
</html>
11、数组中重复的数字
题目 在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。 思路 利用对象或者散列表或者下标检测
// 推荐解法
function duplicate(numbers, duplication)
{
// write code here
//这里要特别注意~找到任意重复的一个值并赋值到duplication[0]
//函数返回True/False
let hash = []
for (let i = 0; i < numbers.length; i++) {
if (!hash[numbers[i]]) {
hash[numbers[i]] = 1
} else {
if (++hash[numbers[i]] === 2) {
duplication[0] = numbers[i]
return true
}
}
}
return false
}
// 解法1:下标检测
function duplicate(numbers, duplication)
{
// write code here
//这里要特别注意~找到任意重复的一个值并赋值到duplication[0]
//函数返回True/False
let obj = {}
for(let i = 0; i < numbers.length; i++) {
if(numbers.indexOf(numbers[i]) !== i) {
duplication[0] = numbers[i]
return true
}
}
return false
}
// 解法2:对象特性
function duplicate(numbers, duplication)
{
// write code here
//这里要特别注意~找到任意重复的一个值并赋值到duplication[0]
//函数返回True/False
let obj = {}
for (let i = 0; i < numbers.length; i++) {
if (!obj[numbers[i]]) {
obj[numbers[i]] = true
} else {
duplication[0] = numbers[i]
return true
}
}
return false
}
// 解法3:哈希散列法,其实原理和obj差不多
function duplicate(numbers, duplication)
{
// write code here
//这里要特别注意~找到任意重复的一个值并赋值到duplication[0]
//函数返回True/False
let hash = []
for (let i = 0; i < numbers.length; i++) {
if (!hash[numbers[i]]) {
hash[numbers[i]] = 1
} else {
if (++hash[numbers[i]] === 2) {
duplication[0] = numbers[i]
return true
}
}
}
return false
}
12、两个数组的交集 ,并集,补集,差集
一、简单数组 1、ES5:
const arr1 = [1,2,3,4,5],
arr2 = [5,6,7,8,9];
// 交集
let intersection = arr1.filter(function (val) { return arr2.indexOf(val) > -1 })
// 并集
let union = arr1.concat(arr2.filter(function (val) { return !(arr1.indexOf(val) > -1) }))
// 补集 两个数组各自没有的集合
let complement = arr1.filter(function (val) { return !(arr2.indexOf(val) > -1) })
.concat(arr2.filter(function (val) { return !(arr1.indexOf(val) > -1) }))
// 差集 数组arr1相对于arr2所没有的
let diff = arr1.filter(function (val) { return arr2.indexOf(val) === -1 })
console.log('arr1: ', arr1);
console.log('arr2: ', arr2);
console.log('交集', intersection);
console.log('并集', union);
console.log('补集', complement);
console.log('差集', diff);
12345678910111213141516171819202122
ES6:
const arr1 = [1,2,3,4,5],
arr2 = [5,6,7,8,9],
_arr1Set = new Set(arr1),
_arr2Set = new Set(arr2);
// 交集
let intersection = arr1.filter(item => _arr2Set.has(item))
// 并集
let union = Array.from(new Set([...arr1, ...arr2]))
// 补集 两个数组各自没有的集合
let complement = [...arr1.filter(item => !_arr2Set.has(item)), ...arr2.filter(item => !_arr1Set.has(item))]
// 差集 数组arr1相对于arr2所没有的
let diff = arr1.filter(item => !_arr2Set.has(item))
console.log('arr1: ', arr1);
console.log('arr2: ', arr2);
console.log('交集', intersection);
console.log('并集', union);
console.log('补集', complement);
console.log('差集', diff);
1234567891011121314151617181920212223
JQ:
const arr1 = [1,2,3,4,5],
arr2 = [5,6,7,8,9];
// 交集
let intersection = $(arr1).filter(arr2).toArray();
// 并集
let union = $.unique(arr1.concat(arr2))
// 补集 两个数组各自没有的集合
let complement = $(arr1).not(arr2).toArray().concat($(arr2).not(arr1).toArray())
// 差集 数组arr1相对于arr2所没有的
let diff = $(arr1).not(arr2).toArray()
console.log('arr1: ', arr1);
console.log('arr2: ', arr2);
console.log('交集', intersection);
console.log('并集', union);
console.log('补集', complement);
console.log('差集', diff);
123456789101112131415161718192021
二、对象数组
// 形如如下数组
let arr1 = [], arr2 = [];
arr1 = [
{
ID: 1,
Name: 1,
desc: 'Number'
},
{
ID: 2,
Name: 2,
desc: 'Number'
},
{
ID: 3,
Name: 3,
desc: 'Number'
},
{
ID: 4,
Name: 4,
desc: 'Number'
},
{
ID: 5,
Name: 5,
desc: 'Number'
}
]
arr2 = [
{
ID: 5,
Name: 5,
desc: 'Number'
},
{
ID: 6,
Name: 6,
desc: 'Number'
},
{
ID: 7,
Name: 7,
desc: 'Number'
},
{
ID: 8,
Name: 8,
desc: 'Number'
},
{
ID: 9,
Name: 9,
desc: 'Number'
}
]
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
// 交集
let intersection = []
for (let i = 0, len = arr1.length; i < len; i++) {
for (let j = 0, length = arr2.length; j < length; j++) {
if (arr1[i].ID === arr2[j].ID) {
intersection.push(arr1[i])
}
}
}
console.log('交集', intersection)
// 并集
let union = [...arr1, ...arr2]
for (let i = 0, len = arr1.length; i < len; i++ ) {
for (let j = 0, length = arr2.length; j < length; j++) {
if (arr1[i].ID === arr2[j].ID) {
union.splice(union.findIndex(item => item.ID === arr1[i].ID), 1)
}
}
}
console.log('并集', union)
// 补集
let complement = [...arr1, ...arr2]
for (let i = 0, len = arr1.length; i < len; i++ ) {
for (let j = 0, length = arr2.length; j < length; j++) {
if (arr1[i].ID === arr2[j].ID) {
complement.splice(complement.findIndex(item => item.ID === arr1[i].ID), 1)
complement.splice(complement.findIndex(item => item.ID === arr2[j].ID), 1)
}
}
}
console.log('补集', complement)
// 差集
let diff = [...arr1]
for (let i = 0, len = arr1.length; i < len; i++ ) {
let flag = false
for (let j = 0, length = arr2.length; j < length; j++) {
if (arr1[i].ID === arr2[j].ID) {
flag = true
}
}
if (flag) {
diff.splice(diff.findIndex(item => item.ID === arr1[i].ID), 1)
}
}
console.log('差集', diff)
13、旋转数组
题目: 给定一个数组,将数组中的元素向右移动k个位置,其中k是非负数。 输入:[1,2,3,4,5,6,7] 和 k=3 输出:[5,6,7,1,2,3,4]
解释: 向右旋转1步:[7,1,2,3,4,5,6] 向右旋转2步:[6,7,1,2,3,4,5] 向右旋转3步:[5,6,7,1,2,3,4]
解题思路: 旋转数组实际上就是把数组的数字向后移动k位,末位的数字自动填充到前面的位置。
方法一: 可以看作是把后面的k位数字直接截取出来(this.slice(-k)),与前面的数字的数组(this.slice(0,this.length - k)拼接即可。
slice() 方法可从已有的数组中返回选定的元素。 slice() 参数使用负值则表示从数组的尾部选取元素。 slice()传start和end参数,则表示返回一个新的数组,包含从 start 到 end (不包括该元素)的 arrayObject 中的元素。 concat() 方法用于连接两个或多个数组。
function rotate(k) {
if(k < 0 || k === 0 || k === this.length) {
return this;
}
//旋转数组-方法1
return this.slice(-k).concat(this.slice(0,this.length - k));
}
Array.prototype.rotate = rotate;
let arr = [1,2,3,4,5,6,7];
console.log(arr.rotate(3));
运行结果如图:
方法二: 思路同方法1一样。使用扩展运算符…
splice() 方法向/从数组中添加/删除项目,然后返回被删除的项目。(该方法会改变原始数组) splice() 方法参数index,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置。 扩展运算符… 把数组或类数组对象展开成一系列用逗号隔开的值
function rotate(k) {
if(k < 0 || k === 0 || k === this.length) {
return this;
}
//旋转数组-方法2
return [...this.splice(this.length - k),...this]
}
Array.prototype.rotate = rotate;
let arr = [1,2,3,4,5,6,7];
console.log(arr.rotate(3));
运行结果如图
方法三: 遍历数组,执行k次删除数组的最后一个元素,并将返回的元素加在数组的头部。
备注: pop() 方法用于删除并返回数组的最后一个元素。 unshift() 方法可向数组的开头添加一个或更多元素,并返回新的长度。
function rotate(k) {
if(k < 0 || k === 0 || k === this.length) {
return this;
}
//旋转数组-方法3
for(let i = 0;i < k;i ++) {
this.unshift(this.pop());
}
return this;
}
Array.prototype.rotate = rotate;
let arr = [1,2,3,4,5,6,7];
console.log(arr.rotate(3));
运行结果如图
方法四: 创建一个k位的空数组,遍历空数组,删除原数组的最后一个元素,并将返回的元素加在数组的头部。(思路同方法3)
new Array(k).fill(’’)表示初始化一个长度为k的空数组,并用fill()传的指定的参数value填充空数组。
function rotate(k) {
if(k < 0 || k === 0 || k === this.length) {
return this;
}
//旋转数组-方法4
new Array(k).fill('').forEach(()=> {
this.unshift(this.pop());
})
return this;
}
Array.prototype.rotate = rotate;
let arr = [1,2,3,4,5,6,7];
console.log(arr.rotate(3));
运行结果如图
14、两数之和
题目如下:
给定一个整数数组和一个目标值,找出数组中和为目标值的两个数。 你可以假设每个输入只对应一种答案,且同样的元素不能被重复利用。
示例
给定 nums = [2, 7, 11, 15], target = 9 因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1]
leetCood地址:两数之和
题目不难理解,首先一上来我们马上就能想到使用两个循环解决的办法。
遍历所有组合,找到符合要求的组合就行。
双层循环
代码如下:
const twoSum = (nums, target) => {
let arr = [];
for(let i = 0; i < nums.length; i++) {
for(let j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] === target) {
arr = [i, j];
break;
}
}
}
return arr;
};
两个循环,时间复杂度为O(N*N)。
leetCode测试数据如下:
我的提交执行用时已经战胜 17.42 %
的 javascript 提交记录
在第二个循环中,我们只是要寻找目标值是否在数组里,因此可想到javascript中的一个方法----indexOf
。
indexOf 用法
代码可改写如下:
const twoSum = (nums, target) => {
let a, b;
for(let i = 0; i < nums.length; i++) {
b = nums.indexOf(target - nums[i]);
if(b > -1 && b !== i) {
a = i;
break;
}
}
return [a, b];
};
但是 Array
的 indexOf
方法实际上是对数组再遍历一次,虽然在写法上有优化,但是实际时间复杂度还是O(N*N)。
在时间复杂度上做优化,我们需要解决一个问题,如何快速地在数组中检索值?使用 indexOf
其实已经在数据中检索特定值的思路上了。只不过 indexOf
内部还是对数组进行循环检索,因此并没有达到更快的要求。在这方面, hash表
可以帮助到我们。
什么意思呢?比如我们有一个对象 obj = { ..., a: 1}
,当我们取值 Obj.a
时,是个直接寻址的过程,因此效率是很高的。回到题目,在这个思路上改进我们的代码:
使用对象索引
const twoSum = (nums, target) => {
let mapObj = {};
let res = [];
nums.forEach((e, i) => mapObj[e] = i);
for(let i=0;i<nums.length;i++) {
let j = mapObj[targer - nums[i]];
if(j && j !== i) {
res = [i, j];
break;
}
}
return res;
};
我们创建一个对象,并给它赋值,对象的键值是我们想要检索的值,对象的值是在数组中的索引。
虽然多了创建对象这一过程,但是我们少了一层循环。
然后我们来看执行效率:
我的提交执行用时已经战胜 86.24 %
的 javascript 提交记录。
从 17.42 %
到 86.24 %
,可以说是个质的飞跃。
es6
中,给我们提供了一个新对象 Map
,在这里就可以拍上用途。
使用 map
const twoSum = (nums, target) => {
let map = new Map();
let res = [];
nums.forEach((e, i) => map.set(e, i));
for(let i=0;i<nums.length;i++) {
let j = map.get[targer - nums[i]];
if(j && j !== i) {
res = [i, j];
break;
}
}
return res;
};
最终使用 Map
优化的代码,让我们战胜了 97.21 %
的提交。