省脑子的Javascript数据结构与算法教程 - 熟练操作数组

Posted Jtag特工

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了省脑子的Javascript数据结构与算法教程 - 熟练操作数组相关的知识,希望对你有一定的参考价值。

省脑子的javascript数据结构与算法教程(1) - 熟练操作数组

随着经济形势的变化,很多同学主动和被动的换工作。而对于技术同学来说,目前刷题的形势确实是比以前要卷一些。
题目变多变难了,所以之前上学时学的东西可能不够用了,需要重新学习。

但是,当你真正开始做题的时候,就会发现几个问题。

  1. 传统教科书都是喜欢直接给结论,给出比较精妙的算法。但是,却完全不讲这个算法是如何进化来的。这就导致看这些书的效果比较差,只学习到了结论,遇到了新问题就缺少泛化的能力。
  2. 因为最开始数据结构是以Pascal和C语言为模板写出来的,后来针对其他语言虽然也做了一些优化,比如Java版的。但是,这些静态语言的风格跟动态语言如Javascript差异还是比较大的。而且原始C语言能力较弱,我们没必要把Javascript本身的能力弱化成跟C一样。我们可以不引用三方库,但是用Javascript语言的内置对象这个天经地义吧?
  3. 各种语言的题解虽然都比较丰富,但是并不是每种语言都有系统化讲解的材料。不容易形成一张地图。
  4. 题解的书有些是奥赛高手写的,对于运行时间和效率都有很高的追求。但是现在很多刷题的同学并没有参加信息学奥赛的目的,我们不妨采用迭代式演进的方法,先完成一个可以用的版本,然后一点一点的优化。如果你觉得优化到某一程度就足够用了,就可以跳过不感兴趣的内容了。

我个人做题的感觉是,很多题目其实并不难,只是需要一定的耐烦的能力。
一些题目的步骤和细节比较多,拆解出来之后每一步都很简单,但是组合在一起有些同学反馈就有点吃不消,容易顾此失彼。
这时候,首先要有一个整体视角,先把问题拆解成小问题,然后再一个一个攻克,就会大大节省脑力,提升效率。

从数组说起

虽然同样叫数组,Javascript这样动态语言的数组比起C语言的数组来说,不知道强大了多少倍。比起Java语言的Vector也是强不少的。

JavaScript语言的数组是完全动态的,既可以当栈用,也可以当队列用。

与Java使用类来初始化Vector不同,用“[]”字面量初始化数组是最典型的Javascript方式。

JavaScript数组当栈用的时候,使用push来压栈,pop来退栈;当队列用的时候,可以用push来入队列,shift来出队列。

  • push(): 数组末尾增加一个元素
  • pop(): 数组末尾删除一个元素
  • shift(): 数组头部删除一个元素

栈的一个经典例子就是括号配对,来自力扣的第20题:https://leetcode.cn/problems/valid-parentheses/

给定一个只包括 ‘(’,‘)’,‘’,‘’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。

function isValid(s) 
    let stack = [];
    let result = false;
    for(c of s)
        if(c==='(' ||c==='[' || c==='')
            stack.push(c);
        else if(c===')')
            if(stack[stack.length-1]==='(')
                stack.pop();
            else
                return false;
            
        else if(c===']')
            if(stack[stack.length-1]==='[')
                stack.pop();
            else
                return false;
            
        else if(c==='')
            if(stack[stack.length-1]==='')
                stack.pop();
            else
                return false;
            
        
    
    if(stack.length===0)
        return true;
    
    return false;
;

不考虑O(1)存储的要求,翻转链表的操作,完全可以借助一个栈来完成。
出自力扣第206题:https://leetcode.cn/problems/reverse-linked-list/

function reverseList(pHead)

    if(pHead == null)
        return pHead;
    
     
    let queue = [];
    let pThis = pHead;
    for(;;)
        queue.push(pThis.val);
        if(pThis.next!==null)
            let pTemp = pThis;
            pThis = pThis.next;
            pTemp.next = null;
            pTemp = null;
        else
            break;
        
    
     
    let pHead2 = new ListNode(-1);
    pThis = pHead2;
    let len = queue.length;
    for(let i=0;i<len;i++)
        let pNode = new ListNode(queue.pop());
        pThis.next = pNode;
        pThis = pThis.next;
    
     
    return pHead2.next;

数组任意位置插入与删除

很多时候我们要在数组头增加一个元素,此时我们可以用unshift()函数。

在任意位置插入或者删除一个元素的时候,往往首先需要找到一个元素来进行定位。

  • includes():用来判断一个元素是否在数组存在
  • indexOf(): 用来判断一个元素在数组中第一次出现的位置,如果找不到则返回-1.
  • splice(位置,删除元素的个数,新增的值):用来在任一个位置删除或增加元素

例如,要删除1个元素,就是splice(pos, 1);
只增加不删除,就是splice(pos,0,新值1);

比如我们要实现一个LRU功能的列表,最近访问的放在列表头,超过长度的删除掉,可以用数组这么实现:

        if(this.lruList.includes(key))
            let pos = this.lruList.indexOf(key);
            this.lruList.splice(pos,1);
        
        this.lruList.unshift(key);
        //如果超了,则删除最后一个
        if(this.lruList.length>this.capacity)
            let last = this.lruList.pop();
        

当然这么写的性能可以优化的,这里只是用来说明数组的用法。

比如一个可行的优化,就是用存储key-value值的map来判断key是否存在,而不用去遍历数组:

class LRUCache
    constructor(capacity)
        this.capacity = capacity;
        this.map = new Map();
        this.lruList = [];
    

    updateLRU(key)
        //key如果存在,就先删除之
        if(this.map.has(key))
            let pos = this.lruList.indexOf(key);
            this.lruList.splice(pos,1);
        
        this.lruList.unshift(key);
        //如果超了,则删除最后一个
        if(this.lruList.length>this.capacity)
            let last = this.lruList.pop();
            this.map.delete(last);    
        
    

    get(key)
        if(this.map.has(key))
            this.updateLRU(key);
            return this.map.get(key);
        else
            return -1;
        
    

    put(key,value)
        this.updateLRU(key);
        this.map.set(key,value);
    

排序

再次强调一下,Javascript是一门动态语言,而且函数是一等公民,不要用C语言的静态思想来思考。最好是有一点函数式编程的思维。
至少对于数组排序来说,我们需要写一个比较函数。

对于使用其它语言的小伙伴们,可以理解不了Array.sort()的默认逻辑:

const nums = [1,2,3,10,100,1000];

nums.sort();

在Javascript里,这个值输出的结果为:

[ 1, 10, 100, 1000, 2, 3 ]

因为默认排序顺序是在将元素转换为字符串,然后比较它们的 UTF-16 代码单元值序列时构建的。

如果想按数字大小排序的话,我们就给一个比较函数吧:

const nums = [1, 2, 3, 10, 100, 1000];

nums.sort((a, b) =>  b - a );

console.log(nums);

可以想见的是,这种简单排序都要涉及用户代码调用的场景,性能是不会特别好的。

另外有一点需要注意的是,就像运行在浏览器的任何Javascript代码一样,sort也是有兼容性问题的。在ECMAScript 2019之前,是不支持稳定排序的。
稳定排序的意思是针对比较的字段是同样的元素是否保持原来的顺序。在ES2019之前是不稳定的,从ES2019之后才是稳定的。支持稳定排序的版本是Chrome 70, 2018年10月推出。

小结

从利用数组做数据结构题,我们可以体会到动态语言在表达上的强大,和对性能的一丝无奈。

与编译型语言不同,Javascript的运行环境受到比较多的制约,比如浏览器兼容性的问题。

我们可以先用Javascript现有的能力把题目的功能先实现掉,后面再考虑做优化。就算是Javascript不擅长的优化,我们现在有wasm了,可以使用比如rust来写更高效的代码。

以上是关于省脑子的Javascript数据结构与算法教程 - 熟练操作数组的主要内容,如果未能解决你的问题,请参考以下文章

省脑子的Javascript数据结构与算法教程 - 熟练操作数组

极简教程:算法与数据结构

数据结构与算法--JavaScript 链表

学习一数据结构与算法——算法复杂度

2-算法的时间复杂度与空间负责度

想参加noip看啥书?