前端面试准备的50道算法题上

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

1. 时间复杂度和空间复杂度

算法是指用来操作数据,解决程序问题的一组方法。通过时间复杂度和空间复杂度可以衡量算法的优劣。通常时间复杂度比空间复杂度更容易出问题,所以更多研究的是时间复杂度。

1.1 时间复杂度

通常来说,运行一遍程序就可以得到对应的消耗时间,可是由于机器性能,数据规模等因素不同都有可能得出不同的结果。理论上来说,只要得到一个时间评估的指标,获得算法消耗时间的大致趋势即可。

通常,一个算法所花费的时间和代码语句执行次数成正比,代码执行次数越多,消耗的时间越长。我们把一个算法的执行次数称为时间频度,记作T(n)。渐进时间复杂度(简称时间复杂度)用大写O表示,所以也称作大O表示法,算法的时间复杂度为:T(n)=O(f(n))。它表示当 n 趋于正无穷大时,T(n) 的趋近于 f(n)。常见的时间复杂度有:O(1)常数型;O(log n)对数型;O(n)线性型;O(nlog n)线性对数型;O(n2)平方型;O(2n)指数型。

image.png 上图为不同类型函数的增长趋势图,可以看出,随着问题规模 n 的不断扩大,时间复杂度也跟着不断增大。常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(log n)<Ο(n)<Ο(nlog n)<Ο(n2)<<Ο(2^n)。值的注意的是,算法时间复杂度的大小比较只是从增长趋势来看,在某个阈值之前可能会出现相反的结果。

时间复杂度一般是这样计算的,找到执行语句,计算语句的执行次数,用大O进行表示。其中在用大O表示的时候,使用1表示常量级的执行次数,只保留函数的最高阶项,存在最高阶去掉前面的系数。

int j = 0; // ①
for (int i = 0; i < n; i++) { // ②
   j = i; // ③
   j++; // ④
}
复制代码

上述代码中,语句①的频度是1,语句②的频度是 n,语句③的频度是 n-1,语句④的频度是 n-1,所以时间频度 T(n)=1+n+n-1+n-1=3n-1,省略系数和常数项得到 T(n)=O(n)

1.2 空间复杂度

空间复杂度主要指执行算法所需的内存的大小,类似于时间复杂度,空间复杂度也是预估的,使用 S(n)=O(f(n)) 表示,S(n) 是空间复杂度,所需的存储空间使用 f(n) 表示。

int j = 0;
int[] m = new int[n];
for (int i = 1; i <= n; ++i) {
   j = i;
   j++;
}
复制代码

上述代码中,只有创建 m 的时候分配了数组空间,在 for 循环当中没有增加内存空间,所以空间复杂度 S(n)=O(n)。

2. 题目

2.1 两数之和

leetcode 地址

题目是需要返回两数之和等于目标数的数组下标,不同于返回两数之和等于目标数的两个数,这里不能修改数组下标的位置,因而不能使用双指针法。在编写具体的代码过程中,使用了 map,然后对数组中的数据进行遍历,map 中没有的数则保存和为目标数的另一个数做为键,而该数的下标作为值;map 中有该数则获取对应的值和该数的下标放到数组中并返回。

var twoSum = function (nums, target) {
    let map = new Map()
    for (let i = 0; i < nums.length; i++) {
        if (!map.has(nums[i])) {
            map.set(target - nums[i], i)
        } else {
            return [map.get(nums[i]), i]
        }
    }
    return []
};
复制代码

2.2 三数之和

leetcode 地址

求三数之和的时候用到了双指针法,所以首先需要按照递增的顺序进行排列。第一个数通过数组遍历进行获取,第二和第三个数通过双指针法进行获取,并把每次满足条件的三个数放到数组里面。值得注意的是,题意中明确说明不能包含重复的三元组,所以每确定一个数都需要进行去重,防止重复。而且获取到符合题意的三元组后,需要先去重然后再移动下标。

var threeSum = function (nums) {
    if(nums.length<3) return []
    nums.sort((a, b) => a - b)
    let res = []
    for (let i = 0; i < nums.length - 1; i++) {
        if (i > 0 && nums[i] === nums[i - 1]) continue // 去重
        let l = i + 1, r = nums.length - 1
        while (l < r) {
            let sum = nums[i] + nums[l] + nums[r]
            if (sum === 0) {
                res.push([nums[i], nums[l], nums[r]])
                while (l < r && nums[l] === nums[l + 1]) {  // 去重
                    l++
                }
                while (l < r && nums[r] === nums[r - 1]) {  // 去重
                    r--
                }
                l++
                r--
                
            } else if (sum < 0) {
                l++
            } else {
                r--
            }
        }
    }
    return res
};
复制代码

2.3 四数之和

leetcode 地址

看过三数之和后,四数之和很好理解,前两个数通过数组遍历获取,后两个通过双指针获取。

var fourSum = function (nums, target) {
    if (nums.length < 4) return []
    nums.sort((a, b) => a - b)
    let res = []
    for (let i = 0; i < nums.length - 2; i++) {
        if (i > 0 && nums[i] === nums[i - 1]) continue
        for (let j = i + 1; j < nums.length - 1; j++) {
            if (j > i + 1 && nums[j] === nums[j - 1]) continue
            let l = j + 1, r = nums.length - 1
            while (l < r) {
                let sum = nums[i] + nums[j] + nums[l] + nums[r]
                if (sum === target) {
                    res.push([nums[i], nums[j], nums[l], nums[r]])
                    while (l < r && nums[l] === nums[l + 1]) {
                        l++
                    }
                    while (l < r && nums[r] === nums[r - 1]) {
                        r--
                    }
                    l++
                    r--
                } else if (sum < target) {
                    l++
                } else {
                    r--
                }
            }
        }
    }
    return res
}
复制代码

2.4 最接近的三数之和

leetcode 地址

类似于三数之和,这里另外需要做的是,每次得到三数之和后都需要和之前的进行比较,找到最接近的。

var threeSumClosest = function (nums, target) {
    nums.sort((a,b)=>a-b)
    let res=nums[0]+nums[1]+nums[nums.length-1]
    for(let i=0;i<nums.length-1;i++){
        let l=i+1,r=nums.length-1
        while(l<r){
            let sum=nums[i]+nums[l]+nums[r]
            if(sum<target){
                l++
            }else{
                r--
            }
            if(Math.abs(target-res)>Math.abs(target-sum)){
                res=sum
            }
        }
    }
    return res
};
复制代码

2.5 二叉树的前序遍历

leetcode 地址

首先要知道,前序遍历先获取根节点,再获取左子树节点,最后获取右子树节点。当然递归的方式比较简单,感兴趣的话可研究迭代的写法。

var preorderTraversal = function (root) {
    let res = []
    function traversal(tree) {
        if (!tree) return
        res.push(tree.val)
        traversal(tree.left)
        traversal(tree.right)
    }
    traversal(root)
    return res
};
复制代码

2.6 二叉树的中序遍历

leetcode 地址

中序遍历先获取左子树的节点,再获取根节点,最后获取右子树的节点。

var inorderTraversal = function(root) {
     let result=[]
     function inorder(tree){
         if(!tree) return 
         inorder(tree.left)
         result.push(tree.val)
         inorder(tree.right)
     }
     inorder(root)
     return result
};
复制代码

2.7 二叉树的后序遍历

leetcode 地址

前序遍历先获取左子树的节点,再获取右子树节点,最后获取根节点。

var postorderTraversal = function(root) {
   let result=[]
   function postorder(tree){
       if(!tree) return 
       postorder(tree.left)
       postorder(tree.right)
       result.push(tree.val)
   }
   postorder(root)
   return result
};
复制代码

2.8 二叉树的最大深度

leetcode 地址

使用了 DFS,如果节点为空返回0,否则获取左子树和右子树的深度并计算出较大的值,由于当前节点不为空所以需要增加1。

var maxDepth = function (root) {
    if (!root) return 0
    return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1
};
复制代码

2.9 平衡二叉树

leetcode 地址

这题和求二叉树的最大深度关联。如果节点为空直接判断为平衡二叉树,否则判断左右子树的高度差是否大于1,大于的话不是平衡二叉树,否则继续递归判断左子树和右子树。

var isBalanced = function (root) {
    if (!root) return true
    function getHeight(tree) {
        if (!tree) return 0
        return Math.max(getHeight(tree.left), getHeight(tree.right)) + 1
    }
    if (Math.abs(getHeight(root.left) - getHeight(root.right)) > 1) {
        return false
    }
    return isBalanced(root.left) && isBalanced(root.right)
};
复制代码

2.10 二叉树的镜像

leetcode 地址

如果节点为空,返回 null,否则获取左右子树的镜像,交换子树的位置。

var mirrorTree = function (root) {
  if (!root) return null;
  let left = mirrorTree(root.right);
  let right = mirrorTree(root.left);
  root.left = left;
  root.right = right;
  return root;
}
复制代码

2.11 对称的二叉树

leetcode 地址 这里通过 compare 函数比较两个相同的树,如果都为空则是对称的二叉树,一个为空另一个不为空则不是对称的二叉树,其他情况需要比较两个节点的值,如果相等需要递归比较第一颗树的左节点和第二颗树的右节点,如果也相等则需要递归比较第一颗树的右节点和第二颗树的左节点。

var isSymmetric = function (root) {
    if (!root) return true
    function compare(left, right) {
        if (!left && !right) return true
        else if (!left || !right) return false
        return left.val === right.val && compare(left.left, right.right) && compare(left.right, right.left)
    }
    return compare(root, root)
};
复制代码

2.12 合并两个有序链表

leetcode 地址 如果一个链表为空,则返回另一个链表。如果其中一个链表的结点值比另一个链表的结点值要大,则将值较大的链表和该链表同另一个链表 merge 后的结果进行合并,返回该链表。

var mergeTwoLists = function (list1, list2) {
    if (!list1) return list2
    if (!list2) return list1
    if (list1.val < list2.val) {
        list1.next = mergeTwoLists(list1.next, list2)
        return list1
    } else {
        list2.next = mergeTwoLists(list1, list2.next)
        return list2
    }
};
复制代码

2.13 相交链表

leetcode 地址

本题使用了双指针的思想。如果两个链表有一个为空,则返回 null。在两个链表都不为空的情况,将指针 a,b 分别指向 headA 和 headB,如果 a,b 两个指针不相等则一直进行判断,指针 a 不为空指向下一个节点,为空则指向 headB,同理,指针 b 不为空指向下一个节点,为空志向 headA;如果 a,b 两个指针相等(可能都为 null)则返回其中一个指针,这个就是相交节点(可能为 null)

var getIntersectionNode = function (headA, headB) {
    if (headA === null || headB === null) {
        return null
    }
    let a = headA, b = headB
    while (a !== b) {
        a = a !== null ? a.next : headB
        b = b !== null ? b.next : headA
    }
    return a
};
复制代码

2.14 删除链表中的节点

leetcode 地址

这里要删除 node 节点,就直接把 node 节点值修改为 node.next 节点值即可。

var deleteNode = function (node) {
    node.val = node.next.val
    node.next = node.next.next
};
复制代码

2.15 环形链表

leetcode 地址

本题使用了快慢指针的思想。使用快指针进行迭代,如果快指针的下一个节点为 null,则返回 null,否则快指针走两步,慢指针走一步。当快慢指针相遇后就说明有环,于是将快指针重置到头节点,然后快慢指针再没次走一步,当它们再次相遇后就是环的入口。

var detectCycle = function (head) {
    let fast = head, slow = head
    while (fast) {
        if (fast.next === null) return null
        fast = fast.next.next
        slow = slow.next
        if (fast === slow) {
            fast = head
            while (true) {
                if (fast === slow) return slow
                fast = fast.next
                slow = slow.next
            }
        }
    }
    return null
};
复制代码

2.16 从尾到头打印链表

leetcode 地址

使用一个数组存放打印结果,遍历链表的每一个节点,把结果从数组的头部进行插入,得到的数组则是从尾到头的打印结果。

var reversePrint = function (head) {
    let res = []
    while (head) {
        res.unshift(head.val)
        head = head.next
    }
    return res
};
复制代码

2.17 链表中倒数第K个节点

leetcode 地址

本题是用了快慢指针的思想。先让快指针走 k 步,走完 k 步之后,快慢指针一起走,返回慢指针。

var getKthFromEnd = function (head, k) {
    let fast = head,slow = head
    while (fast) {
        fast = fast.next
        k--
        if (k < 0) {
            slow = slow.next
        }
    }
    return slow
}
复制代码

2.18 反转链表

leetcode 地址

将 prev 做为 head 的前一个节点,将 prev 指向 head.next 节点,将 head.next 和 head 分别指向对应的前一个节点

var reverseList = function (head) {
    let prev = null
    while (head) {
        [head.next, prev, head] = [prev, head, head.next]
    }
    return prev
};
复制代码

2.19 爬楼梯

leetcode 地址

本题可使用动态规划的思想。首先,可以把 f(x) 当作爬 x 阶楼梯的方案数,那么可以得到方程 f(x)=f(x-1)+f(x-2),也就是说 x 阶楼梯的方案数就是 x-1 阶楼梯的方案数加上 x-2 阶楼梯的方案数。如果使用数组来存放每一阶楼梯的方案数可以得出空间复杂度为 O(n) 的方案,可实际上只需要得出 x 阶楼梯的方案数,于是可以利用滚动数组思想,把空间复杂度降为 O(1)

var climbStairs = function (n) {
    let prev = 1, cur = 1
    for (let i = 2; i <= n; i++) {
        const temp = cur
        cur += prev
        prev = temp
    }
    return cur
};
复制代码

2.20 斐波那契数列

leetcode 地址

爬楼梯类似,应该说爬楼梯就是斐波那契数列的应用。

var fib = function (n) {
    if (n < 2) return n
    let prev = 0, cur = 1
    for (let i = 2; i <= n; i++) {
        const temp = cur
        cur = (cur + prev) % 1000000007
        prev = temp
    }
    return cur
};
复制代码

2.21 连续子数组的最大和

leetcode 地址

本题使用动态规划的思想。遍历数组,把每个数和增加该数后的累加值进行比较,取较大的一个值,再把得到的值和总的累加和进行比较取较大值。

var maxSubArray = function (nums) {
    let prev = 0, sum = nums[0]
    for (const num of nums) {
        prev = Math.max(prev + num, num)
        sum = Math.max(prev, sum)
    }
    return sum
}
复制代码

2.22 和为s的连续正数序列

leetcode 地址

本题采用滑动窗口的思想。首先找到中间值 mid,当 i 小于或等于 mid 的时候执行循环。当累加值小于目标值的时候累加值递增,数值存放到临时数组 temp;当累加值大于或等于目标值的时候,如果累加值等于目标值,临时数组 temp 可以放到结果数组中,而累计值通过减少 temp 数组中第一个元素值的方式进行递减。递减到小于目标值的时候又再次递增。

var findContinuousSequence = function (target) {
    let mid = Math.ceil(target / 2)
    let i = 1, sum = 0, res = [], temp = []
    while (i <= mid) {
        while (sum < target) {
            sum += i
            temp.push(i++)
        }
        while (sum >= target) {
            if (sum === target) {
                res.push([...temp])
            }
            sum -= temp.shift()
        }
    }
    return res
}
复制代码

2.23 无重复字符的最长子串

leetcode 地址

本题采用滑动窗口的思想。遍历数组的每个元素,存放到一个数组中,遇到已经存在于数组的元素,则删除该重复元素之前的数和当前元素。每次遍历的时候都将数组的长度和之前数组的长度进行比较,取较大值。

var lengthOfLongestSubstring = function (s) {
    let arr = [], max = 0
    for (const char of s) {
        const index = arr.indexOf(char)
        if (index !== -1) {
            arr.splice(0, index + 1)
        }
        arr.push(char)
        max = Math.max(arr.length, max)
    }
    return max
}
复制代码

2.24 排序数组中只出现一次的数字

leetcode 地址

看到采用异或的思路解决很快,所以直接用上了。

var singleNonDuplicate = function (nums) {
    return nums.reduce((acc, cur) => acc ^ cur)
}
复制代码

2.25 二叉搜索树的第k大节点

leetcode 地址

利用二叉搜索树的特点,左子树的节点的值比根节点的值小,右子树的节点值比根节点的值大,那么通过反中序遍历即可得到从大到小的数组序列。又因为题意只需要得出第K大节点,所以得到该节点的值后就可以停止递归运算。

var kthLargest = function (root, k) {
    let res = root.val
    function traversal(tree) {
        if (!tree) return
        traversal(tree.right)
        k--
        if (k === 0) {
            res = tree.val
            return
        }
        traversal(tree.left)
    }
    traversal(root)
    return res
}
复制代码

参考文献 cloud.tencent.com/developer/a…

猜你喜欢

转载自juejin.im/post/7080174781508616206