前言

okkjoo-leetcodeHot-byJs带你用 JS 刷高频面试算法题~ 每周日/周一更新~ 合集仓库:okkjoo-leetcodeHot-byJs

这是第二周的刷题记录与题解分享,如果你已经按题型分类系统地刷了一遍算法面试题的各个题型,想感受一下面试题的”随机性”的话,欢迎一起~

本周只刷了九题

整体进度

image.png

上题

206. 反转链表|简单、高频

题目描述

反转一个单链表。

解题思路

链表入门题,用三个指针:

  • 前驱 pre
  • 后续 nxt
  • 当前 cur

注意:

  • 链表指针基本操作
  • while 循环什么时候结束

时间复杂度O(n),空间复杂度O(1)

代码

  1. /**
  2. * Definition for singly-linked list.
  3. * function ListNode(val, next) {
  4. * this.val = (val===undefined ? 0 : val)
  5. * this.next = (next===undefined ? null : next)
  6. * }
  7. */
  8. /**
  9. * @param {ListNode} head
  10. * @return {ListNode}
  11. */
  12. var reverseList = function(head) {
  13. if(!head || !head.next) return head
  14. let cur = head, pre = null, nxt
  15. while(cur != null){
  16. nxt = cur.next
  17. cur.next = pre
  18. pre = cur
  19. cur = nxt
  20. }
  21. return pre
  22. };

102. 二叉树的层序遍历|中等、高频

题目描述

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

解题思路

二叉树经典题目——遍历专题中的层序遍历

常用递归或者队列来写:

递归

将当前节点和所在level、存储结果的数组一起传入递归函数,在递归中取出节点的value,根据level将value存储在对应的位置

队列

队列简单一点,第一步将节点放入队列中,再以 null 为该层结束的标志放入队列中。

每处理一个节点都将其左右节点放入队列中,注意这里要保持左右的顺序

不断出队,当出到 null 就表示该层出完到下一层了,这时再往队列中塞一个 null,作为下一层结束的标志

时间复杂度O(n),空间复杂度O(n)

注意⭐:

注意 JS 数组重置的方式,虽然经测试arr.length = 0的速度会比arr = []快很多,但是这样是得不到正确答案的,原因是因为:

arr =[]创建的是一个新的数组,并为其赋予一个新的引用。其他引用不收影响,仍指向原数组

arr.length = 0修改数组本身,就算通过不同的变量访问它,得到的是同一个数组

我还顺手写(水)了篇文章:JS 基础! |清空数组性能最好的方式是…但能全都用这个吗?

还有就是队列要还有子节点再添加 null 作为标志~ 不然就要掉进循环里咯~

代码

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val, left, right) {
  4. * this.val = (val===undefined ? 0 : val)
  5. * this.left = (left===undefined ? null : left)
  6. * this.right = (right===undefined ? null : right)
  7. * }
  8. */
  9. /**
  10. * @param {TreeNode} root
  11. * @return {number[][]}
  12. */
  13. var levelOrder = function(root) {
  14. if(!root) return[]
  15. const res = [],
  16. que = [root, null]
  17. let tmpLevel = []
  18. while(que.length){
  19. const t = que.shift()
  20. if(t){
  21. tmpLevel.push(t.val)
  22. t.left && que.push(t.left)
  23. t.right && que.push(t.right)
  24. }else {// t为null
  25. res.push(tmpLevel)
  26. // tmpLevel.length = 0
  27. // tmpLevel.splice(0,tmpLevel.length)
  28. tmpLevel = []
  29. que.length && que.push(null) //注意这里
  30. }
  31. }
  32. return res
  33. };

也可以每次都存储队列当前的长度作为该层的个数,就不用 null 作为结束标志了,在 while 里面用 for 根据队列长度遍历

15. 三数之和|中等、高频

题目描述

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

注意:答案中不可以包含重复的三元组。

解题思路

要是直接暴力,那可就是O(n^3)的时间复杂度,没人顶得住~

ok,那我们先剩下一个n,我们可以直接认为 nums 中的每一项 nums[i] 都能成为 三数中的一数,所以我们现在就可以将问题转换为 两数之和 = 0 - nums[i],那看来模仿第一道题两数之和就能以 O(n) 的复杂度通过了。

no,这道题所求的组数不止是一组,那一道两数之和只要求一组,所以才能找到的时候就 return

还有一个要注意的点:不重复的三元组,怎么样保证不重复?

  • 前面循环得到的元素不小于后面循环得到的元素,当然不大于也可以,只要有序就行

ok,我们就选不小于,所以要求最后的三元组[a, b, c],要满足 a <= b <= c。那么首先就要先对数组排一下序,排序算法时间复杂度为O(nlogn)

然后再用双指针遍历~

最终时间复杂度为O(n^2),空间复杂度没什么消耗,主要在于排序算法可能会消耗空间

代码

  1. /**
  2. * @param {number[]} nums
  3. * @return {number[][]}
  4. */
  5. var threeSum = function(nums) {
  6. //特例
  7. if(nums.length < 3) return []
  8. const res = []
  9. nums.sort((a, b) => a - b) //升序
  10. for(let i = 0; i < nums.length; i++){
  11. if(nums[i] > 0) break; //升序的数组,她大于0就不会后面的加上他能等于0了
  12. if(i > 0 && nums[i] === nums[i - 1]) continue //一样就跳过,避免重复三元组
  13. //双指针
  14. let left = i + 1, right = nums.length - 1
  15. while(left < right){ // 保证 i < left < right
  16. if(nums[left] + nums[right] + nums[i] === 0){//找到合适的
  17. res.push([nums[i], nums[left], nums[right]])
  18. //跳过重复的
  19. while(nums[left] === nums[left + 1]) left++
  20. left++
  21. while(nums[right] === nums[right + 1]) right--
  22. right--
  23. // 不符合的根据情况调整
  24. }else if(nums[left] + nums[right] + nums[i] > 0){
  25. right--
  26. }else left++
  27. }
  28. }
  29. return res
  30. };

215. 数组中的第K个最大元素|中等、高频

题目描述

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

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

解题思路

直接排序

最直观的就是直接排序,然后选择下标为k的就好了

时间复杂度O(nlogn)

小顶堆

维护一个大小为 k 的小顶堆,当堆大小大于 k ,就删除堆顶。等遍历完数组的时候,堆顶就是第k大的元素了

时间复杂度O(nlogk),空间复杂度O(k)

快速选择

快选有点像快排,就是找基准,然后将大于他的放一边,小于他的放一边。如果大于他的树有 k-1 个,那他自然就是第 k 个大的。

时间复杂度平均为O(n),最坏为O(n^2)

代码

小顶堆

  1. /**
  2. * @param {number[]} nums
  3. * @param {number} k
  4. * @return {number}
  5. */
  6. class MinHeap {
  7. constructor() {
  8. this.heap = [];
  9. }
  10. swap(a, b) {
  11. [this.heap[a], this.heap[b]] = [this.heap[b], this.heap[a]];
  12. }
  13. getParentIndex(i) {
  14. return (i - 1) >> 1;
  15. }
  16. getleftIndex(i) {
  17. return 2 * i + 1;
  18. }
  19. getrightIndex(i) {
  20. return 2 * i + 2;
  21. }
  22. shiftUp(index) {
  23. if (index === 0) return;
  24. const parentIndex = this.getParentIndex(index);
  25. if (this.heap[parentIndex] > this.heap[index]) {
  26. this.swap(parentIndex, index);
  27. this.shiftUp(parentIndex);
  28. }
  29. }
  30. shiftDown(index) {
  31. const leftIndex = this.getleftIndex(index);
  32. const rightIndex = this.getrightIndex(index);
  33. if (this.heap[leftIndex] < this.heap[index]) {
  34. this.swap(leftIndex, index);
  35. this.shiftDown(leftIndex);
  36. }
  37. if (this.heap[rightIndex] < this.heap[index]) {
  38. this.swap(rightIndex, index);
  39. this.shiftDown(rightIndex);
  40. }
  41. }
  42. insert(value) {
  43. this.heap.push(value);
  44. this.shiftUp(this.heap.length - 1);
  45. }
  46. pop() {
  47. // pop删除数组最后一个元素并返回,赋值给堆顶
  48. this.heap[0] = this.heap.pop();
  49. // 对堆顶重新排序
  50. this.shiftDown(0);
  51. }
  52. peek() {
  53. return this.heap[0];
  54. }
  55. size() {
  56. return this.heap.length;
  57. }
  58. }
  59. const findKthLargest = (nums, k) => {
  60. const minHeap = new MinHeap();
  61. nums.forEach(n => {
  62. // 将数组元素依次插入堆中
  63. minHeap.insert(n);
  64. // 如果堆大小超过k,将堆顶(最小) 的去掉
  65. if (minHeap.size() > k) {
  66. minHeap.pop();
  67. }
  68. })
  69. // 返回堆顶,此时就是第k大的元素
  70. return minHeap.peek();
  71. };

快速选择

  1. /**
  2. * @param {number[]} nums
  3. * @param {number} k
  4. * @return {number}
  5. */
  6. const findKthLargest = (nums, k) => {
  7. const n = nums.length;
  8. const quick = (l, r) => {
  9. if (l > r) return;//递归终止条件
  10. let random = Math.floor(Math.random() * (r - l + 1)) + l; //随机选一个索引
  11. swap(nums, random, r); //将它和位置r的元素交换,让nums[r]作为基准元素
  12. //对基准元素进行partition
  13. let pivotIndex = partition(nums, l, r);
  14. /*
  15. partition之后,基准左边的都小于它 右边的都大于它
  16. 基准元素的位置pivotIndex正好是n-k 则找大了第k大的数
  17. 如果n-k<pivotIndex,说明偏大了,去pivotIndex的左边递归查找
  18. 如果n-k>pivotIndex,说明偏小了,去pivotIndex的右边递归查找
  19. */
  20. if (n - k < pivotIndex) {
  21. quick(l, pivotIndex - 1);
  22. } else {
  23. quick(pivotIndex + 1, r);
  24. }
  25. };
  26. quick(0, n - 1);//函数开始传入的left=0,right= n - 1
  27. return nums[n - k]; //最后找到了正确的位置 也就是n-k等于pivotIndex 这个位置的元素就是第k大的数
  28. };
  29. function partition(nums, left, right) {
  30. let pivot = nums[right]; //最右边的元素为基准
  31. let pivotIndex = left; //pivotIndex初始化为left
  32. for (let i = left; i < right; i++) { //遍历left到right-1的元素
  33. if (nums[i] < pivot) { //如果当前元素比基准元素小
  34. swap(nums, i, pivotIndex); //把它交换到pivotIndex的位置
  35. pivotIndex++; //pivotIndex往前移动一步
  36. }
  37. }
  38. swap(nums, right, pivotIndex); //最后交换pivotIndex和right
  39. return pivotIndex; //返回pivotIndex
  40. }
  41. function swap(nums, p, q) {//交换数组中的两个元素
  42. const temp = nums[p];
  43. nums[p] = nums[q];
  44. nums[q] = temp;
  45. }

121. 买卖股票的最佳时机|简单、高频

题目描述

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

解题思路

获取最大利润,看起来就像贪心~

只有一笔交易,那想要获取最多利润,那就是在最低点买,最高点卖。当然,两个点的出现时间要注意~

其实很简单,在遍历的时候,记录到当前这个时间节点前的最低点价格即可。后面出现更高价格的时候,就与之相减再比较是否是最高利润,出现更低价格的时候就更新最低点价格。

代码

  1. /**
  2. * @param {number[]} prices
  3. * @return {number}
  4. */
  5. var maxProfit = function(prices) {
  6. let min = prices[0], profit = 0
  7. for(let i = 1; i < prices.length; i++){
  8. if(prices[i] > prices[i-1]){
  9. profit = Math.max(profit, prices[i] - min)
  10. }else{
  11. min = Math.min(min, prices[i])
  12. }
  13. }
  14. return profit
  15. };

5. 最长回文子串|中等、高频

题目描述

给你一个字符串 s,找到 s 中最长的回文子串。

解题思路

暴力

没啥好说的,O(n^3)

dp

dp[i][j]表示 [i, j]字符串可以形成回文,那么往两边延展一下,就可以有这样的转台转移

  1. if(s[i] === s[j] && dp[i+1][j-1]) dp[i+1][j-1] = true

时间复杂度O(n^2),空间复杂度O(n^2)

manacher 马拉车算法

这是一个专门处理回文串的算法~

  1. 先对字符串进行预处理,两个字符之间加上特殊符号#
  2. 然后遍历整个字符串,用一个数组来记录以该字符为中心的回文长度,为了方便计算右边界,在数组中记录长度的一半(向下取整)
  3. 每一次遍历的时候,如果该字符在已知回文串最右边界的覆盖下,那么就计算其相对最右边界回文串中心对称的位置,得出已知回文串的长度
  4. 判断该长度和右边界,如果达到了右边界,那么需要进行中心扩展探索。当然,如果第3步该字符没有在最右边界的“羽翼”下,则直接进行中心扩展探索。进行中心扩展探索的时候,同时又更新右边界
  5. 最后得到最长回文之后,去掉其中的特殊符号即可

时间复杂度的O(n)

这个代码我不背真写不下来… 希望面试只回答出思路就够了…😂

代码

  1. /**
  2. * dp
  3. * @param {string} s
  4. * @return {string}
  5. */
  6. var longestPalindrome = function (s) {
  7. if (!s || !s.length) return "";
  8. let res = s[0];
  9. const dp = [];
  10. // dp[i][] 依赖 dp[i-1][] --> 干脆反着遍历
  11. for (let i = s.length - 1; i >= 0; i--) {
  12. dp[i] = [];
  13. for (let j = i; j < s.length; j++) {
  14. if (j === i) dp[i][j] = true; // d[i][i] 一个字符当然回文
  15. else if (j === i + 1 && s[i] === s[j]) dp[i][j] = true; //dp[i][i+1]
  16. else if (s[i] === s[j] && dp[i + 1][j - 1]) dp[i][j] = true;
  17. // 有更长的就要更新
  18. if (dp[i][j] && j - i + 1 > res.length) res = s.slice(i, j + 1);
  19. }
  20. }
  21. return res;
  22. };
  23. /**
  24. * dp
  25. * @param {string} s
  26. * @return {string}
  27. */
  28. var longestPalindrome = function (s) {
  29. if (!s || !s.length) return "";
  30. let res = s[0];
  31. const dp = [];
  32. // dp[i][] 依赖 dp[i-1][] --> 干脆反着遍历
  33. for (let i = s.length - 1; i >= 0; i--) {
  34. dp[i] = [];
  35. for (let j = i; j < s.length; j++) {
  36. if (j === i) dp[i][j] = true; // d[i][i] 一个字符当然回文
  37. else if (j === i + 1 && s[i] === s[j]) dp[i][j] = true; //dp[i][i+1]
  38. else if (s[i] === s[j] && dp[i + 1][j - 1]) dp[i][j] = true;
  39. // 有更长的就要更新
  40. if (dp[i][j] && j - i + 1 > res.length) res = s.slice(i, j + 1);
  41. }
  42. }
  43. return res;
  44. };
  45. /**
  46. * manacher
  47. * @param {string} s
  48. * @return {string}
  49. */
  50. var longestPalindrome = function (s) {
  51. const lens = s.length;
  52. // 预处理字符数组
  53. let str = "#";
  54. for (let i = 0; i < lens; i++) {
  55. str = str + s[i] + "#";
  56. }
  57. // 当前回文子串能到达的右边界和它的中心
  58. let mid = 0,
  59. right = 0;
  60. // 最长的回文子串的中心和长度
  61. let maxLen = 0,
  62. maxLenMid = 0;
  63. // child[i]: 以i为中心的最长回文
  64. const child = [];
  65. // 遍历处理过的字符串,以每个字符中心进行扩展
  66. for (let i = 0; i < str.length; i++) {
  67. // 第i个字符,如果在最右边界的羽翼下,就选择对称字符的回文长度
  68. // 不在右边界内就赋值1
  69. child[i] = i < right ? Math.min(child[2 * mid - i], right - i) : 1;
  70. // 不论怎么样都要试一试暴力扩展
  71. while (
  72. i - child[i] >= 0 &&
  73. i + child[i] < str.length &&
  74. str.charAt(i + child[i]) == str.charAt(i - child[i])
  75. ) {
  76. child[i]++;
  77. }
  78. // 更新右边界
  79. if (right < child[i] + i) {
  80. mid = i;
  81. right = child[i] + i;
  82. }
  83. // 是否更新最长回文子串
  84. if (maxLen < child[i]) {
  85. maxLen = child[i];
  86. maxLenMid = i;
  87. }
  88. }
  89. return s.substring(
  90. (maxLenMid + 1 - maxLen) / 2,
  91. (maxLenMid - 1 + maxLen) / 2
  92. );
  93. };

141. 环形链表|简单、中频

题目描述

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false

解题思路

map

遍历所有节点并记录,遇到以及记录过的节点就说明有环,直接return。如遍历完了都还没有return就说明没环。

时间O(n),空间O(n)

代码简单,不写了

快慢指针

两个指针,一开始都在 head

  • 快指针一次走两个节点
  • 慢指针一次走一个节点

快指针绕一圈追上慢指针就说明有环~

时间复杂度O(n),空间复杂度O(1)

代码

  1. /**
  2. * Definition for singly-linked list.
  3. * function ListNode(val) {
  4. * this.val = val;
  5. * this.next = null;
  6. * }
  7. */
  8. /**
  9. * @param {ListNode} head
  10. * @return {boolean}
  11. */
  12. var hasCycle = function(head) {
  13. let slow = head, fast = head
  14. while(fast && fast.next){
  15. slow = slow.next
  16. fast = fast.next.next
  17. if(fast == slow) return true
  18. }
  19. return false
  20. };

912. 排序数组|中等|手写排序算法

题目描述

给你一个整数数组 nums,请你将该数组升序排列。

解题思路

肯定不是考API啦,手写排序算法啦

看题目的数据范围正负五万,总共十万,优先考虑O(n) 级别的算法~

计数排序

准备一个待排序数组取值范围的数组,遍历待排序数组,将每个元素放到对应下标位置,值就是其出现的次数

时间复杂度O(n+k),空间复杂度O(50000*2+1) (中值取值范围,或者说 为了下标不为负数的偏移diff)

快速排序

每次将数组分为两半,一部分比 关键元素大,另一部分比关键元素小(小于等于)。然后递归~

关键元素的选取方式有多种,一般我是最左边的那个,你可以选中间或者最右边或者随机

时间复杂度O(nlogn),空间复杂度O(logn)

代码

计数排序

  1. /**
  2. * @param {number[]} nums
  3. * @return {number[]}
  4. */
  5. var sortArray = function(nums) {
  6. const diff = 50000
  7. const counts = Array(diff * 2 + 1).fill(0)
  8. const res = []
  9. for(const a of nums) counts[a + diff]++
  10. for(let i in counts){
  11. while(counts[i]--) res.push(i - diff)
  12. }
  13. return res
  14. };

快速排序

  1. /**
  2. * 快速排序
  3. * @param {number[]} nums
  4. * @return {number[]}
  5. */
  6. var sortArray = function (nums) {
  7. if (nums.length <= 1) return nums; //递归中止
  8. const pivotIdx = Math.floor(nums.length / 2);
  9. const pivot = nums.splice(pivotIdx, 1)[0];
  10. const left = [],
  11. right = [];
  12. for (let num of nums) {
  13. if (num < pivot) left.push(num);
  14. else right.push(num);
  15. }
  16. return sortArray(left).concat([pivot], sortArray(right));
  17. };

129. 求根节点到叶节点数字之和|中等|递归|树|深搜|dfs

题目描述

给你一个二叉树的根节点 root ,树中每个节点都存放有一个 0 到 9 之间的数字。
每条从根节点到叶节点的路径都代表一个数字:

例如,从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123 。
计算从根节点到叶节点生成的 所有数字之和 。

叶节点 是指没有子节点的节点

解题思路

简单的递归题,注意因为要存储节点深度方便计算该条路径的“value”,所以借助一个额外函数 dfs 进行递归

  1. /**
  2. * Definition for a binary tree node.
  3. * function TreeNode(val, left, right) {
  4. * this.val = (val===undefined ? 0 : val)
  5. * this.left = (left===undefined ? null : left)
  6. * this.right = (right===undefined ? null : right)
  7. * }
  8. */
  9. /**
  10. * @param {TreeNode} root
  11. * @return {number}
  12. */
  13. var sumNumbers = function (root) {
  14. return dfs(root, 0);
  15. };
  16. const dfs = (node, cur) => {
  17. if (node === null) return 0;
  18. const v = node.val + cur * 10;
  19. if (node.left === null && node.right === null) return v;
  20. return dfs(node.left, v) + dfs(node.right, v);
  21. };