JavaScript笔试题(js高级代码片段)

目录

  • 1、哈希表+计数类型
    • 1.1、判断是否存在重复元素
    • 1.2、字符串中的第一个唯一字符
    • 1.3、有效的字母异位词
    • 1.4、多数元素
    • 1.5、只出现一次的数字
    • 1.6、位1的个数
  • 2、哈希表+映射功能
    • 2.1、两数之和
    • 2.2、两数组交集1
  • 3、找规律题
    • 3.1、罗马数字转整数
    • 3.2、最长公共前缀
    • 3.3、合并两个有序链表
    • 3.4、实现 str()
    • 3.5、杨辉三角
    • 3.6、买卖股票的最佳时机
    • 3.7、买卖股票的最佳时机 2
    • 3.8、反转链表
  • 4、双指针
    • 4.1、删除数组中的重复项
    • 4.2、合并两个有序数组
    • 4.3、验证回文串
    • 4.4、回文链表
    • 4.5、删除链表中的节点
    • 4.6、移动零
    • 4.7、反转字符串
    • 4.8、两数组交集2
  • 5、二叉树(DFS)
    • 5.1、二叉树前中后遍历套路详解
      • 5.5.1、前序遍历
      • 5.1.2、中序遍历是一个意思,在前序遍历的基础上改造一下图片
      • 5.1.3、后序遍历有点不太一样,但是套路是一样的,我们需要先遍历右子树,再遍历左子树,反着来,就可以了
    • 5.2、对称二叉树
      • 5.2.1、二叉树的最大深度
      • 5.2.2、将有序数组转化为二叉搜索树
  • 6、栈
      • 6.1、有效的括号
      • 6.2、最小栈
    • 7、动态规划
      • 7.1、最大子序和
      • 7.2、爬楼梯
    • 8、数学问题
      • 8.1、加一
      • 8.2、x的平方根
      • 8.3、Excel表序列号
      • 8.4、阶乘中的零
      • 8.5、颠倒二进制位
      • 8.6、丢失的数字
      • 8.7、3的幂
      • 8.8、Fizz Buzz
    • 9、环问题
      • 9.1、环形链表
      • 9.2、相交链表
      • 9.3、快乐数


1、哈希表+计数类型

1.1、判断是否存在重复元素

给定一个整数数组,判断是否存在重复元素。如果存在一值在数组中出现至少两次,函数返回 true 。如果数组中每个元素都不相同,则返回 false 。

解决思路
遍历数组时,经过数组中的每一项就往 map 中添加,比如 [1, 2, 3, 1]。
第一项:遍历到第一个1时,对象返回 { 1: 1 },代表 1 出现 1 次。
第二项:遍历到 2 时,返回 { 1: 1, 2: 1 }。
第三项:遍历到 3 时,返回 { 1: 1, 2: 1, 3: 1 }。
第四项:遍历到第二个 1 时,发现原来的对象里已经有 1 了,返回 false。

const containsDuplicate = function(nums) {
	let map = new Map();
    for (let i of nums) {
		if (map.has(i)) {
			return true;
        } else {
            map.set(i, 1);
        }
    }
    return false;
};
console.log(containsDuplicate([2, 6, 3, 9, 3])); // true
console.log(containsDuplicate([2, 6, 3, 9, 7])); // false

1.2、字符串中的第一个唯一字符

给定一个字符串,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1。

解决思路
遍历字符串,用一个对象 {} 来记数,出现过一次就 +1,遍历完毕,再次遍历字符串,看它们在之前记录的对象里的值,是否是 1,是就返回下标,不是返回 -1。

var firstUniqChar = function(str) {
	const map = {};
	for (let v of str) map[v] = (map[v] || 0) + 1;
	for (let i = 0; i < str.length; i++) if (map[str[i]] === 1) return i;
	return -1;
};
console.log(firstUniqChar("leetcode")); // 0
console.log(firstUniqChar("loveleetcode")); // 2

1.3、有效的字母异位词

给定两个字符串 s 和 t,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。

解决思路
声明计数器,一个对象 const obj = {}。遍历 s 字符串,如果遍历到字符串的 ‘a’ 字母,去看 obj[a] 是否存在,不存在说明第一次遍历到 a 字母,那么初始化 obj[a] = 1,如果存在则 obj[a] += 1。t 字符串同理,它每次减 1,遍历完 s 字符串后,遍历 obj 对象,看它的每一对 key: value,是否 value 都是 0。

var isAnagram = function (s, t) {
	const sLen = s.length;
	const tLen = t.length;
	if (sLen !== tLen ) {
		return false;
	}
	const obj = {};
	for (let i = 0 ; i < sLen ; i++) {
		const currentS = s[i];
		const currentT = t[i];
		obj[currentS] ? obj[currentS]++ : obj[currentS] = 1;
		obj[currentT] ? obj[currentT]-- : obj[currentT] = -1;
	}
	return Object.values(obj).every(v => v === 0);
};
console.log(isAnagram('anagram', 'nagaram')); // true
console.log(isAnagram('rat', 'car')); // false

1.4、多数元素

给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 n/2 的元素。可以假设数组是非空的,并且给定的数组总是存在多数元素。

解决思路
1、声明一个计数器,也就是一个对象 const map = {}。
2、遍历字符串,开始记数,如果字符串的字母第一次碰见,map[第一次碰见的字母] = 1。
3、如果 map 已经记录过这个字母,则 map[记录过的的字母] += 1。
4、在遍历的过程中,看 map[记录过的的字母] 是否大于数组总长度/2。

var majorityElement = function (nums) {
	const map = {};
	const n = nums.length >> 1; // >>右移运算符,意思是除以2
	for (let i = 0; i < nums.length; i++) {
		map[nums[i]] = map[nums[i]] !== undefined ? map[nums[i]] + 1 : 1;
		if(map[nums[i]] > n) return nums[i];
	}
};
console.log(majorityElement([3, 2, 3])); // 3
console.log(majorityElement([2, 2, 1, 1, 1, 2, 2])); // 2

1.5、只出现一次的数字

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现一次的元素。

算法应该具有线性时间复杂度,可以不使用额外空间来实现。

解决思路一
用 map 记录一遍,类似这样的代码,最后再遍历一次 countMap,然后看谁的次数是 1,就解决了。

const countMap = {};
array.forEach((item) => { 
	countMap[item] ? countMap[item] += 1 : countMap[item] = 1;
});

解决思路二
用异或运算符,先看异或运算符有啥用,异或运算符 ^,了解一下这个运算符的功能。
1、任何数和自己做异或运算,结果为 0,即 a^a = 0。
2、任何数和 0 做异或运算,结果还是自己,即 a^0 = a。
3、异或运算中,满足交换律和结合律,也就是 a^b^a = b^a^a = b^(a^a) = b^0 = b。

var singleNumber = function (nums) {
	let init = nums[0],
		i = 1;
	for (; i < nums.length; i++) {
		init ^=  nums[i];
	}
	return init;
};
console.log(singleNumber([2, 2, 1])); // 1
console.log(singleNumber([4,1,2,1,2])); // 4

1.6、位1的个数

编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 1 的个数(也被称为汉明重量)。

解决思路
计算个数,按照思路,把整个数字转为字符串,类似这样,数字 0001 => String(0001) => ‘0001’ => 遍历看 1 的个数。然后直接遍历计算就可以了。

var hammingWeight = function (n) {
	let ret = 0;
    while (n) {
        n &= (n - 1);
        ret++;
    }
    return ret;
};
console.log(hammingWeight(00000000000000000000000000001011)); // 3
console.log(hammingWeight(00000000000000000000000010000000)); // 1

原理

每执行一次 x = x & (x-1),会将 x 用二进制表示时最右边的一个 1 变为 0,因为 x-1 会将该位(x 用二进制表示时最右边的一个 1)变为 0。因此,对 x 重复该操作,直到 x 变成 0,则操作次数即为 x 的二进制数中的 1 的数目。


2、哈希表+映射功能

哈希表有一个非常常见的功能就是建立映射关系,比如说设计模式里的策略模式,思路是一样的,映射表常常见于后端的枚举类型,typescript 也是一样。

示例

// 后端只会返回 0, 1, 2
const TYPE = {
	2: 'orange',
	1: 'red',
	0: 'blue'
};
// 前端使用 
console.log(TYPE[0]); // orange
console.log(TYPE[1]); // red
console.log(TYPE[2]); // blue

2.1、两数之和

给定一个整数数组 nums 和一个整数目标值 target,请在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。

可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。可以按任意顺序返回答案。

解决思路
用 hashMap 存储遍历过的元素和对应的索引。每遍历一个元素,看看 hashMap 中是否存在满足要求的目标数字。所有事情在一次遍历中完成(用了空间换取时间)。

var twoSum = function (nums, target) {
	const map = new Map();
    for (let i = 0, len = nums.length; i < len; i++) {
		if (map.get(nums[i]) !== undefined) {
			return [map.get(nums[i]), i];
        } else {
			map.set(target - nums[i], i);
        }
    }
    return [];
};
console.log(twoSum([2, 7, 11, 15], 9)); // [0, 1]
console.log(twoSum([3, 2, 4], 6)); // [1, 2]
console.log(twoSum([3, 3], 6)); // [0, 1]

2.2、两数组交集1

给定两个数组,编写一个函数来计算它们的交集。

说明
输出结果中的每个元素一定是唯一的。可以不考虑输出结果的顺序。

解决思路一
可以用 set,很简单,但是空间复杂度和时间复杂度都太高,不太优雅。

var intersection = function (nums1, nums2) {
	return result = [...new Set(nums1)].filter(item => new Set(nums2).has(item));
};
console.log(intersection([1, 2, 2, 1], [2, 2])); // [2]
console.log(intersection([4, 9, 5], [9, 4, 9, 8, 4])); // [4, 9]

解决思路二
可以用 map 来做,时间和空间复杂度都低很多,用一个 map 去存 nums1 数组里的每一项,类似 map[nums1[i]] = true,然后去遍历 nums2,如果在 map 中已经有的值,类似 map[nums2[i]],就把它 push 到一个数组里。并且将 map[nums2[i]] 设为 false,后面有相同的值就不 push 到数组里了。

var intersection = function (nums1, nums2) {
	const map = {},
		ret = [];
    for (let i = 0; i < nums1.length; i++) {
        map[nums1[i]] = true;
    }
    for (let i = 0; i < nums2.length; i++) {
        if (map[nums2[i]]) {
            ret.push(nums2[i]);
            map[nums2[i]] = false;
        }
    }
    return ret;
};
console.log(intersection([1, 2, 2, 1], [2, 2])); // [2]
console.log(intersection([4, 9, 5], [9, 4, 9, 8, 4])); // [4, 9]

3、找规律题

一般画个图或者稍微分析一下就能得出结果。


3.1、罗马数字转整数

罗马数字对应的阿拉伯数字表。
罗马数字 计算公式 阿拉伯数字
I 1 1
II 1+1 2
III 1+1+1 3
IV 5-1 4
V 5 5
VI 5+1 6
VII 5+1+1 7
VIII 5+1+1+1 8
IX 10-1 9
X 10 10
XI 10+1 11
XII 10+1+1 12
XIII 10+1+1+1 13
XIV 10-1+5 14
XV 10+5 15
XVI 10+5+1 16
XVII 10+5+1+1 17
XVIII 10+5+1+1+1 18
XIX 10-1+10 19
XX 10+10 20
XXX 10+10+10 30
XL 50-10 40
L 50 50
LX 50+10 60
LXX 50+10+10 70
LXXX 50+10+10+10 80
XC 100-10 90
XCIX 100-10-1+10 99
C 100 100
CI 100+1 101
CII 100+1+1 102
CXCIX 100-10+100-1+10 199
CC 100+100 200
CCC 100+100+100 300
CD 500-100 400
D 500 500
DC 500+100 600
DCCC 500+100+100+100 800
CM 1000-100 900
M 1000 1,000
MCD 1000-100+500 1,400
MCDXXXVII 1000-100+500+10+10+10+5+1+1 1,437
MD 1000+500 1,500
MDCCC 1000+500+100+100+100 1,800
MDCCCLXXX 1000+500+100+100+100+50+10+10+10 1,880
MCM 1000-100+1000 1,900
MM 1000+1000 2,000
MMM 1000+1000+1000 3,000
MMMCCCXXXIII 1000+1000+1000+100+100+100+10+10+10+1+1+1 3,333
MMMCMXCIX 1000+1000+1000-100+1000-10+100-1+10 3,999

给定一个罗马数字,将其转换成整数。输入确保在 1 到 3999 的范围内。

解释
L = 50, V= 5, III = 3。

解决思路
这些案例的规律,就是把 map 表里面对应的数字加起来。比如说,LVIII = L(对应 map 表 50)+ V(对应 map 表5)+ I(对应 map 表1)+ I(对应 map 表1)+ I(对应 map 表1),所以,就是遍历数字把对应的值加起来。

var romanToInt = function (s) {
	const map = {
        I: 1,
        V: 5,
        IV: 4,
        IX: 9,
        X: 10,
        XL: 40,
        XC: 90,
        L: 50,
        C: 100,
        CD: 400,
        CM: 900,
        D: 500,
        M: 1000,
    }
    let res = 0,
    	index = 0,
    	len = s.length;
    while (index < len) {
        if (index + 1 < len && map[s.slice(index, index+2)]) {
            res += map[s.slice(index, index+2)];
            index += 2;
        } else {
            res += map[s.slice(index, index+1)];
            index += 1;
        }
    }
    return res;
};
console.log(romanToInt("III")); // 3
console.log(romanToInt("IV")); // 4
console.log(romanToInt("IX")); // 9
console.log(romanToInt("LVIII")); // 58

3.2、最长公共前缀

编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀,返回空字符串。

提示
0 <= strs.length <= 200。0 <= strs[i].length <= 200。strs[i] 仅由小写英文字母组成。

解决思路
假如求数组里 3 个元素的最长公共前缀。
1、先拿前两个比较,求出他们两个的最长公共前缀。
2、然后上面求出的结果去跟第三个元素求最长公共前缀。
3、n 个元素就一直这么 reduce 下去。

// 这个是求出两个元素最长公共前缀的方法
var longestCommonPrefix = function (strs) {
	if (strs.length === 0) return '';
	if (strs.length === 1) return strs[0];
	return strs.reduce(getSameStr, strs[0]);
};

function getSameStr(a, b) {
	let res = '';
	for (let j = 0; j < a.length; j++) {
		if (a[j] === b[j]) {
			res += a[j];
		} else {
			return res;
		}
	}
	return res;
};
console.log(longestCommonPrefix(["flower", "flow", "flight"]));
// fl
console.log(longestCommonPrefix(["dog", "racecar", "car"]));
// ""

3.3、合并两个有序链表

将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

JavaScript笔试题(js高级代码片段)_第1张图片

提示
两个链表的节点数目范围是 [0, 50],-100 <= Node.val <= 100。l1 和 l2 均按非递减顺序排列。

解决思路
挨个遍历,按顺序谁小拼接谁,接着进入下一轮循环。

function ListNode (val, next) {
	this.val = (val === undefined ? 0 : val);
    this.next = (next === undefined ? null : next);
};

var mergeTwoLists = function (l1, l2) {
	const dummpy = node = new ListNode();
	while (l1 && l2) {
		if (l1.val >= l2.val) {
			node.next = l2;
			node = node.next;
			l2 = l2.next;
		} else {
			node.next = l1;
			node = node.next;
			l1 = l1.next;
		}
	}
	node.next = l1 || l2;
	return dummpy.next;
};
console.log(mergeTwoLists([1, 2, 4], [1, 3, 4])); 
// [1, 2, 4, next: Array(3)]
console.log(mergeTwoLists([], []));
// [next: Array(0)]
console.log(mergeTwoLists( [], [0]));
// [next: Array(1)]

console.log(mergeTwoLists([1, 2, 4], [1, 3, 4]).next); 
// [1, 3, 4]
console.log(mergeTwoLists([], []).next);
// []
console.log(mergeTwoLists( [], [0]).next);
// [0]

解决思路二
数组排序 sort() 和 new Set(arr)。

function arrSort (arr1, arr2) {
	let arr = [...arr1, ...arr2];
	arr = [...new Set(arr.sort((a, b) => a - b))];
	return arr;
};
let arr1 = [1, 2, 9, 10, 60, 20, 30, 7, 6],
	arr2 = [60, 70, 3, 5, 8];
console.log(arrSort(arr1, arr2));
// [1, 2, 3, 5, 6, 7, 8, 9, 10, 20, 30, 60, 70]

3.4、实现 str()

实现 strStr() 函数。给定两个字符串 haystack 和 needle,请在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。

提示
0 <= haystack.length,needle.length <= 5 * 104。haystack 和 needle 仅由小写英文字符组成。

解决思路
1、遍历字符串看是否有和需要找的字符串第一个字母相同。
2、如果相同,就截取字符串跟需要找的字符串相同长度的字符串对比。
3、相同就返回下标,不同就继续遍历原字符串。

var strStr = function (haystack, needle) {
	if (needle === "") return 0;
	for (var i = 0; i < haystack.length; i++) {
		if (haystack[i] === needle[0]) {
			if (haystack.substring(i, i + needle.length) === needle) return i;
		}
	}
	return -1
};
console.log(strStr("hello", "ll")); // 2
console.log(strStr("aaaaa", "bba")); // -1
console.log(strStr("", "")); // 0

3.5、杨辉三角

给定一个非负整数 numRows,生成杨辉三角的前 numRows 行。

JavaScript笔试题(js高级代码片段)_第2张图片

在杨辉三角中,每个数是它左上方和右上方的数的和。

解决思路
1、看到上图可以发现,生成杨辉三角 numRows 行,数组就有 numRows 行。
2、每一行,它的数组第一个位置和最后一个位置都是1
3、每一行,除了第一个和最后一个位置,其它位置的值等于上一行的两个值相加

var generate = function (numRows) {
	if (numRows === 0) return [];
	const result = Array.from(new Array(numRows), () => []);
	for (let i = 0; i < numRows; i++) {
		result[i][0] = 1; 
		result[i][i] = 1;
		for (let j = 1; j < i; j++) {
			result[i][j] = result[i-1][j-1] + result[i-1][j];
		}
	}
	return result;
};
console.log(generate(6));
// [Array(1), Array(2), Array(3), Array(4), Array(5), Array(6)]
// [[1], [1, 1], [1, 2, 1], [1, 3, 3, 1], [1, 4, 6, 4, 1], [1, 5, 10, 10, 5, 1]]

3.6、买卖股票的最佳时机

给定一个数组 prices,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。只能选择 某一天 买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算所能获取的最大利润。返回可以从这笔交易中获取的最大利润。如果不能获取任何利润,返回 0。

提示
1 <= prices.length <= 105,0 <= prices[i] <= 104

解决思路
先看一张图,假设给定的数组为 [7, 1, 5, 3, 6, 4]

JavaScript笔试题(js高级代码片段)_第3张图片

第一天是 7,因为还没到第二天不知道这个价格是高是低,标记最小值是 7。
第二天是 1,比 7 小,那么只要当前天数的值比前面小,就说明不卖,因为它是最小值,标记最小值是 7。
第三天是 5,5 比前一天大,说明比最小值要大,那么可以卖,利润就是 5-1 = 4。
第四天是 3,比 5 小,还是一样的道理,比之前小,最小值就要变为当前值,啥也不干,标记最小值是 3。
第五天是 6。
第六天是 4,规律是一样的。
意思是只要今天比昨天低,就可以用今天的减去最小值,就是利润,然后每次都比较这个利润是不是最大值就行了。

var maxProfit = function (prices) {
	let res = 0;
	let min = prices[0];
	for(let i = 1; i < prices.length; i++){
		if(prices[i] < min){
			min = prices[i];
		} else {
			res = Math.max(res, prices[i] - min);
		}   
	}
	return res;
};
console.log(maxProfit([7, 1, 5, 3, 6, 4])); // 5
// 在第 2 天(股票价格 = 1)的时候买入,
// 在第 5 天(股票价格 = 6)的时候卖出,
// 最大利润 = 6-1 = 5 。
// 注意:利润不能是 7-1 = 6, 
// 因为卖出价格需要大于买入价格;
// 同时,不能在买入前卖出股票。
console.log(maxProfit([7, 6, 4, 3, 1])); // 0
// 在这种情况下, 没有交易完成, 所以最大利润为 0。

3.7、买卖股票的最佳时机 2

给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。设计一个算法来计算所能获取的最大利润。可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:不能同时参与多笔交易(必须在再次购买前出售掉之前的股票)。

解决思路

JavaScript笔试题(js高级代码片段)_第4张图片

利润就跟上图绿色部分显示的一样,也就是说只要今天减去昨天,是正数就是利润。

var maxProfit = function (prices) {
	let result = 0;
	for(let i = 1; i < prices.length; i++) {
		if (prices[i] > prices[i-1]) {
			result += prices[i] - prices[i - 1];
		}
	}
	return result;
};
console.log(maxProfit([7, 1 , 5, 3, 6, 4])); // 7
// 解释:在第 2 天(股票价格 = 1)的时候买入,
// 在第 3 天(股票价格 = 5)的时候卖出, 
// 这笔交易所能获得利润 = 5-1 = 4。
// 随后,在第 4 天(股票价格 = 3)的时候买入,
// 在第 5 天(股票价格 = 6)的时候卖出, 
// 这笔交易所能获得利润 = 6-3 = 3。
console.log(maxProfit([1, 2, 3, 4, 5])); // 4
// 解释:在第 1 天(股票价格 = 1)的时候买入,
// 在第 5 天(股票价格 = 5)的时候卖出, 
// 这笔交易所能获得利润 = 5-1 = 4。
// 注意:不能在第 1 天和第 2 天接连购买股票,
// 之后再将它们卖出。因为这样属于同时参与了多笔交易,
// 必须在再次购买前出售掉之前的股票。
console.log(maxProfit([7, 6, 4, 3, 1])); // 0
// 解释:在这种情况下, 没有交易完成, 所以最大利润为 0。

3.8、反转链表

给定单链表的头节点 head,请反转链表,并返回反转后的链表。

JavaScript笔试题(js高级代码片段)_第5张图片

JavaScript笔试题(js高级代码片段)_第6张图片

解决思路一(未能实现)
看图找规律,把链表前面加一个 null,这样翻转前和翻转后就一致了。

JavaScript笔试题(js高级代码片段)_第7张图片

var reverseList = function (head) {
	let [pre, node] = [null, head];
	while (node) {
		const temp = node.next;
		node.next = pre;
		pre = node;
		node = temp;
	}
	return pre;
};
console.log(reverseList([1, 2, 3, 4, 5])); // [1, 2, 3, 4, 5, next: null]
console.log(reverseList([1, 2])); // [1, 2, next: null]

解决思路二
使用 array.reverse()。

console.log([1, 2, 3, 4, 5].reverse()); // [5, 4, 3, 2, 1]
console.log([1, 2].reverse()); // [2, 1]

4、双指针

双指针是解数组类型题最常见解法。
1、头尾式指针,然后依次向中间靠拢的双指针;
2、快慢式指针,两个指针都是从左边开始,一个走得快,一个走得慢。


4.1、删除数组中的重复项

给一个有序数组 nums,请原地删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。不要使用额外的数组空间,必须在原地修改输入数组,并在使用 O(1) 额外空间的条件下完成。

提示
0 <= nums.length <= 3 * 104,-104 <= nums[i] <= 104,nums 已按升序排列。

解决思路

JavaScript笔试题(js高级代码片段)_第8张图片

1、慢指针是 i,快指针是 j;
2、如果 nums[i] 等于 nums[j] 说明是相同的元素,j 继续走,i 还在原位。
3、如果 nums[i] 不等于 nums[j] 说明是不相同的元素,那么 nums[i++] = nums[j],j 继续向前走。
依次类推,就相当于 i 指针保证它和它前面的数字都是不重复的,j 就是一个遍历器。

var removeDuplicates = function (nums) {
	let i = 0;
	for (let j = 1; j < nums.length; j++) {
		if (nums[j] !== nums[i]) {
			nums[i + 1] = nums[j];
			i++;
		}
	}
	return i + 1;
};
console.log(removeDuplicates([1, 1, 2])); // 2
// 解释:函数应该返回新的长度 2,
// 并且原数组 nums 的前两个元素被修改为 1, 2。
// 不需要考虑数组中超出新长度后面的元素。
console.log(removeDuplicates([0, 0, 1, 1, 1, 2, 2, 3, 3, 4])); // 5
// 解释:函数应该返回新的长度 5, 
// 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。
// 不需要考虑数组中超出新长度后面的元素。

4.2、合并两个有序数组

给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。可以假设 nums1 的空间大小等于 m + n,这样它就有足够的空间保存来自 nums2 的元素。

提示
nums1.length == m + n,nums2.length == n,0 <= m, n <= 200,1 <= m + n <= 200,-109 <= nums1[i], nums2[i] <= 109。

解决思路
新创建一个数组,然后分别比较这两个数组里的每一项,push 进去即可。然而因为是有序数组,第一个数组还有正好满足假如第二数组的空间,所以这里可以采取双指针来解答,从后往前遍历。

var merge = function (nums1, m, nums2, n) {
	let len = m + n - 1;
	m--, n--;
	while (m >= 0 && n >= 0) {
		if (nums1[m] > nums2[n]) {
			nums1[len] = nums1[m--];
		} else {
			nums1[len] = nums2[n--];
		}
		len--;
	}
	if (m === -1) {
		return nums1.splice(0, len + 1, ...nums2.slice(0, n + 1));
	}
	if (n === -1) {
		return nums1;
	}
};
console.log(merge([1,2,3,0,0,0], 3, [2,5,6], 3));
// [1, 2, 2, 3, 5, 6]
console.log(merge([1], 1, [], 0));
// [1]

4.3、验证回文串

给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。

说明
可以将空字符串定义为有效的回文串。

var isPalindrome = function (s) {
	s = s.replace(/[^\w]/g, '').toLowerCase();
	let leftPointer = 0,
		rightPointer = s.length - 1;
	while (rightPointer > leftPointer) {
		if (s[leftPointer++] === s[rightPointer--]) {
			continue;
		} else {
			return false;
		}
	}
	return true;
};
console.log(isPalindrome("A man, a plan, a canal: Panama"));
// true
// 解释:"amanaplanacanalpanama" 是回文串。
console.log(isPalindrome("race a car"));
// false
// 解释:"raceacar" 不是回文串。
console.log(isPalindrome("上海自来水来自海上"));
// true

4.4、回文链表

这个题思路跟上面一样,都是双指针对比,但是主要这个题写起来很麻烦,要用到我们之前说的翻转链表。

解决思路
1、先用快慢指针的手法,才能知道这个链表的中点在哪,然后从中点截断;
2、截断成为两个链表,把后面的链表翻转;
3、最后依次去判断这两个链表每一项是否相同。
关键点:如何从中点截断这个链表,让一个指针每次走一步,另一个指针每次走两步,这样他们每次走的倍数就相差两倍。

断成两个链表

let fast = head;
let slow = head;
let prev;
while (fast && fast.next) {
	prev = slow;
	slow = slow.next;
	fast = fast.next.next;
}
prev.next = null;  // 断成两个链表

翻转链表

// 翻转后半段
let head2 = null;
while (slow) {
	const tmp = slow.next;
	slow.next = head2;
	head2 = slow;
	slow = tmp;
}

对比

const isPalindrome = (head) => {
	if (head == null || head.next == null) {
		return true;
	}
	let fast = head;
	let slow = head;
	let prev;
	while (fast && fast.next) {
		prev = slow;
		slow = slow.next;
		fast = fast.next.next;
	}
	prev.next = null;  // 断成两个链表
	// 翻转后半段
	let head2 = null;
	while (slow) {
		const tmp = slow.next;
		slow.next = head2;
		head2 = slow;
		slow = tmp;
	}
	// 比对
	while (head && head2) {
		if (head.val != head2.val) {
			return false;
		}
		head = head.next;
		head2 = head2.next;
	}
	return true;
};

4.5、删除链表中的节点

请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点。传入函数的唯一参数为要被删除的节点。现有一个链表 head = [4, 5, 1, 9],它可以表示为:

删除链表中的节点

示例 1
输入:head = [4, 5, 1, 9], node = 5
输出:[4, 1, 9]
解释:给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.

示例 2
输入:head = [4, 5, 1, 9], node = 1
输出:[4, 5, 9]
解释:给定你链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9.

提示
链表至少包含两个节点。链表中所有节点的值都是唯一的。给定的节点为非末尾节点,并且一定是链表中的一个有效节点。 不要从函数中返回任何结果。

解决思路一(未知如何调用)
node 是个引用类型,需要把 node 的 val 变为 node.next 的 val,然后 node 的 next 指向 node.next.next,就移花接木的完成任务了。

var deleteNode = function (node) {
	node.val = node.next.val;
	node.next = node.next.next;
};

解决思路二

let deleteItem = function (arrData, node) {
	let newData = [];
	arrData.forEach(item=>{
		if (item !== node) {
			newData.push(item);
		}
	});
	return newData;
};
console.log(deleteItem([5, 7, 3, 6, 1, 9], 2));
// [5, 7, 3, 6, 1, 9]
console.log(deleteItem([5, 7, 3, 6, 1, 9], 5));
// [7, 3, 6, 1, 9]

4.6、移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

说明
必须在原数组上操作,不能拷贝额外的数组。尽量减少操作次数。

解决思路
如动画所示,可以用快慢指针来解答,具体不好用语言叙述,看动图。

JavaScript笔试题(js高级代码片段)_第9张图片

var moveZeroes = function (nums) {
	let i = j = 0;
	while (i < nums.length) {
		if (nums[i] !== 0) {
			[nums[i], nums[j]] = [nums[j], nums[i]];
			j++;
		}
		i++;
	}
	return nums;
};
console.log(moveZeroes([0, 1, 0, 3, 12]));
// [1, 3, 12, 0, 0]

4.7、反转字符串

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。不要给另外的数组分配额外的空间,必须在原地修改输入的数组、使用 O(1) 的额外空间解决这一问题。可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。

解决思路一

var reverseString = function (s) {
	let l = 0 ;
	let r = s.length - 1;
	while (l < r) {
		[s[l], s[r]] = [s[r], s[l]];
		l++; 
		r--;
	}
	return s;
};
console.log(reverseString(["h", "e", "l", "l", "o"]));
// ["o", "l", "l", "e", "h"]
console.log(reverseString(["H", "a", "n", "n", "a", "h"]));
// ["h", "a", "n", "n", "a", "H"]

解决思路二

let stringReverse = function (str) {
	let strings = "";
	str = [...`${str}`].map(item => item).reverse();
	str.forEach(item => strings += item);
	return strings;
};
console.log(stringReverse(["hello"]));
// olleh
console.log(stringReverse(["Hannah"]));
// hannaH

4.8、两数组交集2

给定两个数组,编写一个函数来计算它们的交集。

说明
输出结果中每个元素出现的次数,应与元素在两个数组中出现次数的最小值一致。可以不考虑输出结果的顺序。

解决思路
这个取交集需要保留重复元素,可以是用双指针来实现。
1、如果是两个有序数组,则可以使用双指针的方法得到两个数组的交集。
2、首先对两个数组进行排序,然后使用两个指针遍历两个数组。
3、初始时,两个指针分别指向两个数组的头部。每次比较两个指针指向的两个数组中的数字,如果两个数字不相等,则将指向较小数字的指针右移一位,如果两个数字相等,将该数字添加到答案,并将两个指针都右移一位。当至少有一个指针超出数组范围时,遍历结束。

var intersect = function (nums1, nums2) {
	nums1 = nums1.sort((a, b) => a - b);
	nums2 = nums2.sort((a, b) => a - b);
	let l1 = 0,
		l2 = 0;
	const nums1Len = nums1.length,
		nums2Len = nums2.length,
		ret = [];
	while (l1 < nums1Len && l2 < nums2Len) {
		if (nums1[l1] === nums2[l2]) {
			ret.push(nums1[l1]);
			l1++;
			l2++;
		}
		if (nums1[l1] > nums2[l2]) l2++;
		if (nums1[l1] < nums2[l2]) l1++;
	}
	return ret;
};
console.log(intersect([1, 2, 2, 1], [2, 2]));
// [2, 2]
console.log(intersect([4, 9, 5], [9, 4, 9, 8, 4]));
// [4, 9]
console.log(intersect([3, 9, 5, 3], [9, 3, 9, 8, 3]));
// [3, 3, 9]

5、二叉树(DFS)

5.1、二叉树前中后遍历套路详解

5.5.1、前序遍历

root 节点是 A 节点(下图的 A 节点),然后按照下图数字的顺序依次打印出节点。

JavaScript笔试题(js高级代码片段)_第10张图片

可以看到这其中的规律,就是深度优先遍历,先遍历左子树,再遍历右子树,这里不用递归,因为一些大厂严格要求二叉树遍历不用递归,递归太简单了。

解决思路
深度优先遍历,先遍历左子树,再遍历右子树,所以,需要一套如何遍历一颗二叉树,并且是先左子树,再右子树的通用模板

var Traversal = function (root) {
	const stack = [];
	while (root || stack.length) {
		while (root) {
			stack.push(root);
			root = root.left;
		}
		root = stack.pop();
		root = root.right;
	}
	return res;
};

根据图片不难发现这个遍历产生的整体压栈顺序
1、A、B、D 入栈
2、D 出栈
3、B 出栈
4、E 入栈
5、E 出栈
6、A 出栈
7、C 入栈
8、C 出栈
9、F 入栈
10、F 出栈

把上面入栈的元素按顺序排列一下 A、B、D、E、C、F,而这就是前序遍历的顺序。

是不是很有意思,下面的中序遍历,我们看看出栈顺序是不是中序遍历的要求:D、B、E、A、C、F(这就是中序遍历的要求,好了,两个题解决)

前序遍历的具体代码

var preorderTraversal = function (root) {
	// 初始化数据
	const res = [];
	const stack = [];
	while (root || stack.length) {
		while (root) {
			res.push(root.val);
			stack.push(root);
			root = root.left;
		}
		root = stack.pop();
		root = root.right;
	}
	return res;
};

5.1.2、中序遍历是一个意思,在前序遍历的基础上改造一下图片

JavaScript笔试题(js高级代码片段)_第11张图片

var preorderTraversal = function (root) {
	// 初始化数据
	const res = [];
	const stack = [];
	while (root || stack.length) {
		while (root) {
			stack.push(root);
			root = root.left;
		}
		root = stack.pop();
		res.push(root.val);
		root = root.right;
 	}
	return res;
};

5.1.3、后序遍历有点不太一样,但是套路是一样的,我们需要先遍历右子树,再遍历左子树,反着来,就可以了

JavaScript笔试题(js高级代码片段)_第12张图片

var postorderTraversal = function (root) {
	// 初始化数据
	const res = [];
	const stack = [];
	while (root || stack.length) {
		while (root) {
			stack.push(root);
			res.unshift(root.val);
			root = root.right;
		}
		root = stack.pop();
		root = root.left;
	}
	return res;
};

5.2、对称二叉树

判断一个二叉树是否对称,比如说:[1, 2, 2, 3, 4, 4, 3] 是对称的。

      1
    /   \
   2      2
  / \    / \
 3   4  4   3

但是下面这个[1, 2, 2, null, 3, null, 3]则不是镜像对称。

    1
   / \
  2   2
   \   \
   3    3

解决思路
递归解决
5.2.1、判断两个指针当前节点值是否相等.
5.2.2、判断 A 的右子树与 B 的左子树是否对称。
5.2.3、判断 A 的左子树与 B 的右子树是否对称。

function isSame(leftNode, rightNode){
    if(leftNode === null && rightNode === null) return true;
    if(leftNode === null || rightNode === null) return false;
    return leftNode.val === rightNode.val && isSame(leftNode.left, rightNode.right) && isSame(leftNode.right, rightNode.left);
};

var isSymmetric = function(root) {
    if(!root) return root;
    return isSame(root.left, root.right);
};

5.2.1、二叉树的最大深度

5.2.1.1、只要遍历到这个节点既没有左子树,又没有右子树的时候。
5.2.1.2、说明就到底部了,这个时候如果之前记录了深度,就可以比较是否比之前记录的深度大,大就更新深度。
5.2.1.3、然后以此类推,一直比较到深度最大的。

var maxDepth = function(root) {
	if(!root) return root;
    let ret = 1;
    function dfs(root, depth){
        if(!root.left && !root.right) ret = Math.max(ret, depth);
        if(root.left) dfs(root.left, depth+1);
        if(root.right) dfs(root.right, depth+1);
    }
    dfs(root, ret);
    return ret
};

5.2.2、将有序数组转化为二叉搜索树

给一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。

高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。

JavaScript笔试题(js高级代码片段)_第13张图片

输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:

JavaScript笔试题(js高级代码片段)_第14张图片

输入:nums = [1,3]
输出:[3,1]
解释:[1,3][3,1] 都是高度平衡二叉搜索树。

提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums 按 严格递增 顺序排列

解决思路
5.2.2.1、构建一颗树包括:构建root、构建 root.left 和 root.right。
5.2.2.2、题目要求"高度平衡" — 构建 root 时候,选择数组的中间元素作为 root 节点值,即可保持平衡。
5.2.2.3、递归函数可以传递数组,也可以传递指针,选择传递指针的时候:l r 分别代表参与构建BST的数组的首尾索引。

var sortedArrayToBST = function(nums) {
    return toBST(nums, 0, nums.length - 1);
};

const toBST = function(nums, l, r) {
    if(l > r) {
		return null;
    }
    const mid = l + r >> 1;
    const root = new TreeNode(nums[mid]);
    root.left = toBST(nums, l, mid - 1);
    root.right = toBST(nums, mid + 1, r);

    return root;
};

6、栈

栈是一种先进先出的数据结构。其次栈跟递归很相似,递归是不是先压栈,然后先进来的先出去,就跟函数调用栈一样。


6.1、有效的括号

给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串 s ,判断字符串是否有效。有效字符串需满足:左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。

示例 1:
输入:s = "()"
输出:true

示例 2:
输入:s = "()[]{}"
输出:true

示例 3:
输入:s = "(]"
输出:false

示例 4:
输入:s = "([)]"
输出:false

解决思路

6.1.1、右括号前面,必须是相对应的左括号,才能抵消!
6.1.2、右括号前面,不是对应的左括号,那么该字符串,一定不是有效的括号!
也就是说左括号我们直接放入栈中即可,发现是右括号就要对比是否跟栈顶元素相匹配,不匹配就返回false

var isValid = function(s) {
    const map = { '{': '}', '(': ')', '[': ']' };
    const stack = [];
    for(let i of s){
        if(map[i]){
            stack.push(i);
        } else {
            if(map[stack[stack.length - 1]] === i){
                stack.pop()
            }else{
                return false;
            }
        }
    }
    return stack.length === 0;
};

console.log(isValid("()[]{}")); // true
console.log(isValid("([)]")); // false

6.2、最小栈

设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
6.2.1、push(x) —— 将元素 x 推入栈中。
6.2.2、pop() —— 删除栈顶的元素。
6.2.3、top() —— 获取栈顶元素。
6.2.4、getMin() —— 检索栈中的最小元素。

MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin();   --> 返回 -3.
minStack.pop();
minStack.top();      --> 返回 0.
minStack.getMin();   --> 返回 -2.

提示:
pop、top 和 getMin 操作总是在 非空栈 上调用。

先不实现getMin方法,实现pop、top和push。

var MinStack = function() {
    this.stack = [];
};

MinStack.prototype.push = function(x) {
    this.stack.push(x);
};

MinStack.prototype.pop = function() {
    this.stack.pop();
};

MinStack.prototype.top = function() {
    return this.stack[this.stack.length - 1];
};

如何保证每次取最小呢,举一个例子

JavaScript笔试题(js高级代码片段)_第15张图片

如上图,需要一个辅助栈来记录最小值,
6.2.1、开始我们向stack push -2
6.2.2、此时辅助栈minStack,因为此时stack最小的是-2,也push -2
6.2.3、stack push 0
6.2.4、此时辅助站minStack 会用 0 跟 -2对比,-2更小,minstack会push -2
6.2.5、stack push -3
6.2.6、此时辅助站minStack 会用 -3 跟 -2对比,-3更小,minstack会push -3
所以取最小的时候,总能在minStack中取到最小值。

var MinStack = function() {
    this.stack = [];
    // 辅助栈
    this.minStack = [];
};

MinStack.prototype.push = function(x) {
    this.stack.push(x);
    // 如果是第一次或者当前x比最小栈里的最小值还小才push x
    if(this.minStack.length === 0 || x < this.minStack[this.minStack.length - 1]){
        this.minStack.push(x)
    } else {
         this.minStack.push( this.minStack[this.minStack.length - 1])
    }
};

MinStack.prototype.pop = function() {
    this.stack.pop();
    this.minStack.pop();
};

MinStack.prototype.top = function() {
    return this.stack[this.stack.length - 1];
};

MinStack.prototype.getMin = function() {
    return this.minStack[this.stack.length - 1];
};

7、动态规划

动态规划,一定要知道动态转移方程,有了这个,就相当于解题的钥匙,我们从题目中体会一下


7.1、最大子序和

题目如下:

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例 1:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:

输入:nums = [1]
输出:1
示例 3:

输入:nums = [0]
输出:0
复制代码
思路:

这道题可以用动态规划来解决,关键是找动态转移方程
我们动态转移方程中,dp表示每一个nums下标的最大自序和,所以dp[i]的意思为:包括下标i之前的最大连续子序列和为dp[i]。
确定转义方程的公示:

dp[i]只有两个方向可以推出来:

1、如果dp[i - 1] < 0,也就是当前遍历到nums的i,之前的最大子序和是负数,那么我们就没必要继续加它了,因为dp[i] = dp[i - 1] + nums[i] 会比nums[i]更小,所以此时还不如dp[i] = nums[i],就是目前遍历到i的最大子序和呢
2、同理dp[i - 1] > 0,说明nums[i]值得去加dp[i - 1],此时回避nums[i]更大
这样代码就出来了,其实更多的就是求dp,遍历nums每一个下标都会产生最大子序和,我们记录下来即可

var maxSubArray = function(nums) {
let res = nums[0];
const dp = [nums[0]];
for(let i=1;i < nums.length;i++){
if(dp[i-1]>0){
dp[i]=nums[i]+dp[i-1]
}else{
dp[i]=nums[i]
}

res=Math.max(dp[i],res)

}
return res
};


7.2、爬楼梯

先看题目:

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例 1:

输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。

  1. 1 阶 + 1 阶
  2. 2 阶
    示例 2:

输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。

  1. 1 阶 + 1 阶 + 1 阶
  2. 1 阶 + 2 阶
  3. 2 阶 + 1 阶
    复制代码
    涉及到动态规划,一定要知道动态转移方程,有了这个,就相当于解题的钥匙,

这道题我们假设dp[10]表示爬到是你爬到10阶就到达楼顶的方法数,

那么,dp[10] 是不是就是你爬到8阶,然后再走2步就到了,还有你走到9阶,再走1步就到了,

所以 dp[10] 是不是等于 dp[9]+dp[8]

延伸一下 dp[n] 是不是等于 dp[n - 1] + dp[n - 2]

代码如下:

var climbStairs = function(n) {
const dp = {};
dp[1] = 1;
dp[2] = 2;
for(let i = 3; i <= n; i++){
dp[i] = dp[i-1] + dp[i-2]
}
return dp[n]
};


8、数学问题

以下更多的是涉及数学问题,这些解法非常重要,因为在中级题里面会经常用到,比如我们马上讲到的加一这个题, 中级的两数相加都是一个模板。


8.1、加一

题目如下:

给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。

最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。

你可以假设除了整数 0 之外,这个整数不会以零开头。

示例 1:

输入:digits = [1,2,3]
输出:[1,2,4]
解释:输入数组表示数字 123。
示例 2:

输入:digits = [4,3,2,1]
输出:[4,3,2,2]
解释:输入数组表示数字 4321。
示例 3:

输入:digits = [0]
输出:[1]
复制代码
这个题的关键有两点:

需要有一个进位的变量carry记录到底进位是几
还需要一个每次迭代都重置和的变量sum来帮我们算是否进位,以及进位后的数字
记住这个题,这是两数字相加的套路,这次是+1,其实就是两数相加的题(腾讯面试遇到过两数相加)

var plusOne = function(digits) {
let carry = 1; // 进位(因为我们确定+1,初始化进位就是1)
for(let i = digits.length - 1; i >= 0; i–){
let sum = 0; // 这个变量是用来每次循环计算进位和digits[i]的值的
sum = digits[i] + carry;
digits[i] = sum % 10; // 模运算取个位数
carry = (sum / 10) | 0; // 除以10是取百位数,并且|0表示舍弃小数位
}
if(digits[0] === 0) digits.unshift(carry);
return digits
};


8.2、x的平方根

题目如下:实现 int sqrt(int x) 函数。

计算并返回 x 的平方根,其中 x 是非负整数。

由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

示例 1:

输入: 4
输出: 2
复制代码
示例 2:

输入: 8
输出: 2
说明: 8 的平方根是 2.82842…,
由于返回类型是整数,小数部分将被舍去。
复制代码
这道题是典型的二分法解题,所以我们需要熟悉二分法的通用模板,我们出一个题:

在 [1, 2, 3, 4, 5, 6] 中找到 4,若存在则返回下标,不存在返回-1

const arr = [1, 2, 3, 4, 5, 6];
function getIndex1(arr, key) {
let low = 0;
const high = arr.length - 1;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
if (key === arr[mid]) {
return mid;
}
if (key > arr[mid]) {
low = mid + 1;
} else {
height = mid - 1;
}
}
return -1;
}
console.log(getIndex1(arr, 5)); // 4
复制代码
所以这道题的意思就是,我们找一个数平方跟x最相近的数,二分法的用法中也有找相近数的功能

所以代码如下:

var mySqrt = function(x) {
let [l , r] = [0, x];
let ans = -1;
while(l <= r) {
const mid = (l + r) >> 1;
if(mid * mid > x){
r = mid - 1
} else if(mid * mid < x){
ans = mid; // 防止越界
l = mid + 1;
} else {
ans = mid;
return ans;
}
}
return ans;
};
};


8.3、Excel表序列号

这个题比较重要,也比较基础,简而言之就是进制转换,必须牢牢掌握

题目如下:

给你一个整数 columnNumber ,返回它在 Excel 表中相对应的列名称。

例如:

A -> 1
B -> 2
C -> 3

Z -> 26
AA -> 27
AB -> 28

复制代码
示例 1:

输入:columnNumber = 1
输出:“A”
示例 2:

输入:columnNumber = 28
输出:“AB”
示例 3:

输入:columnNumber = 701
输出:“ZY”
示例 4:

输入:columnNumber = 2147483647
输出:“FXSHRXW”
复制代码
说白了,这就是一道26进制的问题,以前我们知道10进制转2进制就是不停的除2,把余数加起来,26进制也是一样,不停的除26

思路:

初始化结果 ans = 0,遍历时将每个字母与 A 做减法,因为 A 表示 1,所以减法后需要每个数加 1,计算其代表的数值 num = 字母 - ‘A’ + 1
因为有 26 个字母,所以相当于 26 进制,每 26 个数则向前进一位
所以每遍历一位则ans = ans * 26 + num
以 ZY 为例,Z 的值为 26,Y 的值为 25,则结果为 26 * 26 + 25=701
var titleToNumber = function(columnTitle) {
let ans = 0;
for(let i = 0; i < columnTitle.length; i++){
ans = ans * 26 + (columnTitle[i].charCodeAt() - ‘A’.charCodeAt() + 1)
}
return ans;
};


8.4、阶乘中的零

题目:

给定一个整数 n,返回 n! 结果尾数中零的数量。

示例 1:

输入: 3
输出: 0
解释: 3! = 6, 尾数中没有零。
示例 2:

输入: 5
输出: 1
解释: 5! = 120, 尾数中有 1 个零.
复制代码
这道题很简单,有多少个5就有多少个0,为什么这么说呢,我们分析一下题目

比如说 5!,

也就是 5 * 4 * 3 * 2 * 1 = 120,我们发现只有1个0,怎么产生的呢,主要造成者就是 2 * 5 构造了一个0

再看看10!

10! = 10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1 其中,除了10 = 2 * 5和本身有一对2 * 5,所以有两个0,这样这道题的规律就出来了,我们再精进一步

图片
image.png
如上图,每四个数字都会出现一个或者多个2的因子,但是只有每 5 个数字才能找到一个或多个5的因子。所以总体上看来,2的因子是远远多于5的因子的,所以我们只需要找5的倍数就可以了。

我们再进一步,按照上面的说法,我们需要计算比如10的阶乘有多少个0,要把10的阶乘算出来,其实我们只需要算10有几个5就好了,为什么呢

我们发现只有5的倍数的阶乘,才会产生5, 所以我们需要看看阶层数有多少个5,代码如下:

var trailingZeroes = function (n) {
let r = 0;
while (n > 1) {
n = Math.floor(n / 5);
r += n;
}
return r;
};


8.5、颠倒二进制位

题目如下:

颠倒给定的 32 位无符号整数的二进制位。

示例 1:

输入: 00000010100101000001111010011100
输出: 00111001011110000010100101000000
解释: 输入的二进制串 00000010100101000001111010011100 表示无符号整数 43261596,
因此返回 964176192,其二进制表示形式为 00111001011110000010100101000000。
复制代码
示例 2:

输入:11111111111111111111111111111101
输出:10111111111111111111111111111111
解释:输入的二进制串 11111111111111111111111111111101 表示无符号整数 4294967293,
因此返回 3221225471 其二进制表示形式为 10111111111111111111111111111111 。
复制代码
这类题,就是翻转字符串,我们可以把其转为字符串,再转成数组,再reverse一下,这里我们选用数学的方式去解答,不用这种转字符串的方式。

解答这道题之前,我们需要了解的前置知识:

与预算 &
1 & 1 // 1的2进制最后一位是1,得到1
2 & 0 // 2的2进制最后一位是0,得到0
3 & 1 // 3的2进制最后一位是1,得到1
4 & 0 // 4的2进制最后一位是0,得到0
复制代码
所以我们知道了怎么取10进制最后1位的2进制是几。

JavaScript 使用 32 位按位运算数(意思是我们的按位运算都会转成32位,你的数字不能超过32位,会出问题)
JavaScript 将数字存储为 64 位浮点数,但所有按位运算都以 32 位二进制数执行。

在执行位运算之前,JavaScript 将数字转换为 32 位有符号整数。

执行按位操作后,结果将转换回 64 位 JavaScript 数。

‘<< 1’ 运算
这个运算实际上就是把10进制乘以2,这个乘2在2进制上表现出右边填了一个0,我们距举例来说,

2的2进制是 10,2 << 1 得到4, 4的2进制是100,所以比10多了个0
3的2进制是 11,3 << 1 得到6。6的2进制是110,所以比11多了个0
以上就是规律

思路:循环取最后一位拼接起来即可

var reverseBits = function (n) {
let result = 0
for (let i = 0; i < 32; i++) {
result = (result << 1) + (n & 1)
n = n >> 1
}
// 为什么要 >>> 0 呢,一位javascript没有无符号整数,全是有符号的
// 不>>>0的话,得出来的值是负数,但是无符号整数是没有符号的
// javascript 有符号转化为无符号的方法就是>>>0
return result >>> 0
}


8.6、丢失的数字

题目如下:

给定一个包含 [0, n] 中 n 个数的数组 nums ,找出 [0, n] 这个范围内没有出现在数组中的那个数。

进阶:

你能否实现线性时间复杂度、仅使用额外常数空间的算法解决此问题?

示例 1:

输入:nums = [3,0,1]
输出:2
解释:n = 3,因为有 3 个数字,所以所有的数字都在范围 [0,3] 内。2 是丢失的数字,因为它没有出现在 nums 中。
示例 2:

输入:nums = [0,1]
输出:2
解释:n = 2,因为有 2 个数字,所以所有的数字都在范围 [0,2] 内。2 是丢失的数字,因为它没有出现在 nums 中。
复制代码
这题很简单,就是用0-n的总和减去数组总和

0 - n 的总和用等差数列:(首数+尾数)* 项数 / 2 来求
var missingNumber = function(nums) {
const len = nums.length

let sum = ((1 + len) * len) / 2

for (let i = 0; i < len; i++) {
sum -= nums[i]
}

return sum
}


8.7、3的幂

题目如下:

给定一个整数,写一个函数来判断它是否是 3 的幂次方。如果是,返回 true ;否则,返回 false 。

整数 n 是 3 的幂次方需满足:存在整数 x 使得 n == 3的x次方

示例 1:

输入:n = 27
输出:true
示例 2:

输入:n = 0
输出:false
示例 3:

输入:n = 9
输出:true
复制代码
思路

我们拿27来说:27 = 3 * 3 * 3,所以27是3的幂次方
我们拿29来说:29 = 3 * 3 * 3点几
也就是说,如果是3的幂次方,一直除以3,除到最后就等于1比如27/3/3/3等于1 如果不是3的幂次方,除到最后就是3点几/3 等于1点几

代码就出来了判断是不是等于1即可

var isPowerOfThree = function(n) {
while(n >= 3){
n /= 3;
}
return n === 1;
};


8.8、Fizz Buzz

这个题没啥好说的,就按照题目说的写代码就行,先看题目:

写一个程序,输出从 1 到 n 数字的字符串表示。

如果 n 是3的倍数,输出“Fizz”;

如果 n 是5的倍数,输出“Buzz”;

如果 n 同时是3和5的倍数,输出 “FizzBuzz”。

示例:

n = 15,

返回:
[
“1”,
“2”,
“Fizz”,
“4”,
“Buzz”,
“Fizz”,
“7”,
“8”,
“Fizz”,
“Buzz”,
“11”,
“Fizz”,
“13”,
“14”,
“FizzBuzz”
]
复制代码
var fizzBuzz = function (n) {
const list = [];
for (let i = 1; i <= n; i++) {
const is3Times = i % 3 === 0; // 是否是3的倍数
const is5Times = i % 5 === 0; // 是否是5的倍数
const is15Times = is3Times && is5Times; // 是否是15的倍数
if (is15Times) {
list.push(‘FizzBuzz’);
continue;
}
if (is3Times) {
list.push(‘Fizz’);
continue;
}
if (is5Times) {
list.push(‘Buzz’);
continue;
}
list.push(${i});
}
return list;
};
复制代码

整数反转
这个题跟之前的excel序号题差不多,我们先看题目:

图片
屏幕快照 2021-07-27 上午10.55.40.png
思路如下:这道题可以将数字转字符串然后翻转,我们不用这种方法,用更纯正的数学方法,速度和效率更好。

假设我们有一个数字12345,下图展示了翻转的过程

图片
image.png
var reverse = function(x) {
let ret = 0;
while(x){
ret = ret * 10 + x % 10;
if(ret > Math.pow(2, 31) - 1 || ret < Math.pow(-2, 31)) return 0;
x = (x / 10) | 0
}
return ret
};


9、环问题

这类问题的特点就是,你要循环寻找,到底怎么循环寻找,看题便知。


9.1、环形链表

题目如下:

给定一个链表,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

如果链表中存在环,则返回 true 。否则,返回 false 。

示例 1:

图片
image.png
输入:head = [3,2,0,-4], pos = 1
输出: true
解释: 链表中有一个环,其尾部连接到第二个节点。
复制代码
示例 2:图片

输入:head = [1,2], pos = 0
输出: true
解释: 链表中有一个环,其尾部连接到第一个节点。
复制代码
我们采用标记法:

给遍历过的节点打记号,如果遍历过程中遇到有记号的说明已环

var hasCycle = function(head) {
let traversingNode = head;
while(traversingNode){
if(traversingNode.isVistitd) return true
traversingNode.isVistitd = true
traversingNode = traversingNode.next
}
return false;
};


9.2、相交链表

题目如下:

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。

图示两个链表在节点 c1 开始相交:

图片
image.png
题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构 。

示例 1:

图片
image.png
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at ‘8’
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
复制代码
示例 2:

图片
image.png
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at ‘2’
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
复制代码
稍后更新本文章


9.3、快乐数

题目如下:编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」定义为:

对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果 可以变为 1,那么这个数就是快乐数。
如果 n 是快乐数就返回 true ;不是,则返回 false 。
图片
image.png
快乐数怎么分析呢?

我们来看一个表,就会得出结论,一个数按照快乐数定义的方式分别每个数字平方,会有两种情况

得到1

无限循环
无限循环参照下图

图片
image.png
有人会说会不会一直变大,答案是不会:我们看下面列表,

可以看到如果你是13位,你的下一次快乐数算法会变为4位1053,
如果你是9999, 4位,下一个快乐数是324
位数 位数对应最大值 下一个快乐数
1 9 81
2 99 162
3 999 243
4 9999 324
13 9999999999999 1053
所以代码只要判断这两种就行了,代码如下:

// 封装获取快乐数的方法
function getNext(n){
n = String(n);
let sum = 0;
for(let num of n){
sum = sum + Math.pow(+num, 2);
}
return sum;
}
var isHappy = function(n) {
// 哈希表来看是否循环
const map = {};
while( n !== 1 ){
map[n] = true;
n = getNext(n)
if(map[n]) return false
}
return true
};
复制代码
后面会写中级算法的题,请大家务必把这些基础算法题掌握好,基础不牢地动山摇,后面中级题很多都是在这些基础题的基础上的。

你可能感兴趣的:(JavaScript,javascript)