算法练习题-系列一

1. 柯里化

实现柯里化函数

var currying = function(fn) {
    // fn 指官员消化老婆的手段
    var args = [].slice.call(arguments, 1);
    // args 指的是那个合法老婆
    return function() {
        // 已经有的老婆和新搞定的老婆们合成一体,方便控制
        var newArgs = args.concat([].slice.call(arguments));
        // 这些老婆们用 fn 这个手段消化利用,完成韦小宝前辈的壮举并返回
        return fn.apply(null, newArgs);
    };
};

// 下为官员如何搞定7个老婆的测试
// 获得合法老婆
var getWife = currying(function() {
    var allWife = [].slice.call(arguments);
    // allwife 就是所有的老婆的,包括暗渡陈仓进来的老婆
    console.log(allWife.join(";"));
}, "合法老婆");

// 获得其他6个老婆
getWife("大老婆","小老婆","俏老婆","***蛮老婆","乖老婆","送上门老婆");

// 换一批老婆
getWife("超越韦小宝的老婆");

柯里化函数作用

  1. 参数复用;2. 提前返回;3. 延迟计算/运行

2. 扁平化

请你编写一个函数,它接收一个 多维数组 arr 和它的深度 n ,并返回该数组的 扁平化 后的结果

// 输入
arr = [1, 2, 3, [4, 5, 6], [7, 8, [9, 10, 11], 12], [13, 14, 15]]
// 输出
[1, 2, 3, 4, 5, 6, 7, 8, [9, 10, 11], 12, 13, 14, 15]
n = 1

方法一: Array.flat

方法二: 递归

/* 
1. 创建一个递归函数 flattening,该函数接受数组 nums 和层级 l 作为参数
2. 在 flattening 中,我们使用 for...of 循环来迭代 nums 数组的元素
3. 对于每个元素,我们检查它是否是数组以及层级 l 是否在指定范围内(l > 0 且 l <= n)。
    如果元素是数组且满足层级条件,我们会递归调用 flattening,并将嵌套数组和层级减小 1(即 l - 1)传递给它。

    如果元素不是数组或层级条件不满足,我们将元素推送到结果数组 res 中
*/
// 递归
var flat = function (arr, n) {
    const res = [];
    const flatting = (nums, l) => {
        for(const num of nums) {
            if(Array.isArray(num) && l > 0) {
                flatting(num, l-1);
            } else {
                res.push(num)
            }
        }
    }
    flatting(arr, n);
    return res;
};
// 队列
var flat = function (arr, n) {
    let nestedArrayElement = true; // 仍然有嵌套数组需要扁平化
    let queue; // 扁平化过程中存储元素
    let depth = 0; // 当前深度级别
    // 只要还有单个数组元素需要处理(nestedArrayElement 为 true),且深度小于 n
    while(nestedArrayElement && depth < n) {
        nestedArrayElement = false; 
        queue = [];
        // for 循环遍历数组 arr 中的每个元素
        for(let i = 0; i < arr.length; i++) {
            // 如果元素是数组,将其元素展开到队列 queue 中
            // 并将 nestedArrayElement 设置为 true,表示遇到嵌套数组
            if(Array.isArray(arr[i])) {
                queue.push(...arr[i]);
                nestedArrayElement = true; // 只要还有单个数组元素需要处理
            } else {
                queue.push(arr[i]);
            }
        }
        // 处理完数组 arr 的所有元素后,将数组 arr 更新为队列中的元素
        arr = [...queue];
        depth++; // 增加深度 depth
    }

    return arr;
};

3. [双指针]有序数组合并

给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目

输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3] 和 [2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素
/**
解法一:
1. 使用三个指针 i、j 和 k,其中 i 和 j 分别指向数组 A 和 B 的末尾元素,k 指向合并后的数组 A 的末尾
2. 它从后向前比较 A 和 B 的元素,并将较大的元素放入 A 的末尾,直到遍历完两个数组的所有元素
3. 如果数组 B 还有剩余元素,将它们依次放入数组 A 的前面。这样,两个有序数组就被合并到了数组 A 中
时间复杂度为 O(m + n),空间复杂度为 O(1)
 */
var merge = function (A, m, B, n) {
    let i = m - 1;
    let j = n - 1;
    let curIndex = m + n - 1;
    while (i >= 0 && j >=0) {
        if (A[i] > B[j]){
            A[curIndex] = A[i];
            i--;
        } else {
            A[curIndex] = B[j];
            j--;
        }
        curIndex--;
    }
    while(j>=0) {
        A[curIndex] = B[j];
        j--;
        curIndex--;
    }
    // if (j + 1 !== 0) {
    //    let restNum2 = nums2.slice(0, j + 1);
    //    nums1.splice(0, j + 1, ...restNum2);
    // }
    return A;
};

/**
解法二
 */

// 从后往前确定两组中该用哪个数字
var merge = function(nums1, m, nums2, n) {
    let len = nums1.length - 1;
    let i = m - 1;
    let j = n - 1;
    var update = function(i, j, vi, vj) {
        nums1[i] = vi;
        nums1[j] = vj;
    }
    // 第二个数组全都插入进去为止
    while (j >= 0) {
        while (i >= 0 && nums1[i] > nums2[j]) {
            update(i, len, 0, nums1[i]);
            i--;
            len--;
        }
        nums1[len] = nums2[j]
        j--;
        len--;
    }
    return nums1;
}

4. 判断一个字符串是否是回文字符串

// 双指针 字符串的开头和结尾开始,向中间移动,并比较对应位置的字符是否相同。在比较字符时,可以将字符转换为小写字母来忽略大小写。
var isPalindrome = function(s) {
    s = s.toLowerCase().replace(/[^a-z0-9]/g, '');
    let left = 0;
    let right = s.length - 1;
    while(left < right) {
        if (s[left] != s[right]) {
            return false;
        }
        left++;
        right--;
    }
    return true;
};

5. [字符串]两个版本号 version1 和 version2

示例 1:
输入:version1 = "1.01", version2 = "1.001"
输出:0
解释:忽略前导零,"01" 和 "001" 都表示相同的整数 "1"
示例 2:
输入:version1 = "1.0", version2 = "1.0.0"
输出:0
解释:version1 没有指定下标为 2 的修订号,即视为 "0"
示例 3:
输入:version1 = "0.1", version2 = "1.1"
输出:-1
解释:version1 中下标为 0 的修订号是 "0",version2 中下标为 0 的修订号是 "1" 。0 < 1,所以 version1 < version2

/**
 * @param {string} version1
 * @param {string} version2
 * @return {number}
 */
const compareVersion = function (version1, version2) {
    const versions1s = version1.split('.');
    const versions2s = version2.split('.');
    const v1Len = versions1s.length;
    const v2Len = versions2s.length;
    const len = Math.max(v1Len, v2Len);
    let flag = 0;

    for (let i = 0; i < len; i++) {
        // let x = 0, y = 0;
        // if (i < v1Len) {
        //     x = parseInt(versions1s[i])
        // }
        // if (i < v2Len) {
        //     y = parseInt(versions2s[i])
        // }
        // if (x > y) {
        //     flag = 1;
        // }
        // if (x < y) {
        //     flag = -1;
        // }
        if ((parseInt(versions1s[i]) || 0) > (parseInt(versions2s[i]) || 0)) {
            flag = 1;
            break;
        }
        if ((parseInt(versions1s[i]) || 0) < (parseInt(versions2s[i]) || 0)) {
            flag = -1;
            break;
        }
    }
    return flag;
};

6. [字符串]版本号大小比较排序 ['1.45.0','1.5','6','3.3.3.3.3.3.3'] => ['1.5','1.45.0','3.3.3.3.3.3','6']

function sortVersions(versions) {
  return versions.sort(compareVersion);
}

const versions = ['1.45.0', '1.5', '6', '3.3.3.3.3.3.3'];
const sortedVersions = sortVersions(versions);
console.log(sortedVersions);

7. 给定两个 01 字符串 a 和 b ,请计算它们的和,并以二进制字符串的形式输出。

示例 1:

输入: a = "11", b = "10"
输出: "101"
示例 2:

输入: a = "1010", b = "1011"
输出: "10101"

// 逐位相加的方法,并考虑进位
var addBinary = function (a, b) {
    let result = '';
    let carry = 0;
    let i = a.length - 1;
    let j = b.length - 1;
    while (i >= 0 || j >= 0 || carry > 0) {
        const digitA = i >= 0 ? parseInt(a[i]) : 0;
        const digitB = j >= 0 ? parseInt(b[j]) : 0;
        const sum = digitA + digitB + carry;

        result = (sum % 2) + result;
        carry = Math.floor(sum / 2);

        i--;
        j--;
    }
    return result;
};

8. [双指针 哈希]无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长连续子字符串 的长度。

示例 1:

输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子字符串是 "abc",所以其长度为 3。
示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子字符串是 "b",所以其长度为 1。
示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
示例 4:

输入: s = ""
输出: 0

var lengthOfLongestSubstring1 = function(s) {
    if (s === '') {
        return 0;
    }
    if (s.length === 1) {
        return 1;
    }
    let i = 0; // 左指针
    let j = i + 1; // 右指针
    const results = []; // 记录长度 最终选择最长的输出

    while(j < s.length) {
        let temRes = s[i] + '';
        while (!temRes.includes(s[j]) && j < s.length) {
            temRes+= s[j];
            j++;
        }
        let curLen = j - i;
        results.push(curLen);
        i = i + 1;
        j = i + 1;
    }
    const max = results.reduce((pre, cur, initial) => {
        return pre > cur ? pre : cur;
    });
    return max;
};

var lengthOfLongestSubstring = function(s) {
    // hash集合
    const hash = new Set();
    let rp = -1; // 右指针
    let temp = 0; // 比较的结果
    const len = s.length;
    for (i = 0; i < len; i++) {
        if (i !== 0) {
            hash.delete(s[i - 1]);
        }
        while (rp + 1 < len && !hash.has(s[rp + 1])) {
            hash.add(s[rp + 1]);
            rp++;
        }
        temp = Math.max(temp, rp - i + 1);
    }
    return temp;
};

9. [哈希]两数之和

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

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:

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

输入:nums = [3,3], target = 6
输出:[0,1]

var twoSum1 = function(nums, target) {
    const len = nums.length;
    for (let i = 0; i < len; i++) {
        for (let j = i + 1; j < len; j++) {
            if (nums[i] + nums[j] === target) {
                return [i, j];
            }
        }
    }
    return [];
};

var twoSum2 = function(nums, target) {
    const len = nums.length;
    let rp = 0;
    for (let i = 0; i < len; i++) {
        rp = i + 1;
        while (rp < len) {
            if (nums[i] + nums[rp] === target) {
                return [i, rp];
            }
            rp++;
        }
    }
    return [];
};
// hash
var twoSum = function(nums, target) {
    const map = new Map();
    for (let i = 0; i < nums.length; i++) {
        if (map.has(target - nums[i])) {
            return [map.get(target - nums[i]), i];
        }
        map.set(nums[i], i);
    }
    return [];
};

10. [栈]有效的括号

给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
示例 1:

输入:s = "()"
输出:true
示例 2:

输入:s = "()[]{}"
输出:true
示例 3:

输入:s = "(]"
输出:false

/**
判断字符串是否有效的问题可以使用栈来解决。我们可以遍历字符串,当遇到左括号时,将其压入栈中;当遇到右括号时,检查栈顶元素是否与当前右括号匹配。如果匹配,则将栈顶元素弹出,继续遍历;如果不匹配或者栈为空,则说明字符串无效。最后,如果栈为空,且字符串遍历完毕,则字符串有效。
 * @param {string} s
 * @return {boolean}
 */
var isValid = function(s) {
    const stack = [];
    const pairs = {
        '{': '}',
        '[': ']',
        '(': ')',
    };
    for (let i = 0; i < s.length; i++) {
        const char = s[i];
        if (char === '(' || char === '[' || char === '{') {
            stack.push(char);
        } else {
            const top =  stack.pop();
            if (pairs[top] !== char) {
                return false;
            }
        }
    }
    return stack.length === 0;
};

11. [双指针]三数之和

给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a ,b ,c ,使得 a + b + c = 0 ?请找出所有和为 0 且 不重复 的三元组。

示例 1:

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

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

输入:nums = [0]
输出:[]

解决这个问题的一种常见方法是使用双指针。首先,对数组进行排序,然后遍历排序后的数组。在遍历过程中,固定一个元素,然后使用双指针从当前元素的下一个位置和数组末尾开始向中间移动,寻找满足条件的三个元素

var threeSum = function (nums) {
    const result = [];
    let sortNums = nums.sort((a, b) => a - b);
    const len = sortNums.length
    for (let i = 0; i < len - 2; i++) {
        let m = i + 1;
        let n = len - 1;
        let cur = nums[i];
        while (m < n) {
            const sum = cur + nums[m] + nums[n];
            if (sum === 0) {
                result.push([cur, nums[m], nums[n]]);
                // 跳过重复的元素
                while (m < n && nums[m] === nums[m+1]) {
                    m++;
                }
                while (m < n && nums[n] === nums[n-1]) {
                    n--;
                }
                m++;
                n--;
            } else if(sum < 0) {
                m++;
            } else {
                n--;
            }
        }
    }
    return Array.from(new Set(result.map(JSON.stringify)), JSON.parse);
};

12. [回溯 递归] 全排列 (再A一遍)

给定一个不含重复数字的整数数组 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]]

/**回溯算法来生成全排列。通过不断地选择一个数字,将其添加到当前排列中,并递归生成剩余数字的排列,然后撤销选择,继续尝试下一个数字,直到所有数字都被使用。最终,所有可能的排列都被生成并存储在 result 数组中,然后返回给调用者。
 * @param {number[]} nums
 * @return {number[][]}
 */
function permute(nums) {
  const result = [];

  function backtrack(current, remaining) {
    if (remaining.length === 0) {
      result.push(current.slice()); // 将当前排列添加到结果中
      return;
    }

    for (let i = 0; i < remaining.length; i++) {
      const num = remaining[i];
      current.push(num); // 将当前数字添加到当前排列中
      remaining.splice(i, 1); // 从剩余数字中移除当前数字
      backtrack(current, remaining); // 递归生成排列
      remaining.splice(i, 0, num); // 恢复剩余数字的原始顺序
      current.pop(); // 移除当前数字
    }
  }

  backtrack([], nums);
  return result;
}

13. [排序 二分]手撕快速排序

/**
 * 解法一:快速排序
 * 思路:
 * (1)快速排序的主要思想是通过划分将待排序的序列分成前后两部分,其中前一部分的数据都比后一部分的数据要小,
 * (2)然后再递归调用函数对两部分的序列分别进行快速排序,以此使整个序列达到有序。
 * 时间复杂度:O(nlogn)
 * 空间复杂度:O(h),其中 h 为快速排序递归调用的层数。我们需要额外的 O(h) 的递归调用的栈空间,
 * 由于划分的结果不同导致了快速排序递归调用的层数也会不同,最坏情况下需 O(n) 的空间,最优情况下每次都平衡,
 * 此时整个递归树高度为 logn,空间复杂度为 O(logn)。
 */
function quikSort(arr) {    
    if (arr.length <= 1) {
        return arr;
    }
    //取中间数值
    var standardNum = arr.splice(Math.floor(arr.length / 2), 1);
    var leftArr = [];
    var rightArr = [];
    for (var i = 0; i < arr.length; i++) {
        if (arr[i] <= standardNum[0]) {
            leftArr.push(arr[i]);
        } else {
            rightArr.push(arr[i]);
        }
    }
    return quikSort(leftArr).concat(standardNum, quikSort(rightArr));
}
quikSort([5, 100, 6, 3, -12]); //[-12, 3, 5, 6, 100]
//冒泡
function sort(arr) {
    for(let i = 1; i < arr.length; i++) {
        for(let j = 0; j < i; j++) {
            if(arr[j] > arr[i]) {
                let temp;
                temp = arr[j];
                arr[j] = arr[i];
                arr[i] = temp;
            }
        }
    }
    console.log(arr)
    return arr;
}

14. [哈希]LRU缓存机制

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

示例:

输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
const lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4

/**
 * @param {number} capacity
 */
var LRUCache = function(capacity) {
    this.capacity = capacity;
    this.cache = new Map(); // Map作为缓存的数据结构 提供了键值对的存储和访问 保持插入顺序
};

/** 
 * @param {number} key
 * @return {number}
 */
LRUCache.prototype.get = function(key) {
    if(this.cache.has(key)) {
        const value = this.cache.get(key);
        // 将当前访问的项移到最后,表示最近使用
        this.cache.delete(key);
        this.cache.set(key, value);
        return value;
    }
    return -1;
};

/** 
 * @param {number} key 
 * @param {number} value
 * @return {void}
 */
LRUCache.prototype.put = function(key, value) {
    if(this.cache.has(key)) {
        this.cache.delete(key); // 如果键已经存在,更新值,并将其移到最后,表示最近使用
    } else if (this.cache.size >= this.capacity) {
        // 如果缓存已满,删除最久未使用的项(即第一个项)
        // .keys() 是 Set 或 Map 对象的方法之一,用于返回一个包含所有键的迭代器对象
        // .next() 是迭代器对象的方法,用于返回一个包含 value 和 done 属性的对象 value 属性表示迭代器的当前值,即第一个键
        const oldestKey = this.cache.keys().next().value;
        this.cache.delete(oldestKey);
    }
    // 添加新的键值对,并将其移到最后,表示最近使用
    this.cache.set(key, value);
};

/**
 * Your LRUCache object will be instantiated and called as such:
 * var obj = new LRUCache(capacity)
 * var param_1 = obj.get(key)
 * obj.put(key,value)
 */

15. 二分查找

/**
 * 解法一:循环
 * 思路:
 * (1)从数组首尾开始,每次取中点值。
 * (2)如果中间值等于目标即找到了,可返回下标,如果中点值大于目标,
 *  说明中点以后的都大于目标,因此目标在中点左半区间,如果中点值小于目标,则相反。
 * (3)根据比较进入对应的区间,直到区间左右端相遇,意味着没有找到。
 * 时间复杂度:O(log2n),对长度为n的数组进行二分,最坏情况就是取2的对数
 * 空间复杂度:0(1),无额外空间
 */
var search1 = function (nums, target) {
    const len = nums.length - 1;
    let left = 0;
    let right = len;
    while (left <= right) {
        const mid = Math.floor((left + right) / 2);
        const midValue = nums[mid];
        if (midValue < target) {
            left = mid + 1;
        } else if (midValue > target) {
            right = mid - 1;
        } else if (midValue === target) {
            return mid;
        }
    }
    return -1;
};
/**
 * 解法二:递归
 * 思路:
 * (1)从数组首尾开始,每次取中点值。
 * (2)如果中间值等于目标即找到了,可返回下标,如果中点值大于目标,说明中点以后的都大于目标,
 *  因此目标在中点左半区间,如果中点值小于目标,则相反。
 * (3)根据比较进入对应的区间,直到区间左右端相遇,意味着没有找到。
 * 时间复杂度:O(logn),对长度为n的数组进行二分,最坏情况就是取2的对数
 * 空间复杂度:0(1)
 */
const recursiveSearch = function (nums, target, left, right) {
    if (left > right) {
        return -1;
    }
    const mid = Math.floor((left + right) / 2);
    if (nums[mid] < target) {
        return recursiveSearch(nums, target, mid + 1, right);
    } else if (nums[mid] > target) {
        return recursiveSearch(nums, target, left, mid - 1);
    } else {
        return mid;
    }
};
var search = function (nums, target) {
    return recursiveSearch(nums, target, 0, nums.length - 1);
};

16. 数组中第k个最大数

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
// js搞定
// var findKthLargest1 = function(nums, k) {
//     return nums.sort((a, b) => b - a)[k-1];
// };
/**
 * 解法一:快速排序
 * 思路:快排,取得升序数组,然后取下标 nums.length - k + 1 对应的值
 * 时间复杂度:O(nlogn)
 * 空间复杂度:最优 O(logn), 最差 O(n)
 */
var findKthLargest = function(nums, k) {
    const len = nums.length;
    return quikSort(nums)[len - k]
};
const quikSort = (nums) => {
    const len = nums.length;
    if(len === 0) {
        return nums;
    }
    const midIndex = Math.floor(len / 2);
    const midValue = nums.splice(midIndex, 1);
    const leftArr = [];
    const rightArr = [];
    for (let i = 0; i < nums.length; i++) {
        if (nums[i] < midValue[0]) {
            leftArr.push(nums[i]);
        } else {
            rightArr.push(nums[i]);
        }
    }
    return quikSort(leftArr).concat(midValue, quikSort(rightArr));
};

17. [贪心 动规]买股票的最佳时机

/**
 * 解法一:暴力法(嵌套循环)
 * 思路:
 * (1)我们需要找出给定数组中两个数字之间的最大差值(即,最大利润)。
 * (2)此外,第二个数字(卖出价格)必须大于第一个数字(买入价格)。
 * 时间复杂度:O(n^2)。
 * 空间复杂度:0(1),只使用了一个常数变量。
 */
var maxProfit1 = function (prices) {
    let maxProfit = 0;
    for (let i = 0; i < prices.length; i++) {
        for (let j = i + 1; j < prices.length; j++) {
            maxProfit = Math.max(prices[j] - prices[i], maxProfit);
        }
    }
    return maxProfit;
};
/**
 * 解法二:贪心
 * 思路:
 * (1)将第一天看成价格最低,后续遍历的时候遇到价格更低则更新价格最低,
 * (2)每次都比较最大收益与当日价格减去价格最低的值,选取最大值作为最大收益
 * 时间复杂度:O(n)。
 * 空间复杂度:0(1),只使用了常数变量。
 */
var maxProfit2 = function (prices) {
    let maxProfit = 0;
    if (prices.length === 0) return maxprofit
    let minPrice = prices[0]; // 维护最低股票价格
    for (let i = 0; i < prices.length; i++) {
        if (prices[i] < minPrice) {
            minPrice = prices[i];
        }
        maxProfit = Math.max(maxProfit, prices[i] - minPrice);
    }
    return maxProfit;
};
/**
 * 解法三:动态规划
 * 思路:
 * (1)用dp[i][0]表示 第i天不持股到该天为止的最大收益,dp[i][1]表 示第i天持股,到该天为止的最大收益。
 * (2)(初始状态) 第一天不持股,则总收益为0,dp[0][0]=0; 第一天持股,则总收益为买股票的花费,此时为负数,dp[0][1] = - prices[0]。
 * (3)(状态转移) 对于之后的每一天,如果当天不持股,有可能是前面的若干天中卖掉了或是还没买,因此到此为止的总收益和前一天相同,也有可能是当天才
 *     卖掉,我们选择较大的状态dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i])
 * (4) 如果当天持股,有可能是前面若千天中买了股票,当天还没卖,因此收益与前一天相同,也有可能是当天买入,此时收益为负的股价,同样是选取最大值:dp[i][1] = max(dp[i - 1][1], -prices[i])。
 * 时间复杂度:O(n),遍历一次数组
 * 空间复杂度:0(n),动态规划富足数组的空间。
 */

var maxProfit = function (prices) {
    const len = prices.length;
    if (len === 0) return 0;
    const dp = [] // dp[i][0]表示某一天不持股到该天为止的最大收益,dp[i] [1]表示某天持股,到该天为止的最大收益

    for (let i = 0; i < len; i++) {
        dp[i] = [];
    }
    dp[0][0] = 0; // 第一天不持股,总收益为0
    dp[0][1] = -prices[0] // 第一天持股,总收益为减去该天的股价
    for (let i = 1; i < len; i++) {
        dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
        dp[i][1] = Math.max(dp[i - 1][1], -prices[i]);
    }
    return dp[len - 1][0];
};

18. [递归/迭代]合并两个有序链表

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
// 示例用法list1 [1, 2, 4], list2 [1, 3, 4] 
const list1 = new ListNode(1);
list1.next = new ListNode(2);
list1.next.next = new ListNode(4);

const list2 = new ListNode(1);
list2.next = new ListNode(3);
list2.next.next = new ListNode(4);

const mergedList = mergeTwoLists(list1, list2); // [1, 1, 2, 3, 4, 4]
console.log(mergedList);
/**
 * 解法一:迭代
 * 思路:
 * (1)新建一个空的表头后面连接两个链表排序后的结点。
 * (2)遍历两个链表都不为空的情况,取较小值添加在新的链表后面,每次只把被添加的链表的指针后移。
 * (3)遍历到最后肯定有一个链表还有剩余的结点,它们的值将大于前面所有的,直接连在新的链表后面即可。
 * 时间复杂度: O(n),最坏情况遍历2 * n个结点
 * 空间复杂度: 0(1),无额外空间使用,新建的链表属于返回必要空间
**/
var mergeTwoLists = function (list1, list2) {
    const dummy = new ListNode(0);
    let current = dummy;
    while (list1 !== null && list2 !== null) {
        if (list1.val < list2.val) {
            current.next = list1;
            list1 = list1.next;
        } else {
            current.next = list2;
            list2 = list2.next;
        }
        current = current.next;
    }
    if (list1 !== null) {
        current.next = list1;
    }
    if (list2 !== null) {
        current.next = list2;
    }
    return dummy.next;
};

/**
 * 解法二:递归
 * 思路:
 { 
list1[0]+merge(list1[1:],list2)     list1[0]<list2[0]
list2[0]+merge(list1,list2[1:])     otherwise
}
 * (1)每次比较两个链表当前结点的值,然后取较小值的链表指针往后,另一个不变送入递归中。
 * (2)递归回来的结果我们要加在当前较小值的结点后面,相当于不断在较小值后面添加结点。
 * (3)递归的终止是两个链表为空。
 * 时间复杂度: O(n),最坏相当于遍历两个链表每个结点一次
 * 空间复杂度: O(n), 递归栈长度最大为 n
 */
var mergeTwoLists = function (list1, list2) {
    const dummy = new ListNode(0);
    let current = dummy;
    if (list1 === null) {
        return list2;
    }
    if (list2 === null) {
        return list1;
    }
    if (list1.val < list2.val) {
        list1.next = mergeTwoLists(list1.next, list2);
        return list1
    } else {
        list2.next = mergeTwoLists(list1, list2.next);
        return list2;
    }
};

19. [动规]最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
示例 1:

输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
示例 2:

输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。
示例 3:

输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。

/**
 * @param {string} text1
 * @param {string} text2
 * @return {number}
创建一个二维数组 dp,其中 dp[i][j] 表示 text1 的前 i 个字符和 text2 的前 j 个字符的最长公共子序列的长度。

1. 初始化第一行和第一列的值为 0,即 dp[0][j] = dp[i][0] = 0。这是因为一个字符串和空字符串之间没有公共子序列。
2. 遍历 text1 和 text2 的每个字符,使用以下递推关系计算 dp[i][j] 的值:
    如果 text1[i-1] 等于 text2[j-1],则 dp[i][j] = dp[i-1][j-1] + 1。即当前字符相等,最长公共子序列的长度加一。
    如果 text1[i-1] 不等于 text2[j-1],则 dp[i][j] = max(dp[i-1][j], dp[i][j-1])。即当前字符不相等,最长公共子序列的长度取前一个字符的最长公共子序列长度或者当前字符的最长公共子序列长度的较大值。
3. 最终结果存储在 dp[text1.length()][text2.length()] 中,即最后一个元素。
 */
var longestCommonSubsequence = function (text1, text2) {
    const m = text1.length;
    const n = text2.length;
    const dp = Array.from(Array(m + 1), () => Array(n + 1).fill(0));

    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            if (text1[i - 1] === text2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }

    return dp[m][n];
};

20. [链表 递归]反转链表

function ListNode(val, next) {
    this.val = (val === undefined ? 0 : val)
    this.next = (next === undefined ? null : next)
}
/**
1.定义三个指针:current、previous 和 next。初始时,current 指向链表的头节点,previous 和 next 都为 null;
2. 遍历链表,每次迭代执行以下操作
    2.1 将 next 指针指向 current 的下一个节点,以便在反转后能够继续遍历链表。
    2.2 将 current 的 next 指针指向 previous,完成当前节点的反转。
    2.3 更新 previous 指针为 current,将其向前移动一步。
    2.4 更新 current 指针为 next,将其向后移动一步。
3. 当遍历完整个链表时,previous 指针将指向反转后的链表的头节点 返回出去。
 */
var reverseList = function (head) {
    let current = head;
    let previous = null;
    let next = null;
    while (current !== null) {
        next = current.next;
        current.next = previous;
        previous = current;
        current = next;
    }
    return previous;
};
const node1 = new ListNode(1);
const node2 = new ListNode(2);
const node3 = new ListNode(3);
const node4 = new ListNode(4);
const node5 = new ListNode(5);
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;

21. [DFS 递归]路径总和

/**
解法一: 广度优先遍历
使用广度优先搜索的方式,记录从根节点到当前节点的路径和,以防止重复计算。
用两个队列,分别存储将要遍历的节点,以及根节点到这些节点的路径和即可。
时间复杂度:O(N)
空间复杂度:O(N)
*/
var hasPathSum = function (root, targetSum) {
    if (root === null) {
        return false;
    }
    const nodeQueue = [root];
    const valQueue = [root.val];
    while (nodeQueue.length > 0) {
        const currentNode = nodeQueue.shift();
        const currentVal = valQueue.shift();
        if (currentNode.left) {
            nodeQueue.push(currentNode.left);
            valQueue.push(currentNode.left.val + currentVal);
        }
        if (currentNode.right) {
            nodeQueue.push(currentNode.right);
            valQueue.push(currentNode.right.val + currentVal)
        }
        if (currentNode.left === null && currentNode.right === null && currentVal === targetSum) {
            return true;
        }
    }
    return false;
};
/**
解法二: 递归
1. 判断当前节点是否为叶子节点,如果是叶子节点,则判断当前节点的值是否等于目标和。
2.如果当前节点不是叶子节点,则递归判断左子树和右子树是否存在满足条件的路径,即将目标和减去当前节点的值,然后递归判断左子树和右子树
时间复杂度:O(N)

空间复杂度:O(H) H是树的高度。空间复杂度主要取决于递归时栈空间的开销,最坏情况下,树呈现链状,空间复杂度为 O(N)。平均情况下树的高度与节点数的对数正相关,空间复杂度为 O(log⁡N

*/
var hasPathSum = function (root, targetSum) {
    if (root === null) {
        return false;
    }
    if (root.left === null && root.right === null) {
        return root.val === targetSum;
    }
    return (
        hasPathSum(root.left, targetSum - root.val)
        || hasPathSum(root.right, targetSum - root.val)
    );
};

22. [动规] 最长回文子串

输入:s = "babad"
输出:"bab"
解释:"aba"

/**
解法一: 暴力解法
*/
var longestPalindrome2 = function (s) {
    if (s.length < 2) {
        return s;
    }
    let maxLength = 1;
    let begin = 0;
    for (let i = 0; i < s.length - 1; i++) {
        for (let j = i + 1; j < s.length; j++) {
            if (j - i + 1 > maxLength && validPalindrome(s, i, j)) {
                maxLength = j - i + 1;
                begin = i;
            }
        }
    }
    return s.substring(begin, begin + maxLength);
};
/**
解法二: 动态规划 (空间换时间)
思路: 两边向中间判断,不相等则不是回文子串 相等则看s[i+1...j-1], (j - 1) - (i + 1) > 2反复判断,临界点还是相等 则找到最大回文子串 二维数组记录是否是回文字符串
1. dp二维数组初始化 对角线为true
2. 二层循环 从左向右填充列 再填充行 
    随时记录最大的maxLength和begin

时间复杂度:O(n^2)
空间复杂度:O(n^2)
*/
var longestPalindrome = function (s) {
    if (s.length < 2) {
        return s;
    }
    let maxLength = 1;
    let begin = 0;
    const dp = fillDiagonal(s);
    // 长度大于2
    for (let j = 1; j < s.length; j++) {
        for (let i = 0; i < j; i++) {
            if (s[i] !== s[j]) {
                dp[i][j] = false;
            } else {
                if (j - i + 1 < 4) {
                    dp[i][j] = true;
                } else {
                    dp[i][j] = dp[i + 1][j - 1];
                }
            }
            if (j - i + 1 > maxLength && dp[i][j]) {
                maxLength = j - i + 1;
                begin = i;
            }
        }
    }
    return s.substring(begin, begin + maxLength);
};

23. [深度优先遍历] 树的根到叶子节点的最长路径。

// 解法一: 递归
class TreeNode {
  constructor(val, left = null, right = null) {
    this.val = val;
    this.left = left;
    this.right = right;
  }
}

function findMaxPath(root) {
  if (!root) return 0;

  let maxPath = 0;

  function dfs(node, path) {
    if (!node) return;

    // 更新最长路径
    if (!node.left && !node.right) {
      maxPath = Math.max(maxPath, path + node.val);
    }

    // 递归遍历左子树和右子树
    dfs(node.left, path + node.val);
    dfs(node.right, path + node.val);
  }

  dfs(root, 0);

  return maxPath;
}

// 创建一个示例树
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
root.right.right = new TreeNode(6);

// 找到最长路径
const maxPath = findMaxPath(root);
console.log("最长路径长度:", maxPath);
/*解法二: 栈 
从栈中弹出一个节点和对应的路径长度。
如果该节点是叶子节点,则更新最长路径的长度。
如果该节点有左子节点,则将左子节点和路径长度加上当前节点值入栈。
如果该节点有右子节点,则将右子节点和路径长度加上当前节点值入栈。*/
class TreeNode {
  constructor(val, left = null, right = null) {
    this.val = val;
    this.left = left;
    this.right = right;
  }
}

function findMaxPath(root) {
  if (!root) return 0;

  let maxPath = 0;
  const stack = [];
  stack.push({ node: root, path: 0 });

  while (stack.length > 0) {
    const { node, path } = stack.pop();

    // 更新最长路径
    if (!node.left && !node.right) {
      maxPath = Math.max(maxPath, path + node.val);
    }

    // 将左子节点和右子节点入栈
    if (node.left) {
      stack.push({ node: node.left, path: path + node.val });
    }
    if (node.right) {
      stack.push({ node: node.right, path: path + node.val });
    }
  }

  return maxPath;
}

// 创建一个示例树
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
root.right.right = new TreeNode(6);

// 找到最长路径
const maxPath = findMaxPath(root);
console.log("最长路径长度:", maxPath);

posted @ 2024-01-13 19:33  Lz_Tiramisu  阅读(9)  评论(0编辑  收藏  举报