JavaScript之算法设计思想

1️⃣排序算法

一、冒泡排序

  1. 比较所有相邻的元素,如果第一个比第二个大,则交换他们的位置
  2. 一轮比较可以保证最后一个数是最大的,然后继续遍历剩余的数
  3. 代码实现
const arr = [5,4,3,7,0,2,1]
let temp = 0;
//执行arr.length-1次数组中冒泡的过程
for(let i = 0; i < arr.length -1; i++){
	//遍历整个数组,将数组中最小的一个移动到数组最后面
	//因为每一轮循环后,都是将数组中最小的移动到后面,所以arr.length-i,数组末尾已经是最小元素就不再遍历了
    for(let j = 0; j < arr.length - i; j++){
    //将如果相邻元素中,第一个比第二个大,则第一个元素向后移
        if(arr[j]>arr[j+1]){
            temp = arr[j+1];
            arr[j+1] = arr[j];
            arr[j] = temp
        }
    }
}

二、选择排序

  1. 找到未排序的数组中的最小值,将他和第一位互换位置
  2. 找到未排序数组中第二小的值,选中并将其放置在第二位
  3. 执行n-1轮后得到的就是一个升序数组
  4. 代码实现
const arr = [5,4,3,7,0,2,1]
let temp = 0;
//执行arr.length-1次选择
for(let i = 0; i < arr.length - 1; i++){
	//遍历整个数组
	//一轮循环数组中最小的元素保证在第一位。后续循环以此类推
	//所以每一轮循环结束,数组前面都是交换出来的最小值
	//使j=i,可以在后续遍历中忽略前面已经确认的较小值
    for( let j = i; j < arr.length - 1; j++){
    //如果数组中存在元素小于第一位,则两者互换位置-
        if(arr[j] > arr[j+1]){
            temp = arr[j+1];
            arr[j+1] = arr[i];
            arr[i] = temp
        }
   
    }    
}

三、插入排序

  1. 从第二个数开始往前比
  2. 遇到大于自己的就将其(大值)位置向后移一位,小于自己的就插入到这个值后面
  3. 以此类推执行n-1次

const arr = [0,5,2,6,1,9]
//因为是从第二位开始比所以i=1开始
for(let i = 1; i < arr.length; i++){
    let temp = arr[i];
    let j = i;
    while( j > 0){
        if( arr[j-1] > temp ){
            //将大于当前元素的值向后移
            arr[j] = arr[j-1]
        }
        //当前元素大于前面元素时跳出循环,比较下一位
        //这里break跳出的是while循环,因为前面的元素比较过了,都是有序的数组,所以当arr[j-1] < temp是可以判断temp比之前所有的元素都大,跳出本次循环即可
        else break;
        // j-=1如果放在if前面,当前元素大于前面值时arr[j] = temp依然会将当前元素插入到前面的位置
        //而放在后面break跳出循环j-=1没有执行,相当于arr[j] = arr[j],插入操作值未改变
        j -= 1;
    }
//交换元素位置(此时j已经执行了-1操作),如果是通过break跳出来的,实际上j并未发生改变,arr[j] = temp相当于给自身赋值
    arr[j] = temp;
}
//console.log(arr)

四、归并排序

  1. 分:把数组从中间分成两半,在不断递归的对数组进行“分”的操作,直到分成一个个只存在单独数的数组
  2. 合:把不能再分的两个数组合并为有序数组,然后再对有序数组进行合并,直到全部子数组合并为一个完整的数组

合并方法:

  1. 新建一个空数组res用于存放最终排序后的数组
  2. 比较两个有序数组的头部·,较小者出队(将拆分后的数组视为为队列)并推入res数组中
  3. 如果数组还有值就重复一二步操作

代码实现

const merge = (arr) => {
    // 如果数组长度为1,相当于不能再拆分时返回这些单个数组
    if(arr.length === 1) {return arr;}
    // 获取数组的中间值
    const mid = Math.floor(arr.length / 2)
    // 将数组从中间拆分成开,记为左右新两个数组
    const left = arr.slice(0,mid)
    const right = arr.slice(mid,arr.length)

    // 递归拆分
    const orderLeft = merge(left)
    const orderRight = merge(right)
    let res = []
    //当拆分后的数组不为空时
    while(orderLeft.length || orderRight.length){
        // 如果拆分后左右都存在就比较头元素的大小
        if(orderLeft.length && orderRight.length){
            // 将头元素较小的取出然后加到res数组中
            res.push(orderLeft[0] < orderRight[0] ? orderLeft.shift() : orderRight.shift())
            // 拆分后只有左边数组存在
        }else if(orderLeft.length){
            res.push(orderLeft.shift())
            // 拆分后只有右边数组存在
        }else if(orderRight.length){
            res.push(orderRight.shift())
        }
    }
    return res;
}
const arr = [1,4,3,9,2]
console.log(merge(arr))

五、快速排序

  1. 分区: 从数组中任意选择一个基准,比基准大的元素放在基准后面,比基准小的元素放在基准前面
  2. 递归: 分区后得到的数组左边都比基准值小,右边都比基准值大。然后递归的对基准值前后的子数组再进行分区

代码实现

const fastSort = (arr) => {
    if(arr.length === 0) {return arr;}
    // 定义左右两个数组,大于基准的放右边数组,小于基准的放左边基准
    const left = []
    const right = []
    // 基准是随机挑选的一个数
    const stand = arr[0]
    // 因为数组第一个数被挑出来作为基准了,所以这里i从1开始
    for(let i = 1; i < arr.length; i++){
        if(arr[i] < stand){
            left.push(arr[i])
        }else{
            right.push(arr[i])
        }
    }
    // 这里左数组、右数组、基准为三个独立的部分。需要将三者合成一个数组
    //...是ES6中新增的扩展运算符,数组中的扩展运算符(…)用于取出参数数组中的所有可遍历属性,拷贝到当前数组之中。
    return [...fastSort(left),stand,...fastSort(right)]
}
const arr = [1,4,0,3,7]
console.log(fastSort(arr))

但是其实这种快速排序方法是错误的,虽然也是利用快速排序的原理左右分区然后递归,但是使用了两个数组,空间复杂度增加

下面因该是比较贴切一点的快速排序,可以使用console.time()方法来测试一下排序用时

// 计算程序执行时间
console.time('a')
// 函数传入三个参数,依次是:待排序的数组,待排序数组左边第一个元素的索引,待排序元素右边得第一个元素的索引
const fastSort = (arr,began,end) => {  
    // 取数组的第一个元素为基准值
    let print = arr[began]
    //如果不定义i,j 直接改变began,end的值,会影响后面递归开始和结束的索引值
    let i = began;
    let j = end;
    //当左边的索引值大于右边的时,说明本轮排序结束
    if(began >= end) {
        // 将本轮排序后的数组返回
        return arr
    }
    //把所有比基准值小的元素放在基准值左边,大的元素放在基准值右边
    while(i < j){
        // 从后往前查找到比基准值小的元素交换
        //继续判断i<j是因为:如果print值较小,arr[j]>print会持续成立,导致j<=i,索引j<=i表示整个数组已经排序结束
        while(arr[j] >= print && i < j){
            j--
        }
        // 比基准值小的元素放左边,如果不存在比基准值小的元素,此时j=i相当于自身给自身赋值
        arr[i] = arr[j] 
        // 找到比基准值大的元素交换
        // 继续判断i<j,因为print值较大时,arr[i] <= print会持续成立,导致j<=i
        while(arr[i] <= print && i < j){
            i++
        }
        // 比基准值大的元素放在右边,如果不存在比基准值大的元素,此时i=j相当于自身给自身赋值
        arr[j] = arr[i]       
    }
    //一次循环结束,将基准值放在中间位置(此时i=j,arr[i]或者arr[j]都可以)
    arr[j] = print
    //递归,继续对基准值左右两边的数组进行排序
    // 这里began不能直接传入0,会影响到第三轮之后的递归,
    // 因为对基准值右边的数组再进行分区排序时也会使用到began,而此时began并不是0
    fastSort(arr,began,j-1)
    fastSort(arr,j+1,end)
    //返回排序后的数组
    return arr
}	 	  
const arr = [9,5,1,4,8]
console.log(fastSort(arr,0,arr.length-1))
console.timeEnd('a')

之所以从右边开始排序,是因为基准值选择的是最左边,举个例子7 1 2 3 4执行快速排序,假如从左开始排,就会出现1,7,2,3,4

附:这里有一个25w个数据的乱序数组(有重复),可以用来测试这些排序算法的速度 https://download.csdn.net/download/aaahuahua/34715763

2️⃣查找方法

搜索:js中通常使用indexOf方法(返回元素在数组中的下标,不存在返会-1)

一、顺序查找

  1. 遍历数组
  2. 找到和目标值相等的元素,返回下标
  3. 遍历所有值后如果没有目标值返回-1

代码实现

// 在Array数组原型上添加一个方法
Array.prototype.searchWay = function (item){
// this指代这个数组
    for(let i = 0; i < this.length; i++){
        if(item === this[i]){
            return i
        }
    }
    return -1
}
const arr = [1,3,5,2,9]
//调用searchWay方法
console.log(arr.searchWay(5))

二、二分查找

前提: 查找的是有序数组

如果是一个无序数组,需要先将其化为有序数组之后再进行二分搜索

搜索方法

  1. 从中间元素开始,如果正好是目标值,则搜索结束
  2. 如果目标值小于或者大于中间元素,则在大于或者小于目标值的部分数组中继续进行搜索
// 在数组的原型上添加一个search方法
Array.prototype.search = function (item){4
    // 定义查找的起始位置和结束位置
    let low = 0;
    let height = this.length - 1;
    // 等于号用于判断数组中只剩下最后一个数时,是否为目标值
    while(low <= height){
        // 选取数组中间元素为基准值
        let mid = Math.floor((low + height) / 2);
        const num = this[mid]
        // 如果目标值就是基准值则返回基准值的下标
        if(item === num){    
            return console.log(mid)
        }else if(item > num){ //当目标值大于基准值时,将查找范围缩小为基准值右边的数组
            low = mid + 1
        }else if(item < num ){//当目标值小于基准值时,将查找范围缩小为基准值左边的数组
            height = mid - 1
        }
    }
    // 查找结束后如果还是没有找到目标元素,则返回该元素不存在
    return console.log("该元素不存在")
}
//调用数组原型上的search方法
const arr = [1,2,3,4,5].search(0)

3️⃣算法设计思想

一、动态规划

将一个问题分解成相互重叠的子问题,通过反复求解子问题,来解决原来的问题

步骤:

  1. 定义子问题
  2. 循环执行子问题中定义的公式

实战练习

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。

输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1 阶 + 1 阶 + 1 阶
1 阶 + 2 阶
2 阶 + 1 阶

动态规划

  1. 定义子问题,到达第n阶可以有两种方法
    (1)从第n-1阶爬一阶
    (2)从第n-2阶爬两阶

所以到达第n阶的方法就是到达第n-1阶的方法加上到达第n-2阶方法之和:F(n)=F(n-1)+F(n-2)

  1. 从n=2开始循环执行F(n)=F(n-1)+F(n-2)

代码实现


var climbStairs = function(n) {
//当阶数小于2,也就是1阶时,直接返回1(n为正整数所以不存在复数的情况)
    if(n<2) return 1;
//定义数组dp存放到达每一阶的方法数,下标代表阶数,对应值为到达改阶的方法数
//虽然这里0阶定义的是1,但是n为正整数,不会存在n=0的情况
    const dp = [1,1];
//因为前两次已经被列举出来了,所以i从2开始
    for(let i = 2;i <= n; i++){
        dp[i]=dp[i-1]+dp[i-2]
    }
    return dp[n]
};

//代码优化,因为定义了数组,所以空间复杂度为O(n),可以只定义两个变量将复杂度降到O(1)
var climbStairs = function(n) {
    if(n<2) return 1;
    const dp0 = 1;
    const dp1 = 1;
    for(let i = 2;i <= n; i++){
    //临时存储到达第n-2阶的方法
        const temp = dp0;
        dp0 = dp1;
        dp1 = temp + dp1;
    }
    return dp1;
};

二、分而治之

将原问题为很多个相似的小问题,递归的解决这些小问题,然后再将结果并以解决原有的问题

1. 设计案例之归并排序

  • 分:将数组一分为二
  • 解:递归的对两个子数组进行归并排序
  • 合:合并有序数组(将递归后长度为1的数组合并成有序数组,然后再把这些有序数组继续合并)

2. 设计案例之快速排序

  • 分:选择基准值将原数组分成大于基准值和小于基准值的两个数组
  • 解:递归的对两个子数组进行快速排序
  • 合:递归到数组中只剩下一个·元素时排序完毕,返回该数组即可

实战练习

猜数字游戏的规则如下:

每轮游戏,都会从 1 到 n 随机选择一个数字。 请你猜选出的是哪个数字。如果你猜错了,我会告诉你,你猜测的数字比我选出的数字是大了还是小了。

你可以通过调用一个预先定义好的接口 int guess(int num) 来获取猜测结果,返回值一共有 3 种可能的情况(-1,1 或 0):

  1. -1:选出的数字比你猜的数字小 pick < num
  2. 1:选出的数字比你猜的数字大 pick > num
  3. 0:选出的数字和你猜的数字一样。恭喜!你猜对了!pick == num 返回选出的数字。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/guess-number-higher-or-lower

示例 1: 输入:n = 10, pick = 6 输出:6
示例 2:输入:n = 1, pick = 1 输出:1
示例 3:输入:n = 2, pick = 1 输出:1
示例 4:输入:n = 2, pick = 2 输出:2

解题思路:

使用二分搜索,分,解,和,利用分而治之的思想来做

  1. 分:计算中间元素,分割数组
  2. 解:递归的在较大的或者较小的数组中进行二分搜索
  3. 合:这一步可以省略,因为如果在子数组中搜索到目标值后就直接将其返回了

代码实现:

var guessNumber = function(n) {
//这里定义一个二分查找的函数
    const divide = (low,height) => {
    //当最小值大于最大值时直接return
        if(low > height) return;
        //取两个数的中间值
        const mid = Math.floor((low+height)/2);
        //调用接口判断猜测结果是否正确
        const res = guess(mid);
        if(res === 0 ){
            return mid
        }else if(res === -1){
        //猜测结果小于真实值
            return divide(1,mid-1)
        }else if(res === 1 ){
        //猜测结果大于真实值
            return divide(mid+1,height)
        }
    }
    return divide(1,n)
};

对称二叉树

给定一个二叉树,检查它是否是镜像对称的。

例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
在这里插入图片描述
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
在这里插入图片描述

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/symmetric-tree

解题思路:

可以转换为左右子树是否为镜像的,然后分解为树1的左子树和树2的右子树是否为互为镜像,树1右子树和树2的左子树是否为互为镜像

代码实现:

var isSymmetric = function(root) {
//如果二叉树为空,返回为true
  if(!root) return true;
  //定义一个镜像判断函数
  const isCheck = (left,right) => {
  //如果左右子树都不存在,返回为true
      if(!right && !left){
          return true
      }
      //左右子树存在,值相等,递归左右子节点
      if(right && left && right.val === left.val &&
        isCheck(left.left,right.right) &&
        isCheck(left.right,right.left)
      ){
          return true
      }
      //其他情况下返回false
      return false
  }
    return isCheck(root.left,root.right)
}; 

三、贪心算法

期盼通过每个阶段的局部最优选择,从而达到全局的最优。但是结果有可能并不是最优的

分发饼干

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/assign-cookies

示例1:

输入: g = [1,2,3], s = [1,1]
输出: 1
解释: 你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。 所以你应该输出1。

示例2:

输入: g = [1,2], s = [1,2,3]
输出: 2
解释:
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.

解题思路:

局部最优:分发的饼干既能满足孩子,还消耗最少,即先将较小的饼干分给胃口小的孩子

解题步骤:

  1. 对饼干数组和胃口数组进行升序排序
  2. 遍历饼干数组,找到能满足第一个孩子(胃口最小的孩子)的饼干
  3. 继续遍历饼干数组,找到剩下能满足第二、第三、第四…个孩子的饼干

代码实现:

var findContentChildren = function(g,s) {
//快速排序
    const fastSort = (arr,began,end) => {
        let print = arr[began]
        let i = began;
        let j = end;
        if(began >= end){
            return arr;
        }
        while(i < j){
            while(arr[j] >= print && i < j){
                j--
            }
            arr[i] = arr[j]
            while(arr[i] <= print && i < j){
                i++
            }
            arr[j] = arr[i];
        }
        arr[j] = print;
        fastSort(arr,began,j-1)
        fastSort(arr,j+1,end)
        return arr
    } 
    //对小孩的胃口和饼干尺寸两个数组进行排序
    fastSort(s,0,s.length-1)
    fastSort(g,0,g.length-1)
    //定义能满足的小孩个数
    let i = 0;
    //遍历所有饼干
    s.forEach(item =>{
    //如果饼干尺寸大于小孩的胃口,能满足的小孩数加1
        if(item >= g[i]){
            i+=1
        }
    })
    //返回满足的小孩个数
    return i
};

四、回溯算法

一种渐进式寻找并构建问题解决方式的策略,即从一个可能的情况开始解决问题,如果不行,就回溯到另一种情况,直到问题被解决(也可以理解为暴力破解)

回溯思路

  1. 使用递归模拟出所有的情况
  2. 遇到不满足条件的情况就回溯
  3. 收集所有能到达递归终点的情况,并返回

实战练习

全排列规则如下:

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序返回答案。


示例 1:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:
输入:nums = [1]
输出:[[1]]

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/permutations

代码实现

var permute = function(nums) {
	//定义结果集
    let res = []
    //定义已排列结果
    let way = []
    //used(标记数组),用于记录当前该元素是否已经被添加到已排列数组(way)中了
    const backtrack = (arr,used) =>{
    //递归结束条件,当已排列数组的长度等于原数组长度时,表明所有元素都已经被添加到已排列数组中了
        if(way.length === arr.length) {
            //将本次排列的结果添加到结果集中
            res.push([...way])
            return;
        }
        // 遍历数组
        for(let i = 0; i < arr.length; i++){

    // if(way.indexOf(arr[i]) != -1)
    //  不使用这个方法是因为查找的时间复杂度为O(n),同样的引入used数组会增加空间复杂度也是O(n)

            // undefind转换为布尔值是false
            if(used[i])  // 判断该元素是否已经存在于已排列数组中
            continue;
            //向结果集中添加元素
            way.push(arr[i])
            //标记该元素为已添加,使下一层遍历能够区分哪些元素是未排列的剩余元素
            used[i] = true
            // 递归遍历数组,递归遍历arr.length次就可以将数组中的元素全部添加到排列数组中
            backtrack(arr,used)
            // 这个是本次全排列中回溯的重要体现
            // 使用pop方法删除已经添加到排列数组中的元素,可以使排列过程回到上一层递归
            way.pop()
            // 删除已经添加到排列数组中的元素标记
            used[i] = false
        }    
    }
    backtrack(nums,[])
    return res
};

子集规则如下

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集不能包含重复的子集。你可以按任意顺序返回解集。

示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例 2:
输入:nums = [0]
输出:[[],[0]]

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/subsets

代码实现

const subsets = (nums) => {
    const res = [];
    const sun = (index, temp) => {
      if (index == nums.length) { // 索引等于数组长度时
        res.push(temp.slice());   // 加入解集
        return;                   // 结束当前的递归
      }
      temp.push(nums[index]); // 将该元素加入数组
      sun(index + 1, temp);   // 往下递归,添加剩余元素
      temp.pop();             // 上面的递归结束,逐步删除已经添加过的元素
      // 删除已添加的元素后,将剩余结果加入结果集;操作剩余元素
      sun(index + 1, temp);   
    };
    sun(0, []);
    return res;
  };
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值