1-Javascript 数据结构与算法-栈、队列、链表、集合

211 阅读4分钟

image.png

重点关注

数据结构与算法的特点、应用场景、JS实现、时间/ 空间复杂度。

刷题顺序

较为科学的刷题方式,推荐按照类型刷题,相当于集中训练。

数据结构与算法简介

数据结构:计算机存储、组织数据的方式

程序= 数据结构 + 算法

栈、队列、链表: 有序的(羊肉串);

集合、字典:无序的(米饭);

树、堆、图:公司组织架构、道路

学习的算法

  • 【链表】遍历链表、删除链表节点。
  • 【树、图】深度dfs/广度bfs优先遍历。
  • 【数组】冒泡/选择/插入/归并/快速排序、顺序/二分搜索。

时间复杂度的计算

时间复杂度是一个函数,用大O来表示,定性描述该算法的运行时间。

定性

image.png 时间复杂度相加是忽略小的

O(1) + O(n) = O(n) 取时间复杂度大的,因为小的可以忽略。

时间复杂度相乘则不能忽略

嵌套的双层for循环 O(n) * O(n) = O(n^2)

时间复杂度 O(logN) 代表2的多少次方为N

log8 = 3

空间复杂度的计算

空间复杂度也是一个函数,也是用大O来表示的,常见的空间复杂度 O(1) O(n) O(n^2)

算法在运行过程中临时占用存储空间大小的量度,说直白点就是你写的代码占用存储空间大小多不多

let i =1 ; i += 1;  // O(1) 只声明一个单独变量。

// O(n)
let arr = []
for(i = 0; i < 10; i++) {
	arr.push(i)
}

// O(n^2) 其实就是一个矩阵 行列,本质就是一个二维数组

数据结构之-“栈” stack

栈就是一个后进先出,先进后出的数据结构,现实中的例子就是蜂窝煤,拿的时候只能先拿最上面的,push(入栈)、pop(出栈),javascript可以通过Array实现所有栈的功能

什么场景下用“栈”?

  • 需要后进先出的场景(十进制转二进制、判断括号是否有效、函数调用堆栈),我们都可以用“栈”来解决问题

为什么十进制要用到“栈”?

因为我们拿到的余数最后要倒序输出,比如35的二进制就是100011

function decimalismToBinary(num){
    let stack = []
    while(num) {
        stack.push(num % 2);
        num = Math.floor( num / 2 );
    }    
    return stack.reverse()
    
}
// reverse反转数组,返回新的数组,原数组也会被改变

image.png

JS中函数的调用堆栈也是典型的后进先出的,背后的JS解析器就是用的栈的数据结构来控制函数的调用顺序,使它能够满足后进先出的顺序。

如下图就是执行到function3的时候的调用堆栈

image.png

数据结构之-“队列” queue

是一个先进先出,就像水管里面的水一样,与栈是相反的。javaScript没有队列,但是我们可以通过Array实现队列的所有功能。

模拟队列的先进先出需要使用Array的shift方法

let arr = [];
arr.push(1);
arr.push(2);
let item = arr.shift(); // item = 1  arr = [2]

什么场景使用到队列

所有符合先进先出的场景都可以考虑使用队列的数据结构进行解决。先进先出,保证有序。

场景

【食堂打饭,谁先排第一位,谁先打饭就走】、

【JS异步中的任务队列,比如遇见微任务先放到微任务队列,等主进程执行完之后去执行微任务队列的时候,先执行第一个微任务】

【计算最近请求次数】是一道算法题

image.png

数据结构之-“链表”

链表:多个多素组成的列表,元素存储不连续,用next指针连在一起。

链表 vs 数组:链表其实和数组比较像,那么为什么还要用链表?

原因是数组是连续的,增删非首尾的元素时候往往要移动元素,但是链表增删非首尾的元素,不需要移动元素,只需要改变next的指向即可。

JS中的链表

JavaScript是没有链表的数据结构,但是我们可以用Object来模拟一个链表。

JavaScript实现链表

let a = { value: 'a' }
let b = { value: 'b' }
let c = { value: 'c' }
let d = { value: 'd' }

a.next = b
b.next = c
c.next = d

// p 就是一个指针
let p = a

// 遍历链表
while (p) {
  console.log('faith=============', p.value)
  p = p.next
}

// 插入链表
let w = { value: 'w' }
let temp = b.next
b.next = w
w.next = temp

// 删除 w
let temp = w.next
b.next = temp

Js中的原型链

原型链本质上也是链表结构

arr.__proto__.__proto__.__proto__
arr.__proto__ // Array prototype
arr.__proto__.__proto__ // Object prototype
arr.__proto__.__proto__.__proto__ // null       

使用链表指针获取JSON的节点值

const json = {
	a: { b: { c: 100}},
	d: {e: 200}
}

const path = ['a', 'b', 'c']
// 怎么通过链表的思想来获取 a.b.c ?

let p1 = json;
// 模拟链表循环
path.forEach((item) => {
	p1 = p1[item]
})

console.log(p1) // 100

总结

链表里的元素存储不是连续的,是通过next进行连接的。

JavaScript是没有链表的数据结构,但是我们可以通过Object模拟链表。

Js中的原型链也是一个链表,只不过不是通过next连接,而是通过 proto

集合

一种无序且唯一的数据结构、ES6中有集合,Set

集合中常用的操作:去重、判断某元素是否在集合中、求交集

// 去重
let arr = [1, 1, 2]
let arr2 = [...new Set(arr)]

// 判断元素是否在集合中
let arr = [1, 2]
let set = new Set(arr)
set.has(1) // true
set.has(5) // false

使用ES6的Set

1、new add delete has size

2、迭代Set:多重迭代方法、Set与Array互换、求交集/差集

  • 使用for of 迭代集合
let set = new Set([1, 2, 3,4])

// 直接 item of set
for(let item of set) {
    console.log(item)
}

// item of set.keys()
for(let item of set.keys()) {
    console.log(item)
}

// item of set.values()
for(let item of set.values()) {
    console.log(item)
}

set.forEach(item => {
    console.log(item)
})

for(let [k, v] of set.entries()) {
    console.log(k, v) // key value 都是一样
}

// Set与Array互换
Array.from(set)、[...set]

// 差集 和交集比只是判断条件是非 (A差B + B差A = 补集)
[... new Set( nums2.filter((item) => !set.has(item)) )] 

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 */
var intersection = function(nums1, nums2) {
    // 通过字典来实现
    let map = new Map()
    nums1.forEach((item) => {
        map.set(item, true)
    })

    let res = []
    nums2.forEach((item) => {
        if(map.get(item)) {
            res.push(item)
            map.delete(item)
        }
    })
    return res
    
};

总结

1、无序且唯一

2、ES6内是有集合的,它的名字是Set

3、集合的常用操作:去重、判断某元素是否在集合中、求交集/差集/补集

编码

20.有效的括号 LeetCode

/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function(s) {
    // "{[]}"
    if(s.length % 2 !== 0) {
        return false;
    }
    let stack = [];
    let map = new Map();
    map.set(')', '(');
    map.set('}', '{');
    map.set(']', '[')

    for(let i = 0; i < s.length; i++) {
        if(['(', '{', '['].includes(s[i])) {
            stack.push(s[i])
        } else {
            if(map.get(s[i]) !==  stack.pop()) {
                return false
            }
            
        }
    }
    return  stack.length === 0 ? true : false;

};

237.LeetCode 删除链表中的节点

// 输入:head = [4,5,1,9], node = 5
// 输出:[4,1,9]
// 时间复杂度O(1)  空间复杂度O(1)
var deleteNode = function(node) {
    node.val = node.next.val; // 如果要删除5,那么将5改成1(新)
    node.next = node.next.next // 然后再删除1(久)
};

// ```jsx
// 输入:head = [4,5,1,9], node = 5
// 输出:[4,1,9]
// 时间复杂度O(1)  空间复杂度O(1)
var deleteNode = function(node) {
    node.val = node.next.val; // 如果要删除5,那么将5改成1(新)
    node.next = node.next.next // 然后再删除1(久)
};

// 总结: [4, 5, 1, 9] 如果删除5,当前node节点是5,我们无法拿到上个 node.next,那么就把1借过来给5,相当于删除1 [4, 1, 9]

206.反转链表 LeetCode

双指针 p1=head p2=null 基于2个节点的反转思想 p1走一步,p2跟着p1屁股走一步 返回p2

时间复杂度 O(n) 空间复杂度O(1)

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
    //           [1, 2, 3, 4]
    //p2 = null  p1
             //  p2 p1
							//            p2 p1
    let p1 = head, p2 = null;
    while(p1) {
        // 获取临时
        let temp = p1.next;
        // 先反转 再往前
        p1.next = p2; // 一定要先反转,这块要注意
        // 反转之后再往前
        p2 = p1; // p2往前走
        p1 = temp; // p1往前走
    }

    return p2
};

2.两数相加 LeetCode

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
// 时间复杂度 O(n) 空间复杂度O(n)
var addTwoNumbers = function(l1, l2) {
    // 创建输出链表
    let l3 = new ListNode(0)
    // 遍历链表 3个指针
    let p1 = l1, p2 = l2, p3 = l3;
    let carry = 0; // 进位
    // 遍历 l1 l2 
    while(p1 || p2) { // 有可能长度不一样

    // 第一次循环伪代码
    //    p1.val + p2.val  == 7
    //    fllor (7 / 10 ) = 0
    // p3.next = new new ListNode(7)

    // p1 = p1.next
    // p2 = p2.next
    // p3 = p3.next

    // 第二次循环伪代码
    // p1.val + p2.val + fllor (7 / 10 )
    // floor (p1.val + p2.val + fllor (7 / 10 ) / 10) = 

    let value = (p1 ? p1.val : 0) + (p2  ? p2.val : 0)  + carry;

    // 保留10进位的数下次相加
    carry = Math.floor(value / 10)

    // 当前链表取个位数
    p3.next = new ListNode(value % 10) 

    p1 = p1 && p1.next
    p2 = p2 && p2.next
    p3 = p3.next
    }

    // 这个是为了处理 [1,1,9] [8,8,8] 循环了3次 [9, 9, 7] 此刻carry是1但是没有被循环到
    if(carry) {
        p3.next = new ListNode(carry) 
    }

    return l3.next

};

// 简化一下
var addTwoNumbers = function(l1, l2) {
    // 创建输出链表
    let l3 = new ListNode(0)
    // 遍历链表 3个指针
    let p1 = l1, p2 = l2, p3 = l3;
    let carry = 0; // 进位
    // 遍历 l1 l2 
    while(p1 || p2) { // 有可能长度不一样
        let v1 = (p1 ? p1.val : 0);
        let v2 = (p2  ? p2.val : 0);
        let value = v1 + v2  + carry;
        
        // 保留10进位的数下次相加
        carry = Math.floor(value / 10)

        // 当前链表取个位数
        p3.next = new ListNode(value % 10) 
        p1 = p1 && p1.next
        p2 = p2 && p2.next
        p3 = p3.next
    }

    // 这个是为了处理 [1,1,9] [8,8,8] 循环了3次 [9, 9, 7] 此刻carry是1但是没有被循环到
    if(carry) {
        p3.next = new ListNode(carry) 
    }
    return l3.next
};

83.删除排序链表中的重复元素 LeetCode

需要用到:遍历链表、删除链表的节点

时间复杂度 O(n) 空间复杂度O(1)

leetcode-cn.com/problems/re…

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var deleteDuplicates = function(head) {
    // 创建一个指针 p1
    // 判断当前节点val与下个节点的val是否一样,如果一样删除下个节点,指针不用动 [1, 1, 2] >> [1,  2]
    //                                                               p1            p1
    //                                    如果不一样 [1, 2, 3] 不删除,只移动指针
    //                                              p1

    let p1 = head;
    while(p1 && p1.next) {
        if(p1.val === p1.next.val) {
            p1.next = p1.next.next
        } else {
            p1 = p1.next
        }
    }
    return head
    // 总结,遍历链表,相同删除不移动指针,不相同移动指针
};

141.环形链表 LeetCode

leetcode-cn.com/problems/li…

一快一慢的指针遍历链表,如果指针能够相逢就返回true

就像一个圆形操场,一个跑的慢(慢指针)一个快(快指针),跑的快的肯定会与跑的慢的相遇。

时间复杂度 O(n) 空间复杂度O(1)

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
    let p1 = head, p2 = head;

    while(p1 && p2 && p2.next) {
        p1 = p1.next
        p2 = p2.next.next
        if(p1 === p2) {
            return true
        }
    }
    return false
};

349.两个数组的交集 LeetCode

leetcode-cn.com/problems/in…

时间复杂度 O(n^2) filter * has

空间复杂度 O(n)

总结:需要用到集合的has方法与数组的filter方法

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 * 交集-就要使用Set集合
 */
var intersection = function(nums1, nums2) {
    // [1, 2, 2, 1] [2, 2]
    let set = new Set(nums1)
    return [... new Set( nums2.filter((item) => set.has(item)) )]
};

通过字典Map的方式来实现两个数组的交集

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 */
var intersection = function(nums1, nums2) {
    // 通过字典来实现
    let map = new Map()
    nums1.forEach((item) => {
        map.set(item, true)
    })

    let res = []
    nums2.forEach((item) => {
        if(map.get(item)) {
            res.push(item)
            map.delete(item)
        }
    })
    return res
    
};