LeetCode热题Hot100(JS解法)(更新中...)

LeetCode热题Hot100

1.寻找两个正序数组的中位数(leetCode4)

解法1:暴力破解

  • 思路:先将两个数组合并,两个有序数组的合并也是归并排序中的一部分。然后根据奇数,还是偶数,返回中位数。

  • 复杂度:

    • 时间复杂度:O(NlogN),N为两个数组的长度
    • 空间复杂度:O(N)
var findMedianSortedArrays = function(nums1, nums2) {
    const n = nums1.length + nums2.length
    const nums = nums1.concat(nums2).sort((a, b) => a-b)
    return n % 2 === 0 ? (nums[n/2] + nums[n/2 -1]) / 2 : nums[Math.floor(n/2)]
};

解法2:双指针法

  • 思路:

    • 因为两个数组有序,求中位数不需要把两个数组合并;
    • 当合并后的数组总长度len为奇数时,只要知道索引为len/2位置上的数就行了,如果数为偶数,只要知道索引为len/2 - 1和len/2上的数就行,所以不管是奇数还是偶数只要遍历len/2次即可,用两个值来存遍历过程中len/2-1和len/2上的数即可
    • 两个指针point1和point2分别指向nums1和nums2,当nums1[point1] < nums2[point2],则point1指针移动,否则point2指针移动
  • 复杂度:

    • 时间复杂度:O(n + m), n为nums1的长度,m为nums2的长度

    • 空间复杂度:O(1)

      var findMedianSortedArrays = function(nums1, nums2) {
          let n1 = nums1.length
          let n2  = nums2.length
          let len = n1 + n2
      
          let prevalue = -1
          let curvalue = -1
          
          let p1 = 0
          let p2 = 0
          
      
          for(let i = 0; i <= Math.floor(len/2); i++) {
              prevalue = curvalue
              if(p1 < n1 &&(p2 >= n2 || nums1[p1] < nums2[p2])) {
                  curvalue = nums1[p1]
                  p1++
              }else {
                  curvalue = nums2[p2]
                  p2++
              }
          }
      
          return len % 2 === 0 ? (curvalue + prevalue) / 2 : curvalue
      };
      

2.最长回文字串(leetCode5)

  • 思路:

    • 出现的回文字串只有两种情况,一种是回文字串长度为奇数(如如aba,中心是b)
    • 另一种回文子串长度为偶数(如abba,中心是b,b)
    • 因此,我们可以选择对字符串进行遍历,并对取到的每一个值都假设其可能成为最后的中心进行判断
  • 注意点:

    // 注意此处m,n的值循环完后 是恰好不满足循环条件的时刻
    // 此时m到n的距离为n-m+1,但是mn两个边界不能取 所以应该取m+1到n-1的区间 长度是n-m-1

  • 复杂度

    • 时间复杂度:

    • 空间复杂度:

      var longestPalindrome = function(s) {
          if(s.length < 2) {return s;}
          let res = ''
      
          for(let i = 0; i < s.length; i++) {
          	// 回文子串长度是奇数
              helper(i, i)
               // 回文子串长度是偶数
              helper(i, i+1)
          }
      
      
          function helper(m, n) {
              while(m>=0 && n < s.length && s[m] === s[n]) {
                  m--
                  n++
              }
              if(n-m-1 > res.length) {
                  res = s.slice(m+1, n)
              }
          }
          
          return res
      };
      

3.正则表达式匹配(leetCode10)

​ 解法1:动态规划

  • 思路:常规匹配想法:两个指针i、j分别在s和p上移动,看两个字符是否匹配,如果最后两个指针都能移动到字符串的末尾,则匹配成功,否则匹配失败。

    1.状态:i、j指针的位置
    2.选择:模式串p[j]选择匹配几个字符
    3.dp函数的定义

    • 若dp(s, i, p, j) = true,则表示s[0…i]可以匹配p[0…j]
    • 若dp(s, i, p, j) = false,则表示s[0…i]无法匹配p[0…j]

    **情况分析:**根据dp函数,dp(s, 0, p, 0)就是所求结果,指针 i,j 从索引 0 开始移动。

    情况1:如果s[i]等于p[j] (p[j] 可以是点通配符)

    • 当p[j+1]为星号:当星号通配符匹配0次的情况,i不变,j加2; 当星号通配符匹配多次的情况,i加1,j不变,代表s[i]和p[j]匹配了,但星号匹配符可以匹配多次
    • 当p[j+1]不为星号:则i和j同时加1,说明只有s[i]和p[j]匹配

    情况2:如果s[i]不等于p[j]

    • 当p[j+1]为星号:如果星号匹配0次,则i不变,j加2
    • 当p[j+1]不为星号:则s和j一定不会匹配成功

    4.跳出动态规划两个基本情况:

    • j == p.length,即模式串p匹配完了,要看文本串s是否被匹配完,匹配完则说明匹配成功
    • i == s.length,当s匹配完了,不能直接根据p是否匹配完判断是否成功,而是要看p[j…]能够匹配空串,能则算完成匹配,如s = “a”, p = “abc”,当i走到s末尾的时候,j并没有走到p的末尾,但是p依然可以匹配s。
  • 复杂度:

    • 时间复杂度:
    • 空间复杂度:
    var isMatch = function(s, p) {
        const dp = (s, i, p, j) => {
            const m = s.length
            const n = p.length
    		// base case1 模式串p匹配完了,要看文本串s是否被匹配完,匹配完则说明匹配成功
            if(j === n) {
                return i === m
            }
    		
    		// base case2 当s匹配完了,不能直接根据p是否匹配完判断是否成功,
          // 而是要看p[j..]能够匹配空串,能则算完成匹配,如s = "a", p = "ab*c*",当i走到s末尾的时候,j并没有走到p的末尾,但是p依然可以匹配s
            if(i === m) {
            	// p剩下的个数不是成对出现,肯定不能匹配成功
                if((n - j) % 2 === 1) {
                    return false
                }
    			// 检查是否为 x*y*z* 这种形式,是则能匹配成功,不是则不能匹配成功
                for(j; j < n-1; j+=2) {
                    if(p[j+1] !== '*') {
                        return false
                    }
                }
    
                return true
            }
            
    		// 当s[i]等于p[j]时
            if(s[i] === p[j] || p[j] === '.') {
            	// 此时当p[j+1]为*
                if(p[j+1] === '*' && j < (n-1) ) {
                 	// 星号通配符匹配前面的字符0次 或 多次(s跳过一个字符,p不变继续匹配即可)
                    return dp(s, i, p, j+2) || dp(s, i+1, p, j)
                }else {
                	// p[j+1]不为星号,则i和j同时移动
                    return dp(s, i+1, p, j+1 )
                }
            }else {
            	// 当s[i]不等于p[j]时,当p[j+1]为*,此时*匹配0次,则i不变,j跳过两位(跳过当前值和*)
                if(j < p.length-1 && p[j+1] === '*') {
                    return dp(s, i, p, j+2)
                }else {
                	// s[i]不等于p[j],且p[j+1]不是星号,则一定不匹配
                    return false
                }
            }
        }
    	// i、j指针分别从0开始移动
        return dp(s, 0, p, 0)
    };
    

4.盛最多水的容器(leetCode11)

​ 解法:双指针法

  • 思路:设置前后两个指针,根据比较大小移动指针,更新面积最大值,直到两个指针重合

  • 复杂度:

    • 时间复杂度:O(n),n为数组长度
    • 空间复杂度:O(1)
    var maxArea = function(height) {
        let left = 0
        let right = height.length - 1
        let res = Math.min(height[left], height[right]) * (right - left)
    
        while(left < right) {
            res = Math.max(res, Math.min(height[left], height[right]) * (right - left))
            if(height[left] < height[right]) {
                left++
            }else {
                right--
            }
        }
    
        return res
    };
    

5.三数之和(leetCode15)

  • 思路:

    • 首先对数组进行排序,排序后固定一个数nums[i],再使用左右指针指向 nums[i]后面的两端,数字分别为 nums[L] 和 nums[R],计算三个数的和 sum 判断是否满足为 0,满足则添加进结果集
    • 如果 nums[i]大于 0,则三数之和必然无法等于 0,结束循环
    • 如果 nums[i]== nums[i-1],则说明该数字重复,会导致结果重复,所以应该跳过
    • 当 sum == 00时,nums[L] == nums[L+1] 则会导致结果重复,应该跳过,L++
    • 当 sum == 0 时,nums[R] == nums[R−1] 则会导致结果重复,应该跳过,R–
  • 复杂度:

    • 时间复杂度:O(n^2)
    • 空间复杂度:O(1)
    var threeSum = function(nums) {
        let res = []
        const len = nums.length
        if(nums === null || len < 3) {return []}
        nums.sort((a,b) => a - b)
    
        for(let i = 0; i < len; i++) {
        	//如果当前数字大于0,则三数之和一定大于0,所以结束循环
            if(nums[i] > 0) break
            //去重
            if(nums[i] === nums[i-1]) continue 
            let l = i + 1
            let r = len - 1
            while(l < r) {
                const sum = nums[i] + nums[l] + nums[r]
                if(sum === 0) {
                    res.push([nums[i], nums[l], nums[r]])
                    //去重
                    while(l < r && nums[l+1] === nums[l]) {
                        l++;
                    } 
                    //去重
                    while(l < r && nums[r-1] === nums[r]) {
                        r--;
                    }
                    l++
                    r--
                }else if(sum < 0) {
                    l++
                }else if(sum > 0) {
                    r--
                }
                
            }
        }
    
        return res
    
    };
    

6.电话号码的字母组合(leetCode17)

  • 思路:

    • 1.用map保存数字对应的字母
    • 2.从第一个数字开始遍历,取一个字母,然后从第二个数字,取一个字母,第三个数字,取一个字母…
    • 3.数字遍历完了,将拼接好的字符串str加入结果数组res
    • 4.回溯,修改最后一个数字对应的字母
    • 5.重复2-4过程
  • 复杂度:

    • 时间复杂度:
    • 空间复杂度:
    var letterCombinations = function(digits) {
    	//输入为0,直接返回空数组
        if (digits.length === 0) return []
        let res = []
        let map = new Map([
            ['0', ''],
            ['1', ''],
            ['2', 'abc'],
            ['3', 'def'],
            ['4', 'ghl'],
            ['5', 'jkl'],
            ['6', 'mno'],
            ['7', 'pqrs'],
            ['8', 'tuv'],
            ['9', 'wxyz']
        ])
    
        
    
        const dfs = (str, digit) => {
        	//结束递归条件,输入子串为空时,将拼接好的字符串加入数组
           if(digit.length === 0) {
               res.push(str)
           }else {
               const numStr = map.get(digit[0])
               for(let i = 0; i < numStr.length; i++) {
               		//拼接字符串
                   str += numStr[i]
                   //递归调用,通过slice将字符串向后移动
                   dfs(str, digit.slice(1))
                   //将str进行回溯
                   str = str.slice(0, -1)
               }
    
           }
        }
    
        dfs("", digits)
        return res
    };
    

7.删除链表的倒数第N个节点(leetCode19)

​ 解法1:暴力破解

  • 思路:分析题目,我们很容易的可以得到暴力解法,先遍历一遍链表得到链表总长度, 然后用总长度减去 n,我们很快就得到了要删除目标节点的下标,然后就是移动指针到目标节点的前一个节点直接删除即可。

  • 复杂度:

    • 时间复杂度:O(n)
    • 空间复杂度:O(1)
    var removeNthFromEnd = function(head, n) {
    
        let p1 = head
        let p2 = head
        let len = 0
        //获取链表长度
        while(p1) {
            len += 1
            p1 = p1.next
        }
        //当长度等于要求的倒数位置时,直接返回head的next
        if(len === n) {
            return head.next
        }
        
        //删除位置的前一个位置
        let target = len - n - 1
    
        while(target > 0) {
            target--
            p2 = p2.next
        }
    
        if(p2.next) {
            p2.next = p2.next.next
        }else {
            p2.next = null
        }
        return head
    };
    

解法2:快慢指针

  • 思路:要满足进阶要求, 本题目还是采用快慢指针来解决。

    • 1.我们可以设置一个快指针和一个慢指针同时指向头节点,让快指针先走n个节点。
    • 2.然后让慢指针和快指针同时开始,直到快指针遍历结束。
    • 3.让慢指针的next指针指向慢指针的next所指向的next。
    • 4.返回头节点
  • 复杂度:

    • 时间复杂度:O(n)
    • 空间复杂度:O(1)
    var removeNthFromEnd = function(head, n) {
        let fast = head, slow = head
        for(let i = 0; i < n; i+=1) {
            fast = fast.next
        }
        
        //当链表长度等于倒数的位置时
        if(!fast) {return head.next}
    	
    	//慢指针此时恰好指向待删除节点的前一个节点
        while(fast.next !== null) {
            fast = fast.next
            slow = slow.next
        }
        
        if(slow.next) {
            slow.next = slow.next.next
        }else {
            slow.next = null
        }
        return head
    };
    

8.括号生成(leetCode22)

  • 思路:

    回溯,死抓三个要点。
    1.选择
    在这里,每次最多两个选择,选左括号或右括号,“选择”会展开出一棵解的空间树。
    用 DFS 遍历这棵树,找出所有的解,这个过程叫回溯。
    2.约束条件
    即,什么情况下可以选左括号,什么情况下可以选右括号。
    利用约束做“剪枝”,即,去掉不会产生解的选项,即,剪去不会通往合法解的分支。
    比如(),现在左右括号各剩一个,再选)就成了()),这是错的选择,不能让它成为选项(不落入递归):

    if (right > left) { // 右括号剩的比较多,才能选右括号
        dfs(str + ')', left, right - 1);
    }
    

    3.目标
    构建出一个用尽 n 对括号的合法括号串。
    意味着,当构建的长度达到 2n,就可以结束递归(不用继续选了)。
    4.充分剪枝的好处
    经过充分的剪枝,所有不会通往合法解的选项,都被剪掉,只要往下递归,就都通向合法解。
    即只要递归到:当构建的字符串的长度为 2
    n 时,一个合法解就生成了,放心地加入解集。

  • 复杂度:

    • 时间复杂度:
    • 空间复杂度:O(1)
    var generateParenthesis = function(n) {
        const res = []
        //左右括号所剩的数量,str是当前构建的字符串
        const dfs = (lR, rR, str) => {
    		
    		//递归结束条件,将结果放入数组
            if(str.length === 2*n) {
                res.push(str)
                return
            }
            
    		// 只要左括号有剩,就可以选它,然后继续做选择(递归)
            if(lR > 0) {
                dfs(lR-1, rR, str+"(")
            }
    		// 右括号比左括号剩的多,才能选右括号
            if(rR > lR) {
                dfs(lR, rR-1, str + ")")
            }
    
        }
    	// 递归的入口,剩余数量都是n,初始字符串是空串
        dfs(n, n, "")
        return res
    };
    

9.下一个排列(leetCode31)

  • 思路:

    • 如何变大:从低位挑一个大一点的数,交换前面一个小一点的数。

    • 变大的幅度要尽量小。

    • 比如:

      像 [3,2,1] 这样递减的,没有下一个排列,已经稳定了,没法变大。
      像 [1,5,2,4,3,2] 这种,怎么稍微变大?

      从低位挑一个大一点的数,尽量低位,换掉它前面一个小一点的数。

      即从右往左,寻找第一个比右邻居小的数,把它换到后面去

      “第一个”意味着会尽量在低位,“比右邻居小”意味着它是从右往左的第一个波谷

      比如,1 5 (2) 4 3 2,中间这个 2。

      接着依然从右往左,寻找第一个比这个 2 微大的数。15 (2) 4 (3) 2,交换后变成 15 (3) 4 (2) 2。

      还没结束!变大的幅度可以再小一点,仟位微变大了,后三位可以再小一点。

      后三位肯定是递减的,翻转,变成[1,5,3,2,2,4],即为所求。

  • 复杂度:

    • 时间复杂度:

    • 空间复杂度:

      function nextPermutation(nums) {
        let i = nums.length - 2;                   // 向左遍历,i从倒数第二开始是为了nums[i+1]要存在
        while (i >= 0 && nums[i] >= nums[i + 1]) { // 寻找第一个小于右邻居的数
            i--;
        }
        if (i >= 0) {                             // 这个数在数组中存在,从它身后挑一个数,和它换
            let j = nums.length - 1; 
            let temp = 0               // 从最后一项,向左遍历
            while (j >= 0 && nums[j] <= nums[i]) {  // 寻找第一个大于 nums[i] 的数
                j--;
            }
            // 两数交换,实现变大
            temp = nums[i]
            nums[i] = nums[j]
            nums[j] = temp
        }
        // 如果 i = -1,说明是递减排列,如 3 2 1,没有下一排列,直接翻转为最小排列:1 2 3
        let l = i + 1;           
        let r = nums.length - 1;
        while (l < r) {                            // i 右边的数进行翻转,使得变大的幅度小一些
            let temp = 0
            temp = nums[l]
            nums[l] = nums[r]
            nums[r] = temp
            l++;
            r--;
        }
        return nums
      }
      

10.最长有效括号(leetCode32)

​ 解法1:

  • 思路:

    • 在栈中预置 -1 作为“参照物”,并改变计算方式:当前索引 - 出栈后新的栈顶索引。
    • 当遍历到索引 5 的右括号,此时栈顶为 2,出栈,栈顶变为 -1,有效长度为 5 - (-1)。如果照之前那样,5 找不到 -1 减。
    • 现在有个问题:当遍历到索引 6 的右括号,它不是需要入栈的左括号,又没有左括号可匹配,怎么处理它?
    • 它后面也可能出现这么一段有效长度,它要成为 -1 那样的“参照物”。它之前出现的有效长度都已计算,-1 的使命已经完成了,要被替代。
    • 所以我们照常让 -1 出栈。重点是,此时栈空了,让索引 6 入栈取代它。
  • 总结:两种索引会入栈

    • 等待被匹配的左括号索引。
    • 充当「参照物」的右括号索引。因为:当左括号匹配光时,栈需要留一个垫底的参照物,用于计算一段连续的有效长度。
  • 复杂度:

    • 时间复杂度:O(n)
    • 空间复杂度:O(n)
var longestValidParentheses = function(s) {
	//在栈中定义参考点
    const stack = [-1]
    let MaxLen = 0
    
    for(let i = 0; i < s.length; i++) {
        const n = s[i]
        //左括号入栈
        if(n === '(') {
            stack.push(i)
        }else {
        	//右括号出栈
            stack.pop()
            //栈非空时,计算当前连续有效长度
            if(stack.length) {
                const curMaxLen = i - stack[stack.length - 1]
                //更新连续有效长度
                MaxLen = Math.max(MaxLen, curMaxLen)
            }else { //栈空时,在栈中放入新的参考点
                stack.push(i)
            }
        }
    }

    return MaxLen
};

11.探索旋转排序数组(leetCode33)

​ 解法1:顺序排序法(不符合题意)

  • 思路:最简单的思路是直接遍历数组,然后返回下标。不过题目要求希望能让时间复杂度为O(logN)

  • 复杂度:

    • 时间复杂度:O(n)
    • 空间复杂度:O(1)
var search = function(nums, target) {
    for(let i = 0; i < nums.length; i++) {
        const n = nums[i]
        if(n === target) {
            return i
        }
    }

    return -1
};

解法2:二分法

  • 思路:直接使用二分排序法,需要用到按序排列好的数组上。这便是该题的难点。

    首先要知道,我们随便选择一个点,将数组分为前后两部分,其中一部分一定是有序的。

  • 具体步骤:

    我们可以先找出mid,然后根据mid来判断,mid是在有序的部分还是无序的部分
    假如mid小于start,则mid一定在右边有序部分。
    假如mid大于等于start, 则mid一定在左边有序部分。(注意等号的考虑)

    然后我们继续判断target在哪一部分, 我们就可以舍弃另一部分了
    我们只需要比较target和有序部分的边界关系就行了。 比如mid在右侧有序部分,即[mid, end]
    那么我们只需要判断 target >= mid && target <= end 就能知道target在右侧有序部分,我们就
    可以舍弃左边部分了(start = mid + 1), 反之亦然。

  • 复杂度:

    • 时间复杂度:O(logN)
  • 空间复杂度:O(1)

    var search = function(nums, target) {
        let start = 0
        let end = nums.length - 1
        while(start <= end) {
        		//效率高的多
        	  const mid = start + ((end-start)>>1)	
            //const mid = Math.floor((start + end) / 2)
            //跳出循环条件
            if(target === nums[mid]) {return mid}
    		
    		//target 在 [start, mid] 之间
            if(nums[mid] >= nums[start]) {
                if(target >= nums[start] && target < nums[mid]) {
                    end = mid - 1
                }else {
                    start = mid + 1
                }
            }else {
            // target 在 [mid, end] 之间
                if(target > nums[mid] && target <= nums[end]) {
                    start = mid + 1
                }else {
                    end = mid - 1
                }    
            }
        }
        return -1
    };
    

12.在排序数组中查找元素的第一个和最后一个位置(leetCode34)

​ 解法1:简单的for循环直接解决

  • 思路:但是不满足时间复杂度O(logN)的要求。

  • 复杂度:

    • 时间复杂度:O(n)

    • 空间复杂度:O(1)

      var searchRange = function(nums, target) {
          let res = []
          for(let i = 0; i < nums.length; i++) {
              const n = nums[i]
              if(n === target) {
                  if(res.length === 2) {
                      res.pop()
                  }
                  res.push(i)
              }
          }
          if(res.length === 0) {
              res = [-1, -1]
          }
          if(res.length === 1) {
              res.push(res[0])
          }
          return res
      };
      

​ 解法2:二分查找

  • 思路:

    • 定义一个函数用来找出目标值的左边界和右边界。找左边界时end一直向左移动,直到mid和mid-1的值不相等;找右边界时,start一直向右移动,直到mid和mid+1的值不相等
    • 返回左右边界的index
  • 复杂度:

    • 时间复杂度:O(logn)

    • 空间复杂度:O(1)

      function find(isFindFirst, nums, target) {
          let start = 0
          let end = nums.length - 1
          while(start <= end) {
              const mid = start + ((end - start)>>1)
              if(target < nums[mid]) {
                  end = mid - 1 
              }else if(target > nums[mid]) {
                  start = mid + 1
              }else{
              	//查找左边界
                  if(isFindFirst) {
                  // 如果mid不是第一个元素并且前面一个相邻的元素也跟mid相等,则搜索区间向左缩小
                      if(mid > 0 && nums[mid] === nums[mid - 1]) {
                          end = mid - 1
                      }else {
                          return mid
                      }
                  }else {
                  // 如果mid不是最后一个元素并且后面一个相邻的元素也跟mid相等,则搜索区间向右缩小
                      if(mid < nums.length && nums[mid] === nums[mid + 1]) {
                          start = mid + 1
                      }else {
                          return mid
                      }
                  }
              }
          }
          return -1
      }
      
      
      var searchRange = function(nums, target) {
      	// 如果为空或者是空数组的情况
          if(nums.length === 0) {return [-1, -1]}
          // 寻找左边界
          const left = find(true, nums, target)
          // 寻找右边界
          const right = find(false, nums, target)
          return [left, right]
         
      };
      

13.组合总和(leetCode39)

​ 解法1:回溯算法

  • 题意:

    • 给你一个数组,里面都是不带重复的正数,再给定 target,求出所有和为 target 的组合。
    • 元素可以重复使用,但组合不能重复,比如 [2, 2, 3] 与 [2, 3, 2] 是重复的。
  • 思路:

    • 定义一个backTrack方法,传入三个参数:索引起点、当前集合、当前求和
    • 对数组进行遍历,在当前路径中添加元素,递归,回溯
    • 在最前面加上判定递归结束条件,大于target结束递归,等于target加入解集
  • 复杂度:

    • 时间复杂度:O(n!)

    • 空间复杂度:O(n),n为数组长度

      var combinationSum = function(candidates, target) {
          const res = []
          const backTrack = (start, path, sum) => {
          	//加入这层逻辑是防止栈溢出,若sum>target则直接结束递归
              if(sum >= target) {
                  if(sum === target) {
                  	// temp的拷贝 加入解集
                      res.push(path.slice())
                  }
                  // 结束当前递归
                  return
              }
      
              for(let i = start; i < candidates.length; i++) {
                  const n = candidates[i]
                  path.push(n)
                  backTrack(i, path, sum+n) // 基于此继续选择,传i,下一次就不会选到i左边的数
                  path.pop() //回溯,撤销选择,回到选择candidates[i]之前的状态,继续尝试选同层右边的数
              }
          }
          backTrack(0, [], 0)// 最开始可选的数是从第0项开始的,传入一个空集合,sum也为0
          return res
      };  
      

14.接雨水(leetCode42)

​ 解法1:动态规划

  • 思路:

    • 对于下标 i,下雨后水能到达的最大高度等于下标 i 两边的最大高度的最小值,下标 i处能接的雨水量等于下标 i处的水能到达的最大高度减去height[i]。

  • 复杂度:

    • 时间复杂度:O(n),n为数组长度。

    • 空间复杂度:O(n)

      var trap = function(height) {
          const len = height.length
          if(len === 0) {return 0;}
          let res = 0
      	//定义全0数组
          const leftMax = new Array(len).fill(0)
          //设置最左侧的默认值
          leftMax[0] = height[0]
          for(let i = 1; i < len; i++) {
              leftMax[i] = Math.max(leftMax[i-1], height[i])
          }
      
          const rightMax = new Array(len).fill(0)
          //设置最右侧的默认值
          rightMax[len-1] = height[len-1]
          for(let i = len-2; i >= 0; i--) {
              rightMax[i] = Math.max(rightMax[i+1], height[i])
          }
      	
      	//累计中间每一格的积水量
          for(let i = 1; i < len-1; i++) {
              res += Math.min(leftMax[i], rightMax[i]) - height[i]
          }
          return res
      };
      

15.旋转图像(leetCode48)

​ 解法1:使用辅助矩阵(该方法不符合在原矩阵上修改的思路)

  • 思路:

    • 由于矩阵中的行列从 0开始计数,因此对于矩阵中的元素 matrix(row)(col),在旋转后,它的新位置为 matrix(col)(n-row-1)

    • 这样以来,我们使用一个与matrix 大小相同的辅助数组 new matrix ,临时存储旋转后的结果。我们遍历 matrix 中的每一个元素,根据上述规则将该元素存放到 new matrix中对应的位置。在遍历完成之后,再将new matrix 中的结果复制到原数组中即可。

  • 复杂度:

    • 时间复杂度:O(n^2),n为matrix的边长

    • 空间复杂度:O(n^2)

      var rotate = function(matrix) {
          const len = matrix.length
          const matrix_new = new Array(len).fill(0).map(() =>  new Array(len).fill(0))
          for(let i = 0; i < len; i++) {
              for(let j = 0; j < len; j++) {
                  matrix_new[j][len - i - 1] = matrix[i][j]
              }
          }
      
          for(let i = 0; i < len; i++) {
              for(let j = 0; j < len; j++) {
                  matrix[i][j] = matrix_new[i][j]
              }
          }
          
      };
      

解法2:原地旋转

  • 思路:

    • 关键在于添加一个临时变量temp,交换时沿用解法1的规律
    • 而且一次性会替换四个位置,这四个位置形成一个循环
      • matrix(row)(col)
      • matrix(col)(n-row-1)
      • matrix(n-row-1)(n-col-1)
      • matrix(n-col-1)(row)
  • 复杂度:

    • 时间复杂度:O(n^2),n为矩阵边长

    • 空间复杂度:O(1)

      var rotate = function(matrix) {
          const n = matrix.length
          for(let i = 0; i < Math.floor(n/2); i++) {
              for(let j = 0; j < Math.floor((n+1)/2); j++) {
                  const temp = matrix[i][j]
                  matrix[i][j] = matrix[n-j-1][i]
                  matrix[n-j-1][i] = matrix[n-i-1][n-j-1]
                  matrix[n-i-1][n-j-1] = matrix[j][n-i-1]
                  matrix[j][n-i-1] = temp
              }
          }
      };
      

16.字母异位词分组(leetCode49)

  • 思路:

    • 利用map字典解决该题。key为字符串的有序排列,val为遍历的与key字母相同的字符串数组

    • 最后返回map的值,使用map原型上的values()方法和展开运算符返回结果数组

  • 复杂度:

    • 时间复杂度:O(n),n为数组长度。
    • 空间复杂度:O(n)
    var groupAnagrams = function(strs) {
        let map = new Map()
        for(let i = 0; i < strs.length; i++) {
            let str = strs[i].split('').sort().join()
            if(map.has(str)) {
               let temp = map.get(str)
               temp.push(strs[i])
               map.set(str, temp)
            }else {
                map.set(str, [strs[i]])
            }
        }
        return [...map.values()]
    };
    

17.最大子序和(leetCode53)

​ 解法:动态规划

  • 思路:

    • 在每一个扫描点计算以该点数值为结束点的子数列的最大和(正数和)。
    • 该子数列由两部分组成:以前一个位置为结束点的最大子数列、该位置的数值。
    • 因为该算法用到了“最佳子结构”(以每个位置为终点的最大子数列都是基于其前一位置的最大子数列计算得出), 该算法可看成动态规划的一个例子。
    • 状态转移方程:sum[i] = max{sum[i-1]+a[i],a[i]}
    • 其中(sum[i]记录以a[i]为子序列末端的最大序子列连续和)
  • 复杂度:

    • 时间复杂度:O(n),n为数组长度。

    • 空间复杂度:O(1)

      var maxSubArray = function(nums) {
          let res = nums[0]
          let sum = 0
          for(let num of nums) {
          	//当之前的累加数值sum不能对当前num起到加时,则不如重新从当前num处开始加
          	//可以直接改为if(sum > 0)
              if((sum + num) > num) {
                  sum = sum + num
              }else { 
                  sum = num
              }
              res = Math.max(res, sum)
          }
          return res
      };
      

18.跳跃游戏(leetCode55)

解法:贪心算法

  • 思路:

    这道题目关键点在于:不用拘泥于每次究竟跳跳几步,而是看覆盖范围,覆盖范围内一定是可以跳过来的,不用管是怎么跳的。

  • 复杂度:

    • 时间复杂度:

    • 空间复杂度:O(1)

      var canJump = function(nums) {
          if(nums.length === 1) {return true}
          let cover = 0
          for(let i = 0; i <= cover; i++) {
              cover = Math.max(cover, i + nums[i])
              if(cover >= nums.length - 1) {
                  return true
              }
          }
          return false
      };
      

19.合并区间(leetCode56)

解法:贪心算法

  • 思路:

    按照左边界排序,排序之后局部最优:每次合并都取最大的右边界,这样就可以合并更多的区间了。整体最优:合并所有重叠的区间

  • 步骤:

    • 首先,按照左边界对区间进行排序
    • 遍历区间数组,比较当前区间的左边界和前一个区间的右边界
      • 若当前左边界大于前一个区间的右边界,则前一个区间放入解集中
      • 反之,则将前一个区间的右边界换成当前区间的右边界
    • 返回解集
  • 复杂度:

    • 时间复杂度:O(n),n为数组intervals的长度

    • 空间复杂度:O(m),m为res的长度

      var merge = function(intervals) {
          let res = []
          intervals.sort((a, b) => a[0] - b[0])
          let pre = intervals[0]
          for(let i = 1; i < intervals.length; i++) {
              const cur = intervals[i]
              if(cur[0] > pre[1]) {
                  res.push(pre)
                  pre = cur
              }else {
                  pre[1] = Math.max(pre[1], cur[1])
              }
          }
          res.push(pre)
          return res
      };
      

20.不同路径(leetCode62)

解法:动态规划

  • 思路:机器人从(0 , 0) 位置出发,到(m - 1, n - 1)终点。按照动规五部曲来分析:

    1.确定dp数组(dp table)以及下标的含义

    dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。
    

    2.确定递推公式

    想要求dp[i][j],只能有两个方向来推导出来,即dp[i - 1][j] 和 dp[i][j - 1]。
    
    此时在回顾一下 dp[i - 1][j] 表示啥,是从(0, 0)的位置到(i - 1, j)有几条路径,dp[i][j - 1]同理。
    
    那么很自然,dp[i][j] = dp[i - 1][j] + dp[i][j - 1],因为dp[i][j]只有这两个方向过来。
    

    3.dp数组的初始化

    如何初始化呢,首先dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理。
    

    4.确定遍历顺序

    这里要看一下递归公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1],dp[i][j]都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了。
    这样就可以保证推导dp[i][j]的时候,dp[i - 1][j] 和 dp[i][j - 1]一定是有数值的。
    

    5.举例推导dp

  • 复杂度:

    • 时间复杂度:O(m*n)

    • 空间复杂度:O(m*n)

      var uniquePaths = function(m, n) {
      	//创建m*n的空矩阵
          const dp = new Array(m).fill().map(() => new Array(n))
          //初始化第一列
          for(let i = 0; i < m; i++) {
              dp[i][0] = 1
          }
          //初始化第一行
           for(let i = 0; i < n; i++) {
              dp[0][i] = 1
          }
          //输入递归公式
         for(let i = 1; i < m; i++) {
             for(let j = 1; j < n; j++) {
                 dp[i][j] = dp[i-1][j] + dp[i][j-1]
             }
         }
          
          return dp[m-1][n-1]
      };
      

21.最小路径和(leetCode64)

解法:动态规划

  • 思路:

    1、DP方程

    当前项最小路径和 = 当前项值 + 上项或左项中的最小值
    grid[i][j] += Math.min( grid[i - 1][j], grid[i][j - 1] )
    

    2、边界处理

    grid的第一行与第一列 分别没有上项与左项 故单独处理计算起项最小路径和
    计算第一行
    
  • 复杂度:

    • 时间复杂度:O(m*n)

    • 空间复杂度:O(m*n)

      var minPathSum = function(grid) {
          const m = grid.length
          const n = grid[0].length 
          
          //初始化第一列,因为只能往下走
          for(let i = 1; i < m; i++) {
              grid[i][0] += grid[i-1][0]  
          }
      	
      	//初始化第一行,因为只能往右走
          for(let i = 1; i < n; i++) {
              grid[0][i] += grid[0][i-1]  
          }
      
      	//关键点:到达某一位置的值 = 当前位置的值 + 其上或左位置的更小值
          for(let i = 1; i < m; i++) {
              for(let j = 1; j < n; j++) {
                  grid[i][j] += Math.min(grid[i-1][j], grid[i][j-1])
              }
          }
      
          return grid[m-1][n-1]
      };
      

22.编辑距离(leetCode72)

解法:动态规划

  • 思路:

    • 步骤一、定义数组元素的含义

      由于我们的目的是求将 word1 转换成 word2 所使用的最少操作数。那么我们可以定义 dp[i][j]的含义为:当字符串 word1 的长度为 i,字符串 word2 的长度为 j 时,将 word1 转化为 word2 所使用的最少操作次数为 dp[i][j]
      

      步骤二:找出dp数组元素间的关系式

      接下来我们就要找 dp[i][j] 各元素之间的关系了,大部分情况下,dp[i][j] 和 dp[i-1][j]、dp[i][j-1]、dp[i-1][j-1] 肯定存在某种关系。因为我们的目标就是,从规模小的,通过一系列操作操作,推导出规模大的。对于这道题,我们可以对 word1 进行三种操作入手:
      
      插入一个字符
      删除一个字符
      替换一个字符
      由于我们是要让操作的次数最小,所以我们要寻找最佳操作。那么有如下关系式:
      
      如果我们 word1[i] 与 word2[j] 相等,这个时候不需要进行任何操作,显然有 dp[i][j] = dp[i-1][j-1]
      
      如果我们 word1[i] 与 word2[j] 不相等,这个时候我们就必须进行调整,而调整的操作有 3 种,我们要选择一种。三种操作对应的关系试如下(注意字符串与字符的区别):
      
      如果在字符串 word1 末尾插入一个与 word2[j] 相等的字符,则有 dp[i][j] = dp[i][j-1] + 1
      如果把字符 word1[i] 删除,则有 dp[i][j] = dp[i-1][j] + 1
      如果把字符 word1[i] 替换成与 word2[j] 相等,则有 dp[i][j] = dp[i-1][j-1] + 1
      那么我们应该选择哪一种操作,可以使得 dp[i][j] 的值最小?显然有:
      
      dp[i][j] = min(dp[i-1][j-1],dp[i][j-1],dp[[i-1][j]]) + 1
      

      步骤三、找出初始值 base case

      显然,当 dp[i][j] 中,如果 i 或者 j 有一个为 0,那么还能使用关系式吗?答是不能的,因为这个时候把 i - 1 或者 j - 1,就变成负数了,数组就会出问题了,所以我们的初始值是计算出所有的 dp[0…word1.length][0] 和所有的 dp[0][0…word2.length]。这个还是非常容易计算的,因为当有一个字符串的长度为 0 时,转化为另外一个字符串,那就只能一直进行插入或者删除操作了
      
  • 步骤:

    • 首先,创建一个word1长度*word2长度的矩阵,并用0进行填充
    • 第二,遍历第一行和第一列的所以位置使其初始值为1。(0,0)的位置不变,保持为0。
    • 最后写出递推公式代码,使用两个循环矩阵
    • 返回结果
  • 复杂度:

    • 时间复杂度:

    • 空间复杂度:

      var minDistance = function(word1, word2) {
          const m = word1.length, n = word2.length
      
          let dp = new Array(m+1).fill(0).map(() => new Array(n+1).fill(0))
      
          for(let i = 1; i <= m; i++) {
              dp[i][0] = dp[i-1][0] + 1
          }
      
          for(let j = 1; j <= n; j++) {
              dp[0][j] = dp[0][j-1] + 1
          }
      
          for(let i = 1; i <= m; i++) {
              for(let j = 1; j <= n; j++) {
                  if(word1[i-1] == word2[j-1]) {
                     dp[i][j] = dp[i-1][j-1] 
                  }else {
                      dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
                  }
              }
          }
      
          return dp[m][n]
      };
      

23.颜色分类(leetCode75)

解法:双指针法

  • 思路:

    • 1.定义双指针p0、p2,分别指向数组头尾
    • 2.从左向右遍历nums
    • 3.遇到2,交换p2指针的值,p2左移,因为交换过来的,也可能是2,所以需要while继续判断
    • 4.遇到0,交换p1指针的值,p1右移,因为左边是一路遍历过来的,所以不需要修改p1指针
  • 复杂度:

    • 时间复杂度:O(n),其中n是数组长度

    • 空间复杂度:O(1)

      var sortColors = function(nums) {
          let p1 = 0
          let p2 = nums.length - 1
      
          for(let i = 0; i <= p2; i++) {
              while(nums[i] === 2 && i < p2) {
                  let temp = nums[i]
                  nums[i] = nums[p2]
                  nums[p2] = temp
                  p2--
              }
              if(nums[i] === 0) {
                  let temp = nums[i]
                  nums[i] = nums[p1]
                  nums[p1] = temp
                  p1++
              }
          }
      };
      

24.单词搜索(leetCode79)

解法:回溯算法

  • 思路:

    • 以"SEE"为例,首先要选起点:遍历矩阵,找到起点S。
    • 起点可能不止一个,基于其中一个S,看看能否找出剩下的"EE"路径。
    • 下一个字符E有四个可选点:当前点的上、下、左、右。
    • 逐个尝试每一种选择。基于当前选择,为下一个字符选点,又有四种选择。
    • 每到一个点做的事情是一样的。DFS 往下选点,构建路径。
    • 当发现某个选择不对,不用继续选下去了,结束当前递归,考察别的选择。
  • 关键点:采用回溯的原因是,有的选点是错误的,选它就构建不出目标路径,不能继续选。要撤销这个选择,去尝试别的选择。

    // canFindRest 表示:基于当前选择的点[row,col],能否找到剩余字符的路径。
    const canFindRest =
          canFind(row + 1, col, i + 1) ||
          canFind(row - 1, col, i + 1) ||
          canFind(row, col + 1, i + 1) ||
          canFind(row, col - 1, i + 1)
    
    如果第一个递归调用返回 false,就会执行||后的下一个递归调用
    
    这里暗含回溯:当前处在[row,col],选择[row+1,col]继续递归,返回false的话,会撤销[row+1,col]这个选择,回到[row,col],继续选择[row-1,col]递归。
    只要其中有一个递归调用返回 true,||后的递归就不会执行,即找到解就终止搜索,利用||的短路效应,把枝剪了。
    
    如果求出 canFindRest 为 false,说明当前点是错的选择,不仅当前递归要返回false,还要在used矩阵中把当前点恢复为未访问,让它后续能正常被访问。
    
    因为,基于当前路径,选当前点是不对的,但基于别的路径,走到这选它,有可能是对的。
    

    ​ 什么时候返回true:

    在递归中,我们设置了所有返回 false 的情况。
    
    当指针越界,此时已经考察完单词字符,意味着,在该递归分支中,为一个个字符选点,始终没有返回过 false,这些字符都选到对的点。所以指针越界就可以返回 true。
    
  • 步骤:

    • 1.创建一个大小和board相同的二维矩阵used,用来存放bool值

    • 2.使用for循环对board上的所有元素进行遍历,若满足元素和word的首字母相等且递归函数返回true,则直接返回true。若遍历结束了都没有返回true,则返回false。

    • 3.下面定义递归的箭头函数canFind,它是用来对路径上的字母进行判断的,传入行、列、i(表示访问到第几个字符)三个参数

      • 首先将二维矩阵的对应位置设置为true,记录当前点被访问过
      • 定义canFindRest变量,利用逻辑或对该元素的上下左右调用canFind函数,若canFindRest为true,则直接返回true
      • 若canFindRest为false,则首先将二维矩阵对应位置进行回溯,改为false,撤销访问状态。
      • 递归结束条件:
        • 当i === word.length时,均返回true(此时i越界)
        • 当行、列小于0或者大于等于行列长度时,返回false
        • 当当前点已经访问国或不等于目标节点时,返回false
  • 复杂度:

    • 时间复杂度:

    • 空间复杂度:O(m*n)

      const exist = (board, word) => {
          const m = board.length;
          const n = board[0].length;
          const used = new Array(m);    // 二维矩阵used,存放bool值
          for (let i = 0; i < m; i++) {
              used[i] = new Array(n);
          }
          // canFind 判断当前点是否是目标路径上的点
          const canFind = (row, col, i) => { // row col 当前点的坐标,i当前考察的word字符索引
              if (i == word.length) {        // 递归的出口 i越界了就返回true
                  return true;
              }
              if (row < 0 || row >= m || col < 0 || col >= n) { // 当前点越界 返回false
                  return false;
              }
              if (used[row][col] || board[row][col] != word[i]) { // 当前点已经访问过,或,非目标点
                  return false;
              }
              // 排除掉所有false的情况,当前点暂时没毛病,可以继续递归考察
              used[row][col] = true;  // 记录一下当前点被访问了
              // canFindRest:基于当前选择的点[row,col],能否找到剩余字符的路径。
              const canFindRest = canFind(row + 1, col, i + 1) || canFind(row - 1, col, i + 1) ||
                  canFind(row, col + 1, i + 1) || canFind(row, col - 1, i + 1); 
      
              if (canFindRest) { // 基于当前点[row,col],可以为剩下的字符找到路径
                  return true;    
              }
              used[row][col] = false; // 不能为剩下字符找到路径,返回false,撤销当前点的访问状态
              return false;
          };
      
          for (let i = 0; i < m; i++) { // 遍历找起点,作为递归入口
            for (let j = 0; j < n; j++) {
              if (board[i][j] == word[0] && canFind(i, j, 0)) { // 找到起点且递归结果为真,找到目标路径
                return true; 
              }
            }
          }
          return false; // 怎么样都没有返回true,则返回false
      };
      
      

25.柱状图中最大的矩形(leetCode84)

解法:利用栈解题

  • 思路:

    • 我们再思考另一个问题:让 heights 数组的索引 0 入栈,依据是什么?

      • 入栈的依据是当前 bar 比栈顶 bar 高。问题是现在没有栈顶可以比较
      • 我们可以设立一个高为 0 的虚拟 bar ,放在 heights 的 0 位置,它不影响结果,却可以让第一条 bar 的索引,名正言顺地入栈
      • 同时解决了第一个问题:不会有别的 bar 比它更矮了,因此该 bar 永不出栈
    • 最后一个 bar 需要解救

      • 最后一个 bar 不会遇到新 bar 了,如果它在栈中,那就没有机会出栈了,意味着,没有机会计算栈中的长方形面积了
      • 我们设立一个虚拟的高为 0 的 bar,放在 heights 数组的最右,栈中的 bar 都比它高,能一一出栈,得到解救
  • 步骤:

    • 首先定义一个空栈和要返回的结果变量maxArea,在原数组heights的前后各加一个0
    • 第二,使用for循环遍历heights数组
    • 第三,使用while循环,若满足当前bar比栈顶bar矮,则将栈顶元素出栈并赋值给变量stackTopIndex,这时计算maxArea的最大值,高为出栈索引对应的元素,底为当前bar的索引i - 新的栈顶索引 - i,与全局的最大值比较。
    • 最后,返回maxArea
  • 复杂度:

    • 时间复杂度:

    • 空间复杂度:O(n)

      const largestRectangleArea = (heights) => {
          let maxArea = 0
          const stack = []
          heights = [0, ...heights, 0]         
          for (let i = 0; i < heights.length; i++) { 
            while (heights[i] < heights[stack[stack.length - 1]]) { // 当前bar比栈顶bar矮
              const stackTopIndex = stack.pop() // 栈顶元素出栈,并保存栈顶bar的索引
              maxArea = Math.max(               // 计算面积,并挑战最大面积
                maxArea,                        // 计算出栈的bar形成的长方形面积
                heights[stackTopIndex] * (i - stack[stack.length - 1] - 1)
              )
            }
            stack.push(i)                       // 当前bar比栈顶bar高了,入栈
          }
          return maxArea
        }
        
      

26.最大矩形(leetCode85)

解法:动态规划

  • 思路:

    • 用一个dp二维数组记录当前位置行的连续1的数量,只记录其本身和其左侧部分

    • 暴力遍历所有点,以当前点为矩形右下角,求出当前点矩形最大面积

    • 返回答案即可

      这种解法本质是对暴力解法的一个优化,枚举每个点时不必再求出其左侧连续1的数量,访问dp数组即可,然后求面积的话也要直到高对不对,所以枚举到其中一个点时,就要往上求高,并实时更新宽(左侧连续1数量)还有面积

  • 步骤:

    • 首先定义一个大小和matrix相同的矩阵dp
    • 第二,遍历整个矩阵。若matrix对应位置为1,则dp矩阵为1+上一列的值。否则为0
    • 第三,从最后一行向上遍历,定义宽为dp元素,高为0,在令k=行。
    • 第四,写一个while循环,循环条件为k>0且matrix对应位置为1,此时重新赋值width为该元素及上一行中的更小值
    • 第五,令结果为之前结果与现在结果中的更大值。现在结果为width*(++height)。
  • 复杂度:

    • 时间复杂度:O(mn^2)

    • 空间复杂度:O(mn)

      var maximalRectangle = function(matrix) {
          const m = matrix.length
          if(m === 0) {return 0}
          const n = matrix[0].length
          let dp = new Array(m).fill(0).map(() => new Array(n).fill(0))
          let res = 0
      
          for(let i = 0; i < m; i++) {
              for(let j = 0; j < n; j++) {
                  matrix[i][j] == 1 ? dp[i][j] = 1 + (dp[i][j-1] || 0) : 0
              }
          }
      
          for(let i = m - 1; i >= 0; i--) {
              for(let j = 0; j < n; j++) {
                  let width = dp[i][j]
                  let height = 0
                  let k = i
                  while(k >= 0 && matrix[k][j] == 1) {
                      width = Math.min(width, dp[k--][j])
                      res = Math.max(res, width * (++height))
                  }
              }
          }
          return res
      };
      

27.不同的二叉搜索树(leetCode96)

二叉搜索树:左子树上的节点均小于根节点的值,右子树上的节点均大于根节点的值。

解法:动态规划

  • 思路:

    • 由于 1,2…n 这个数列是递增的,所以我们从任意一个位置“提起”这课树,都满足二叉搜索树的这个条件:左边儿子数小于爸爸数,右边儿子数大于爸爸数

    • 从 1,2,…n 数列构建搜索树,实际上只是一个不断细分的过程
      例如,我要用 [1,2,3,4,5,6] 构建
      首先,提起 “2” 作为树根,[1]为左子树,[3,4,5,6] 为右子树

    • 现在就变成了一个更小的问题:如何用 [3,4,5,6] 构建搜索树?
      比如,我们可以提起 “5” 作为树根,[3,4] 是左子树,[6] 是右子树

    • 现在就变成了一个更更小的问题:如何用 [3,4] 构建搜索树?
      那么这里就可以提起 “3” 作为树根,[4] 是右子树
      或 “4” 作为树根,[3] 是左子树

    • 可见 n=6 时的问题是可以不断拆分成更小的问题的

    • 假设 f(n)= 我们有 n 个数字时可以构建几种搜索树
      我们可以很容易得知几个简单情况 f(0) = 1, f(1) = 1, f(2) = 2
      (注:这里的 f(0) 可以理解为 =1 也可以理解为 =0,这个不重要,我们这里理解为 =1,即没有数字时只有一种情况,就是空的情况)

    • 那 n=3 时呢?
      我们来看 [1,2,3]
      如果提起 1 作为树根,左边有f(0)种情况,右边 f(2) 种情况,左右搭配一共有 f(0)*f(2) 种情况
      如果提起 2 作为树根,左边有f(1)种情况,右边 f(1) 种情况,左右搭配一共有 f(1)*f(1) 种情况
      如果提起 3 作为树根,左边有f(2)种情况,右边 f(0) 种情况,左右搭配一共有 f(2)*f(0) 种情况
      容易得知 f(3) = f(0)*f(2) + f(1)*f(1) + f(2)*f(0)

    • 同理,
      f(4)f(4) = f(0)f(0)*f(3)f(3) + f(1)f(1)*f(2)f(2) + f(2)f(2)*f(1)f(1) + f4(3)f4(3)*f(0)f(0)
      f(5)f(5) = f(0)f(0)*f(4)f(4) + f(1)f(1)*f(3)f(3) + f(2)f(2)*f(2)f(2) + f(3)f(3)*f(1)f(1) + f(4)f(4)*f(0)f(0)

  • 步骤:

    • 首先,创建一个长度为n+1的数组dp。

    • 令dp[0]、dp[1]为1,使用for循环,从2开始遍历

    • 定义变量num用于进行累加,再使用一个for循环,用来计算情况,循环完成后进行赋值

    • 最后返回dp[n]

  • 复杂度:

    • 时间复杂度:O(n*m)

    • 空间复杂度:O(n+1)

      var numTrees = function(n) {
          const dp = new Array(n+1)
          dp[0] = 1
          dp[1] = 1
          for(let i = 2; i < n+1; i++) {
              let num = 0
              for(let j = 0; j < i; j++) {
                  num += dp[j] * dp[i-j-1]
              }
              dp[i] = num
          }
      
          return dp[n]
      };
      

28.验证二叉搜索树(leetCode98)

解法:中序遍历

  • 思路:

    • 中序遍历后的数组,若是递增的则为二叉搜索树;否则不是
  • 步骤:

  • 复杂度:

    • 时间复杂度:

    • 空间复杂度:O(n)

      var isValidBST = function(root) {
          if(!root) true
          let nums = []
          inorder = (root) => {
              if(root.left) inorder(root.left)
              nums.push(root.val)
              if(root.right) inorder(root.right)
          }
          inorder(root)
      
          for(let i = 1; i < nums.length; i++) {
              if(nums[i] <= nums[i-1]) {
                  return false
              }
          }
          return true    
      };
      

29.对称二叉树(leetCode101)

解法:

  • 思路:

    • 转化为:左右子树是否镜像。
    • 分解为:树1的左子树和树2的右子树是否镜像,树1的右子树和树2的左子树是否镜像。
    • 符合“分、解、合”特性,考虑选择分而治之
  • 步骤:

    • 分:获取两个树的左子树和右子树。

    • 解:递归地判断树1的左子树和树2的右子树是否镜像,树1的右子树和树2的左子树是否镜像。

    • 合:如果上述都成立,且根节点值也相同,两个树就镜像。

  • 复杂度:

    • 时间复杂度:因为该算法访问了所有的节点,所以时间复杂度是O(n),n为二叉树的节点数。

    • 空间复杂度:空间复杂度为O(h),h为二叉树的高度,最坏的情况下h=n.

      var isSymmetric = function(root) {
          if(!root) {return true}
          const isMirror = (l, r) => {
              if(!l && !r) {return true}
              if(l && r) {
                  if(l.val === r.val && isMirror(l.left, r.right) && isMirror(l.right, r.left)) {
                      return true
                  }
              }
              return false
          }
      
          return isMirror(root.left, root.right)
      };
      

30.二叉树的层序遍历(leetCode102)

解法:

  • 思路:

    • 层序遍历顺序就是广度优先遍历。
    • 不过在遍历时候需要记录当前节点所处的层级,方便将其添加到不同的数组中。
  • 步骤:

    • 广度优先遍历二叉树

    • 遍历过程中,记录每个节点的层级,并将其添加到不同的数组中

  • 复杂度:

    • 时间复杂度:

    • 空间复杂度:

      var levelOrder = function(root) {
          if(!root) {return []}
          const res = []
          const q = [[root, 0]]
          while(q.length) {
              const [n, l] = q.shift()
              if(!res[l]) {
                  res.push([n.val])
              }else {
                  res[l].push(n.val)
              }
              if(n.left) q.push([n.left, l+1])
              if(n.right) q.push([n.right, l+1])
          }
      
          return res
      };
      

31.二叉数的最大深度(leetCode104)

解法:使用深度优先遍历解决该题

  • 步骤:

    • 新建一个变量,记录最大深度

    • 深度优先遍历整棵树,并记录每个节点的层级,同时不断刷新最大深度这个变量

    • 遍历结束返回最大深度这个变量

      var maxDepth = function(root) {
          let res = 0;
          const dfs = (n, l) => {
              if(!n) {return ;};
              if(!n.left && !n.right) {
                  res = Math.max(res, l);
              }
              dfs(n.left, l+1);
              dfs(n.right, l+1);
          }
          dfs(root, 1);
          return res;
      };
      
      

32.从前序与中序遍历构造二叉树(leetCode105)

解法:关键点在于递归构造左右子树时参数如何编写。

  • 思路:

    • 找出根节点的值,然后递归构造左右子树即可
    • 前序遍历的第一个数是根节点,后序遍历的最后一个数是根节点
    • 中序遍历,根节点的左侧数为左子树,根节点的右侧数为右子树,然后递归即可
  • 步骤:

    • 首先创建一个build函数,包含六个输入参数preOrder、preStart、preEnd、inOrder、inStart、inEnd
    • if判断,若preStart大于preEnd时,返回null
    • 得到前序遍历数组的第一个元素rootVal,找出其在中序遍历数组中的索引值index
    • 可以得到左子树的个数leftSize = index - inStart,构造出当前的根节点
    • 递归构造左右子树,左子树的参数为preOrder、preStart+1、peStart+leftSize、inOrder、inStart、index-1
    • 右子树的参数为preOrder、preStart+leftSize+1、preEnd、inOrder、index+1、inEnd
    • 返回root
    • 调用函数build
  • 复杂度:

    • 时间复杂度:

    • 空间复杂度:

      function build(preOrder, preStart, preEnd, inOrder, inStart, inEnd) {
          if(preStart > preEnd) {
              return null
          }
          let rootVal = preOrder[preStart]
          let index = inOrder.indexOf(rootVal)
          let leftSize = index - inStart
          let root = new TreeNode(rootVal)
          root.left = build(preOrder, preStart+1, preStart+leftSize, inOrder, inStart, index-1)
          root.right = build(preOrder, preStart+leftSize+1, preEnd, inOrder, index+1, inEnd)
          return root
      }
      
      var buildTree = function(preorder, inorder) {
          return build(preorder, 0, preorder.length-1, inorder, 0, inorder.length-1)
      };
      

33.二叉树展开为链表(leetCode114)

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值