【js算法】leetcode习题

898 阅读11分钟

leetcode学习

前言

学习算法对于前端人来说也是十分重要的,本文主要记录在做leetcode经典习题的js解答方法,会不定期进行更新和补充,欢迎大家讨论和指教。

一、字符串和数组

1.1、无重复字符的最长子串

LeetCode链接:leetcode-cn.com/problems/lo…

思路:利用Map和双指针来求解,时间复杂度为O(n):

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLongestSubstring = function(s) {
    // map保存出现字符的下标,l和r则是当前无重复子串的左右指针
    let map = new Map(), max = 0, l = 0, r = 0
    for (let l=0, r=0; r<s.length; r++) {
    	// 如果发现重复字符
        if(map.has(s[r])) {
        	// 取max避免l往前移
            l = Math.max(map.get(s[r]) + 1, l)
        }
        // 获取最大值
        max = Math.max(max, r - l + 1)
        map.set(s[r], r)
    }
    return max
};

1.2、最长回文子串

LeetCode链接:leetcode-cn.com/problems/lo…

1.2.1、动态规划

思路:对于一个回文子串,例如cbabc,去掉首尾的c,它依旧是一个回文子串。因此我们可以通过P(i,j)表示从下标i到j是否是回文字符串,状态转移方程:P(i,j) = P(i+1,j-1)^(s[i]===s[j])。

/**
 * @param {string} s
 * @return {string}
 */
var longestPalindrome = function(s) {
    if (s.length === 1) {
        return s
    }
    let n = s.length, res = ''
    // 创建dp数组,默认填充0
    let dp = Array.from({length: n}, () => { return new Array(n).fill(0) })
    for(let i=n-1;i>=0;i--) {
        for (let j=i;j<n;j++) {
            // 当s[i] === s[j]时,判断距离是否小于2或子串为回文串
            dp[i][j] = s[i] === s[j] && (j-i<2 || dp[i+1][j-1])
            if (dp[i][j] && j-i+1 > res.length) {
                res = s.substring(i, j + 1)
            }
        }
    }
    return res
};

1.3、两数之和

LeetCode链接:leetcode-cn.com/problems/tw…

思路:通过哈希表,将出现的元素以(key:数组,value:下标),当我们遍历每一个元素时,只需要判断target - nums[i]是否在哈希表存在值就可以。

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
    var map = new Map();
    for(var i=0;i<nums.length;i++) {
    	// 判断哈希表中是否存在相应target - nums[i]值。
        if (map.has(target - nums[i]) && map.get(target - nums[i]) !== i) {
            return [map.get(target - nums[i]), i];
        }
        map.set(nums[i], i);
    }
    return []
};

1.4、最长公共前缀

LeetCode链接:leetcode-cn.com/problems/lo…

思路:纵向扫描,从前往后遍历字符串的每一列,比较每一列上的字符是否相同。

/**
 * @param {string[]} strs
 * @return {string}
 */
var longestCommonPrefix = function(strs) {
    var arrLen = strs.length;
    if (arrLen === 0) {
        return '';
    }
    // 以strs[0]为基准
    var len = strs[0].length;
    for(var i=0;i<len;i++) {
        var c = strs[0][i];
        // 对比每一列的字符是否相同
        for(var j=1;j<arrLen;j++) {
            if(strs[j][i] !== c) {
                return strs[0].substring(0, i);
            }
        }
    }
    return strs[0];
};

1.5、三数之和

LeetCode链接:leetcode-cn.com/problems/3s…

思路:循环+双指针。首先将数组进行排序,然后第一层循环确定三元组中的一个数left,接着确定mid指针和right指针,mid指针起始点为left+1,right指针启示点为数组长度-1,然后判断这三个值相加和比0大,还是比0小,比0大right--,比0小mid++。

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var threeSum = function(nums) {
    var len = nums.length;
    // 排序数组
    nums.sort(function(a,b) { return a-b;});
    var ans = [];
    for(var left=0;left<nums.length;left++) {
        // 过滤重复值
        if(left > 0 && nums[left] === nums[left-1]) {
            continue;
        }
        // mid,right指针
        var mid = left + 1, right = len - 1;
        while(mid < right) {
            if(nums[left] + nums[mid] + nums[right] === 0) {
                ans.push([nums[left], nums[mid], nums[right]]);
                mid++;
                right--;
                // 过滤重复值
                while(mid<right && nums[mid] === nums[mid-1]) { mid++; }
                while(right>mid && nums[right] === nums[right+1]) { right--; }
            } else if (nums[left] + nums[mid] + nums[right] < 0) {
                mid++;
            } else {
                right--;
            }
        }
    }
    return ans;
};

1.6、最接近的三数之和

LeetCode链接:leetcode-cn.com/problems/3s…

思路:这道题目和三数之和差不多,也是用到循环+双指针。首先将数组进行排序,然后第一层循环确定三元组中的一个数left,接着确定mid指针和right指针,mid指针起始点为left+1,right指针启示点为数组长度-1,然后判断这三个值相加和target相比。

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
var threeSumClosest = function(nums, target) {
    var len = nums.length;
    // 排序数组
    nums.sort(function(a,b) { return a-b;});
    // 初始ans为数组前三个之和
    var ans = nums[0] + nums[1] + nums[2];
    for(var left=0;left<nums.length;left++) {
    	// 过滤重复值
        if(left > 0 && nums[left] === nums[left-1]) {
            continue;
        }
        // mid,right指针
        var mid = left + 1, right = len - 1;
        while(mid < right) {
            var now = nums[left] + nums[mid] + nums[right];
            // 如果等于target直接返回
            if(now === target) {
                return target;
            } 
            // 更新最接近值
            if(Math.abs(target - ans) > Math.abs(target - now)) {
                ans = now;
            }
            if (now < target) {
                mid++;
                while(mid < right && nums[mid] === nums[mid-1]) { mid++; }
            } else {
                right--;
                while(right > mid && nums[right] === nums[right+1]) { right--; }
            }
        }
    }
    return ans;
};

1.7、删除排序数组中的重复项

LeetCode链接:leetcode-cn.com/problems/re…

思路:双指针方式,index为当前不重复数组部分的下标,i为数组遍历的下标,当遇到nums[index] !== nums[i]时,替换index+1位置即可。

/**
 * @param {number[]} nums
 * @return {number}
 */
var removeDuplicates = function(nums) {
    if(nums.length === 0) {
        return 0;
    }
    var index = 0;
    // i增加,跳过重复值
    for(var i=1;i<nums.length;i++) {
    	// 当遇到不重复值时
        if(nums[index] !== nums[i]) {
            // 替换
            index++;
            nums[index] = nums[i];
        }
    }
    return index + 1;
};

1.8、盛最多水的容器

LeetCode链接:leetcode-cn.com/problems/co…

思路:双指针。容器中能盛的水量等于两个指针中的最小高度 * 指针之间的距离,我们初始化left=0,right=len-1,当我们移动指针时候,它们之间的距离是减少的,所以只有最小高度增加时,盛的水容量才会变大,因此我们需要移动高度较小的指针。

/**
 * @param {number[]} height
 * @return {number}
 */
var maxArea = function(height) {
    // 左右指针
    var left = 0, right = height.length - 1;
    var ans = 0;
    while(left < right) {
        // 当前容量
        var temp = Math.min(height[left], height[right]) * (right - left);
        if (temp > ans) {
            ans = temp;
        }
        // 移动指针
        height[left] > height[right] ? right-- : left++;
    }
    return ans;
};

1.9、反转字符串

LeetCode链接:leetcode-cn.com/problems/re…

思路:双指针。反转的字符数组s[0]、s[1]...s[n-2]、s[n-1],反转后为s[n-1]、s[n-2]...s[1]、s[0],其实就是s[i]和s[n-i-1]进行交换,空间复杂度为O(1)。

/**
 * @param {character[]} s
 * @return {void} Do not return anything, modify s in-place instead.
 */
var reverseString = function(s) {
    for(var left=0,right=s.length-1;left<right;left++,right--) {
        var temp = s[left];
        s[left] = s[right];
        s[right] = temp;
    }
};

2.0、存在重复元素

LeetCode链接:leetcode-cn.com/problems/co…

思路:这道题目用哈希表的话很简单只需判断表中是否有存在元素即可,空间复杂度为O(n)。

/**
 * @param {number[]} nums
 * @return {boolean}
 */
var containsDuplicate = function(nums) {
    // 哈希表
    var map = new Map();
    for(var i=0;i<nums.length;i++) {
        if(map.has(nums[i])) {
            return true;
        }
        map.set(nums[i], true);
    }
    return false;
};

2.1、螺旋矩阵

LeetCode链接:leetcode-cn.com/problems/sp…

思路:我们将矩阵看成很多个层次,对矩阵从外到里进行输出就可以得到答案,方向是从:

  • (bottom,left) -> (bottom,right)
  • (bottom+1,right) -> (top,right)
  • (top,right-1) -> (top,left)
  • (top-1,left) -> (bottom+1,left)
/**
 * @param {number[][]} matrix
 * @return {number[]}
 */
var spiralOrder = function(matrix) {
    // 排除特殊情况,即行为0或列为0
    if(matrix.length === 0 || matrix[0].length === 0) {
        return matrix;
    }
    // left,right,bottom,top四个指针
    var left = 0, right = matrix[0].length - 1, bottom = 0, top = matrix.length - 1;
    var ans = [];
    while(left <= right && bottom <= top) {
        for(var i=left;i<=right;i++) {
            ans.push(matrix[bottom][i]);
        }
        for(var i=bottom+1;i<=top;i++) {
            ans.push(matrix[i][right]);
        }
        // 避免重复遍历值,如果有不明白的同学画个图就懂了
        if(left < right && bottom < top) {
            for(var i=right-1;i>=left;i--) {
            ans.push(matrix[top][i]);
            }
            for(var i=top-1;i>=bottom+1;i--) {
                ans.push(matrix[i][left]);
            }
        }
        // 指针改变
        left++,right--,bottom++,top--;
    }
    return ans;
};

2.2、螺旋矩阵 II

LeetCode链接:leetcode-cn.com/problems/sp…

思路:思路和上面螺旋矩阵差不多,我们首先初始化一个二维数组,然后进行向内环绕填值的过程就可以了,每次的值都+1。

/**
 * @param {number} n
 * @return {number[][]}
 */
var generateMatrix = function(n) {
    // 初始化四个指针、二维数组以及累加值start。
    var left = 0, right = n-1, bottom = 0, top = n-1;
    var ans = Array.from({length: n}, () => {return new Array(n).fill(0)});
    var start = 1;
    while(start <= Math.pow(n, 2)) {
        for(var i=left;i<=right;i++) {
            ans[bottom][i] = start++;
        }
        for(var i=bottom+1;i<=top;i++) {
            ans[i][right] = start++;
        }
        // 避免重复,其实这里加不加条件判断都可以。
        if(left< right && bottom < top) {
            for(var i=right-1;i>=left;i--) {
            ans[top][i] = start++;
            }
            for(var i=top-1;i>=bottom+1;i--) {
                ans[i][left] = start++;
            }
        }
        // 改变指针值
        left++,right--,bottom++,top--;
    }
    return ans;
};

2.3、合并两个有序数组

LeetCode链接:leetcode-cn.com/problems/me…

思路:使用双指针+创建数组保存比较简单,但空间复杂度为O(m),所以我们需要nums1数组基础上进行改写,才能达到O(1)的空间复杂度,因此需要从后往前修改数组,因为nums1的末尾是0可以提供给我们改变的。

/**
 * @param {number[]} nums1
 * @param {number} m
 * @param {number[]} nums2
 * @param {number} n
 * @return {void} Do not return anything, modify nums1 in-place instead.
 */
var merge = function(nums1, m, nums2, n) {
    // 设置n1,n2指向nums1,nums2的数组有效值末尾
    var n1 = m - 1, n2 = n - 1;
    // loc指向nums1数组末尾
    var loc = m + n - 1;
    // 从后往前将大的值填充到nums1中
    while((n1 >= 0) && (n2 >= 0)) {
        nums1[loc--] = (nums1[n1] > nums2[n2]) ? nums1[n1--] : nums2[n2--];
    }
    // 如果nums2不为空,说明还需要将nums2较小值添加进入nums1。
    if(n2 >= 0) {
        nums1.splice(0, n2+1, ...nums2.slice(0, n2 + 1));
    }
};

二、链表

2.1、反转链表

LeetCode链接:leetcode-cn.com/problems/re…

思路:涉及到链表处理的方法,我们通常可以想到2种方法,迭代方法和递归方法,对于链表的反转,一般需要修改next指向。

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
 
 // 迭代方法
var reverseList = function(head) {
    var pre = null, cur = head;
    while(cur !== null) {
        var next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    return pre;
};

 // 递归方法
var reverseList = function(head) {
    if(head === null || head.next === null) {
        return head;
    }
    // 注意我们这里传的是head.next过去,也就是说在判断为空时,
    // 如果返回head,实际上此时判断的是head.next.next为空
    var temp = reverseList(head.next);
    head.next.next = head;
    head.next = null;
    return temp;
};

2.2、两数相加

LeetCode链接:leetcode-cn.com/problems/ad…

思路:相加时需要记得判断是否产生进位,求和加上之前的进位,我们创建一个新的链表和在原来的链表上修改都行,我下面是在原来的链表上修改。

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var addTwoNumbers = function(l1, l2) {
    // pre用来保存循环最后遍历到的节点
    var carry = 0, head = l1, pre = l1;
    while(l1 !== null && l2 !== null) {
        var num = l1.val + l2.val;
        // 加上和处理进位,以l1链表为基准
        l1.val = (num + carry) % 10;
        // 进位
        var carry = (num + carry) >= 10 ? 1 : 0;
        pre = l1;
        l1 = l1.next;
        l2 = l2.next;
    }
    // 处理l1较长
    while(l1 !== null) {
        var num = l1.val;
        l1.val = (num + carry) % 10;
        var carry = (num + carry) >= 10 ? 1 : 0;
        pre = l1;
        l1 = l1.next;
    }
    // 处理l2较长,先接上后半部分
    if(l2 !== null) {
        pre.next = l2;
    }
    while(l2 !== null) {
        var num = l2.val
        l2.val = (num + carry) % 10;
        var carry = (num + carry) >= 10 ? 1 : 0;
        pre = l2;
        l2 = l2.next;
    }
    // 处理链表加完后产生的最后进位情况
    if(carry) {
        pre.next = new ListNode(carry);
    }
    return head;
};

2.3、删除链表的倒数第N个节点

LeetCode链接:leetcode-cn.com/problems/re…

思路:要删除倒数第n个,如果先遍历一遍有多少个节点,再循环一遍来删除的话就很麻烦。所以我们需要一个pre节点,来先走n步,走完n步后,另一个temp节点再走,当pre.next为空时,删除temp节点的next即可:

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} n
 * @return {ListNode}
 */
var removeNthFromEnd = function(head, n) {
   // 创建node,node.next = temp,这样所有节点都能删除
   var node = new ListNode();
   node.next = head;
   var pre = node, temp = node;
   // pre为先走n步
   while(n>0) {
       pre = pre.next;
       n--;
   }
   // 走到链表的末尾
   while(pre.next !== null) {
       temp = temp.next;
       pre = pre.next;
   }
   // 删除节点
   temp.next = temp.next.next;
   return node.next;
};

2.4、两两交换链表中的节点

LeetCode链接:leetcode-cn.com/problems/sw…

思路:交换节点,即后面节点nextNode的next指向前面节点preNode,前面节点preNode的next指向NextNode的next,就这样依次更替,下面列举了递归方法和迭代方法。

2.4.1、递归方法

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var swapPairs = function(head) {
    if(!head || !head.next) {
        return head;
    }
    var preNode = head, nextNode = head.next;
    preNode.next = swapPairs(nextNode.next);
    nextNode.next = preNode;
    return nextNode;
};

2.4.2、迭代方法

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var swapPairs = function(head) {
    var temp = new ListNode();
    temp.next = head;
    var newHead = temp;
    while(temp.next && temp.next.next) {
        var node1 = temp.next, node2 = temp.next.next;
        temp.next = node2;
        node1.next = node2.next;
        node2.next = node1;
        temp = node1;
    }
    return newHead.next;
};

2.5、合并两个有序链表

LeetCode链接:leetcode-cn.com/problems/me…

思路:前面说过针对链表,我们一般可以从迭代法和递归法入手,这道题的话我们主要比较两个列表值的大小,对链表进行合并。

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
 
// 迭代方法,通过比较l1和l2当前指针值的大小,改变temp节点的next。
var mergeTwoLists = function(l1, l2) {
    var temp = new ListNode();
    var head = temp;
    while(l1 !== null && l2 !== null) {
        if(l1.val <= l2.val) {
            temp.next = l1;
            l1 = l1.next;
        } else {
            temp.next = l2;
            l2 = l2.next;
        }
        temp = temp.next;
    }
    // 接上不为空的链表
    temp.next = l1 === null ? l2 : l1;
    return head.next;
}

// 递归方法,判断当前l1.val和l2.val值的大小,返回值较小的节点。
var mergeTwoLists = function(l1, l2) {
    if(l1 === null) {
        return l2;
    }
    if(l2 === null) {
        return l1;
    }
    if(l1.val <= l2.val) {
        // l1的next等于l1.next.val和l2.val中较小的值,一直进行比较,直到l1/l2为空。
        l1.next = mergeTwoLists(l1.next, l2);
        return l1;
    } else {
        l2.next = mergeTwoLists(l1, l2.next);
        return l2;
    }
};

2.6、合并K个升序链表

LeetCode链接:leetcode-cn.com/problems/me…

思路:这道题目和上面合并链表一样,只不过现在需要合并k个列表,如果我们循环合并的话,那时间复杂度为O(n^3),因此我们需要考虑别的方法,这里我们就可以用到分治法, 将k个列表不断分开,再两两合并,最终形成有序链表。

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode[]} lists
 * @return {ListNode}
 */
var mergeKLists = function(lists) {
    if(lists.length === 0) {
        return null;
    }
    if(lists.length === 1) {
        // 注意需要反馈lists[0],不然的话返回的是二维数组
        return lists[0];
    }
    // 根据mid分开数组
    var mid = Math.ceil(lists.length / 2);
    return merge(mergeKLists(lists.slice(0, mid)), mergeKLists(lists.slice(mid)));
};

// 合并数组
function merge(list1, list2) {
    if(list1 === null) {
        return list2;
    }
    if(list2 === null) {
        return list1;
    }
    if(list1.val <= list2.val) {
        list1.next = merge(list1.next, list2);
        return list1;
    } else {
        list2.next = merge(list1, list2.next);
        return list2;
    }
}

2.7、旋转链表

LeetCode链接:leetcode-cn.com/problems/ro…

思路:这道题目其实就是找新的头尾节点,修改指针指向,移动k个位置,k可能比链表长度大,因此我们需要先获取链表长度len取余减少无效移动,(k % len)即我们需要移动的次数,实际上就是让链表倒数第(K % len)节点为新的头节点,链表末尾连接之前的head。

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} k
 * @return {ListNode}
 */
var rotateRight = function(head, k) {
    if(head === null || head.next === null) {
        return head;
    }
    var node = head, len = 1;
    // 获取链表长度
    while(node.next !== null) {
        len++;
        node = node.next;
    }
    // 这里我们让链表闭环,末尾执行head
    node.next = head;
    // 获取移动step
    var step = len - k % len;
    var pre = new ListNode();
    pre.next = head;
    while(step--) {
        pre = pre.next;
    }
    // 新节点为前驱pre的next
    head = pre.next;
    // 断开链表
    pre.next = null
    return head;
};

2.8、环形链表

LeetCode链接:leetcode-cn.com/problems/li…

思路:这道题目有两个方法,第一个是哈希表,记录节点是否重复出现;第二个是快慢指针的方法,快指针比慢指针每次多走一步,如果链表存在闭环,那么快指针总会追上慢指针。

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
    if(head === null || head.next === null) {
        return false;
    }
    // 初始化快慢指针
    var slow = head, fast = head.next;
    while(slow !== fast) {
        // 如果快指针走到了链表末尾,则不存在闭环
        if(fast === null || fast.next === null) {
            return false;
        }
        // 快指针比慢指针每次都走快一步
        slow = slow.next;
        fast = fast.next.next;
    }
    return true;
};

2.9、相交链表

LeetCode链接:leetcode-cn.com/problems/in…

思路:这道题目我们仔细想想,链表A和链表B虽然长度不同,但遍历完A再遍历B和遍历完B再遍历A的长度是一致的(A+B=B+A),如果A和B相交,那么说明它们会在某个时刻相遇,一起走完最后的节点,如果它们不相交,那么它们会同时为null。

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} headA
 * @param {ListNode} headB
 * @return {ListNode}
 */
var getIntersectionNode = function(headA, headB) {
    if(headA === null || headB === null) {
        return null;
    }
    var nodeA = headA, nodeB = headB;
    while(nodeA !== nodeB) {
        // A链表遍历到末尾,开始遍历B
        nodeA = nodeA === null ? headB : nodeA.next;
        // B链表遍历到末尾,开始遍历A
        nodeB = nodeB === null ? headA : nodeB.next;
    }
    return nodeA;
};

三、栈

3.1、有效的括号

LeetCode链接:leetcode-cn.com/problems/va…

/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function(s) {
    var len = s.length, stack = [];
    if(len % 2 !== 0) {
        return false;
    }
    var map = {
        ')': '(',
        ']': '[',
        '}': '{'
    }
    for(var i=0;i<s.length;i++) {
        if (map[s[i]]) {
            // 判断栈顶元素是否匹配
            if (stack.length === 0 || stack.pop() !== map[s[i]]) {
                return false;
            }
        } else {
            stack.push(s[i]);
        }
    }
    return stack.length === 0;
};

四、树

4.1、树的遍历方法

针对树的遍历,一般有两种方法:深度优先搜索DFS、广度优先搜索BFS。如果有不太记得算法流程的同学可以参考:

4.1.1、 深度优先搜索 - DFS(非递归)

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var DFS = function(root) {
    var nodeList = [];
    if (root !== null) {
        // 维护一个stack保存当前路径节点
        var stack = [];
        stack.push(root);
        while (stack.length > 0) {
            var temp = stack.pop();
            nodeList.push(temp.val);
            temp.left ? stack.push(temp.left) : ''
            temp.right ? stack.push(temp.right) : ''
        }
    }
    return nodeList;
}

4.1.2、深度优先搜索 - DFS(递归)

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
// 递归搜索
function deepFirstSearch(node, nodeList) {
    if(node !== null) {
        nodeList.push(node.val);
        deepFirstSearch(node.left, nodeList);
        deepFirstSearch(node.right, nodeList);
    }
    return nodeList;
}

var DFS = function(root) {
    var nodeList = [];
    deepFirstSearch(root, nodeList)
    return nodeList;
}

4.1.3、广度优先搜索 - DFS(非递归)

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */

var BFS = function(root) {
    var nodeList = [];
    if(root !== null) {
        // 维护一个队列记录当前遍历节点
        var queue = [];
        queue.push(root);
        while(queue.length > 0) {
            var temp = queue.shift();
            nodeList.push(temp.val);
            temp.left ? queue.push(temp.left) : '';
            temp.right ? queue.push(temp.right) : '';
        }
    }
    return nodeList;
}

4.2、二叉树的层序遍历

LeetCode链接:leetcode-cn.com/problems/bi…

思路:层序遍历,一般就是进行BFS。

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function(root) {
    var result = [];
    if(root) {
        var queue = [];
        queue.push(root);
        while(queue.length > 0) {
            // temp保存每一层所有节点的值
            var temp = [], len = queue.length;
            for(var i=0;i<len;i++) {
                var node = queue.shift();
                temp.push(node.val);
                node.left ? queue.push(node.left) : '';
                node.right ? queue.push(node.right) : '';
            }
            result.push(temp);
        }
    }
    return result;
};

4.2、二叉树的最大深度

LeetCode链接:leetcode-cn.com/problems/ma…

思路:递归方法,二叉树的最大深度等于Max(左子树的最大深度,右子树的最大深度)+ 根节点。

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function(root) {
    // 当访问到空节点时返回0
    if(!root) {
        return 0;
    }
    var left = maxDepth(root.left) + 1;
    var right = maxDepth(root.right) + 1;
    // 取左右子树最大深度
    return Math.max(left, right);
};

五、数学和数字

5.1、整数反转

LeetCode链接:leetcode-cn.com/problems/re…

思路:循环取余,不断累加,但需要判断是否溢出。

/**
 * @param {number} x
 * @return {number}
 */
var reverse = function(x) {
    var ans = 0;
    var max = Math.pow(2,31) - 1, min = Math.pow(-2, 31);
    while(x !== 0) {
        var pop = x % 10;
        // 当x为正数向下取整,x为负数向上取整
        x = x < 0 ? Math.ceil(x/10) : Math.floor(x/10);
        // 判断是否溢出
        if(ans > max/10 || (ans === max/10 && pop > 7)) {
            return 0;
        }
        if(ans < min/10 || (ans === min/10 && pop < 8)) {
            return 0;
        }
        ans = ans * 10 + pop;
    }
    return ans;
};

5.2、回文数

LeetCode链接:leetcode-cn.com/problems/pa…

思路:回文数的前半部分和后半部分是相同的,因此我们可以反转一半数字,判断是否相等即可。

/**
 * @param {number} x
 * @return {boolean}
 */
var isPalindrome = function(x) {
    // 排除特殊情况
    if(x < 0 || (x % 10 === 0 && x !== 0)) {
        return false;
    }
    var temp = 0;
    // 反转后半部分数字
    while(x > temp) {
        temp = temp * 10 + (x % 10);
        x = Math.floor(x / 10);
    }
    // 判断是否相等
    return temp === x || Math.floor(temp / 10) === x;
};

5.3 只出现一次的数字

LeetCode链接:leetcode-cn.com/problems/si…

思路:数组中只有一个数字出现1次,其它都是出现2次,那么将数组所有元素异或,最后的值就是出现一次的数字。a ^ a = 0, 0 ^ b = b;

/**
 * @param {number[]} nums
 * @return {number}
 */
var singleNumber = function(nums) {
    var ans = 0;
    // 异或
    for(var i=0;i<nums.length;i++) {
        ans ^= nums[i];
    }
    return ans;
};

5.4 2的幂

LeetCode链接:leetcode-cn.com/problems/po…

思路:一个数是2的幂次方,假设这个数是n,n&(n-1) = 0;

/**
 * @param {number} n
 * @return {boolean}
 */
var isPowerOfTwo = function(n) {
    if(n <= 0) {
        return false;
    }
    // 返回判断
    return (n & (n-1)) === 0;
};

六、回溯算法

6.1、括号生成

LeetCode链接:leetcode-cn.com/problems/ge…

思路:当我们在遇到找出所有解、所有可能的问题时,一般可以通过去存储相应状态,然后通过遍历或者递归方式,找出符合条件解时候的状态值,而一般我们通过回溯方法往往更容易获取思路。

这道题目,实际上要想获得所有组合,首先一个前提条件就是右括号总数不能多余在该右括号之前字符串的左括号,例如())(,第二个右括号之前只有一个左括号,这个解答显然不符合条件,根据这些条件,我们可以通过递归添加左右括号。

/**
 * @param {number} n
 * @return {string[]}
 */
var generateParenthesis = function(n) {
    var res = [];
    getAll('', 0, 0);
    return res;
    function getAll(cur, left, right) {
        // n是括号对数,n*2就匹配时字符串总长度
        if(cur.length === n * 2) {
            res.push(cur);
        }
        // 因为左括号没有位置条件限制,因此可以任意添加n个左括号
        if(left < n) {
            getAll(cur+'(', left + 1, right);
        }
        // 右括号的添加时,其字符串之前左括号总数必须大于右括号总数
        if(right < left) {
            getAll(cur+')', left, right + 1);            
        }
    }
};

6.2、子集

LeetCode链接:leetcode-cn.com/problems/su…

思路:这道题目,实际上也是使用到了递归来实现子集枚举,字符串的字符有两个状态,选中和未选中,字符的选中和未选中都会影响子集中的元素组成,cur代表当前遍历到的元素下标,此时[0,cur-1]状态是确定的,我们只需要继续枚举nums[cur]取和不取情况即可。

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var subsets = function(nums) {
    var ans = [], temp = [];
    getAll(0);
    return ans;
    function getAll(cur) {
        // cur为数组长度,答案确定
        if(cur === nums.length) {
            // 返回新数组,不然ans会引用同一个数组
            ans.push(temp.slice());
            return;
        }
        // 选中当前元素
        temp.push(nums[cur]);
        getAll(cur + 1);
        // 未选择当前元素
        temp.pop();
        getAll(cur + 1);
    }
};

6.3、全排列

LeetCode链接:leetcode-cn.com/problems/pe…

思路:全排列主要列举数组所有排列可能,实际上可以看作将数组所有元素提取出,从左到右依次尝试排列在不同位置,我们可以通过暴力法,也可以通过回溯法来模拟这个过程,cur代表当前遍历下标,此时[0,cur-1]的排列状态是确定的,而我们则依次尝试cur这个位置上元素值。

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var permute = function(nums) {
    var ans = [], len = nums.length;
    getAll(0);
    return ans;
    function getAll(cur) {
        // cur = len确定答案
        if(cur === len) {
            ans.push(nums.slice())
        }
        for(var i=cur;i<len;i++) {
            // 循环依次尝试当前cur位置的元素值
            [nums[cur], nums[i]] = [nums[i], nums[cur]];
            // 继续确定后面的排列
            getAll(cur+1);
            [nums[cur], nums[i]] = [nums[i], nums[cur]];
        }
    }
};

七、排序与搜索

排序和搜索是我们日常中接触到最多的算法,因此需要花更多时间在这一方面的学习上。

7.1、排序链表

LeetCode链接:leetcode-cn.com/problems/so…

思路:这里我通过对链表进行插入排序,来获得有序的链表。和数组的插入排序不同的是,链表插入排序有2个需要注意的地方:

  1. 数组需要将插入位置后面的有序序列都往后移动一位,而链表只需要改变指针的指向。

  2. 对于单向链表而言,无法通过向前遍历链表,因此只能从头向后遍历。

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var sortList = function(head) {
    if(head === null) {
        return head;
    }
    // 创建新的头节点,next指向head
    var newHead = new ListNode(0);
    newHead.next = head;
    // pre节点代表已排序的节点部分
    var pre = head, cur = head.next;
    while(cur !== null) {
        // 如果有序,则继续向下一个节点遍历
        if(cur.val >= pre.val) {
            pre = pre.next;
            // cur继续下一个节点
            cur = cur.next;
        } else {
            var temp = newHead;
            // 找到cur节点要插入的位置
            while(temp.next.val <= cur.val) {
                temp = temp.next;
            }
            // 改变节点指向
            pre.next = cur.next;
            cur.next = temp.next;
            temp.next = cur;
            // cur继续下一个节点
            cur = pre.next;
        }
    }
    return newHead.next;
};

7.2、搜索旋转排序数组

LeetCode链接:leetcode-cn.com/problems/se…

思路:这道题目可以发现它是将数组分开成两部分,这两个部分都是有序的,如果我们从中间分开,那么一定有一个部分是有序的,我们对这个部分进行二分查找就可以获得答案。

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
var search = function(nums, target) {
    var left = 0, right = nums.length - 1;
    while(left <= right) {
        // 分开为两个部分
        var mid = Math.floor((left + right) / 2);
        // 如果找到值,返回
        if (nums[mid] === target) {
            return mid;
        } else if(nums[0] <= nums[mid]) { // 比较nums[0]和nums[mid]目的是判断左半部分是否有序
            // 判断target是否在左半部分
            if(nums[0] <= target && target < nums[mid]) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        } else { // 判断右半部分是否有序
            // 判断target是否在右半部分
            if(nums[mid] < target && target <= nums[nums.length - 1]) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
    }
    return -1;
};

7.3、数组中的第K个最大元素

LeetCode链接:leetcode-cn.com/problems/kt…

思路:这道题目我主要的思路就是建最大堆,每次获取堆顶元素,移除堆顶,经过第k次后,就获得了第k大元素。

此外我还看到有些同学用来建最小堆的方法,时间复杂度更低。

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function(nums, k) {
    var res;
    // 循环k次,找第k大。
    for(var i=0;i<k;i++) {
        // 找有子节点的节点
        for(var j=Math.floor(nums.length/2);j>=0;j--) {
            // 获取左右子节点值,这里需要注意右节点有可能存在不存在,取值就为undefined
            var left = nums[j*2 + 1];
            var right = nums[j*2 + 2];
            // 左右节点值比父节点大
            if(nums[j] < left || nums[j] < right) {
                // 判断right是否存在
                if(!Number.isInteger(right) || left > right ) {
                    [nums[j], nums[j*2 + 1]] = [nums[j*2 + 1], nums[j]];
                } else {
                    [nums[j], nums[j*2 + 2]] = [nums[j*2 + 2], nums[j]];
                }
            }
        }
        // 交换堆顶元素,并移除
        [nums[0], nums[nums.length - 1]] = [nums[nums.length - 1], nums[0]];
        res = nums.pop();
    }
    return res;
};

7.4、二叉搜索树中第K小的元素

LeetCode链接:leetcode-cn.com/problems/kt…

思路:这道题目有一个前提条件就是二叉搜索树,二叉搜索树的中序遍历就是一个有序序列,这里我们通过迭代去中序遍历二叉搜索树,当数组移除第k个元素就获得结果。

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @param {number} k
 * @return {number}
 */
var kthSmallest = function(root, k) {
    // stack记录当前遍历到的节点
    var stack = [];
    while(true) {
        // 遍历到叶子节点,即最小值
        while(root !== null) {
            stack.push(root);
            root = root.left;
        }
        // 有序弹出节点
        root = stack.pop();
        k--;
        if(k === 0) {
            return root.val;
        } else {
            root = root.right;
        }
    }
};

7.5、二叉树的最大深度

LeetCode链接:leetcode-cn.com/problems/ma…

思路:二叉树最大深度等于Max(左子树的深度,右子树的深度)+ 1,我们依次递归计算左右子树的最大深度就可以获得二叉树的最大深度。

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function(root) {
    // 递归到空节点,返回0
    if(root === null) {
        return 0;
    }
    // 取左右子树的最大深度 + 1
    return Math.max(maxDepth(root.left) + 1, maxDepth(root.right) + 1);
};

7.6、二叉树中的最大路径和

LeetCode链接:leetcode-cn.com/problems/bi…

思路:对于这道题目我们可以看作是考虑每一个节点的最大贡献值,就是以这一个节点为根节点的一条路径的最大值,这个值等于它子节点的最大贡献值 + 自身的节点值。因此我们再遍写函数时需要注意两个地方:

  1. 节点的最大贡献值等于子节点的最大贡献值 + 自身的节点值

  2. 叶子节点的贡献值为自身,空节点贡献值为0。

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxPathSum = function(root) {
    var res = Number.MIN_SAFE_INTEGER;
    getMax(root);
    return res;
    function getMax(root) {
        if(root === null) {
            return 0;
        }
        // 递归计算左右节点最大贡献值,只有贡献值大于0,才会选取子节点
        var maxLeft = Math.max(getMax(root.left), 0);
        var maxRight = Math.max(getMax(root.right), 0);
        // 更新最大值
        res = Math.max(res, root.val + maxLeft + maxRight);
        // 返回当前节点最大值
        return root.val + Math.max(maxLeft, maxRight);
    }
};

7.7、二叉搜索树的最近公共祖先

LeetCode链接:leetcode-cn.com/problems/lo…

思路:对于这道题目同样有一个前提条件就是二叉搜索树,因此我可以判断如果p,q和当前遍历的节点有三种情况:

  1. p,q值均小于当前节点,说明p,q值在当前节点左子树,因此将当前节点移动至它的左子节点。

  2. p,q值均大于当前节点,说明p,q值在当前节点右子树,因此将当前节点移动至它的右子节点。

  3. 除此之外,p,q其中一个等于当前节点,或p,q一个大于当前节点,一个小于当前节点,说明此时该节点就是最近的公共祖先。

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */

/**
 * @param {TreeNode} root
 * @param {TreeNode} p
 * @param {TreeNode} q
 * @return {TreeNode}
 */
var lowestCommonAncestor = function(root, p, q) {
    while(true) {
        if(root.val > p.val && root.val > q.val) { // 移动至左子节点
            root = root.left;
        } else if (root.val < p.val && root.val < q.val) { // 移动至右子节点
            root = root.right;
        } else {
            return root;
        }
    }
};

7.8、二叉树的最近公共祖先

LeetCode链接:leetcode-cn.com/problems/lo…

思路:对于这道题目是针对常规的二叉树的,我们通过取遍历二叉树,对于最近的公共祖先,一定满足这两个条件其中的一个:

  1. 左子树、右子树分别包含p或q其中的一个,即left && right = true。

  2. 当前节点等于p或q,左子树和右子树其中一个保护剩下的另一个节点。

因此我们判断的成立条件为:left && right || (root === p || root === q) && (left || right)。left和right代表左右子树是否包括p或q。

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @param {TreeNode} p
 * @param {TreeNode} q
 * @return {TreeNode}
 */
var lowestCommonAncestor = function(root, p, q) {
    var res = null;
    dfs(root, p, q);
    return res;
    function dfs(root, p, q) {
        if(root === null) {
            return false;
        }
        // 判断当前节点和p或q是否相等
        var cur = (root === p || root === q);
        // 继续遍历左子树和右子树
        var left = dfs(root.left, p, q);
        var right = dfs(root.right, p, q);
        // 符合条件,找到最近公共祖先
        if((left && right) || (cur && (left || right))) {
            res = root;
        }
        // 返回当前节点的包含情况
        return cur || left || right;
    }
};

八、动态规划

8.1、爬楼梯

LeetCode链接:leetcode-cn.com/problems/cl…

思路:针对动态规划的题目,我们首要的目标就是找到动态规划的转移方程。以这道题目为例,当我们在爬台阶,我们可以走1步或者2步,也就是当我们爬到第n阶时候,有可能是从n-1爬一步上来的,也可能是从n-2一下爬两步上去的,因此我们可以列出式子:f(n) = f(n-1) + f(n-2)。

这里意思爬到n阶的方案数等于爬到n-1阶的方案数 + 爬到n-2阶的方案数,我们可以通过dp来记录当爬到某一阶的状态。

/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
    // dp数组记录爬到各阶的方案数
    var dp = [];
    // n = 1时只有一种可能性,n = 2有两种。
    dp[1] = 1;
    dp[2] = 2;
    // 记录状态
    for(var i=3;i<=n;i++) {
        dp[i] = dp[i-1] + dp[i-2];
    }
    return dp[n];
};

var climbStairs = function(n) {
    if(n <= 2) {
        return n;
    }
    // 上面那种解法的空间复杂度为O(n),实际上我们并不需要保存所有阶的方案数
    // 只需要不断累加之前方案数即可,因此通过两个变量来记录,使得空间复杂度降至O(1)
    var a = 1, b = 2;
    for(var i=3;i<=n;i++) {
        var temp = b;
        b = a + b;
        a = temp;
    }
    return b;
};

8.2、最大子序和

LeetCode链接:leetcode-cn.com/problems/ma…

思路:这道题目的是求字符串的最大子序列和,因为数组元素有正有负,我们并不知道中断和继续取值所带来的影响,但我们可以知道以当前遍历下标i结尾连续数组的最大和。

这是因为我们从头开始遍历,首先第一个元素结尾最大子序和肯定是自己,而第二个元素结尾的最大子序和要么就是之前最大子序和 + 自己,要么就是自己(例如,dp[0] = -2,如果nums[1] = -1,那么dp[1] = -1)。因此我们可以得出方程:f(i) = Math.max(f(i-1) + nums[i], nums[i]),我们在循环中不断更新最大值,最后就获得最大子序和。

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxSubArray = function(nums) {
    var dp = [], max = nums[0];
    // 以第一个元素结尾最大子序和一定是自己
    dp[0] = nums[0];
    for(var i=1;i<nums.length;i++) {
        // 以i结尾最大子序和为dp[i-1] + nums[i],nums[i]的最大值
        dp[i] = Math.max(dp[i-1] + nums[i], nums[i]);
        // 更新最大值
        max = Math.max(max, dp[i]);
    }
    return max;
};

8.3、买卖股票的最佳时机

LeetCode链接:leetcode-cn.com/problems/be…

思路:这道题目和7.2类似,我们可以知道在某一天之前的历史最低值,因此通过记录下这个最低值,然后计算在最低值买入,然后在今天卖出时的收益,和已知的最大收益相比,就可以求得解。

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    // 某一天之前的历史最低值
    var min = prices[0], res = 0;
    for(var i=1;i<prices.length;i++) {
        // 当天卖出和之前的历史最低值所获得的收益
        var profit = prices[i] - min;
        // 更新历史最低值
        min = Math.min(min, prices[i]);
        res = Math.max(res, profit);
    }
    return res;
};

8.4、不同路径

LeetCode链接:leetcode-cn.com/problems/un…

思路:列出动态规划的转移方程,f(i, j)代表走到i行j列的路径数,而走到f(i, j)有两种情况:f(i-1, j)向下走一步,f(i, j-1)向右走一步,因此f(i, j) = f(i-1, j) + f(i, j-1),当它们值为0时,说明路是不同的,而只为1是则是通路。

/**
 * @param {number} m
 * @param {number} n
 * @return {number}
 */
var uniquePaths = function(m, n) {
    // 初始化dp数组
    var dp = Array.from({length: m}, () => { return new Array(n) });
    // 初始化边界均为1
    for(var i=0;i<m;i++) {
        dp[i][0] = 1;
    }
    for(var i=0;i<n;i++) {
        dp[0][i] = 1;
    }
    for(var i=1;i<m;i++) {
        for(var j=1;j<n;j++) {
            // f(i, j) = f(i-1, j) + f(i, j-1)
            dp[i][j] = dp[i-1][j] + dp[i][j-1];
        }
    }
    return dp[m-1][n-1];
};