JavaScript——数据结构

193 阅读25分钟

数据结构与算法简介

数据结构与算法的关系

  • 数据结构:数据结构是计算机存储、组织数据的方式;是指相互之间存在一种或多种特定关系的数据元素的集合。数据结构有很多种,一般分为线性结构和非线性结构两类。
  • 算法:算法是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。
  • 程序=数据结构+算法
  • 数据结构为算法提供服务,算法围绕数据结构操作

常见数据结构类型

  • 有序:栈、队列、链表
  • 无序:集合、字典
  • 有连接关系:树、堆、图

时间复杂度

  • 时间维度:是指执行当前算法所消耗的时间,我们通常用「大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
解释:链表中有一个环,其尾部连接到第二个节点。

alt

解题思路

  • 两个人在圆形操场起点同时起跑,速度快的人会超过速度慢的人最后会重逢
  • 用一快一慢两个指针遍历链表,如果指针发生重逢,即指向的值相等。那么链表就有圆。

解题代码

/**
 * 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个字符串,分别为stringBooleannumberfunctionobjectundefined
        • typeof在判断nullarrayobject以及函数实例(new + 函数)时,得到的都是object。这使得在判断这些数据类型的时候,得不到真是的数据类型。由此引出instanceof
      • instanceof
        • 用于判断一个变量是否某个对象的实例 / 判断一个对象在其原型链中是否存在一个构造函数的 prototype 属性
  • 如果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' ]
*/