JavaScript学习笔记--算法题

439 阅读10分钟

斐波那契数列

斐波那契数列的标准公式为:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)

爬楼梯问题

假设你正在爬楼梯,你需要爬n级才能到达楼顶,每次你可以爬1或两个台阶,你有多少种方法可以爬到楼顶?


爬到第一级台阶有1种:1

爬到第二级台阶有2种:(1,1)、2

爬到第三级台阶有3种:(1,1,1)、(1,2)、(2,1)

爬到第四级台阶有5种:(1*5),(1*2,2),(2,1*2),(1,2,1),(2,2) ...

综上:从第三级楼梯开始,f(n)=f(n-1)+f(n-2)

使用递归实现

function climbStairs(n){
    if(n===1||n===2){
        return n;
    }else{
        return climbStairs(n-1)+climbStairs(n-2)
    }
}

缺点:此算法速度太慢,递归造成大量的重复子问题,比如走10阶台阶,递归立马要去计算怎么走到第9阶和第8阶台阶,然后一层层下去,中间造成大量浪费。

改进:将每一阶的走法存下来

function climbStairs(n){
    let arr=[0,1,2];    //0只是来占位
    for(let i=3;i<=n;i++){
        arr[i]=arr[i-1]+arr[i-2]
    }
    return arr[n];  //返回数组的最后一个元素,即是总方法
}

查找值

二分查找

二分查找,是在一个有序序列中查找某一个值,每次从中间开始猜,分界设在开头和结尾的值。如果目标值比中间值大了,则移动开头去指向中间后面的值,否则移动结尾去指向中间前面的值,然后继续这个范围的中间值猜。

递归写法

function binaryFind(arr,target,low=0,high=arr.length-1){
    let mid=Math.floor((low+high)/2);
    if(target>arr[mid]){
        binaryFind(arr,target,mid+1,high)
    }
    else if(target<arr[mid]){
        binaryFind(arr,target,low,mid-1);
    }else{
        return `找到目标值,在第${mid+1}个`
    }
    return -1;
}

使用循环

function binaryFind(arr,target){
    let low=0,high=arr.length-1;
    let mid;
    while(low<high){
        mid=Math.floor((low+high)/2);
        if(arr[mid]===target){
            return `找到目标值,在第${mid+1}个`
        }
        else if(arr[mid]>target){
            high=mid-1;
        }
        else{
            low=mid+1
        }
    }
    return -1;
}

字符串与数组的结合

判断字符串 a 是否被包含在字符串 b 中,并返回第一次出现的位置(找不到返回 -1)。

function f2(a,b) {
        let char_=a.charAt(0);
        let index=b.indexOf(char_);
        if(index!==-1){
            while (index<b.length){
                let substr=b.substr(index,a.length);
                if(substr===a){
                    return index;
                }
                index=index+a.length
            }
        }
        return -1
    }
    console.log(f2('abc', 'aasabc'));//3

实现千位分隔符

比如1234567 -- 1,234,567

可以使用数组的join函数,传入‘,’。

1)首先将数字转为字符串

2)每三位每三位的分隔,作为数组的一个元素

3)但是要先将不足三位的放在数组的第一个元素(如果有的话)

function f3(number) {
        let str=number.toString();
        let subindex=str.length%3;
        let substr=str.substr(0,subindex);
        console.log(substr);//如果subindex是0,则说明number的个数是3的倍数,此时返回的字符串是空字符串
        let arr=[];
        if(substr){ //有东西才加入数组
            arr.push(substr);
        }
        while (subindex<str.length){
            let s=str.substr(subindex,3);
            arr.push(s);
            subindex+=3;
        }
        str=arr.join(",")
        return str;
    }
    console.log(f3(123456));

在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回-1(需要区分大小写)

思路一

用一个map来存储每个字符出现的次数

function fn(str){
        if(!str){
            return -1
        }
        let arr=str.split('');
        let map={};
        for(var i=0;i<arr.length;i++){
            if(map[arr[i]]){
                map[arr[i]]+=1
            }else{
                map[arr[i]]=1
            }
        }
        for(var item in map){
            if(map[item]==1){
                return arr.indexOf(item);
            }
        }
        return -1;
    }

思路二

使用数组的indexOf方法和lastIndexOf方法。判断该字符在这两个方法下是否返回相同的值。

(lastIndexof是查找某个字符最后一次出现的位置。)

function FirstNotRepeatingChar(str) {
      // write code here
      for (var i = 0; i < str.length; i++) {
        if (str.indexOf(str[i]) == str.lastIndexOf(str[i])) {
          return i;
        }
      }
      return -1;
    }

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。

  • 遍历数组,每找到一个奇数,就向前找其他的奇数
  • 将遇到的偶数都向后推,推到遇到其他奇数
  • 此时奇数后面的位置就是此时遍历到的奇数将要插入的位置
function f2(arr){
       for(let i=1;i<arr.length;i++){
           if(arr[i]%2===1){
               let t=arr[i];
               let j=i-1;
               while (j>=0&&arr[j]%2===0){ //查找到第一个数或为奇数的数则退出循环
                   arr[j+1]=arr[j] //将偶数向后移
                   j--;
               }
               arr[j+1]=t; //此时的j指向的是-1或arr[i]前面的第一个奇数
           }
       }
       return arr;
    }

在一个数组中找出两个数的和为目标值,要得到两个数在数组里的下标

// 遍历数组,如果当前元素在目标对象中,则将这两个数字取出并添加到最终结果中
    //          如果不存在,则将当前元素添加到目标对象中
    function twoSum(arr, target) {
        var obj = {},
            res = {},
            index = 0;
        for(var i = 0, len = arr.length; i < len; i++) {
            var dif = target - arr[i];
            if(dif in obj) {
                res[index] = [];
                res[index].push(obj[dif]);
                res[index].push(i);
                // 这里要记得从对象中删除
                delete obj[dif];
                index++;
            } else {
                obj[arr[i]] = i;
            }
        }
        console.log(res);
    };

给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和。

注意:

  • num1 和num2 的长度都小于 5100.
  • num1 和num2 都只包含数字 0-9.
  • num1 和num2 都不包含任何前导零。 -你不能使用任何內建 BigInteger 库,也不能直接将输入的字符串转换为整数形式。

从末位开始计算,如果超过10记住要进1

function fn(num1,num2){
    let arr1=[...num1];
    let arr2=[...num2];
    let jin=0;
    let res=[];
    while(arr1.length>0||arr.length>0){
        let add1=arr1.pop()||0;
        let add2=+arr2.pop()||0
        res.unshift((add1+add2+jin)%10);
        jin=(add1+add2+jin>=10? 1:0)
    }
    if(jin===1){
        res.unshift(1)
    }
    return res.join("")
}

二维数组

1、在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

解题思路:

  • 从左下角的数开始判断,向上递减,向右递增
  • 如果target<这个数,则向上查找,若大于,则向右查找
  • 为什么要找左下角呢?因为这个数的两个方向是一个分水岭,而第一个数两个方向的数都比它大,不好判断
function find(target,array){
        let row=array.length-1;
        let col=0;
        while (row>=0&&row<array.length&&col>=0&&col<array.length){
            if(target===array[row][col]){
                return true;
            }else if(target<array[row][col]){
                row--;
            }
            else{
                col++;
            }
        }

    }

还有一种思路就是一个个找,使用数组的some函数,但是效率没有第一个算法高

function Find(target, array)
 {
      return array.some(arr=>arr.some(i=>target===i))  

}

螺旋式内向输出5 * 5 的二维数组

function f1(num) {
        let res={}, //存放结果
            changeNum=5,    //走几步换方向
            changeFlag=1,   //换方向的标志
            xy={            //此时的坐标
                x:0,y:0
            },
            directionIndex=-1,  //0:向下走,1:左,2:上,3:右
            direction={
                '0':function (res,xy,num) {
                    xy.x++;
                    res[xy.x][xy.y]=num
                },
                '1':function (res,xy,num) {
                    xy.y--;
                    res[xy.x][xy.y]=num
                },
                '2':function (res,xy,num) {
                    xy.x--;
                    res[xy.x][xy.y]=num
                },
                '3':function (res,xy,num) {
                    xy.y++;
                    res[xy.x][xy.y]=num
                },
            };
        //初始化数组
        for (let j=0;j<5;j++){
            res[j]=new Array(5);
        }
        let i=0,currentCount=0;
        for (i=0;i<5;i++){
            res[xy.x][xy.y++]=num+i;
            currentCount++;
        }
        //此时坐标向右走多了一步,退后
        xy.y--;
        for (let k=i+num;k<=25;k++){
            if(currentCount===changeNum){ //在一个方向上走的步数达到换方向的数目
                if(changeFlag){
                    changeFlag=0;
                    changeNum--;    //转变两个方向之后,转方向的条件就减1
                }else{
                    changeFlag=1
                }
                //换方向
                directionIndex=(directionIndex+1)%4;
                currentCount=0; //重新计数在这个方向走的步数
            }
            direction[directionIndex](res,xy,k);    //计算坐标
            currentCount++;
        }
        for (let item in res){
            console.log(res[item].join(' '))
        }
    }
    f1(1)

参考

实现n阶矩阵(上面题目的升级,5阶就是上面的输出结果)

function f(n) {
     let res=[],    //结果
         changeCount=n,
         flag=1,
         direction=-1,
         xy={
            x:0,y:0
         },
         location={
            //向下移动
            0:function (xy,res,step) {
                xy.x++;
                res[xy.x][xy.y]=step;
            },
             //向左移动
             1:function (xy,res,step) {
                 xy.y--;
                 res[xy.x][xy.y]=step;
             },
             //向上移动
             2:function (xy,res,step) {
                 xy.x--;
                 res[xy.x][xy.y]=step;
             },
             //向右移动
             3:function (xy,res,step) {
                 xy.y++;
                 res[xy.x][xy.y]=step;
             }
         };
     //初始化数组
       for(let i=0;i<n;i++){
           res[i]=[];
       }
       let count=0;
       //往第一行填充数据
       for(let i=0;i<n;i++){
           res[xy.x][xy.y++]=i+1;
           count++;
       }
       //退出循环后,此时坐标超出一列,减回去
       xy.y--;
       for(let k=count+1;k<=n*n;k++){
           if(count===changeCount){
               if(flag){    //变换两个方向后,要减少在一个方向上移动的次数
                   changeCount--;
                   flag=0;
               }else{
                   flag=1;
               }
               count=0;
               direction=(direction+1)%4;
           }
           location[direction](xy,res,k);
           count++;
       }
       for(let j=0;j<n;j++){
           console.log(res[j].join(' '));
       }
   }
   f(3)

链表

输入一个链表,输出该链表中倒数第k个结点。

  • 再设置一个指针,last,初始指向head
  • last指针先移动,只要有后一个节点则移动
  • 移动k-1位,加上head节点则算k个节点,相当于一把长度为k的尺子
  • 如果在移动的过程中还未移动k-1位就无节点的话,说明k太大,返回空
  • 获取到尺子之后,将尺子向后移,则head和last一起向后移
  • 当last移动到最后一个节点时,此时head节点就是指向倒数第k个节点
function FindKthToTail(head, k)
{
    if(!head||k<=0)
        return null;
    
    //let last=head.next;
    let last=head;    //设置两个指针
    while(k>1){    //last指针向后走了k-1步,然后加上head节点则相当于一把长度为k的尺
        if(last.next){ 
            last=last.next;
            k--;
        }else{
            return null;    //如果还没走到k-1步就没有节点,说明k太大,超过链表的长度
        }
    }
    //生成一把k的尺子,然后两个指针一起向后移,等last指针移动到最后一个节点时,head节点则为倒数第k个节点
    while(last.next){
        head=head.next;
        last=last.next;
    }
    return head;
}

2、输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

解题思路:

  • 前序是根-左-右,中序是左-根-右,后序是左-右-根。
  • 所有前序遍历的第一个是根,中序遍历中间索引是根,索引前后为左右子树
  • 使用递归算法,每次将左右两棵子树当作新的子树进行处理,按前两步的思路再分成左右子树。方法每次都返回左右子树的根节点
function reConstructBinaryTree(pre, vin)
{
    // write code here
    let node=reConstructBinaryTree(pre,0,pre.length-1,vin,0,vin.length-1);
    return node;

    //这个函数不能写在上面那个函数的外面,不然会出错
    //startIndex_p:此新子树前序遍历的第一个索引,endIndex_p:最后一个索引
    //startIndex_v:此新子树中序遍历的第一个索引,endIndex_v:最后一个索引
    function reConstructBinaryTree(pre,startIndex_p,endIndex_p,vin,startIndex_v,endIndex_v){
        if(startIndex_p>endIndex_p||startIndex_v>endIndex_v){
            return null;
        }
        
        let root=pre[startIndex_p];    //每棵子树的根的值
        let node=new TreeNode(root);
        
        let index;
        for(let i=startIndex_v;i<=endIndex_v;i++){  //在中序遍历里,在传入的左右界限里找到此子树的根的索引
            if(vin[i]===root){
               index=i;
                break;
            }
        } 
        //左子树              
        node.left=reConstructBinaryTree(pre,startIndex_p+1,startIndex_p+index-startIndex_v, vin,startIndex_v,index-1);
        //右子树
        node.right=reConstructBinaryTree(pre,startIndex_p+index-startIndex_v+1,endIndex_p, vin,index+1,endIndex_v);
       
        return node;
    }
}   

队列 栈

用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。