简介
文中所有题目均为精心挑选过的超高频题目,所以大家可以收藏起来
适用人群
针对有一定数据结构基础(了解链表, 二叉树, 二叉堆, 递归)的基本概念,并对时间空间复杂度有基本认知的。
食用指南
将文中列出的每道题至少手写3遍
面试前可以按照本文整理出来的题目直接过一遍
说明
文章更新频率: 除休息日外,每天在题目下方更新一道题的题解
有LeetCode原题的将贴上原地址,不在文章内做题目描述
Tc: Time complexity (时间复杂度)
Sc: Space complexity (空间复杂度)
题目类型
数组篇
1.twoSum [要求Tc: O(n) Sc:O(n)] (字节跳动)
LeetCode第1题
按照题目要求,我们第一时间想到的会是两层循环暴力解法:
解法1:Time = O(n²), Space = O(1)
思路:遍历每个元素nums[j],并查找是否存在一个值与 target - nums[j] 相等的目标元素。
function twoSum(nums, target) {
for (let i = 0; i < nums.length; i++) {
for (let j = i + 1; j < nums.length; j++) {
if (nums[j] == target - nums[i]) {
return [i,j];
}
}
}
return [];
}
解法2:Time = O(n), Space = O(n):
我们可以通过哈希表空间换时间。在进行迭代并将元素插入到表中的同时,我们回过头来检查哈希表中是否已经存在当前元素所对应的目标元素,如果存在,那我们就找到了问题的解,将其返回即可.(时间复杂度为O(n),空间复杂度也为O(n))
符合题目要求bingo✌
var twoSum = function(nums, target) {
let reduceHash = {
};
for (let i = 0; i < nums.length; i++) {
let reduceResult = target - nums[i];
if (reduceHash[reduceResult] !== undefined) {
return [reduceHash[reduceResult], i];
}
reduceHash[nums[i]] = i;
}
};
参考视频:传送门
2.缺失的数字 [要求Tc: O(n) Sc:O(n)] (字节跳动)
剑指Offer第53题
解法:
思路:我们先把所有输入了的数字存入hash表,因为给定的数组是有序的,所以可以再过一遍hash表,遍历过程中如果某个数字在hash表中不存在,则该数字就是缺失的那个数字
var missingNumber = function(nums) {
const hash = {
};
for (let i = 0; i < nums.length; i++) {
hash[nums[i]] = true;
}
let expectedNumCount = nums.length + 1;
for (let i = 0; i < expectedNumCount; i++) {
if (!hash[i]) {
return i;
}
}
return -1;
};
console.log(missingNumber([0,1,2,4]));//3
3.移动零 [要求Tc: O(n) Sc:O(1), 移动后不能改变原来其他元素的相对位置] (二线公司)
LeetCode第283题
解法:
思路: 双指针同向而行,fast指针遇到非0就把slow指针位置的字符替换掉,slow指针前进一步。直到fast指针把数组所有元素遍历完毕。(典型的两个挡板,三个区域思想),再把slow指针后面的所有元素替换为0。
同向性质:
变量的物理意义: slow的左侧不包含slow都是非0的数字,slow的右侧包含slow都应该为0,按照这个物理意义就可以达到原地算法的要求。因为快慢指针是同向而行的,所以算法为稳定算法(不会影响元素的相对位置)
var moveZeroes = function(nums) {
let slow = 0;
for (let fast = 0; fast < nums.length; fast++) {
if (nums[fast] !== 0) {
nums[slow++] = nums[fast];
}
}
while (slow < nums.length) {
nums[slow++] = 0;
}
};
const input = [0,1,5,8,4,3,0,5,0];
moveZeroes(input);
console.log(input);
4.洗牌算法(乱序数组)[要求Tc: O(n) Sc:O(1),要求每个元素的打乱的概率相同] (快手)
LeetCode第384题
解法:
思路: 本题思路就是使用Fisher-Yates洗牌算法。
function shuffle(arr) {
let m = arr.length;
while (m) {
let random = (Math.random() * m--) | 0;
[arr[random],arr[m]] = [arr[m],arr[random]];
}
return arr;
}
console.log(shuffle([1,5,6,7,6]));
5.两个数组的交集 [要求Tc: O(n) Sc:O(n)] (阿里)
LeetCode第349题
解法:
思路: 本题思路是看nums1数组里是否含有nums2的元素,如果有就添加到结果中返回。
let res = [];
for (let i = 0; i < nums1.length; i++) {
const cur = nums1[i];
if (nums2.includes(cur)) {
res.push(cur);
}
}
return Array.from(new Set(res));
6.RainbowSort [要求Tc: O(n) Sc:O(1)] (盛大)
给定一系列球,其中球的颜色只能是红色,黄色或蓝色,对球进行排序,以使所有红色球都分组在左侧,所有黄色球都分组在中间,所有蓝色球分组在右侧。
例:
[红]被排序为[红]
[黄,红]被排序为[红,黄]
[黄,红,红,蓝,黄,红,蓝]被排序为[红,红,红,黄,黄,蓝,蓝]
假设条件:
输入数组不为null。
corner case:
如果输入数组的长度为零怎么办?在这种情况下,我们应该直接返回空数组。
解法:
思路: 本题思路是挡板思想,使用三个挡板四个区域的思想进行划分(交换数组元素位置)
挡板的物理意义: [0-i)全是红色,[i,j)之间为黄色,(k->n-1]全为蓝色,[j-k]为未知探索区域
j为快指针
const input = ['黄','红','红','蓝','黄','红','蓝']
function rainbowSort(rainbow) {
let i = 0, j = 0, k = rainbow.length - 1;
while (j <= k) {
if (rainbow[j] === '红') {
swap(rainbow,i,j);
i++;
j++;
}
if (rainbow[j] === '黄') {
j++;
}
if (rainbow[j] === '蓝') {
swap(rainbow, j, k); //这里不写j++是因为从k交换过来的元素不能保证就是黄色,为了安全起见下次循环再检查一次j位置。
k--;
}
}
}
//辅助交换函数
function swap(arr,i,j) {
[arr[i],arr[j]] = [arr[j],arr[i]]
}
rainbowSort(input);
console.log(input);
7.移除元素 [要求Tc: O(n) Sc:O(1)]
LeetCode第27题
解法:
思路: 双指针同向而行,快指针遇到非val就把slow指针位置的字符替换掉,slow指针前进,直到数组所有元素遍历完毕。(典型的两个挡板,三个区域思想)
变量的物理意义: slow的左侧不包含slow都是非val的元素,slow的右侧包含slow都应该为不包含val的元素,按照这个物理意义就可以达到原地算法的要求。因为快慢指针是同向而行的,所以算法为稳定算法(不会影响元素的相对位置)
挡板性质:
同向而行: slow指针左边是处理好的元素 fast指针右边是未知探索区域,两个挡板中间不关注(最后的结果不会改变元素相对位置)
var removeElement = function(nums, val) {
let slow = 0;
for(let fast = 0; fast < nums.length; fast++) {
if (nums[fast] !== val) {
nums[slow++] = nums[fast];
}
}
return slow; //想要拿到去除后的数组可以: nums.slice(0,slow);
};
8.按奇偶排序数组[要求Tc: O(n) Sc:O(1)] (腾讯)
LeetCode第905题
解法:
思路: 继续使用挡板思想,两个挡板三个区域,同向而行,[0-i)是偶数,[j-n-1]是未探索区域
挡板性质:
同向而行: slow指针左边是处理好的元素 fast指针右边是未知探索区域,两个挡板中间不关注(最后的结果不会改变元素相对位置)
解法1:(不改变元素相对位置:同向而行)
var sortArrayByParity = function(A) {
for (let i = 0, j = 0; j < A.length; j++) {
if (A[j] % 2 === 0) swap(A, i++, j);
}
return A;
};
function swap(arr,l,r) {
[arr[l],arr[r]] = [arr[r],arr[l]];
}
console.log(sortArrayByParity([3,1,2,4]));
挡板性质:
相向而行: left指针左边是处理好的元素,right指针右边也是处理好的元素, 两个挡板中间是未处理区域(最后的结果可能会改变元素相对位置)
解法2:(改变元素相对位置:相向而行)
var sortArrayByParityII = function(A) {
let i = 0, j = A.length - 1;
while (i <= j) {
if (A[i] % 2 === 0) {
i++;
} else if (A[j] % 2 !== 0) {
j--;
} else {
//i % 2 !== 0 && j % 2 === 0
swap(A,i,j);
i++;
j--;
}
}
return A;
};
function swap(arr, l, r) {
[arr[l], arr[r]] = [arr[r], arr[l]];
}
console.log(sortArrayByParityII([3,1,2,4]));
9.数组中出现次数超过一半的数字 [要求Tc: O(n) Sc:O(1)]
LeetCode第169题
思路: 这道题属于火拼问题,见一个sha一个(抵消),最后留下的就是超过一半的元素。
先保留第一个元素,接着遍历,如果遇到和他相同的则加次数,否则就减次数,如果次数为0了就要换另一个元素了。
比如: A B C A
第一次保留A,用A跟剩下的打架,碰到不是A的就把A的个数减1,如果遇到A就增加个数,直到遇到不同的元素把A的次数抵消完就把A踢下去,并且把次数重新设置为1。
如此下去最后肯定有个多出来的就是题解了。
var majorityElement = function(array) {
let count = 1;
let num = array[0];
for (let i = 1; i < array.length; i++) {
if (num !== array[i]) {
count--;
if (count === 0) {
num = array[i];
count = 1;
}
} else {
count++;
}
}
return num;
};
let halfValue = majorityElement([1,1,2,3]);
console.log(halfValue); //1
10.合并两个有序数组 [要求Tc: O(m + n) Sc:O(n)] (腾讯)
例:
输入:nums1 = [1,3], nums2 = [4,5,7]
输出: [1,3,4,5,7]
function merge(left, right) {
let result = [];
let i = 0, j = 0;
while (i < left.length && j < right.length) {
result.push(left[i] < right[j] ? left[i++] : right[j++]);
}
while (i < left.length) {
result.push(left[i++]);
}
while (j < right.length) {
result.push(right[j++]);
}
return result;
}
let merged = merge([1,3],[4,5,7]);
console.log(merged);
11.有序数组中小于某个数的个数 [要求Tc: O(logn) Sc:O(1)] (今日头条)
例:
输入:[1, 2, 3, 4]
输入目标值:2
输出: 1
思路: 题目提到有序数组,第一时间就应该想到二分(再加之复杂度要求logn级别)。其实这道题就是让写个二分查找,仔细想想,你要找的那个数的下标不就代表前面有几个比他小的数字吗?
function binarySearch(array, target) {
let low = 0;
let high = array.length - 1;
while (low <= high) {
const mid = (low + (high - low) / 2) | 0;
const middleValue = array[mid];
if (middleValue > target) {
high = mid - 1;
} else if (middleValue < target) {
low = mid + 1;
} else {
return mid;
}
}
}
const minCount = binarySearch([1, 2, 3, 4], 2);
console.log(minCount); // 1
12.数组去重[要求Tc: O(n) Sc:O(1)]
LeetCode第26题
该算法要求原地算法,所以继续使用挡板思想(没理解的可以回到上文提及处继续理解)。
因为他至少有一个元素是不会重复的(至少会保留一个元素),所以从下标1开始处理。
解法1: 从索引1开始(处理好的元素不包含slow位置)
var removeDuplicates = function(arr) {
let slow = 1;
for (let fast = 1; fast < arr.length; fast++) {
if (arr[fast] === arr[slow - 1]) {
continue;
}
arr[slow++] = arr[fast];
}
return slow; //想要拿到去除后的数组可以: arr.slice(0, slow);
};
解法2: 从索引0开始,(处理好的元素包含slow位置)
var removeDuplicates = function(arr) {
let slow = 0;
for (let fast = 1; fast < arr.length; fast++) {
if (arr[fast] === arr[slow]) {
continue;
}
arr[++slow] = arr[fast];
}
return slow + 1; //想要拿到去除后的数组可以: arr.slice(0, slow + 1);
};
13.去掉连续的a和c,保留所有b [要求Tc: O(n) Sc:O(1) 元素相对位置不变] (字节跳动)
思路: 还是使用快慢指针,同向而行
function removeAC(arr) {
let slow = 0,fast = 0;
while (fast < arr.length) {
if (arr[fast] === 'b') {
arr[slow++] = arr[fast++];
} else {
let begin = fast;
while (fast < arr.length && arr[fast + 1] === arr[begin]) {
fast++;
}
if (fast - begin === 0) {
arr[slow++] = arr[fast++];
} else {
fast++;
}
}
}
return arr.slice(0,slow).join('');
}
const result = j1(['a','a','b','c','b','c','c']);
console.log