数据结构与算法简介
数据结构与算法的关系
- 数据结构:数据结构是计算机存储、组织数据的方式;是指相互之间存在一种或多种特定关系的数据元素的集合。数据结构有很多种,一般分为线性结构和非线性结构两类。
- 算法:算法是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。
- 程序=数据结构+算法
- 数据结构为算法提供服务,算法围绕数据结构操作
常见数据结构类型
- 有序:栈、队列、链表
- 无序:集合、字典
- 有连接关系:树、堆、图
时间复杂度
- 时间维度:是指执行当前算法所消耗的时间,我们通常用「大O符号表示法」来描述。
空间复杂度
- 空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「大O符号表示法」来描述。
栈
栈简介
- 一个后进先出的数据结构
- JavaScript中没有栈,但是可以用Array实现栈的所有功能。
- push:用于向数组最后添加一个元素,返回值为数组的长度
- pop: 用于弹出数组最后一个元素,返回值是弹出的元素值
const stack = [];
stack.push(1);// [1]
stack.push(2);// [1,2]
const item1 = stack.pop(); // 2
const item2 = stack.pop(); // 1
应用场景
- 需要后进先出的场景
- 十进制转二进制
- 判断字符串括号是否有效
- 函数调用堆栈(常用于写js解析器)
- ......
判断字符串括号是否有效(leetcode—20题)
题目
给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。 注意空字符串可被认为是有效字符串。
示例 1:
输入: "([)]"
输出: false
示例 2:
输入: "{[]}"
输出: true
解题思路
- 新建一个栈
- 扫描字符串,遇到左括号入栈,遇到和栈顶括号类型匹配的右括号就出栈,类型不匹配直接判定为不合法
- 最后栈为空则合法,否则不合法
解题代码
/**
* @param {string} s
* @return {boolean}
*/
var isValid = function(s) {
// 判断字符串是否为偶数,如果不为偶数直接返回false
if(s.length%2 === 1){return false}
const stack = []
for(let i=0;i<s.length;i++){
const p = s[i]
if( p==='(' || p==='{' || p==='[' ){
stack.push(p)
}else{
// 获取栈顶元素
const t = stack[stack.length-1]
if(
(t === '(' && p === ')')||
(t === '{' && p === '}')||
(t === '[' && p === ']')
){
stack.pop()
}else{
return false
}
}
}
// 若是最后栈为空,则返回true 否则返回false
return stack.length === 0
};
复杂度分析
-
时间复杂度:O(n),其中 n 是字符串 s 的长度。
-
空间复杂度:O(n ),其中 n 是字符串 s 的长度(最坏的情况下所有字符都push进了栈中)。
Leetcode官方题解
解题思路
官方的解题思路与原先的解题思路大同小异。官方的解题方法使用了ES6提供Map
数据结构。
接替代码
var isValid = function(s) {
const n = s.length;
if (n % 2 === 1) {
return false;
}
//使用哈希映射(HashMap)存储每一种括号。哈希映射的键为右括号,值为相同类型的左括号。
const pairs = new Map([
[')', '('],
[']', '['],
['}', '{']
]);
const stk = [];
// 将字符串分割成字符串数组遍历每一个字符
s.split('').forEach(ch => {
//假设s='{()[]}'
//Map.get(key) 获取对应key值的value
//console.log(pairs.get(ch))
/*输出:
undefined //因为key值'{'在pairs中没有定义。paris中定义的为:key:"}",value:"{"
undefined
( //当传入第三个字符“)”时,在pairs中找到key值为")",对应的value为“(”
undefined
[
{
*/
//Map.has(key) 返回一个布尔值,表示某个键是否在当前 Map 对象之中
//console.log(pairs.has(ch))
/*输出:
false ‘{’这个键值不在Map对象中
false
true
false
true
true
*/
//当pairs.has(ch)为true时,表示传入的为右括号,则将栈顶元素提出进行匹配
if (pairs.has(ch)) {
if (!stk.length || stk[stk.length - 1] !== pairs.get(ch)) {
// 若是栈为空,或者栈顶元素与右括号不匹配,则返回false
return false;
}
stk.pop();
}
else {
stk.push(ch);
}
});
return !stk.length;
};
复杂度分析
-
时间复杂度:O(n),其中 n 是字符串 s 的长度。
-
空间复杂度:O(n + ∣Σ∣),其中 Σ 表示字符集,本题中字符串只包含 6种括号,∣Σ∣=6。栈中的字符数量为 O(n),而哈希映射使用的空间为 O(∣Σ∣),相加即可得到总空间复杂度。
队列
队列简介
- 一个先进先出的数据结构
- JavaScript中没有队列,但是可以用Array实现队列的所有功能。
- push:用于向数组最后添加一个元素,返回值为数组的长度
- shift:用于弹出数组第一个元素,返回值为弹出数值
const queue = [];
queue.push(1); // [1]
queue.push(2); // [1,2]
const item1 = queue.shift();// 1 [2]
const item2 = queue.shift();// 2 []
应用场景
- 需要先进先出的场景
- JS异步中的任务队列
- 计算最近请求次输
- ......
最近的请求次数(leetcode—933题)
题目
写一个 RecentCounter 类来计算最近的请求。
它只有一个方法:ping(int t),其中 t 代表以毫秒为单位的某个时间。
返回从 3000 毫秒前到现在的 ping 数。
任何处于 [t - 3000, t] 时间范围之内的 ping 都将会被计算在内,包括当前(指 t 时刻)的 ping。
保证每次对 ping 的调用都使用比之前更大的 t 值。
示例:
输入:inputs = ["RecentCounter","ping","ping","ping","ping"], inputs = [[],[1],[100],[3001],[3002]]
输出:[null,1,2,3,3]
解题思路
- 输入:inputs = [[],[1],[100],[3001],[3002]]
输出:[null,1,2,3,3]
- 当时间为1毫秒时,请求执行了一次[1]
- 当时间为100毫秒时,请求执行了两次[1],[100]
- 当时间为3001毫秒时,请求执行了三次[1],[100],[3001]。 3000-1=1, [1,3000]请求时间为闭区间。
- 当时间为3002毫秒时,请求执行了三次[100],[3001],[3002],当请求时间为3002时,1毫秒已经不在3000毫秒内,已经出队
解题代码
var RecentCounter = function() {
//将队列挂在到this上,这样他的类方法就都能访问到了
this.q = [];
};
/**
* @param {number} t
* @return {number}
*/
RecentCounter.prototype.ping = function(t) {
this.q.push(t);
// 判断队头是否在3000毫秒内
while(this.q[0] < t - 3000){
// 若队头不在3000毫秒内,则出队
this.q.shift();
}
return this.q.length
};
/**
* Your RecentCounter object will be instantiated and called as such:
* var obj = new RecentCounter()
* var param_1 = obj.ping(t)
*/
复杂度分析
-
时间复杂度:O(n),其中 n 是被踢出队列的请求个数。
-
空间复杂度:O(n ),其中 n 是队列的长度。
带你彻底弄懂Event Loop
链表
链表简介
- 多个元素组成的列表
- 元素存储不连续,用next指针连在一起
- 数组vs链表
- 数组:增删非首尾元素是往往需要移动元素
- 链表:增删非首尾元素时,不需要移动元素,只需要更改next的指向即可
- JavaScript中没有链表,但是可以用Object模拟。
const a = { val: 'a' };
const b = { val: 'b' };
const c = { val: 'c' };
const d = { val: 'd' };
a.next = b;
b.next = c;
c.next = d;
//console.log(a) { val: 'a', next: { val: 'b', next: { val: 'c', next: [Object] } } }
// 遍历链表
let p = a;
while (p) {
console.log(p.val); // a b c d
p = p.next;
}
// 插入
const e = { val: 'e' };
c.next = e;
e.next = d;
// 删除
c.next = d;
删除链表中的节点(leetcode—237题)
题目
示例 1:
输入:head = [4,5,1,9], node = 5
输出:[4,1,9]
解释:给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.
解题思路
- 从链表里删除一个节点的最常见方法是修改之前节点的
next
指针,使其指向之后的节点。 - 但是我们无法访问我们想要删除的节点之前的节点。
- 我们可以将要删除的节点的值替换成后一个节点的值,然后将指针指向要删除的节点的后一个节点的下一个节点(即删除要删除节点的后一个节点)。
解题代码
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} node
* @return {void} Do not return anything, modify node in-place instead.
*/
var deleteNode = function(node) {
// 将要删除节点的后一个节点的值赋值给当前节点
node.val = node.next.val
// 指针只想它的下下个节点
node.next = node.next.next
};
复杂度分析
-
时间复杂度:O(1)。
-
空间复杂度:O(1 )。
反转链表(leetcode—206题)
题目
反转一个单链表。
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
解题思路
-
反转两个节点(n ➡ n+1)➡ (n+1 ➡ n): 将n+1的next指向n即可
-
反转多个节点:使用双指针遍历链表,重复反转两个节点操作
-
输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL
- 首先定义两个指针,指针p1指向头节点1,指针p2指向null
- 开始遍历
- 第一次遍历
- tmp = p1.next = [2,3,4,5] 此时tmp指向2
- p1.next=p2=null
- p2 = p1=[1] 此时p2指向1
- p1 = tmp 此时p1指向2
- null<-1->2->3->4->5(此时p2指向1,p1指向2)
- 第二次遍历
- tmp = p1.next = [3,4,5] 此时tmp指向3
- p1.next=p2=[1]
- p2 = p1=[2,1] 此时p2指向2
- p1 = tmp 此时p1指向3
- null<-1<-2->3->4->5(此时p2指向2,p1指向3)
- .........
- 第一次遍历
解题代码
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
let p1 = head;
let p2 = null;
while(p1){
const tmp = p1.next
p1.next = p2 //反转
// 指针前移
p2 = p1
p1 = tmp
}
return p2
};
复杂度分析
-
时间复杂度:O(1)。
-
空间复杂度:O(1 )。
两数相加(leetcode—2题)
题目
给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。
如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。
您可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例:
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807
解题思路
-
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4) 输出:7 -> 0 -> 8 原因:342 + 465 = 807
-
新建一个空链表
-
遍历两个链表,进行相加操作,各位数追加到新链表上,十位数留到下一位去相加
解题代码
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {ListNode}
*/
var addTwoNumbers = function(l1, l2) {
// 定义一个空链表
const l3 = new ListNode()
// 定义三个指针分别指向三个链表
let p1 = l1
let p2 = l2
let p3 = l3
// 定义一个变量表示十位数的那个数字
let arr = 0
//如果p1,p2任意一个指针不为空则进行遍历操作
while(p1||p2){
// 判断指针指向是否存在值,如果存在则取出,如果不存在则设为0,0加任意数为任意数
const v1 = p1?p1.val:0
const v2 = p2?p2.val:0
const val = v1+v2+arr
// floor() 方法返回小于等于x的最大整数。 1.6->1
arr=Math.floor(val/10)
// 将和插入链表中
p3.next = new ListNode(val%10)
// 判断指针是否为空,不为空则指向下一位,进行遍历
if(p1){
p1=p1.next
}
if(p2){
p2 = p2.next
}
p3 = p3.next
}
// 如果最后两个数相加为十位数,则将这个十位数的数插入的链表中
if(arr){
p3.next = new ListNode(arr)
}
return l3.next
};
};
复杂度分析
-
时间复杂度:O(n),n是链表l1,l2链表长度的较大值。
-
空间复杂度:O(n),n是链表l1,l2链表长度的较大值。
删除排序链表中的重复元素(leetcode—83题)
题目
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
示例 :
输入: 1->1->2
输出: 1->2
解题思路
- 因为链表是有序的,所以重复元素一定相邻
- 遍历链表,如果当前元素和下一个元素值相同,则删除下一个元素
解题代码
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var deleteDuplicates = function(head) {
// 定义一个指针指向链表头节点
let p = head
// 判断如果链表和链表的下一个节点都存在,则执行遍历
while( p && p.next ){
if(p.val===p.next.val){
p.next = p.next.next
}
else{
p = p.next
}
}
return head
};
复杂度分析
-
时间复杂度:O(n),n为链表的长度。
-
空间复杂度:O(1)。
环形链表(leetcode—141题)
题目
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
示例 :
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
解题思路
- 两个人在圆形操场起点同时起跑,速度快的人会超过速度慢的人最后会重逢
- 用一快一慢两个指针遍历链表,如果指针发生重逢,即指向的值相等。那么链表就有圆。
解题代码
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {boolean}
*/
var hasCycle = function(head) {
// 定义两个一快(p2)一慢(p1)指针
let p1 = head
let p2 = head
// 判断p1,p2,p2.next指向不为空
while( p1 && p2 && p2.next ){
p1 = p1.next
p2 = p2.next.next
if(p1 === p2){
return true
}
}
return false
};
复杂度分析
-
时间复杂度:O(1)。
-
空间复杂度:O(1 )。
JS原型链
原型链简介
-
原型链的本质是链表
-
原型链上的节点是各种原型对象,比如:Function.prototype、Object.prototype........
-
原型链通过
__proto__
属性连接各种原型对象 -
原型链结构:
-
对象实例obj:obj -> Object.prototype -> null
- (
obj.__proto__
=== Object.prototype )
- (
-
函数实列func:func -> Function.prototype -> Object.prototype -> null
- (
func.__proto__
=== Function.prototype ||func.__proto__.__proto__
=== Object.prototype )
- (
-
数组实例arr:arr -> Array.prototype -> Object.prototype -> null
- (
arr.__proto__
=== Array.prototype ||arr.__proto__.__proto__
=== Object.prototype )
- (
-
对于JS变量,除了对象类型,其他类型的原型链先指向自己类型的原型对象,最后指向 Object原型对象
-
原型链知识点
-
如果A沿着原型链能找到B.prototype,那么A instanceof B 为true
- 如:func instanceof Object.prototype 结果为:true
- instanceof 和 typeof 的区别
- typeof
- 用于判断数据类型,返回值为6个字符串,分别为
string
、Boolean
、number
、function
、object
、undefined
typeof
在判断null
、array
、object
以及函数实例(new + 函数)
时,得到的都是object
。这使得在判断这些数据类型的时候,得不到真是的数据类型。由此引出instanceof
。
- 用于判断数据类型,返回值为6个字符串,分别为
- instanceof
- 用于判断一个变量是否某个对象的实例 / 判断一个对象在其原型链中是否存在一个构造函数的
prototype
属性
- 用于判断一个变量是否某个对象的实例 / 判断一个对象在其原型链中是否存在一个构造函数的
- typeof
-
如果A对象上没有找到X属性,那么会沿着原型链找x属性
const obj = {} // obj.x = undefind Object.prototype.x = 'x' // obj.x = 'x' const func = () =>{} Function.prototype.y = 'x' //func.y = 'y' func.x = 'x'
-
instanceod 原理和代码实现
- 知识点:如果A沿着原型链能找到B.prototype,那么A instanceof B 为true
- 解法:遍历A的原型链,如果能找到B.prototype,返回true,否则返回false
const instanceOf = (A, B) => { //定义一个指针指向A let p = A while(p){ // 判断p的指向上是否存在一个B.prototype if( p === B.prototype){ return true } // 指针遍历 p = p.__proto__ } return false }
集合
集合简介
- 一种无序且唯一的数据结构
- ES6中有集合,名为Set。
- 集合的常用操作:去重、判断某元素是否在集合中、求交集.......
// 去重
const arr = [1, 1, 2, 2];
const arr2 = [...new Set(arr)];
// 判断元素是否在集合中
const set = new Set(arr);
const has = set.has(3);
// 求交集
const set2 = new Set([2, 3]);
const set3 = new Set([...set].filter(item => set2.has(item)));
ES6-Set
两个数组的交集(leetcode—349题)
题目
给定两个数组,编写一个函数来计算它们的交集。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
解题思路
- 首先对数组进行去重
- 然后遍历nums1,筛选出nums2中包含nums1的值
解题代码
// 方法一
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
*/
var intersection = function(nums1, nums2) {
return [...new Set(nums1)].filter(item => new Set(nums2).has(item))
};
// 方法二
//includes(searchElement,fromIndex) 方法用来判断一个数组是否包含一个指定的值,如果是返回 true,否则false。
//searchElement 必须。需要查找的元素值。
//fromIndex 可选。查询开始的索引位置。默认为 0。
var intersection = function(nums1, nums2) {
return [...new Set(nums1)].filter(item => nums2.includes(item))
};
复杂度分析
-
时间复杂度:O(n2) / O(m*n),其中 m 是filter循环的次数,n是includes遍历的次数。
-
空间复杂度:O(n ),其中 n 是去重后数组的长度。
Set操作
// 实例化一个set
let mySet = new Set();
// 添加元素
mySet.add(1);
mySet.add(5);
mySet.add(5);
mySet.add('some text');
let o = { a: 1, b: 2 };
mySet.add(o);
mySet.add({ a: 1, b: 2 });// 看起来这个对象和o一样,但是他们两个在内存中存储的位置不一样,本质上这是两个不同的对象
// 查询元素是否在数组中
const has = mySet.has(o);
// 删除元素
mySet.delete(5);
//获取set的长度
mySet.size()
// 迭代Set
for(let item of mySet) console.log(item);
for(let item of mySet.keys()) console.log(item);
for(let item of mySet.values()) console.log(item); // 对于Set而言keys和values其实是一样的
//entries() 方法返回一个数组的迭代对象,该对象包含数组的键值对 (key/value)。
for(let [key, value] of mySet.entries()) console.log(key, value);
// Set转Array
const myArray = [...mySet] //解构赋值
const myArr = Array.from(mySet);
// Array转Set
const mySet2 = new Set([1,2,3,4]);
// 求交集
// 将set转array,求交集后再转为set
const intersection = new Set([...mySet].filter(x => mySet2.has(x)));
// 求差集
const difference = new Set([...mySet].filter(x => !mySet2.has(x)));
字典
字典简介
- 与集合类似,字典也是一种存储唯一值的数据结构,但是它是以键值对的形式来存储的。
- ES6 中有字典,名为Map。
- 字典常用操作:键值对的增删改查
const m = new Map();
// 增
m.set('a', 'aa');
m.set('b', 'bb');
// console.log(m) // Map { 'a' => 'aa', 'b' => 'bb' }
// 查
const c = m.get('a')
// console.log(c) // aa
// 删
m.delete('b');
// m.clear(); // 删除所有
// 改
m.set('a', 'aaa');
两个数组的交集(leetcode—349题)
题目
给定两个数组,编写一个函数来计算它们的交集。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
解题思路
- 新建一个字典,用字典建立一个映射关系,记录nums1中的值
- 遍历nums2,选出nums1中也有的值,并将该值从字典中删除,避免重复。
解题代码
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
*/
var intersection = function(nums1, nums2) {
// 新建一个map字典
let map = new Map()
// 遍历nums1,将nums1的值映射为key,value为true
nums1.forEach(n=>{
map.set(n,true)
})
// 新建一个数组存储交集
let res = []
// 遍历nums2查找交集,如果存在,则将字典中的值删除,防止后续重复
nums2.forEach(n => {
if(map.get(n)){
res.push(n)
map.delete(n)
}
})
return res
};
复杂度分析
-
时间复杂度:O(m+n),其m是nums1的长度,n是nums2的长度。
-
空间复杂度:O(m),其m是nums1的长度。
两数之和(leetcode—1题)
题目
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
解题思路
-
nums为条件选项
-
target为匹配条件
-
新建一个字典,存储条件选项中的数字和下标。查询字典中是否存在匹配条件
解题代码
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function(nums, target) {
// 新建一个字典
let map = new Map()
// 遍历数组
for(let i=0; i<nums.length; i++){
// 获取数组下标对应的值
const n = nums[i]
// 获取匹配条件
const m = target - n
// 查询匹配条件,第一次时,字典为空,肯定会将元素添加到字典中。之后则对于字典中的数字进行匹配。因此m一定是字典中已经存在的元素,下表要放在最前面,然后才是当前的下标。
if(map.has(m)){
return [map.get(m),i]
}else{
map.set(n,i)
}
}
};
复杂度分析
- 时间复杂度:O(n),其中 n 是数组的长度。
- 空间复杂度:O(n ),其中 n 是字典的长度。
无重复字符的最长子串(leetcode—3题)
题目
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
解题思路
- 先找出所有的不包含重复字符的字串
- 找出长度最大的那个字串,返回其长度
- 用一个双指针维护一个滑动窗口,用来剪切字串。(如同slice())
- 不断移动右指针,遇到重复字符则把左指针移动到重复字符的下一位
- 过程中记录所有窗口的长度,并返回最大值
解题代码
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
// 定义一个左指针
let l = 0
// 定义无重复字符串长度
let len = 0
const map = new Map()
// 定义右指针遍历字符串
for(let r = 0; r < s.length; r++){
// 判断如果该字符存在与字典中,并且该字符在两个指针包含的范围中
// 如字符串"abbsdwa",当右指针遍历到最后一个a时,此时左指针在第二个字符b上,而重复字符a则在左指针之外。
// 若是没有这个判断,则左指针将跳到第一个字符b上
if(map.has(s[r]) && map.get(s[r])>= l ){
l = map.get(s[r])+1
}
// 获取最大长度
len = Math.max(len,r-l+1)
map.set(s[r],r)
}
return len
};
复杂度分析
- 时间复杂度:O(n),其中 n 是字符串的长度。
- 空间复杂度:O(n ),其中 n 是字符串中不重复的长度。
最小覆盖子串(leetcode—76题)
题目
给你一个字符串 S、一个字符串 T 。请你设计一种算法,可以在 O(n) 的时间复杂度内,从字符串 S 里面找出:包含 T 所有字符的最小子串。
示例:
输入:S = "ADOBECODEBANC", T = "ABC"
输出:"BANC"
提示:
- 如果 S 中不存这样的子串,则返回空字符串 ""。
- 如果 S 中存在这样的子串,我们保证它是唯一的答案。
解题思路
- 找出所有包含T的字串
- 找出长度最小的那个字串,返回即可
- 用双指针维护一个滑动窗口
- 移动右指针,找到包含T的字串,移动左指针,尽量减少包含T的字串的长度
- 循环上述过程,找到包含T的最小字串
解题代码
/**
* @param {string} s
* @param {string} t
* @return {string}
*/
var minWindow = function(s, t) {
// 定义左指针和右指针
let l = 0
let r = 0
// 定义一个字典
const map = new Map
// 遍历T字符,将字符存入字典中,并统计其字符长度
for(let c of t){
map.set(c,map.has(c)?map.get(c)+1:1)
}
// 获取字典长度,即所需字符种类的个数
let needType = map.size
// 记录最小字符串长度
let res = ''
// 右指针开始遍历
while(r < s.length){
// 获取右指针指向的字符
const c = s[r]
// 判断字典中是否存在这个字符
if(map.has(c)){
// 如果存在这个字符,则这个字符所需的个数减一
map.set(c,map.get(c)-1)
// 如果这个字符的个数为0,则说明字符串中已经满足了该字符的条件,则字符种类的个数减一
if(map.get(c)===0){
needType -= 1
}
}
// 当字符种类个数为0,说明字符串中包含了所需字符,此时需要进行左指针的移动,减小字符串的长度
while(needType===0){
// console.log(s.substring(l,r+1)) 打印双指针所有包含所需字符的字符串
// 保存双指针所有包含所需字符的字符串
const newRes = s.substring(l,r+1)
// 比较字符串长度,保存最短字符串
// 因为最开始字符串为空,所以第一轮先将字符串赋值给res
if(!res || newRes.length < res.length){
res = newRes
}
// 获取左指针指向的字符
const c2 = s[l]
// 如果当前左指针指向的字符是T中字符串的一个字符,当左指针右移,这个字符就不在双指针的范围内了
if(map.has(c2)){
// 因此要将字典中该字符的长度加1
map.set(c2,map.get(c2)+1)
// 此时我们再一次需要这个字符,needType也就不再为0
if(map.get(c2) === 1){
// needType不再为0,跳出循环,右指针继续移动
needType += 1
}
}
// 左指针移动
l += 1
}
r += 1
}
return res
};
复杂度分析
- 时间复杂度:O(m+n),其中m是t的长度,n是s的长度 。
- 空间复杂度:O(m),m是t中不同字符的个数。
树
树简介
- 一种分层数据的抽象模型。
- 前端常见树包括:DOM树、级联选择、树形控件......
- JS中没有树,但是可以用Object和Array来构建树
- 树的常用操作:深度/广度优先遍历、先中后序遍历
树的深度/广度优先遍历
- 深度优先遍历(dfs):尽可能深的搜索树的分支
- 算法口诀:访问根节点,对根节点的children挨个进行深度优先遍历(相当于一个递归)
{
a:{//1
b:{//2
d:{},//3
e:{}//4
},
c:{//5
f:{},//6
g:{}//7
}
}
}
const tree = {
val: 'a',
children: [
{
val: 'b',
children: [
{
val: 'd',
children: [],
},
{
val: 'e',
children: [],
}
],
},
{
val: 'c',
children: [
{
val: 'f',
children: [],
},
{
val: 'g',
children: [],
}
],
}
],
};
const dfs = (root) => {
console.log(root.val);
/*
a
b
d
e
c
f
g
*/
root.children.forEach(dfs);
};
dfs(tree);
- 广度优先遍历(bfs):先访问离根节点最近的节点
- 算法口诀:
- 新建一个队列,把根节点入队
- 把队头出队并访问
- 把队头的children挨个入队
- 重复二、三步骤,直到队列为空
- 算法口诀:
{
a:{//1
b:{//2
d:{},//4
e:{}//5
},
c:{//3
f:{},//6
g:{}//7
}
}
}
const tree = {
val: 'a',
children: [
{
val: 'b',
children: [
{
val: 'd',
children: [],
},
{
val: 'e',
children: [],
}
],
},
{
val: 'c',
children: [
{
val: 'f',
children: [],
},
{
val: 'g',
children: [],
}
],
}
],
};
const bfs = (root) => {
// 根节点入队
const q = [root];
// 队列不为空的情况下,循环二、三步骤
while (q.length > 0) {
// 获取队头
const n = q.shift();
console.log(n.val);
// 队头children入队
n.children.forEach(child => {
q.push(child);
});
}
};
bfs(tree);
二叉树的先中后序遍历
二叉树
-
树中每个节点最多只能有两个子节点。
-
在JS中通常用Object来模拟二叉树
-
定义一个二叉树
const bt = { val: 1, left: { val: 2, left: { val: 4, left: null, right: null, }, right: { val: 5, left: null, right: null, }, }, right: { val: 3, left: { val: 6, left: null, right: null, }, right: { val: 7, left: null, right: null, }, }, }; module.exports = bt;
先序遍历
-
算法口诀:
- 访问根节点
- 对根节点的左子树进行先序遍历
- 对根节点的右子树进行先序遍历
-
先序遍历
const bt = require('./bt'); const preorder = (root) => { // 如果节点下为空,则直接返回 if (!root) { return; } console.log(root.val); // 1 2 4 5 3 6 7 preorder(root.left); preorder(root.right); }; // 非递归 // const preorder = (root) => { // if (!root) { return; } // 新建一个栈,保存根节点 // const stack = [root]; // 当栈中有元素时 // while (stack.length) { // 访问栈顶元素,并打印它的值 // const n = stack.pop(); // console.log(n.val); // 如果栈顶元素(根节点)下有左右子树,先将右只需添加到栈中,再将左子树添加到栈中(栈是先将后出,先序遍历访问左子树在,访问右子树在后) // if (n.right) stack.push(n.right); // if (n.left) stack.push(n.left); // } // }; preorder(bt);
中序遍历
-
算法口诀:
- 对根节点的左子树进行中序遍历
- 访问根节点
- 对根节点的右子树进行中序遍历
-
中序遍历
const bt = require('./bt'); const inorder = (root) => { if (!root) { return; } inorder(root.left); console.log(root.val);//4 2 5 1 6 3 7 inorder(root.right); }; // const inorder = (root) => { // if (!root) { return; } // const stack = []; // let p = root; // while (stack.length || p) { // 将所有的左子树推入栈中 // while (p) { // stack.push(p); // p = p.left; // } // const n = stack.pop(); // console.log(n.val); // p = n.right; // } // }; inorder(bt);
后序遍历
-
算法口诀:
- 对根节点的左子树进行后序遍历
- 对根节点的右子树进行后序遍历
- 访问根节点
-
后序遍历
const bt = require('./bt'); const postorder = (root) => { if (!root) { return; } postorder(root.left); postorder(root.right); console.log(root.val); }; // 思路:后序遍历是左右根,如果按根右左倒过来访问,就和先序遍历有些相似了。 // const postorder = (root) => { // if (!root) { return; } // const outputStack = []; // const stack = [root]; // while (stack.length) { // const n = stack.pop(); // outputStack.push(n); // if (n.left) stack.push(n.left); // if (n.right) stack.push(n.right); // } // while(outputStack.length){ // const n = outputStack.pop(); // console.log(n.val); // } // }; postorder(bt);
二叉树的最大深度(leetcode—104题)
题目
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例 : 给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最大深度 3 。
解题思路
- 求最大深度,考虑使用深度优先遍历
- 在深度优先遍历的过程中,记录每个节点所在的层级,找出最大层级即可
- 新建一个变量,记录最大深度
- 深度优先遍历整棵树,并记录每个节点的层级,同时不断刷新最大深度这个变量
- 遍历结束后返回最大深度这个变量
解题代码
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
var maxDepth = function(root) {
// 定义一个res存储最大深度
let res = 0
// 定义一个深度遍历函数n为节点,l为深度
const dfs = (n,l) =>{
if(!n){return;}
// 如果节点无左右子树,表示该节点为叶子节点,进行一个深度比较,获取最大深度
if(!n.left && !n.right){
res = Math.max(res,l)
}
// console.log(n.val)
dfs(n.left,l+1)
dfs(n.right,l+1)
}
dfs(root,1)//最开的深度为1
return res
};
复杂度分析
- 时间复杂度:O(n),其n是整棵树的节点数。
- 空间复杂度:O(n),函数中调用函数,会形成隐形的堆栈,因此会存在空间复杂度。函数dfs嵌套的函数的层数其实就是二叉树最大的深度。二叉树的最大深度和节点数的关系:最坏的情况下(节点沿着一个方向延续下去),节点数等于最大深度空间复杂度为O(n)。最好的情况下就是完全二叉树,其空间复杂度为O(logn)
二叉树的最小深度(leetcode—111题)
题目
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例 : 给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最小深度 2 。
解题思路
-
求最小深度,考虑使用广度优先遍历
-
在广度优先遍历的过程中,遇到叶子节点,停止遍历,返回节点层级
解题代码
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
var minDepth = function(root) {
if(!root){return 0;}
// 定义一个栈存储节点和节点层级
const q = [[root, 1]];
while(q.length){
// 提取栈顶元素
const [n, l] = q.shift();
// 如果该节点没有左右子树,表示到达叶子节点,返回其层级
if(!n.left && !n.right){
return l;
}
// 如果该节点有左/右子树,将其推入栈中,并将层级+1
if(n.left){q.push([n.left,l+1])}
if(n.right){q.push([n.right,l+1])}
}
};
复杂度分析
- 时间复杂度:O(n),其中 n 是树的节点数量。
- 空间复杂度:O(n ),其中 n 是树的节点数量。
二叉树的层序遍历(leetcode—102题)
题目
给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
示例: 二叉树:[3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其层次遍历结果:
[
[3],
[9,20],
[15,7]
]
解题思路
- 层级遍历顺序就是广度优先遍历
- 不过在遍历的时候需要记录当前节点所处的层级,方便将其添加到不同的数组中
解题代码
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number[][]}
*/
// 方法一:
var levelOrder = function (root) {
if (!root) return [];
// 定义一个栈,保存节点及层级
const q = [[root, 0]]
// 定义一个空数组存放结果
const res = []
// 开始遍历
while(q.length) {
// 取出栈顶元素及层级
const [n, level] = q.shift();
// console.log(n.val,level)
// 判断数组对应的下表是否为空,如果为空,直接将节点值传入。如果不为空,则在对应的下标(层级)中添加元素
if(!res[level]){
res.push([n.val])
}else{
res[level].push(n.val)
}
// 遍历左右子树,并将其推入栈中
if (n.left) {
q.push([n.left, level + 1])
}
if (n.right) {
q.push([n.right, level + 1])
}
}
return res;
};
// 方法二:
var levelOrder = function (root) {
if (!root) return [];
// 定义一个栈,保存节点
const q = [root]
// 定义一个空数组存放结果
const res = []
// 开始遍历
while (q.length) {
// 获取栈中的长度,及层级上的节点个数
let len = q.length
//先向数组中传入一个空数组,以便在循环中向数组中添加元素
res.push([])
// 保证每次循环推入的值都是一个层级的
while (len--) {
// 取出栈顶元素及层级
const n = q.shift();
// 将该层级的节点值依次加入先前创建的空数组中
res[res.length-1].push(n.val)
if (n.left) {
q.push(n.left)
}
if (n.right) {
q.push(n.right)
}
}
}
return res;
};
复杂度分析
- 时间复杂度:O(n),其中 n 是树的节点数。
- 空间复杂度:O(n ),其中 n 是树的节点数。
二叉树的中序遍历(leetcode—94题)
题目
-
定一个二叉树,返回它的中序 遍历。
** 示例:**
输入: [1,null,2,3] 1 \ 2 / 3 输出: [1,3,2]
解题思路
- 中序遍历口诀
解题代码
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
// 递归法
var inorderTraversal = function (root) {
// 创建一个存放结果的数组
const res = []
// 创建一个递归函数
const rec = (n)=>{
if(!n) return;
// 中序遍历
rec(n.left)
res.push(n.val)
rec(n.right)
}
rec(root)
return res
};
// 迭代法
var inorderTraversal = function (root) {
// 创建一个存放结果的数组
const res = []
// 创建一个栈
const stack = []
// 创建一个指针指向根节点
let p = root
while (stack.length || p) {
// 当指针不为空将左子树推入栈中
while (p) {
stack.push(p)
p = p.left
}
// 将栈顶元素取出,将其值推入数组中
const n = stack.pop()
res.push(n.val)
// 左子树推入数组后,将指针指向其右子树
p = n.right
}
return res
};
复杂度分析
- 时间复杂度:O(n),其中n是树的节点数量 。
- 空间复杂度:O(n),其中n是树的节点数量 。
路径总和(leetcode—112题)
题目
-
给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。
说明: 叶子节点是指没有子节点的节点。
示例: 给定如下二叉树,以及目标和 sum = 22,
5 / \ 4 8 / / \ 11 13 4 / \ \ 7 2 1
返回 true, 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2。
解题思路
- 在深度优先遍历的过程中,记录当前路径的节点值的和
- 在叶子节点处,判断当前路径的节点值的和是否等于目标值
解题代码
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @param {number} sum
* @return {boolean}
*/
var hasPathSum = function(root, sum) {
if(!root) return false;
// 定义一个默认返回值
let res = false
const dfs = (n,m) =>{
// console.log(n.val,sum)
// 如果节点为叶子节点。并且和为目标和,则res设为true
if(!n.left && !n.right && m===sum) res = true
// 深度优先遍历,并将个节点的值相加
if(n.left) dfs(n.left,m+n.left.val)
if(n.right) dfs(n.right,m+n.right.val)
}
// 调用深度优先遍历,并将根节点的值传入
dfs(root,root.val)
return res
};
复杂度分析
- 时间复杂度:O(n),其中n是树的节点树 。
- 空间复杂度:O(n),n是递归堆栈的高度,即树的高度n,如果是完全二叉树则是logn。
遍历JSON的所有节点值
const json = {
a: { b: { c: 1 } },
d: [1, 2],
};
const dfs = (n, path) => {
console.log(n, path);
Object.keys(n).forEach(k => {
dfs(n[k], path.concat(k));
});
};
dfs(json, []);
/* console.log输出
//第一次n为json中的所有属性,节点为空
{ a: { b: { c: 1 } }, d: [ 1, 2 ] } []
// 第二次通过object.keys()获取了节点为a的属性值
{ b: { c: 1 } } [ 'a' ]
// 通过递归调用dfs,第三次获取了节点为b的属性值
{ c: 1 } [ 'a', 'b' ]
// 第三次获取了节点为c的属性值
1 [ 'a', 'b', 'c' ]
// 第四次获取节点为d的属性值
[ 1, 2 ] [ 'd' ]
// 第五次获取下标为0的属性值
1 [ 'd', '0' ]
// 第六次获取下表为1的属性值
2 [ 'd', '1' ]
*/