前端算法入门1(js)

265 阅读6分钟

算法基础篇(1)

本科自大三开始自学前端非计算机相关专业,不懂算法,所以需要勤能补拙.

很多题思路相似,而且内容鸡肋,这里分享一点有必要掌握的

最短无序连续子数组(581题)

题目:
给定一个整数数组,你需要寻找一个连续的子数组,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。
你找到的子数组应是最短的,请输出它的长度。
输入: [2, 6, 4, 8, 10, 9, 15]
输出: 5
解释: 你只需要对 [6, 4, 8, 10, 9] 进行升序排序,那么整个表都会变为升序排序。
/**
 * @param {number[]} nums
 * @return {number}
 */
var findUnsortedSubarray = function(nums) {
    const oldNums = [...nums];
    const arr = nums.sort((a,b)=>a-b);
    let a=0;
    let b=0;
    for(let i=0;i< oldNums.length;i++){
        if(oldNums[i] !== arr[i]){
            a= i;
            break;
        }
    }
    for(let i=oldNums.length;i>0;i--){
        if(oldNums[i] !== arr[i]){
            b= i;
            break;
        }
    }
    if(a===0&& b===0) return 0;
    return b-a+1;
};

思路: 先排序,然后找到第一位和最后一位不一致的索引,相减加1(都为0代表没有不同的) (排序后,找到首位首次不同的,那么这个区间里面所有的数字都是需要排序的)

42. 接雨水

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



上面是由数组 [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

var trap = function(heightList) {
    if (!heightList.length) return 0
    
    function getMaxIndex(arr){
        var maxIndex = 0;
        arr.forEach((_, i) => {
            if (_ > arr[maxIndex]) {
                maxIndex = i
            }
        });

        return maxIndex;
    }
    var maxIndex = getMaxIndex(heightList)
     
    let leftArr = heightList.slice(0, maxIndex + 1)
    let rightArr = heightList.slice(maxIndex).reverse()
     
    function getRect (arr) {
        let leftMax = arr[0] || 0
        return arr.reduce((all, height) => {
            if (height > leftMax) {
                leftMax = height
            }
            let currentHeight = leftMax - height >=0 ? leftMax - height : 0
            return all + currentHeight
        }, 0)
    }
     
    return getRect(leftArr) + getRect(rightArr)
};

思路

  • 获取到最大坐标
  • 从最大坐标拆分为左右两个数组(右数组reverse)
  • 每个数组执行函数,每次更新最大值后获取最大值和当前值的差进行累加
  • 左右两个数组执行函数的和就是最终结果

计数质数(204题)

题目:
统计所有小于非负整数 n 的质数的数量。

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

/**
 * @param {number} n
 * @return {number}
 */
var countPrimes = function(n) {
    let count = 0;
    let signs = new Uint8Array(n);

    for (let i = 2; i < n; i=i+1) {
        if (!signs[i - 1]) {
            count++;

            for (let j = i; i * j <= n; j++) {
                signs[i * j - 1] = true;
            }
        }
    }
    return count;
};

删除链表的倒数第N个节点(19)

给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。

给定一个链表: 1->2->3->4->5, 和 n = 2.

当删除了倒数第二个节点后,链表变为 1->2->3->5.
/**
 * @param {ListNode} head
 * @param {number} n
 * @return {ListNode}
 */
var removeNthFromEnd = function(head, n) {
  let target = head,
      cur = head;
  while (n--) {
    cur = cur.next; 
    console.log({
        cur
    })
  }
  while (cur && cur.next) {
    cur = cur.next;
    target = target.next;
    console.log({
        cur,target
    })
  }
  if (!cur) return head.next;
  target.next = target.next.next;
  return head;
};

思路:

  • 双指针思路,第一个指针先走n下
  • 两个指针同时走,直到第一个指针走到结束,第二个指针就是我们的目标节点
  • 第二个指针的next指向next的next

同构字符串(205)

给定两个字符串 s 和 t,判断它们是否是同构的。
如果 s 中的字符可以被替换得到 t ,那么这两个字符串是同构的。
所有出现的字符都必须用另一个字符替换,同时保留字符的顺序。两个字符不能映射到同一个字符上,但字符可以映射自己本身。(你可以假设 s 和 t 具有相同的长度。)
输入: s = "egg", t = "add"
输出: true

输入: s = "foo", t = "bar"
输出: false

输入: s = "paper", t = "title"
输出: true

/**
 * @param {string} s
 * @param {string} t
 * @return {boolean}
 */
let isIsomorphic = function (s, t) {
    for (let i = 0; i < s.length; i++) {
        if (s.indexOf(s[i]) !== t.indexOf(t[i])) return false;
    }
    return true;
};

说明: 枚举每一位,找到对应另一个字符串i位以后最先出现的该字符,有一个不相同就代表,两个字符转不是同构的。 (因为如果对于每一位来说,都能在对应的另一个字符串下一个相同字符找到相同的索引, 那么代码第i位符合同构,一次枚举完成就可以判断出来结果)

缺失的第一个正数(41)

给定一个未排序的整数数组,找出其中没有出现的最小的正整数。

输入: [1,2,0]
输出: 3

输入: [3,4,-1,1]
输出: 2

输入: [7,8,9,11,12]
输出: 1

/**
 * @param {number[]} nums
 * @return {number}
 */
var firstMissingPositive = function(nums) {
    const len = nums.length;
    const obj = new Set(nums);
    let index = 1;
    while(true){
        if(obj.has(index) ) ++index;
        else return index;
    }
};

该题相对简单, 用set存储整个数组,然后依次查询Set的是否有下一位正整数,没有就返回

爬楼梯(70)

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1.  1 阶 + 1 阶
2.  2 阶
/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
    const cache = [1,1];
    function find(i){
        if(cache[i])return cache[i];
        cache[i] = find(i-1) +find(i-2);
        return cache[i];
    }
    return find(n);
};
该题目经典而简单,注意用个cache对象做存储,避免栈溢出,或者用while避免栈溢出问题

只出现一次的数字(136)

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
输入: [2,2,1]
输出: 1

输入: [4,1,2,1,2]
输出: 4

/**
 * @param {number[]} nums
 * @return {number}
 */
var singleNumber = function(nums) {
  const n = nums.length
  if (n === 1) {
    return nums[0]
  }
  return nums.reduce((a, b) => a ^ b)
}

该问题简单,但是解法较多, 目前个人觉得不错的方法是用位运算符, 举个例子
6^6.  -> 0 
7^1.  -> !==0
2^7^2.    -> 7(因为7只出现了一次)
利用这个特性一次对比很容易找出来只出现一次的数字

寻找两个有序数组的中位数(4)

给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。
请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。
你可以假设 nums1 和 nums2 不会同时为空。
nums1 = [1, 3]
nums2 = [2]
则中位数是 2.0

nums1 = [1, 2]
nums2 = [3, 4]
则中位数是 (2 + 3)/2 = 2.5

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number}
*/
var findMedianSortedArrays = function(nums1, nums2) {
    const getMedium = (arr) => {
        if (arr.length % 2 == 0) {
            return (arr[arr.length / 2] + arr[arr.length / 2 - 1]) / 2;
        } else {
            return arr[Math.floor(arr.length / 2)]
        }
    }
    const newArr = [];
    while (nums1.length || nums2.length) {
        const num1Len = nums1.length;
        const num2Len = nums2.length;
        if (!num2Len || !num1Len) {
            const arr = num1Len ? nums1 : nums2;
            while (arr.length) {
                newArr.push(arr.shift())
            };
            return getMedium(newArr);
        }

        const min = Math.min(nums1[0], nums2[0])
        if (min === nums1[0]) {
            newArr.push(nums1.shift());
        } else {
            newArr.push(nums2.shift());
        }
    }
};
思路:两个数组合并成一个有序数组,然后取中位数
     优化1.0: index只获取中间两位之后就break; 减少了一半的遍历
     优化2.0:  每次两个数组二分获取中间数,然后动态调整,直到两遍数字变为可排序数字,然后移动指针找到中间索引。
     
    1.0很容易写,但是2.0写完了总有case过不去,等以后再反过来重新看这个问题吧。。。。

获取数组的最大子串

const maxSubArray = function(nums) {
 let max = nums[0]; // 初始化最大值
 let newMax = nums[0]; // 数组元素相加的缓存值
 for (let i = 1; i < nums.length; i++) {
   newMax = Math.max(newMax + nums[i], nums[i]); // 相加是否大于当前值
   max = Math.max(newMax, max); // 与最大值相比
 }
 return max;
};
var arr = [-1,2,3,4,1-5,3,0,10,-10];

maxSubArray(arr)

思路:动态规划
规律:
加到第i位和第i位比较,取最大的,因为如果加上第i位小于第i位,证明(1)前面i-1位是负数, 就可以放弃了
(2)如果是正数,那么加上第i位一定大于不加第i位

!!!!!注意,对比的是i位总和与第i位的大小

获取几个字符串公共的prefix

方案一,遍历

var longestCommonPrefix = function(strs) {
    if(strs.length === 0) return '';
    let result = '';
    let len = Math.min.apply(Math, strs.map(item => item.length));
    for(let i = 0; i < len; i++) {
        let tmp = strs.map(item => item.substring(0, i+1));
        if (new Set(tmp).size === 1) result = tmp[0];
    }
    return result;
};

方案二,动态规划

function lcs(str1, str2) {
    let record = [];
    let max = 0;
    let pos = 0;
    let result = "";
    //初始化记录图
    for (let i = 0; i < str1.length; i++) {
        record[i] = [];
        for (let j = 0; j < str2.length; j++) {
            record[i][j] = 0;
        }
    }
    //动态规划遍历
    for (let i = 0; i < str1.length; i++) {
        for (let j = 0; j < str2.length; j++) {
            if (i === 0 || j === 0) {
                record[i][j] = 0;
            } else {
                if (str1[i] === str2[j]) {
                    record[i][j] = record[i - 1][j - 1] + 1;
                } else {
                    record[i][j] = 0;
                }
            }
            //更新最大值指针
            if (record[i][j] > max) {
                max = record[i][j];
                pos = [i];
            }
        }
    }
    //拼接结果
    if (!max) {
        return "";
    } else {
        for (let i = pos; i > pos - max; i--) {
            result = str1[i] + result;
        }
        return result;
    }
}

console.log(lcs("havoc", "raven"));
console.log(lcs("abbcc", "dbbcc"));

01背包问题

function max(a, b) {
    return a > b ? a : b;
}

/**
 *
 * @param  {[type]} capacity 背包容量
 * @param  {[type]} size     物品体积数组
 * @param  {[type]} value    物品价值数组
 * @param  {[type]} n        物品个数
 * @return {[type]}          最大价值
 */
function knapsack(capacity, size, value, n) {
    //K[n][capacity]表示0~n-1这n个物品入选时的最优值
    let K = [];
    let pick = [];
    let result = 0;
    for (let i = 0; i <= n; i++) {
        K[i] = [];
        for (let j = 0; j <= capacity; j++) {
            if (j === 0 || i === 0) {
                //j=0表示背包容量为0,无法放入故结果为0,边界数值,避免动态规划时候益处
                K[i][j] = 0;
            } else if (size[i - 1] > j) {
                K[i][j] = K[i - 1][j];
            } else {
                //动态规划解,当第i个物品可以放入时,K[i][j]等同于放入i时最值和不放i时的值取最大
                K[i][j] = max(
                    K[i - 1][j - size[i - 1]] + value[i - 1],
                    K[i - 1][j]
                );
                // 对比上方
            }
        }
    }
    console.table(K);
    console.log({ K });
    result = K[n][capacity];

    //如何求解到底选了哪些物品?
    while (n > 0) {
        if (
            K[n - 1][capacity - size[n - 1]] + value[n - 1] >
            K[n - 1][capacity]
        ) {
            capacity -= size[n - 1];
            n--;
            pick[n] = 1;
        } else {
            n--;
            pick[n] = 0;
        }
    }
    console.log("答案的选择情况为:", pick);
    return result;
}

let value = [4, 5, 10, 11, 13];
let size = [3, 4, 7, 8, 9];
// let value = [10,5,10,11,13];
// let size = [1,40,70,8,9];
let capacity = 16;
let n = 5;

let result = knapsack(capacity, size, value, n);
console.log("结果:", result);

思路: 动态规划
1、建模生成一个K数组i行j列,i表示物品,j表示背包。
2、讨论
   100一定为0,用来支持边界数,注意for循环需要去到等号,然后value[i-1]才表示当前位。
   2.1)如果背包容量比第i个物品的重量还小,则第i个物品必然无法放入,相当于前i-1个物品放入j容量背包时的最值
   2.2)最后动态规划,(当前包能承载的最大数量为背包容量减1的情况)和(当前空间减去size[i-1]的容量时的value加上当前物品的value和)取最大值