JavaScript算法题100道

排序算法

1.冒泡排序

从开始位置两两比较,持续n轮

 function bubbleSort (arr) {
        // 执行第 i + 1 轮
        for (var i = 0; i < arr.length - 1; i++) {
            for (var j = 0; j < arr.length - 1; j++) {
                // 前一个与后一个两两比较
                if (arr[j] > arr[j + 1]) {
                    // 交换两个变量值
                    [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
                }
            }
        }
    }
    var arr = [4, 23, 14, 52, 66, 1134, 567, 23]
    bubbleSort(arr)
    console.log(arr);//[4, 14, 23, 23, 52, 66, 567, 1134]

2.选择排序

每次选出最大/最小值 持续n轮

 function selectSort (arr) {
        // 找出最小值,length - 1 次
        for (let i = 0; i < arr.length - 1; i++) {
            let index = i
            for (let j = i + 1; j < arr.length; j++) {
                if (arr[index] > arr[j]) {
                    index = j
                }
            }
            // 判断最小索引是否改变,若是,换
            if (index !== i) {
                [arr[i], arr[index]] = [arr[index], arr[i]]
            }
        }
    }
    var arr = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
    selectSort(arr)
    console.log(arr);//[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

3.插入排序

默认一个已排好序的数组 递增的往这个数组内插入元素

版本一

function insertSort (arr) {
  // 将要插入的索引
  for (let i = 1, len_i = arr.length; i < len_i; i++) {
    let cur = i
    // 插入到已经排好序的序列中
    while (cur > 0 && arr[cur] < arr[cur - 1]) {
      [arr[cur], arr[cur - 1]] = [arr[cur - 1], arr[cur]]
      cur--
    }
  }
}

版本二

function insertSort (arr) {
  // 将要插入的索引
  for (let i = 1, len = arr.length; i < len; i++) {
    let tmp = arr[i]
    // 插入到已经排好序的序列中
    for (let j = i - 1; j >= 0 && arr[j] > tmp; j--) {
      arr[j + 1] = arr[j]
    }
    arr[j + 1] = tmp
  }
}

4.希尔排序

增量式的 插入排序 正常的插入排序间隔为1 希尔排序默认设置间隔大于1 然后递减为1

function shellSort (arr) {
  let len = arr.length
  let step = 1
  let dis = 3
  // 设置最大间隔
  while (step < len / dis) {
    step = step * dis + 1
  }
  // 间隔递减
  for (; step > 0; step = Math.floor(step / dis)) {
    // 单个间隔的插入排序
    for (let i = step; i < len; i++) {
      let tmp = arr[i]
      let j = i - step
      while (j >= 0 && arr[j] > tmp) {
        arr[j + step] = arr[j]
        j -= step
      }
      arr[j + step] = tmp
    }
  }
}

5.归并排序

排序之前将数组拆分为两部分排序 然后将排好序的两个部分进行合并排序

function mergeSort (arr) {
  if (arr.length <= 1) return ;
  let left = 0,
  let right = arr.length - 1,
  let mid = parseInt((left + right) * 0.5)
  sort(arr, left, mid)
  sort(arr, mid + 1, right)
  merge(arr, left, mid, right)
}
function sort (arr, left, right) {
  // 分解为一个元素
  if (left >= right) return ;
  let mid = parseInt((left + right) * 0.5)
  sort(arr, left, mid)
  sort(arr, mid + 1, right)
  merge(arr, left, mid, right)
}
function merge (arr, left, mid, right) {
  // 合并的[left, mid] [mid + 1, right] 部分 都已经排好序
  let i = left
  let j = mid + 1
  let tmp = []
  // 依次插入连个数组的元素
  while (i <= mid && j <= right) {
    // 这里的等于保证了排序算法稳定性(即两个相等的数排序后的位置不会改变)
    if (arr[i] <= arr[j]) {
      tmp.push(arr[i])
      i++
    } else {
      tmp.push(arr[j++])
    }
  }
  // 处理剩余的左边/右边元素
  while (i <= mid) tmp.push(arr[i++])
  while (j <= right) tmp.push(arr[j++])
  for (let i = 0, len = tmp.length; i < len; i++) {
    arr[left + i] = tmp[i]
  }
}

6.快速排序

排序之前将数组拆按照指定指定基点拆分为两部分 一部分小于等于基点 一部分大于基点

版本一

function quickSort (arr) {
  if (arr.length <= 1) return ;
  let left = 0
  let right = arr.length - 1
  sort(arr, left, right)
}
function sort (arr, left, right) {
  if (left >= right) return ;
  let baseValue = arr[left]
  let start = left
  let end = right
  while (start < end) {
    // 找到右边小于基点的值的索引
    while (start < end && arr[end] >= baseValue) end--
    // 找到左边大于基点的值的索引
    while (start < end && arr[start] <= baseValue) start++
    if (start < end) {
      [arr[start], arr[end]] = [arr[end], arr[start]]
    }
  }
  [arr[left], arr[start]] = [arr[start], arr[left]]
  sort(arr, left, start - 1)
  sort(arr, start + 1, right)
}

版本二

function quickSort(arr) {
  if (arr.length <= 1) return ;
  let left = 0
  let right = arr.length - 1
  sort(arr, left, right)
}
function sort (arr, left, right) {
  if (left >= right) return ;
  let baseValue = arr[left]
  let leftArr = []
  let rightArr = []
  for (let i = left; i <= right; i++) {
    if (arr[i] <= baseValue) {
      leftArr.push(arr[i])
    } else {
      rightArr.push(arr[i])
    }
  }
  let mid = left + leftArr.length - 1
  for (let i = 0, len = leftArr.length; i < len; i++) {
    arr[left + i] = leftArr[i]
  }
  for (let i = 0, len = rightArr.length; i < len; i++) {
    arr[mid + i + 1] = rightArr[i]
  }
  [arr[left], arr[mid]] = [arr[mid], arr[left]]
  sort(arr, left, mid - 1)
  sort(arr, mid + 1, right)
}

7.堆排序

将数组看作为一个堆结构 其中 index = 0 为根节点

index 指向的节点的子节点有 2*index + 1 2*index + 2 0 -> 1, 2 1 -> 3, 4 2 -> 5, 6...

实际的操作就是对这个堆进行调整 堆这个数据结构可以去了解一下

// 堆排序
function heapSort (arr) {
  // 从最后一个非叶子节点开始  构建大顶堆   即每次找出最大的一个数
  for (let i = Math.floor(arr.length * 0.5 - 1); i >= 0; i--) {
    adjustHeap(arr, i, arr.length)
  }
  // 将最大的一个节点放在末尾 等于排好序了 在对剩下的元素构建大顶堆
  for (j = arr.length - 1; j > 0; j--) {
    [arr[0], arr[j]] = [arr[j], arr[0]]
    // 大部分都不需要调整了 所以直接从根节点开始
    adjustHeap(arr, 0, j)
  }
}
// 调整堆
function adjustHeap (arr, i, len) {
  // k = 2 * k + 1 是在交换两个节点时会导致原来的堆结构混乱,需要重新调整子节点
  let cnt = 0
  for (let k = 2 * i + 1; k < len; k = 2 * k + 1) {
    // 找出左子节点与右子节点中最大的节点
    if (k + 1 < len && arr[k] < arr[k + 1]) k++
    // 判断是否与子节点进行交换
    if (arr[k] > arr[i]) {
      [arr[k], arr[i]] = [arr[i], arr[k]]
      // 交换完成后要对下一级节点进行重新调整
      i = k
    } else {
      break ;
    }
  }
}

简单难度算法题

8.汉诺塔问题

// 三根柱子分别为 A B C  A 为初始位置
function hanoi (n, current = 'A', temp = 'B', target = 'C') {
  if (n <= 0) return 0
  let sum = 1
  sum += hanoi(n - 1, current, target, temp)
  console.log(current + ' ---> ' + target)
  sum += hanoi(n - 1, temp, target, current)
  return sum
}

9.数组去重

数组去重算法

10.两个数组的交集

交集就是相同的部分 没有说是两个集合 所以最终的交集需要保留索引不同的相同的元素

基础版

function intersection (arr1, arr2) {
  let flag = {}
  let result = []
  for (let i = 0, len_i = arr1.length; i < len_i; i++) {
    let item = arr1[i]
    // 搜索有没有    使用 indexOf 或 includes 也没问题
    for (let j = 0, len_j = arr2.length; j < len_j; j++) {
      // 判断相等建议使用之前封装的 isEqual 方法
      // isEqual(arr1[i], arr2[i])
      if (item === arr2[j] && !flag[j]) {
        result.push(item)
        flag[j] = true
        break;
      }
    }
  }
  return result
}

API版本

// 更适合求两个集合的交集
function intersection (arr1, arr2) {
  // return noRepeat(arr1).filter(v => arr2.includes(v))
  return [...new Set(arr1)].filter(v => arr2.includes(v))
}

11.旋转数组

倒序? 或者 把数组往右旋转k步,要求不返回新的数组,直接改变原数组?

倒序

function reverse (arr) {
  let len = arr.length
  for (let i = 0, mid = parseInt(len * 0.5); i < mid; i++) {
    [arr[i], arr[len - i - 1]] = [arr[len - i - 1], arr[i]]
  }
  return arr
}

旋转k步

function rotateArr (arr, k) {
  let len = arr.length
  let tmp = [...arr]
  if (k < 0) k = (k % len) + len
  for (let i = 0; i < len; i++) {
    let index = (i + k) % len
    arr[index] = tmp[i]
  }
  return arr
}

12.写出利用线性表求集合交集、并集和差集的算法。

// 集合交集:
			function fn1(arr, brr) {
				return brr.filter((v) => arr.indexOf(v) > -1);
			}
			// 并集
			function fn2(arr, brr) {
				var crr = [];
				arr.forEach((v) => crr.push(v));
				return crr.concat(brr.filter((v) => arr.indexOf(v) === -1));
			}
			// 差集
			function fn3(arr, brr) {
				return fn2(arr, brr).filter((v) => fn1(arr, brr).indexOf(v) === -1);
			}
			console.log(fn3([1, 2, 3], [3, 4, 5]));//[1, 2, 4, 5]

13.写出求n!的递归算法。

 function fn (n) {
            if (n === 0 || n === 1) return 1
            else return n * fn(n - 1)
        }

14.写出求1+2+…+n、求1+1/2+1/3+..+1/n的递归算法

 // ①1+2+…+n:
        function fn (n) {
            if (n === 1) return 1;
            else return f(n - 1) + n;
        }
        // ②1+1/2+1/3+..+1/n:
        function fn (n) {
            if (n === 1) return 1;    
            else return fn(n - 1) + (1 / n);
        }

15.写出求斐波那契数列的递归算法。

0 ,1,1,2,3,5,8,13...这样的数列称为斐波那契数列

 function fn (n) {
            if (n <= 1) return n
            else return fn(n - 1) + fn(n - 2)
        }

16.生成六位不重复随机数

 function generateRandomNumber () {
                var arr = [];
                while (arr.length < 6) {
                    var randomNum = Math.floor(Math.random() * 10);
                    if (arr.indexOf(randomNum) === -1) arr.push(randomNum);
                }
                return arr.join('');
            }
            console.log(generateRandomNumber())

17.写一个traverse函数,输出所有页面宽度和高度大于50像素的节点。

 function traverse () {
            var nodes = [...document.querySelectorAll('*')]
            nodes = nodes.filter(node => node.offsetWidth > 50 &&
                node.offsetHeight > 50);
            nodes.forEach(node => console.log(node))
        }
        traverse()

中等难度算法题

18.两数之和

给定一个整数数组nums 和一个目标值target,请你在该数组中找出和为目标值的那两个整数,并返回他们的数组下标。

你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。

示例 nums = [2, 7, 11, 15] target = 9 则结果返回 [0, 1] 即是2 和 7的下标:

function twoSum (arr, target) {
  let map = new Map()
  for (let i = 0, len = arr.length; i < len; i++) {
    let diff = target - arr[i]
    if (map.has(diff)) {
      return [map.get(diff), i]
    }
    map.set(arr[i], i)
  }
  return 'not found'
}

19.实现一个transform方法,使得输入数据的key命名为下划线的情况下转换成驼峰大小写形式。例如: { get_key: 1, item_list: [], name: '123' } => {getKey: 1, itemList: [], name: '123'}

 var obj = { get_key: 1, item_list: [], name: '123' }
        function transform(obj) {
            var newObj = {}
            for (let key in obj) {
                if (key.includes('_')) {
                    var arr = key.split('_')
                    var _arr = arr.map((item, index) => {
                        if (index > 0) {
                            var arr = item.split('')
                            arr[0] = arr[0].toUpperCase()
                            return arr.join('')
                        } else {
                            return item
                        }
                    })
                    newObj[_arr.join('')] = obj[key]
                } else {
                    newObj[key] = obj[key]
                }
            }
            return newObj
        }
        console.log(transform(obj))

20.队列的最大值

请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。

若队列为空,pop_front 和 max_value 需要返回 -1

示例 1:

输入: 
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]

示例 2:

输入: 
["MaxQueue","pop_front","max_value"]
[[],[],[]]
输出: [null,-1,-1]

限制:

  • 1 <= push_back,pop_front,max_value的总操作数 <= 10000
  • 1 <= value <= 10^5
    var MaxQueue = function () {
        this.queue = []
        this.help = []
    };
    MaxQueue.prototype.max_value = function (arr) {
        if (this.queue.length === 0) return -1
        else return Math.max(...this.queue)
    };
    MaxQueue.prototype.push_back = function (value) {
        this.queue = [...this.queue, value]
    };
    MaxQueue.prototype.pop_front = function () {
        if (this.queue.length === 0) return -1
        else return this.queue.pop()
    };
    

     21.写出将十进制整数M转化为N进制数的算法。

    M是32位整数,2<=N<=16.

    题目分析:

    因为转化的进制是在2进制到16进制之间,所以单纯的除法无法满足需求,所以利用字符串来进行进制转换,转换完成后进行字符串翻转即可。同时,要注意负数的情况,不然会出现死循环。

     function fn (m, n) {
                if (m === 0) return 0
                // m是否为正数
                var flag = true;
                if (m < 0) {
                    m = -m;
                    flag = false;
                }
                var arr = []
                var str = "0123456789ABCDEF"
                while (m !== 0) {
                    arr.push(str.charAt(m % n))
                    m = parseInt(m / n)
                }
                return flag ? arr.reverse().join("") : "-" + arr.reverse().join("")
            }

     22.输出一个空心菱形

    可以使用字符串拼接的方法输出空心菱形。

    使用了两个循环,一个从上往下输出,一个从下往上输出,以此来形成一个空心菱形。

     let n = 5;
            for (let i = 0; i < n; i++) {
                let line = "";
                for (let j = 0; j < n - i - 1; j++) {
                    line += " ";
                }
                for (let k = 0; k < 2 * i + 1; k++) {
                    if (k == 0 || k == 2 * i) {
                        line += "*";
                    } else {
                        line += " ";
                    }
                }
                console.log(line);
            }
            for (let i = n - 2; i >= 0; i--) {
                let line = "";
                for (let j = 0; j < n - i - 1; j++) {
                    line += " ";
                }
                for (let k = 0; k < 2 * i + 1; k++) {
                    if (k == 0 || k == 2 * i) {
                        line += "*";
                    } else {
                        line += " ";
                    }
                }
                console.log(line);
            }

     23.分红包

    平均

    这个函数接受两个参数:总金额和总人数。它会返回一个数组,其中包含每个人应该得到的金额。这个函数使用了一个简单的算法:每个人得到的金额都是总金额除以总人数,然后将余下的金额平均分配给剩余的人。

     function distributeRedPacket (totalAmount, totalPeople) {
                var result = [];
                var remainingAmount = totalAmount;
                var remainingPeople = totalPeople;
                for (var i = 0; i < totalPeople; i++) {
                    var amount = Math.floor(remainingAmount / remainingPeople);
                    result.push(amount);
                    remainingAmount -= amount;
                    remainingPeople--;
                }
                return result;
            }
            console.log(distributeRedPacket (1000, 10))

    随机

     function distributeRedPacket (totalAmount, totalPeople) {
                var result = [];
                var remainingAmount = totalAmount;
                var remainingPeople = totalPeople;
                for (var i = 0; i < totalPeople - 1; i++) {
                    var amount = Math.floor(Math.random() *
                        remainingAmount / remainingPeople * 2);
                    result.push(amount);
                    remainingAmount -= amount;
                    remainingPeople--;
                }
                result.push(remainingAmount);
                return result;
            }
            console.log(distributeRedPacket (1000, 10))

     24.合并两个有序升序的整数数组A和B变成一个新的数组。新数组也要有序。

    示例1:

    输入[1,2],[3,4]

    输出[1,2,3,4]

     const A = [1, 3, 5, 7];
            const B = [2, 4, 6, 8];
            console.log(A.concat(B).sort())

    可以使用双指针的方法,分别从数组A和数组B的开头开始比较,将较小的数放入新数组中,直到其中一个数组遍历完毕,然后将另一个数组中剩余的数依次放入新数组中即可。

     function mergeSortedArrays (A, B) {
                let i = 0, j = 0;
                const result = [];
                while (i < A.length && j < B.length) {
                    A[i] < B[j] ? result.push(A[i++]) : result.push(B[j++])
                }
                while (i < A.length) {
                    result.push(A[i++]);
                }
                while (j < B.length) {
                    result.push(B[j++]);
                }
                return result;
            }
            const A = [1, 3, 5, 7];
            const B = [2, 4, 6, 8];
            const C = mergeSortedArrays(A, B);
            console.log(C); 

     25.计算无重复字符的最长子字符的长度

    例如,对于字符串 "abcabcbb",该函数将返回 3,因为最长的无重复字符子字符串是 "abc"。

    注意:该算法的时间复杂度为 O(n),其中 n 是字符串的长度。

    可以使用滑动窗口算法来解决这个问题。具体步骤如下:

    1. 定义一个哈希表,用于存储每个字符最后一次出现的位置。

    2. 定义两个指针,分别指向子字符串的起始位置和当前位置。

    3. 遍历字符串,如果当前字符已经出现过,更新起始位置为该字符最后一次出现的位置的下一位。

    4. 计算当前子字符串的长度,并更新最大长度。

    5. 更新哈希表中当前字符的位置。

    6. 返回最大长度。

    以下是实现代码:

     function lengthOfLongestSubstring (s) {
                let map = new Map();
                let maxLen = 0;
                let start = 0;
                for (let i = 0; i < s.length; i++) {
                    let ch = s.charAt(i);
                    // has(key):判断 Map 对象中是否存在指定键
                    if (map.has(ch)) {
                        // get(key):获取指定键的值。
                        start = Math.max(start, map.get(ch) + 1);
                    }
                    maxLen = Math.max(maxLen, i - start + 1);
                    // set(key, value):向 Map 对象中添加一个键值对。
                    map.set(ch, i);
                    console.log(map)
                }
                return maxLen;
            }
            console.log(lengthOfLongestSubstring("abdca51234567890"))//10

    26.输入一串字符串,判断是否回文串,只考虑字母和数字,可忽略字母的大小写,输出true或者false

    可以使用正则表达式和字符串翻转的方法来判断是否为回文串

     function isPalindrome (str) {
                // 将字符串中的非字母数字字符去除,并转为小写
                str = str.replace(/[^0-9a-zA-Z]/g, '').toLowerCase();
                // 将字符串翻转
                var reversedStr = str.split('').reverse().join('');
                // 判断翻转后的字符串是否与原字符串相等
                return str === reversedStr;
            }
            // 测试
            console.log(isPalindrome("A man, a plan, a canal: Panama")); // true
            console.log(isPalindrome("race a car")); // false

     27.用一次循环找到数组中两个最大的值

     function getMaxSecond (arr) {
                var [max, second] = arr[0] > arr[1] ? [arr[0], arr[1]] : [arr[1], arr[0]];
                for (var i = 2; i < arr.length; i++) {
                    if (arr[i] > max) {
                        second = max;
                        max = arr[i];
                    } else if (arr[i] <= max && arr[i] > second) {
                        second = arr[i];
                    }
                }
                return [max, second]
            }
            console.log(getMaxSecond([1, 4, 10, 11, 5, 7, 2, 3, 4]))

     28.写一个求和的函数sum,达到下面的效果

    // Should equal 15

    sum(1, 2, 3, 4, 5);

    // Should equal 0

    sum(5, null, -5);

    // Should equal 10

    sum('1.0', false, 1, true, 1, 'A', 1, 'B', 1, 'C', 1, 'D', 1, 'E', 1, 'F', 1, 'G', 1);

    // Should equal 0.3, not 0.30000000000000004

    sum(0.1, 0.2);

    答案:

    function sum(...arr){
        if(arr.length===0){return ;}
        var sum=0;
        for(var i=0;i

     29.爬台阶

    假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

    每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

    注意:给定 n 是一个正整数。

    示例 n = 2 ---> 2 两种方法 1阶 + 1阶 2阶

    // 首先题目要求枚举出所有爬楼梯的台阶 而且可以一次只爬一个台阶(不管怎么爬最后都能只爬一步到达楼顶)
    // 这是可以给出一个基本思路  爬一次(1或2) -> 爬剩余的台阶  终止条件是剩余台阶数为0
    function climb (n) {
      if (n === 0) return 1  // 这里返回一次 代表可以爬到楼顶的一种方法
      climb(n - 1)  // 爬1阶后爬剩余的台阶
      if (n - 2 >= 0) {
          climb(n - 2)  // 爬2阶后爬剩余的台阶 这时候要判断剩余台阶数是不是大于等于2      
      }
    }
    // 改进一下  终止条件设为 n <= 2
    function climb (n) {
      if (n <= 0) return 0    // 直接终止
      if (n <= 2) return n    // 剩余1阶有一种解法 2阶有两种解法
      return climb(n - 1) + climb(n - 2)
    }
    // 此时可以发现climb(n - 1)内部又调用了climb(n - 2) 而 climb(n)也调用了climb(n - 2)
    // 不妨保存一下计算结果
    let cache = {}
    function climb (n) {
      if (n <= 0) return 0    // 直接终止
      if (n <= 2) return n    // 剩余1阶有一种解法 2阶有两种解法
      if (cache[n]) return cache[n]
      return cache[n] = climb(n - 1) + climb(n - 2)
    }
    // 大功告成

    高等难度算法题

    30.打家劫舍

    你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,
    影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,
    如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
    给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,
    能够偷窃到的最高金额。

    通过题目,我们发现,需要满足以下条件:

    相邻的房屋不能偷。

    偷的金额需要最大。

    可以使用动态规划来解决这个问题。假设我们已经计算出了前i - 1个房屋能够偷窃到的最高金额,那么对于第i个房屋,我们有两种选择:偷或不偷。如果前i - 2个房屋能够偷窃到的最高金额加上第i个房屋中的现金大于前i - 1个房屋能够偷窃到的最高金额,就偷。反之,不偷。如果我们选择不偷第i个房屋,那么前i个房屋能够偷窃到的最高金额就是前i - 1个房屋能够偷窃到的最高金额。因此,我们可以得到如下的状态转移方程:

    dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])

    其中,dp[i]表示前i个房屋能够偷窃到的最高金额,nums[i]表示第i个房屋中的现金。

    最终的答案就是dp[nums.length - 1],即前nums.length个房屋能够偷窃到的最高金额。

    代码解决

     function rob (nums) {
                if (nums.length === 0) {
                    return 0;
                }
                if (nums.length === 1) {
                    return nums[0];
                }
                let dp = [];
                dp[0] = nums[0];
                dp[1] = Math.max(nums[0], nums[1]);
                for (let i = 2; i < nums.length; i++) {
                    dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
                }
                return dp[nums.length - 1];
            }
            console.log(rob([1, 2, 3, 1])); // 4
            console.log(rob([2, 7, 9, 3, 1])); // 12

    31.零钱兑换问题

    给定一个总金额 amount和不同面额的硬币 coins。
    编写一个函数来计算可以凑成总金额所需的最少的硬币个数。
    如果没有任何一种硬币组合能组成总金额,返回 -1。假设每种硬币数量无限。

    解题思路:动态规划(自下而上)

    我们首先需要找一下初始条件,很明显,就是 dp[0]=0。

    由于在迭代的过程中需要不断的寻找最小值,所有可以把dp里面的值都初始化为一个较大的值,比如amount+1,是无法达到的。

    然后就是自下而上的迭代条件了:

    dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1),dp[i]表示金额为 i 需要的最少硬币数。

     function coinChange (coins, amount) {
                const dp = new Array(amount + 1).fill(amount + 1);
                dp[0] = 0;
                for (let i = 1; i <= amount; i++) {
                    for (let j = 0; j < coins.length; j++) {
                        if (coins[j] <= i) {
                            dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
                        }
                    }
                }
                return dp[amount] > amount ? -1 : dp[amount];
            }
            // 示例
            const coins = [1, 2, 5];
            const amount = 11;
            console.log(coinChange(coins, amount)); // 输出 3

    32.最长上升子序列

    给定一个无序的整数数组,找到其中最长上升子序列的长度。

    可以使用动态规划来解决这个问题。定义一个数组dp,其中dp[i]表示以第i个元素为结尾的最长上升子序列的长度。初始时,dp数组中的每个元素都为1,因为每个元素本身都可以作为一个长度为1的上升子序列。然后,从第二个元素开始遍历整个数组,对于每个元素,再从第一个元素开始遍历到当前元素,如果发现有比当前元素小的元素,就更新dp数组中的值,即dp[i] = Math.max(dp[i], dp[j] + 1),其中j表示比当前元素小的元素的下标。最终,dp数组中的最大值即为最长上升子序列的长度。

    function fn(nums) {
    				if (!nums || nums.length === 0) {
    					return 0;
    				}
    				const dp = new Array(nums.length).fill(1);
    				for (let i = 1; i < nums.length; i++) {
    					for (let j = 0; j < i; j++) {
    						if (nums[i] > nums[j]) {
    							dp[i] = Math.max(dp[i], dp[j] + 1);
    						}
    					}
    				}
    				console.log(dp); //[1, 1, 1, 2, 2, 3, 4, 4]
    				return Math.max(...dp);
    			}
    			console.log(fn([10, 9, 2, 5, 3, 7, 101, 18])); //4,对应的最长上升子序列为 [2, 3, 7, 101]

    33.最低票价

    在一个火车旅行很受欢迎的国度,你提前一年计划了一些火车旅行。在接下来的一年里,你要旅行的日子将以一个名为 days 的数组给出。每一项是一个从 1 到 365 的整数。

    火车票有三种不同的销售方式:

    一张为期一天的通行证售价为 costs[0] 美元;

    一张为期七天的通行证售价为 costs[1] 美元;

    一张为期三十天的通行证售价为 costs[2] 美元。

    通行证允许数天无限制的旅行。 例如,如果我们在第 2 天获得一张为期 7 天的通行证,那么我们可以连着旅行 7 天:第 2 天、第 3 天、第 4 天、第 5 天、第 6 天、第 7 天和第 8 天。

    返回你想要完成在给定的列表 days 中列出的每一天的旅行所需要的最低消费。

    输入示例:days = [1,4,6,7,8,20], costs = [2,7,15]

    输出示例:11

    解释:例如,这里有一种购买通行证的方法,可以让你完成你的旅行计划:

    在第 1 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 1 天生效。

    在第 3 天,你花了 costs[1] = $7 买了一张为期 7 天的通行证,它将在第 3, 4, …, 9 天生效。

    在第 20 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 20 天生效。

    你总共花了 $11,并完成了你计划的每一天旅行。

    提示:1 <= days.length <= 365 1 <= days[i] <= 365 costs.length == 3

    1 <= costs[i] <= 1000 days 按顺序严格递增

    这是一个动态规划问题,可以使用一个 dp 数组来记录到每一天为止的最小花费。

    dp[i] 表示到第 i 天为止的最小花费。对于每一个旅行日,我们可以选择购买一天、七天或三十天的通行证,然后取最小值作为 dp[i] 的值。如果当前不是旅行日,则 dp[i] 的值与 dp[i-1] 相同。最后返回 dp[days[days.length - 1]] 即可。

    代码解决

     function mincostTickets (days, costs) {
                const dp = new Array(366).fill(0);
                let dayIndex = 0;
                for (let i = 1; i <= 365; i++) {
                    if (dayIndex >= days.length) {
                        break;
                    }
                    if (i === days[dayIndex]) {
                        const oneDayCost = dp[i - 1] + costs[0];
                        const sevenDayCost = dp[Math.max(0, i - 7)] + costs[1];
                        const thirtyDayCost = dp[Math.max(0, i - 30)] + costs[2];
                        dp[i] = Math.min(oneDayCost, sevenDayCost, thirtyDayCost);
                        dayIndex++;
                    }
                    else {
                        dp[i] = dp[i - 1];
                    }
                }
                return dp[days[days.length - 1]];
            }

    34.不同的二叉搜索树

    给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?

    输入示例:3

    输出示例:5

    解释:对于序列1,2,3,一共可以构成5种不同的二叉搜索树

    使用动态规划,函数的参数 n 表示二叉搜索树的节点数,返回值是可能构成的二叉搜索树的组合数。时间复杂度为 O(n^2)。

    function numTrees(n) {
    				const dp = new Array(n + 1).fill(0);
    				dp[0] = 1;
    				dp[1] = 1;
    				for (let i = 2; i <= n; i++) {
    					for (let j = 1; j <= i; j++) {
    						dp[i] += dp[j - 1] * dp[i - j];
    					}
    				}
    				console.log(dp); //[1, 1, 2, 5, 14, 42]
    				return dp[n];
    			}
    			console.log(numTrees(5)); //42

    35.礼物的最大价值

    在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

    输入:

    [

    [1,3,1],

    [1,5,1],

    [4,2,1]

    ]

    输出: 12

    解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物

    代码解决--动态规划

     function getMaxValue (grid) {
                if (!grid || grid.length === 0 || grid[0].length === 0) {
                    return 0;
                }
                const m = grid.length;
                const n = grid[0].length;
                const dp = new Array(m).fill(0).map(() => new Array(n).fill(0));
                dp[0][0] = grid[0][0];
                for (let i = 1; i < m; i++) {
                    dp[i][0] = dp[i - 1][0] + grid[i][0];
                }
                for (let j = 1; j < n; j++) {
                    dp[0][j] = dp[0][j - 1] + grid[0][j];
                }
                for (let i = 1; i < m; i++) {
                    for (let j = 1; j < n; j++) {
                        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) +
                            grid[i][j];
                    }
                }
                return dp[m - 1][n - 1];
            }
            console.log(getMaxValue([
                [1, 3, 1],
                [1, 5, 1],
                [4, 2, 1]
            ])); // 12

    36.接雨水

    给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,编写js函数计算按此排列的柱子,下雨之后能接多少雨水。

    下图是由数组[0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水。

    输入示例:[0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]

    输出示例:6

    这个问题可以使用动态规划来解决。我们可以先计算每个柱子左边的最高柱子高度和右边的最高柱子高度,然后根据这些信息计算每个柱子能够接到的雨水量。

    我们可以将 dp 数组定义为两个维度,分别表示左右两侧的最大高度。

    function trap(height) {
    				if (height.length === 0) return 0;
    				const n = height.length;
    				// dp数组,存储左右两侧的最大高度
    				const dp = new Array(2).fill().map(() => new Array(n).fill(0));
    				// 初始化左右两侧的最大高度
    				dp[0][0] = height[0]; // 左侧最大高度
    				dp[1][n - 1] = height[n - 1]; // 右侧最大高度
    				for (let i = 1; i < n; i++) {
    					dp[0][i] = Math.max(dp[0][i - 1], height[i]);
    					dp[1][n - 1 - i] = Math.max(dp[1][n - i], height[n - 1 - i]);
    				}
    				console.log(dp[0]); //[0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3]
    				console.log(dp[1]); //[3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 1]
    				// 计算所有柱子能接到的雨水量的总和
    				let water = 0;
    				for (let i = 0; i < n; i++) {
    					let _water = Math.min(dp[0][i], dp[1][i]) - height[i];
    					console.log(_water); //0 0 1 0 1 2 1 0 0 1 0 0
    					water += _water;
    				}
    				return water;
    			}
    			// 示例用法
    			const height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1];
    			console.log(trap(height)); // 输出 6,表示可以接到的雨水量

    37.去除整数

    已知存在集合A包含n个整数,从1到n。

    存在m个整数a[1..m]。

    在集合A中去除这m个整数的的倍数。

    输出集合中包含的元素的个数。

    输入描述:

    第一行输入n,m。(1<=n<=1e9,1<=m<=10)

    第二行输入m个32位整数。

    输出描述:

    输出集合中包含元素的个数。

    输入样例:

    5 2

    2 3

    输出样例:

    2

    我们可以使用动态规划数组 dp 来记录每个数字是否被去除。dp[i] 表示数字 i 是否被去除,如果被去除则为 true,否则为 false。

    function removeMultiples(n, m, removedIntegers) {
    				// 创建大小为 n 的数组,初始状态为未去除
    				const dp = new Array(n + 1).fill(false);
    				// 标记每个整数的倍数为已去除
    				for (let i = 0; i < m; i++) {
    					const multiple = removedIntegers[i];
    					for (let j = multiple; j <= n; j += multiple) {
    						dp[j] = true;
    					}
    				}
    				// 统计未被去除的整数个数
    				let count = 0;
    				for (let i = 1; i <= n; i++) {
    					if (!dp[i]) {
    						count++;
    					}
    				}
    				return count;
    			}
    			// 示例用法
    			const n = 5;
    			const m = 2;
    			const removedIntegers = [2, 3];
    			console.log(removeMultiples(n, m, removedIntegers)); // 输出 2