js刷Leecode时常用到的数据结构和内置方法以及一些常见的算法 …持续更新中…
1. js的sort()方法
首先sort()若无参数,则是按照字母顺序进行排序
若对数字进行从小到大进行排序,则必须传入一个排序函数:
var arr = new Array(6)
arr[0] = "10"
arr[1] = "5"
arr[2] = "40"
arr[3] = "25"
arr[4] = "1000"
arr[5] = "1"
arr.sort((a,b)=>{
return a-b; //从小到大进行排序
return b-a; //从大到小进行排序
});
2.什么是 for…of 循环 for…in循环
for...of
语句创建一个循环来迭代可迭代的对象。在 ES6 中引入的 for...of
循环,以替代 for...in
和 forEach()
,并支持新的迭代协议。for...of
允许你遍历 Arrays(数组), Strings(字符串), Maps(映射), Sets(集合)等可迭代的数据结构等。
语法
for (variable of iterable) {
statement
}
- variable:每个迭代的属性值被分配给该变量。
- iterable:一个具有可枚举属性并且可以迭代的对象。
// array-example.js
const iterable = ['mini', 'mani', 'mo'];
for (const value of iterable) {
console.log(value);
}
// Output:
// mini
// mani
// mo
for…in遍历的变量是数组的索引
var mycars = ["Saab", "Volvo", "BMW"];
mycars.color = "white"
for (var x in mycars)
{
console.log(mycars[x]);
}
2.map()方法
//返回一个新数组
let arr1 = arr.map(el=>{
return el*el;
})
//返回一个新对象
let obj1 = obj.map(()=>{
return {
name:"",
id:""
}
})
3.let和var的区别
建议,一般在写算法题时,一般都用let不用var
1、作用域不同
var是函数作用域,let是块作用域。
在函数中声明了var,整个函数内都是有效的,比如说在for循环内定义的一个var变量,实际上其在for循环以外也是可以访问的
而let由于是块作用域,所以如果在块作用域内定义的变量,比如说在for循环内,在其外面是不可被访问的,所以for循环推荐用let
2、let不能在定义之前访问该变量,但是var可以。
let必须先声明,在使用。而var先使用后声明也行,只不过直接使用但没有定义的时候,其值是undefined。var有一个变量提升的过程,当整个函数作用域被创建的时候,实际上var定义的变量都会被创建,并且如果此时没有初始化的话,则默认为初始化一个undefined
3、let不能被重新定义,但是var是可以的
4,js中的除法不是结果不是整数
比如 3/2=1.5.
在js中一般用右移运算符 3>>1 结果就是1
当然也可以使用parseInt函数,比如:parseInt(3/2);其就是一个整数1
5.js中创建二维数组
js不能使用 let arr = new Array[2][2];来创建数组
正确写法:
方法一:创建一个n*n数组
let nums = new Array(n); //先创建一个一维数组
//在一维数组的每一个元素都是一个n为数组
for (let i = 0; i < n; i++) {
nums[i] = new Array(n);
}
方法二:
let towMetric = new Array(5).fill(0).map(ele=>new Array(5).fill(0)); //创建一个5行5列的二维数组
6.数组的一些函数
reverse()函数
//reverse()
let arr = [1,1,2,34,5];
arr.reverse(); //arr将变为[5,34,2,1,1]并return数组arr翻转后的结果
填充函数fill()
//fill()
let arr = new Array (10);
// 参数:1.填充元素 2.填充初始位置 3.填充结束位置
// arr.fill (0, 0, 5); //代表索引0-5的元素被填充初始化为0
// 如果只有一个参数,那么就是给这个数组的所有元素都填充为这个元素
// arr.fill (0);
// 也可以通过fill创建一个二维数组,参数设置为一个数组即可
arr.fill (new Array (10).fill (0)); //将会是一个10*10的二维0数组
console.log (arr);
indexOf()完整语法:
array.indexOf(item,start)
参数:
item:必须。查找的元素。
start:可选的整数参数。规定在字符串中开始检索的位置。它的合法取值是 0 到 stringObject.length - 1。如省略该参数,则将从字符串的首字符开始检索。
push()函数
在数组尾部插入元素
arr.push(1)
unshift()函数
在数组首部插入元素
pop()函数
在数组尾部移除元素,并返回所移除的元素
shift()函数
在数组首部删除元素
7.字符串的一些方法
replace()方法
// let str = "wanghe is a handsome boy!";
let str = new String("wanghe is a handsome boy!");
// 替换字符串str中的所有空格并用#代替,g代表全局替换,表示替换所有的空格,如果没有g则只能替换匹配到的第一个元素,第一个参数可以是字符串,也可以是正则表达式
let str1 = str.replace(/ /g, "#");
console.log(str1);
输出:
trim()方法
let str2 = " 1234 "
console.log(str2.trim().length);
输出:4
str.indexOf(searchString,startIndex)方法;
返回子字符串第一次出现的位置,从startIndex开始查找,找不到时返回-1
let str = new String("wanghe is a handsome boy!");
// 字符串查找方法
console.log(str.indexOf("n", 0));
输出:2
str.slice(start,end)方法
两个参数都为正数,返回值:[start,end) 也就是说返回从start到end-1的字符.
let str = new String("wanghe is a handsome boy!");
console.log(str.slice(1, 4));
输出:
str.substr(start,len)方法
let str = new String("wanghe is a handsome boy!");
console.log(str.substr(1, 4));
//输出:angh
str.chatAt(index)方法
返回字符串index位置的子字符串
let str = new String("wanghe is a handsome boy!");
console.log(str.charAt(1));
输出:a
字符串分割成数组str.split(separator,limit)
let str = "wanghe is a handsome boy!";// 字符串分割为数组
strA = str.split(" ") //返回 [ 'wanghe', 'is', 'a', 'handsome', 'boy!' ]
//数组拼接成字符串
// 数组转化为字符串,参数代表怎么连接数组的元素,如果字符串为空表示就是无缝连接,如果为空格则拼接的字符串中每个数组元素之间都会有个空格隔开
let strB = strA.join(""); // 返回wangheisahandsomeboy!
8. 哈希表
哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表,大家看到这两个名称知道都是指hash table就可以了
哈希表是根据关键码的值而直接进行访问的数据结构。
这么这官方的解释可能有点懵,其实直白来讲其实数组就是一张哈希表。
哈希表中关键码就是数组的索引下表,然后通过下表直接访问数组中的元素,如下图所示:
一般哈希表都是用来快速判断一个元素是否出现集合里。
例如要查询一个名字是否在这所学校里。
要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1) 就可以做到。
我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。
将学生姓名映射到哈希表上就涉及到了hash function ,也就是哈希函数。
哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下表快速知道这位同学是否在这所学校里了。
哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,怎么办呢?
此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,就要我们就保证了学生姓名一定可以映射到哈希表上了。
9. 二叉树
二叉树种类
解题中常用到的二叉树:满二叉树、平衡二叉树
满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
如图所示:这棵二叉树为满二叉树,也可以说深度为k,有2^k-1个节点的二叉树。
完全二叉树:
在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^h -1 个节点。
我来举一个典型的例子如题:
二叉树的存储方式
二叉树可以链式存储,也可以顺序存储。
那么链式存储方式就用指针, 顺序存储的方式就是用数组。
顾名思义就是顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在散落在各个地址的节点串联一起。
链式存储
顺序存储
其实就是用数组来存储二叉树,顺序存储的方式如图:
如果父节点的数组下表是i,那么它的左孩子就是i * 2 + 1,右孩子就是 i * 2 + 2。
但是用链式表示的二叉树,更有利于我们理解,所以一般我们都是用链式存储二叉树。
二叉树的遍历方式
二叉树主要有两种遍历方式:
- 深度优先遍历:先往深走,遇到叶子节点再往回走。
- 广度优先遍历:一层一层的去遍历。
那么从深度优先遍历和广度优先遍历进一步拓展,还有如下遍历方式:
-
深度优先遍历
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
-
广度优先遍历
- 层次遍历(迭代法)
这里前中后,其实指的就是中间节点的遍历顺序,只要大家记住 前中后序指的就是中间节点的位置就可以了。
比如:
二叉树的定义
function TreeNode(val, left, right) {
this.val = (val===undefined ? 0 : val)
this.left = (left===undefined ? null : left)
this.right = (right===undefined ? null : right)
}
二叉树的递归遍历
每次写递归,都按照这三要素来写,可以保证大家写出正确的递归算法!
- 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
- 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
- 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
// 二叉树定义
function TreeNode(val, left, right) {
this.val = val == undefined ? 0 : val;
this.left = left == undefined ? null : left;
this.right = right == undefined ? null : right;
}
// 前序遍历
var preorderTraversal = function (root, res = []) {
if (!root) {
return res;
}
// 中
res.push(root.val)
// 左
preorderTraversal(root.left, res)
// 右
preorderTraversal(root.right, res)
return res;
}
// 中序遍历
var inorderTraversal = function (root, res = []) {
if (!root) return res;
// 左
inorderTraversal(root.left, res);
// 中
res.push(root.val);
// 右
inorderTraversal(root.right, res);
return res;
}
// 后序遍历
var postorderTraversal = function (root, res = []) {
if (!root) return res;
// 左
postorderTraversal(root.left, res);
// 右
postorderTraversal(root.right, res);
// 中
res.push(root.val);
return res;
}
let root = new TreeNode(2, new TreeNode(1, new TreeNode(7)), new TreeNode(3));
/* // 前序遍历(中间节点最先遍历)为 2 1 7 3
let res = preorderTraversal(root); */
/* // 中序遍历(中间节点中间遍历)为 7 1 2 3,
let res = inorderTraversal(root); */
// 后序遍历(中间节点最后遍历)为 7 1 3 2
let res = postorderTraversal(root);
console.log(res);
二叉树的迭代遍历
为什么可以用迭代法(非递归的方式)来实现二叉树的前后中序遍历呢?
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
我们用栈也可以是实现二叉树的前后中序遍历了。
10. 动态规划DP问题
状态转移公式(递推公式)是很重要,但动规不仅仅只有递推公式。
对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
比如:
/*利用动态规划做此题
斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。
也就是: F(0) = 0,F(1) = 1 F(n) = F(n - 1) + F(n - 2),其中 n > 1 给你n ,请计算 F(n) 。
示例 1: 输入:2 输出:1 解释:F(2) = F(1) + F(0) = 1 + 0 = 1
示例 2: 输入:3 输出:2 解释:F(3) = F(2) + F(1) = 1 + 1 = 2
示例 3: 输入:4 输出:3 解释:F(4) = F(3) + F(2) = 2 + 1 = 3 */
var fiber = function(n) {
//创建dp数组
let arr = new Array(n);
arr[0] = 0;
arr[1] = 1;
for (let i = 2; i <= n; i++) {
arr[i] = arr[i - 1] + arr[i - 2];
}
console.log(arr);
return arr[n];
}
fiber(5);
回溯问题
回溯算法能解决如下问题:
- 组合问题:N个数里面按一定规则找出k个数的集合
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 棋盘问题:N皇后,解数独等等
每一道回溯法的题目我都将遍历过程抽象为树形结构方便大家的理解
在关于回溯算法,你该了解这些! (opens new window)还用了回溯三部曲来分析回溯算法,并给出了回溯法的模板:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}