javascript的算法学习记录

1.求n项累加和

function sum(n){
  return (n/2)*(n-1)
}

时间复杂度:O(1) 常数时间复杂度

2.判断一个数是不是质数

​ 质数的特点:

​ 1.除了1以外的自然数如果只能被1和它本身整除这个数就是质数

​ 2.质数还有一个特点,就是它总是等于 6x-1(等同于6x+5) 或者 6x+1,其中 x 是大于等于1的自然数

​ 3.合数 能被除1和本身以外的数整除, 这个数一定小于等于合数的开平方

需求分析:

- 1 既不是质数也不是合数
- 小于等于3的数,是否大于1可判断是不是质数
- 不是6的倍数两侧的一定不是质数  number % 6 != 1 && number % 6 !=5
- i = 2;i<=Math.sqrt(number);i++;  number % i === 0 false 否则 true
function isPrime(number){
  if(number<=3){
    return number>1
  }
  // 不在6的倍数两侧的一定不是质数
  if (number % 6 != 1 && number % 6 != 5) {
    return false;
  }
  for(let i =2;i<= Math.sqrt(number);i++){
  	if(number%i === 0){
      return false
    }
  }
  return true
}

时间复杂度:O(开平方n)

2-1 扩展

示例 1:

输入:n = 10
输出:4
解释:小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。
示例 2:

输入:n = 0
输出:0
示例 3:

输入:n = 1
输出:0

提示:

0 <= n <= 5 * 106

第一种方法 枚举 + 用上面的isPrime方法

var countPrimes = function(n){
  let count = 0
  n--
  for(let i = 2;i<n;i++){
    if(isPrime(n)){
      count ++
    }
  }
  return count
}
function isPrime(n){
  if(n<=3){
    return n>1
  }
  if(n % 6 != 1 && n % 6 !=5){
    return false
  }
  for(let i = 2;i<=Math.sqrt(n);i++){
    if(n%i ===0) return false
  }
  return true
}
	var countPrimes = function(n) {
    for(var i = 2, r = 0, isPrime = Array(n).fill(true); i < n; i++) 
        if(isPrime[i]) {
            r++
            for(var j = 2 * i; j < n; j += i) isPrime[j] = false
        }
    return r
};

知识补充:Array(n).fill(true) 创建一个长度为n的数组并且填充值为true

埃氏筛 · 优化

上图中黄色数字,被重复标记。例如10,2时标记1次,5时又1次。如何减少重复呢?
举例5。16以内,从2起,5的倍数,5 * 2,5 * 3。在2和3时倍数时,被标记过
n以内,从2起,任意数i的倍数,i * 2,i * 3 ... i * i ... i * x(x > i) < n
在i之前的倍数,一定在遍历到i前,被标记过

埃氏筛 · 优化运行过程图解

代码

var countPrimes = function(n) {
for(var i = 2, r = 0, isPrime = new Array(n).fill(true); i < n; i++)
if(isPrime[i]) {
r++
for(var j = i * i; j < n; j += i) isPrime[j] = false
}
return r
};

奇数筛

思路:

质数一定是奇数,偶数一定不是质数(2除外)。只要在奇数范围标记合数,未标记的是质数

奇数乘以偶数一定是偶数,只用奇数乘以奇数,确保在奇数范围内标记

用上面两条优化埃氏筛解法,过程如图

奇数筛运行过程

var countPrimes = function(n){
  var isCom = new Array(n),
      b=Math.sqrt(n),
      r = n>2?1:0,
      t = 0
  for(var i = 3;i<n;i+=2){
    if(!isCom[i]){
      r++
      if(i <= b){
        for(var j = i;(t=i*j) < n;j+=2){
          isCom[t] = 1
        }
      }
    }
  }
  return r
}

个人总结:奇数+2 得到的还是奇数

作者:mantoufan
链接:https://leetcode-cn.com/problems/count-primes/solution/mei-ju-ai-shi-shai-xian-xing-shai-qi-shu-shai-5xin/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

4.计算N的阶乘 n! (递归方法)

如果n = 5 5! = 5 * 4 * 3 * 2 * 1

function factorial(number){
	if(number <= 1){
    return 1
  }
  return number*factorial(number-1)
}

时间复杂度:O(n)

5.是不是2的整数次幂

需求分析

小于1的肯定不是2的整数次幂

如果一个数对2取余数不等于0肯定不是2的整数次幂

一个数除以2得到商和余数,商不等于1 & 取余数不等于0,直到最后商等于1,余数等于0返回true,否则是false

8%2 =0 8 / 2 = 4

4%2 =0 4/2 = 2

2%2 = 0 2/2 = 1

得到商1 余0 得出结论8 是2的整数次幂

function isPowerTwo(number){
 if(number<1){
   return false
 }
 let divideNumber = number
 while(divideNumber !== 1){
   if(divideNumber % 2 !== 0){
			return false
   }
   divideNumber = divideNumber / 2
 }
  return true
}

时间复杂度:O(logn) 对数时间复杂度

2.求斐波拉(那)契数列指定位置的值 (从底层构建)

function fibNum(number){
  let fibArray = [1,1]
  for(let i =2;i<number+1;i++){
    fibArray.push(fibArray[i-2]+fibArray[i-1])
  }
  return fibArray[number]
}

时间复杂度:O(n) 线性时间复杂度

什么是动态规划

动态规划是解决多阶段决策过程中最优化的一种有效的数学方法

递归 Recursion + 存储数据 Memoization

非常适合重复计算 通过存储数据避免不必要的递归步骤

可能导致重复的工作 中间结果被存储并重复使用

动态规划

递归 + 斐波那契数列 算法

从底层构建 也就是自下而上方法 找到的是下标

需求分析:

创建中间值变量result

进到函数体执行,先验证传进来的中间值memo是否存在,存在则直接返回

因为斐波那契数列前两项为1 ,所以当n 值为0 或者 1的时候,直接返回1

否则 中间值result 赋值回调函数 fib(n-1,memo) + fib(n-2,memo)

条件外赋值 memo[n] = result

返回result

function fib(n,memo={}){
  let result // 存储中间值 
  if(memo[n]) return memo[n] // 验证这个中间值是否存在,若存在就返回结果,
  if(n === 0 || n === 1){
    result = 1
  }else{
    result = fib(n-1,memo) + fib(n-2,memo)
  }
  memo[n] = result
  return result
}
[1,1,2,3,5,8,13]
fib(5,{}) // 传入空对象目的:在递归过程中,重复中间值才会被存储到对象中
fib(6,{})

时间复杂度 O(n) 线性时间复杂度

搜索算法

线性搜索

可以用于有序和无序的列表,它会验证所有item项知道它找到你需要搜索的元素为止

[2,17,-1,41,5,8,10,32]

查找数字 5

function findElement(arr,element,comparatorFn){
  let index = 0
  for(const item of arr){
    //判断是否为对象
    if(typeof element === 'object' &&
      element !== null &&
      comparatorFn(element,item)){
      return index;
    }
    //数字或对象
    if(item === index){
      return index
    }
    index++
  }
}
const arr = [2,17,-1,41,5,8,10,32]
const object = [
  {name:'Summer',age:26},
  {name:'Henry',age:18}
]
findElement(arr,5,(element,item)=>{
  return element.name === item.name
})
arr.findIndex(el=>el === 5)


知识补充

ES6为Array增加了find(),findIndex函数。

find 函数用来查找目标元素,找到就返回该元素,找不到返回undefined

findIndex 函数也是查找目标元素,找到就返回元素位置,找不到就返回-1

const arr = [1,2,3,4,5]
arr.find(item=>item===5)  5
arr.find(item=>item === 6) undefined
arr.findIndex(item=>item === 5) 4
arr.findIndex(item=>item === 6) -1

二分查找法

前提 : 数组必须是有序的

循环 查找数组里是否有指定的数字,并返回下标

function findElement(sortedArr,element){
  let startIndex = 0
  let endIndex = sortedArr.length - 1
  while(startIndex <= endIndex){
    //中间元素的下标
    const middleIndex = startIndex + Math.floor((endIndex - startIndex)/2)
    //判断中间元素和查找元素是否相等
    if(element === sortedArr[middleIndex]){
      return middleIndex
    }
    //比较中间元素和查找元素的大小
    if(sortedArr[middleIndex] < element){
      startIndex = middleIndex + 1
    }else{
      endIndex = middleIndex - 1
    }
  }
}
const arr = [1,5,9,13,99,100]
fineElement(arr,99)

递归二分查找

中间值offset 记录下标,每次重置数组传进递归函数里面,返回值 middleIndex + offset

startIndex 和 endIndex 必须是大于等于0的数,否则跳出递归

function findElement(sortedArr,element,offset = 0){
  //offset 记忆原始数组下标
  let startIndex = 0
  let endIndex = sortedArr.length - 1
  //这里的中间元素下标与循环二分查找相比 不比加startIndex了 
  const middleIndex = Math.floor((endIndex - startIndex ) /2)
  //判断中间元素和查找元素是否相等 basecase 跳出递归的关键代码
  if(element === sortedArr[middleIndex]){
    return middleIndex + offset
  }
  if(sortedArr[middleIndex] < element){
		startIndex = middleIndex + 1 //重置数组起点下标    
    offset = middleIndex + 1
  }else{
    endIndex = middleIndex - 1 // 重置数组终点下标
  }
 	if(startIndex >= 0&&endIndex >= 0){
  	return findElement(sortedArr.slice(startIndex,endIndex+1),element,offset)  
  }else{
    return -1
  }
  
}
const arr = [1,5,9,13,99,100]
console.log(findElement(arr,99,0)) 

时间复杂度 : O(logn) [这种拆分成小部分的算法时间复杂度 都为O(logn)]

知识补充:

slice 返回一个新的数组,包含从 start 到 end (不包括该元素)

arr.slice(start,end) 取值 区间:[ start, end )

排序算法

  • 什么是排序算法

    排序算法是对一些无序数据进行排序

    • 冒泡排序
    • 归并排序
    • ...
  • 不同类型的排序算法

冒泡排序 Bubble Sort

重复扫描待排序序列,并比较每一对相邻的元素,当该元素顺序不正确时进行交换。一直重复这个过程,知道没有任何两个相邻元素可以交换,就表明完成了排序

每一遍循环都是拿外层这个位置的元素跟内层的元素进行比较,与数无关与位置有关

升序
function sort(arr){
  const resultArr = [...arr] //得到一个新数组
  // 外层循环
  for(let outer = 0;outer<resultArr.length;outer++){
		// 里层循环
    let outerEl = resultArr[outer]
    for(let inner = outer + 1;inner<resultArra.length;inner++ ){
      let innerEl = resultArr[inner]
      //对比
      if(outerEl > innerEl){
        //交换位置
        resultArr[outer] = innerEl
        resultArr[inner] = outerEl
        // 交换完位置重新赋值
        outerEl = resultArr[outer]
        innerEl = resultArr[inner] // 这行感觉可以不用写
      }
    }
  }
  return resultArr
}
const sortedArr = [4,12,-3.-8,1,72,36]
console.log(sort(sortedArr))

冒泡排序的时间复杂度:

最好情况 : 元素项已经排序 O(n)

平均情况:元素随机排序,不知道元素的位置 趋向于 O(n^2)

最差情况:元素项按照相反排序 O(n^2)

快速排序 Quicksort

1.选择一个分界元素将数组拆分为较小的快

2.小于分界元素(左边),等于分界元素(中间),大于分界元素(右边);

3.左右两侧的元素又独立排序,执行重复步骤;

4.最后左右两部分都排序完成后,整个数组就实现排序

function sort(arr) {
    //   复制数组
    const copyArr = [...arr]
    // base case 当分割数组只有一项时就返回数组,跳出递归
    if(copyArr.length <= 1){
        return copyArr
    }
    //初始化不同类型的数组容器
    const smallerElementsArr = []; //放置比分界值小的元素
    const biggerElementArr = []; //放置比分界值大的元素
    const pivotElement = copyArr.shift();//找到每个数组的分界值,(用数组第一项作为分界值去进行比较)
    const centerElementArr = [pivotElement]; //存放分界值

    // 递归步骤
    while(copyArr.length){
        const currentElement = copyArr.shift();
        // 和分界值对比
        if(currentElement === pivotElement){
            centerElementArr.push(currentElement)
        }else if(currentElement < pivotElement){
            smallerElementsArr.push(currentElement)
        }else{
            biggerElementArr.push(currentElement)
        }
    }
    // 递归
    const smallerElementsSortedArr = sort(smallerElementsArr)
    const biggerElementsSortedArr =  sort(biggerElementArr)

    return smallerElementsSortedArr.concat(centerElementArr,biggerElementsSortedArr)
}
const sortedArray = sort([-2, 9, 3, 41,-2, -6, 35, 18])
console.log(sortedArray)
/**
 * 快速排序,找到一个分界值,数组里所有元素与它比较,比它小的放左边,比它大的放右边,不断的去找这个值,最后合并到一起
 */

快速排序的时间复杂度

最好情况 :元素随机排序(不是升序和降序) 时间复杂度 O(n*logn)

平均情况 : 元素是随机排序(不是升序和降序) 时间复杂度趋向于 O(n*logn)

最差情况 : 元素已经排序(升序或降序) 时间复杂度O(n^2)

归并排序 Merge Sort

归并排序是建立在归并操作上的一种有效稳定的排序方法

1) 多次拆分数组,直到只剩下2个元素的数组

2) 对这些数组进行排序,然后将它们合并在一起

![1841606291477_.pic_hd](/Users/zhangfanglan/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9/d52199f38055a59ff7217d3e8ca0cada/Message/MessageTemp/9e20f478899dc29eb19741386f9343c8/Image/1841606291477_.pic_hd.jpg)

function sort(arr){

  //递归  base case 跳出递归语句
  // 验证分割数组长度小于2的时候
  if(arr.length < 2){
    return arr
  }
  // 验证分割数组长度为2的时候,进行排序
  if(arr.length === 2){
    return arr[0] > arr[1] ? [arr[1],arr[0]] : arr 
  }
  // 对数组进行分割,创建中间值,Math.floor() 向下取整,Array.slice(start,end) [start,end) 
  const middle = Math.floor(arr.length / 2)
  const leftArray = arr.slice(0,middle)
  const rightArray = arr.slice(middle,arr.length)
  
  // 递归步骤
  const leftSortedArray = sort(leftArray)
  const rightSortedArray = sort(rightArray)
  
  // 实现归并和排序
  let mergedArr = []
  let leftArrIndex = 0
  let rightArrIndex = 0
  // 两个下标值同时大于等于两个数组的长度时跳出循环
  while(leftArrIndex < leftSortedArray.length || rightArrayIndex < rightSortedArray.length){
    // 比较数组的元素大小
    if(leftArrIndex >= leftSortedArray.length || leftSortedArray[leftArrIndex] > rightSortedArray[rightArrIndex]){
      mergedArr.push(rightSortedArray[rightArrIndex])
      rightArrIndex++
    }else {
      mergedArr.push(leftSortedArray[leftArrIndex])
      leftArrIndex++
    }
  }
  return mergedArr
}
sort([43,-12,5,12,8,2,-20,300])

小结:

  • 冒泡排序 是 双层循环嵌套

  • 快速排序,找到一个分界值(默认是数组第一项),数组里所有元素与它比较,比它小的放左边,比它大的放右边,不断的去找这个值(递归),最后合并到一起

  • 归并排序,数组从中间分隔,俩数组如果长度大于2就不断分割(递归),直到长度为1或者2 就排序并返回,然后进行归并比较数组大小, 归并就是一个空数组,然后比较得到的left和right数组对比大小放进新的空数组并返回,两个下标值同时大于等于两个数组的长度时跳出循环

空间复杂度 Space Algorithms

什么是空间复杂度

是对一个算法在运行过程中临时占用存储空间大小的量度(算法占用多少内存空间)

一个算法在计算机存储器上所占用的存储空间,包括存储算法本身所占用的存储空间,算法的输入输出数据所占用的存储空间和算法在运行过程中临时占用的存储空间

JavaScript中所有的值都存储在内存中,数组和对象会占用更多空间

通常,在JS中,我们不必担心空间复杂性和内存过多的情况

如何推导算法的空间复杂度

查找算法中 “永久”存储的数据(值)【 有没有开辟新的内存空间】

​ ||

计算这种永久存储的值被创建的频次【保持存在】

​ ||

判断这些值的数量是如何取决于输入的“n‘”【找和输入n的关系】

得出对应的空间复杂度,例如O(n) O (1) 等

案例分析

数学算法和搜索算法

算法 空间复杂度 理由
阶乘(循环) O(1) 我们运算的是相同或同一个数值,每次迭代都不会创建新的(永久)的值
阶乘(递归) O(n) 每个嵌套函数被调用的时候就创建一个新的值(函数接收到的参数)
线性搜索 O(1) 函数在迭代过程中并没有创建新的“永久”的值
二分查找 O(1) 函数在迭代过程中并没有创建新的“永久”的值

阶乘循环

function fact(number){
  let result = 1 
  // 这里 number 、 result 被存储在内存空间中
  for(let i = 2;i<=number;i++){
     // i 随着变化,result 和 i 都自增,空间复杂度并不受输入number的输入而影响(输入number与result 和 i 变化无关)
    result += result * i
  }
 
  return result
}
// 空间复杂度 O(1) => 常数空间复杂度

阶乘递归

function fact(number){
  if(number === 1){
    	return 1
  }
  return number*fact(number-1)
}

线性搜索

function findElement(arr,element,comparatorFn){
  let index = 0
  for(const item of arr){
    if(typeof element === 'object' &&element != null && comparatorFn(element,item)){
      return index
    }
    if(item === element){
      return index
    }
    index++
  }
}

二分查找法

  • 循环
function findElement(sortedArr,element){
  let startIndex = 0
  let endIndex = sortedArr.length - 1
  while(startIndex <= endIndex){
    const middleIndex = startIndex + Math.floor((endIndex-startIndex)/2)
    const currentEle = sortedArr[middleIndex]
    if(element === currentEle){
      return middleIndex
    }
    if(element > currentEle){
      startIndex = middleIndex + 1
    }else{
      endIndex  = middleIndex - 1
    }
  }
}
  • 递归

    function findElement(sortedArr,element,offset =  0){
      let startIndex = 0,
          endIndex = sortedArr.length - 1,
          middleIndex = Math.floort((endIndex-startIndex)/2),
          currentEle = sortedArr[middleIndex]
      if(element === currentEle){
        return middleIndex + offset
      }
      if(element > current){
        startIndex = middleIndex + 1
        offset = middleIndex + 1
      }else{
        endIndex = middleIndex - 1
      }
      if(startIndex >= 0 && endIndex >=0){
        findeElement(sortedArr(startIndex,endIndex+1),element,offset)
      }else{
        return -1
      }
      
    }
    

    排序算法

    算法 空间复杂度 理由
    冒泡排序 O(1) 函数在迭代过程中,并没有创建新的“永久”值
    快速排序

    冒泡排序

function sort(arr){
  let result = [...arr]
  for(let outer = 0;outer<arr.length;outer++){
    let outerEl = result[outer]
    for(let inner = outer+1;inner<arr.length;inner++){
      let innerEL = result[inner]
      if(outerEl < inner){
        result[outer] = innerEL
        result[inner] = outerEl
        
        outerEl = result[outer]
      }
    }
  }
  return result
}

快速排序

function sort(arr){
  const copyArr = [...arr]
  if(copyArr.length <=1){
    return copyArr
  }
  const smallerElementArr = []
  const biggerElementArr = []
  const privotElemenet = copyArr.shift()
  const centerElmentArr = [privotElement]
  while(copyArr.length){
    const currentElement = copyArr.shift()
    if(currentElement  === privotElement){
      centerElmentArr.push(currentElement)
    }else if(currentElement < privotElement){
      smallerElementArr.push(currentElement)
    }else{
      biggerElementArr.push(currentElement)
    }
  }
  const smallerElementSortedArr = sort(smallerElementArr)
  const biggerElementSortedArr = sort(biggerElementArr)
  return smallerElementSortedArr.concat(centerElmentArr,biggerElementSortedArr)
}

题目练习

反转字符串

给定一个字符串,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。

示例:

javascript
输入:"Let's take LeetCode contest"
输出:"s'teL ekat edoCteeL tsetnoc"

提示:在字符串中,每个单词由单个空格分隔,并且字符串中不会有任何额外的空格

var reverseWords = function(str){
//字符串按空格进行分隔,保存数组,数组的元素的先后顺序就是单词的顺序
return str.split(' ').map(val=>{
return val.split('').reverse().join('')
}).join('')
}

Split 把一个字符串分割成字符串数组
用正则

      var reverseWords = function(str){
            return str.split(/s/g).map(val=>{
                  return val.split('').reverse().join('')
            }).join(' ')
      }

卡牌分组

给定一副牌,每张牌上都写着一个整数。

此时,你需要选定一个数字 X,使我们可以将整副牌按下述规则分成 1 组或更多组:

每组都有 X 张牌。
组内所有的牌上都写着相同的整数
仅当你可选的 X >= 2 时返回 true。

示例 1:

输入:[1,2,3,4,4,3,2,1]
输出:true
解释:可行的分组是 [1,1],[2,2],[3,3],[4,4]
示例 2:

输入:[1,1,1,2,2,2,3,3]
输出:false
解释:没有满足要求的分组。
示例 3:

输入:[1]
输出:false
解释:没有满足要求的分组。
示例 4:

输入:[1,1]
输出:true
解释:可行的分组是 [1,1]
示例 5:

输入:[1,1,2,2,2,2]
输出:true
解释:可行的分组是 [1,1],[2,2],[2,2]

tips: 统计相同数字出现次数的最大公约数,如果大于等于2则满足要求,否则不满足

Map 数据结构不会出现重复元素,

let timeMap = new Map()

deck.forEach(num=>{

	timeMap.set(num,timeMap.has(num)?timeMap.get(num)+1:1)

})
let timeArr = [...timeMap.values()]
 
获得最大公约数
function gcd(num1,num2){
  return num2 === 0?num1:gcd(num2,num1%num2)
}
/**
 * @param {number[]} deck
 * @return {boolean}
 */
var hasGroupsSizeX = function(deck) {
    // 最大公约数计算公式
    function gcd(num1, num2) {
        // 利用辗转相除法来计算最大公约数
        return num2 === 0 ? num1 : gcd(num2, num1 % num2); 
    }

    // 相同牌出现次数Map
    let timeMap = new Map();

    // 遍历牌
    deck.forEach(num => {
        // 统计每张牌出现的次数
        timeMap.set(num, timeMap.has(num) ? timeMap.get(num) + 1 : 1);
    });

    // Map.values()返回的是一个新的Iterator对象,所以可以使用扩展运算符(...)来构造成数组
    let timeAry = [...timeMap.values()];

    /*
    最大公约数
    因为该数组是出现次数数组,最小值至少为1(至少出现1次),所以默认赋值为数组首位对公约数计算无干扰
    */
    let g = timeAry[0];

    // 遍历出现次数,计算最大公约数
    timeAry.forEach(time => {
        // 因为需要比较所有牌出现次数的最大公约数,故需要一个中间值
        g = gcd(g, time);
    });

    // 判断是否满足题意
    return g >= 2;
};

作者:gatsby-23
链接:https://leetcode-cn.com/problems/x-of-a-kind-in-a-deck-of-cards/solution/javascriptzhan-zhuan-xiang-chu-fa-tong-su-yi-dong-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。}

原文地址:https://www.cnblogs.com/angfl/p/14138527.html