1 二维数组的查找
题目描述
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
题目分析
该二维数组中的一个数,它左边的数都比它小,下边的数都比它大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间,当前元素的查找区间为左下角的所有元素。
代码
function Find(target, array) {
// 边界条件
if (array == null || array.length === 0 || array[0].length === 0) {
return false;
}
var rows = array.length;
var cols = array[0].length;
var r = 0;
var c = cols - 1;
while (r <= rows - 1 && c >= 0) {
if (target == array[r][c]) {
return true;
} else if (target > array[r][c]) {
r++;
} else {
c--;
}
}
return false;
}
2 替换空格
题目描述
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
题目分析
我们如果要替换空格,两步
- 先知道空格的位置
- 替换,但是字符串中有多个空格,所以我们就要循环,替换完之后再去查找字符串空格位置
当然你也可以选择用正则
代码
function replaceSpace(str)
{
return str.replace(/\s/g, '%20')
}
3 从尾到头打印链表
题目描述
输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。
题目分析
代码
function printListFromTailToHead(head)
{
// write code here
let arr = [];
while(head) {
arr.push(head.val);
head = head.next
}
return arr.reverse();
}
4 重建二叉树
题目描述
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
题目分析
前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果。
代码
/* function TreeNode(x) {
this.val = x;
this.left = null;
this.right = null;
} */
function reConstructBinaryTree(pre, vin)
{
// write code here
// 边界条件
if(pre.lenght === 0 || vin.length === 0) {
return null;
}
let index = vin.indexOf(pre[0]);
let left = vin.slice(0, index);
let right = vin.slice(index + 1);
return {
val: pre[0],
left: reConstructBinaryTree(pre.slice(1, index + 1), left),
right: reConstructBinaryTree(pre.slice(index + 1), right)
}
}
5 用两个栈实现队列
题目描述
用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
题目分析
in 栈用来处理入栈(push)操作,out 栈用来处理出栈(pop)操作。一个元素进入 in 栈之后,出栈的顺序被反转。当元素要出栈时,需要先进入 out 栈,此时元素出栈顺序再一次被反转,因此出栈顺序就和最开始入栈顺序是相同的,先进入的元素先退出,这就是队列的顺序。
代码
var inStack = [];
var outStack = [];
function push(node)
{
// write code here
inStack.push(node);
}
function pop()
{
if(!outStack.length) {
while(inStack.length) {
outStack.push(inStack.pop());
}
}
return outStack.pop();
}
6 旋转数组中的最小数字
题目描述
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
题目分析
在一个有序数组中查找一个元素可以用二分查找,二分查找也称为折半查找,每次都能将查找区间减半,这种折半特性的算法时间复杂度都为O(logN)
。
本题可以修改二分查找算法进行求解:
- 当 nums[mid] <= nums[high] 的情况下,说明解在 [low, mid] 之间,此时令 high = mid;
- 否则解在 [mid + 1, high] 之间,令 low = mid + 1。
代码
暴力使用sort
function minNumberInRotateArray(rotateArray)
{
// 边界条件
if(rotateArray.length === 0) return 0;
return rotateArray.sort(function(a, b){return a - b})[0];
}
二分法
function minNumberInRotateArray(rotateArray) {
if (rotateArray.length == 0) return 0;
var low = 0;
var high = rotateArray.length - 1;
while (low < high) {
var mid = low + high >> 1;
if (rotateArray[mid] <= rotateArray[high])
high = mid;
else
low = mid + 1;
}
return rotateArray[low];
}
7 斐波那契数列
题目描述
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。
n<=39
题目分析
代码
递归
虽然可以实现,但是运行超时:您的程序未能在规定时间内运行结束,请检查是否循环有错或算法复杂度过大。
function Fibonacci(n)
{
// write code here
if(n >= 0 && n < 2) return n;
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
动态规划
function Fibonacci(n)
{
// write code here
let arr = [];
arr[0] = 0;
arr[1] = 1;
for (let i = 2; i <= n; i++) {
arr[i] = arr[i - 1] + arr[i - 2];
}
return arr[n];
}
8 跳台阶
题目描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
题目分析
稍微分析就知道这是斐波那契数列,所以可以动态规划来做
- 如果两种跳法,1阶或者2阶,那么假定第一次跳的是一阶,那么剩下的是n-1个台阶,跳法是f(n-1)
- 假定第一次跳的是2阶,那么剩下的是n-2个台阶,跳法是f(n-2)
- 由1、2假设可以得出总跳法为:f(n) = f(n-1) + f(n-2)
- 通过实际的情况可以得出:只有一阶的时候 f(1) = 1 ,只有两阶的时候可以有 f(2) = 2
- 可以发现最终得出的是一个斐波那契数列
代码
递归
function jumpFloor(number)
{
// write code here
if (number <= 2) return number;
return jumpFloor(number - 1) + jumpFloor(number - 2);
}
动态规划
function jumpFloor(number)
{
// write code here
let arr = new Array(number + 1).fill(null);
arr[0] = 0;
arr[1] = 1;
arr[2] = 2;
for(let i = 3; i <= number; i++) {
arr[i] = arr[i - 1] + arr[i - 2];
}
return arr[number]
}
9 变态跳台阶
题目描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
题目分析
根据上一个题目可以知道,青蛙只跳1或2可以得出是一个斐波那契问题,即a[n] = a[n-1] + a[n-2]
,那么能跳1,2,3个台阶时a[n] = a[n-1] + a[n-2] + a[n-3] + ... + a[1]
那么有:
a[n] = a[n-1] + a[n-2] + ... + a[1] ①
a[n-1] = a[n-2] + a[n-3] + ... + a[1] ②
② - ①
可得:a[n] = 2*a[n-1]
代码
function jumpFloorII(number)
{
// write code here
let acc = 1;
while(--number) {
acc = acc * 2;
}
return acc;
}
10 矩形覆盖
题目描述
我们可以用21的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?
题目分析
其本质就是一个斐波那契数列
代码
function rectCover(number)
{
// write code here
var arr = [];
arr[0] = 0;
arr[1] = 1;
arr[2] = 2;
for (let i = 3; i <= number; i++) {
arr[i] = arr[i - 2] + arr[i - 1];
}
return arr[number];
}
11 二进制中1的个数
题目描述
题目分析
如果一个整数与1做与运算的结果是1,则表示该整数最右边一位是1,否则是0
那么解法就出来了:一个一个向右移位,并且判断最右边的那个位是否为1,为1就count++
但是这样输入负数时会陷入死循环,因为负数右移时,最高位补得是1,那么这样会有无数个1
此时这时候有两个解决办法:
- 既然不能对要操作的数一个一个右移位,那么我们可以考虑对另一个数1进行左移位计算
- 把一个整数减去1,再和原整数做与运算,会把该整数最右边一个1变成0。那么一个整数的二进制有多少个1,就可以进行多少次这样的操作
代码
方法1
function NumberOf1(n) {
let count = 0;
let flag = 1;
while (flag) {
// 循环的次数等于整数二进制的位数,32位的整数需要循环32位
if (flag & n) count++;
flag = flag << 1;
}
return count;
}
方法2
function NumberOf1(n) {
let count = 0;
while (n) {
// 有几位就循环几次,效率高
n = n & n - 1
count++;
}
return count;
}
Java
public class Solution {
public int NumberOf1(int n) {
int count = 0;
int flag = 1;
while(flag != 0) {
if((n & flag) != 0) {
count++;
}
flag = flag << 1;
}
return count;
}
}
12 数值的整数次方
题目描述
给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。
题目分析
- js中所有数字都是浮点数,所以
3 / 2 === 1.5
,所以在进行位运算和乘除运算时,最好都使用parseInt()
- 用右移运算(
>>
)代替除运算(/
),所以parseInt(3) / 2 === parseInt(3) >> 1
,直接3 >> 1
也可以,但是浮点数位运算效率十分低 - 用位与运算代替求余运算(
%
),所以parseInt(3) % 2 === parseInt(3) & 1
,直接3 & 1
也可以,但是浮点数位运算效率十分低
代码
function Power(base, exponent) {
// write code here
if (exponent === 0) return 1;
if (exponent === 1) return base;
var isNegative = false;
if (exponent < 0) {
exponent = -exponent;
isNegative = true;
}
var pow = Power(base * base, parseInt(exponent) >> 1);
if (parseInt(exponent) & 1 !== 0) pow = pow * base;
return isNegative ? 1 / pow : pow;
}
13 调整数组顺序使奇数位于偶数前面
题目描述
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
题目分析
两个变量作为奇数和偶数的下标,我们可以维护两个指针
- 第一个指针初始化时指向数组的第一个数字,它只向后移动
- 第二个指针初始化时指向数组的最后一个数字,它只向前移动
在两个指针相遇之前,第一个指针总是位于第二个指针的前面。如果第一个指针指向的数字是偶数,并且第二个指针指向的数字是奇数,则交换这两个数字
代码
function reOrderArray(array)
{
// write code here
let oddbegin = 0;
let oddcount = 0;
let arr = [];
for (let i = 0; i < array.length; i++) {
if(array[i] & 1) oddcount++
}
for (let i = 0; i < array.length; i++) {
if (array[i] & 1){
arr[oddbegin++] = array[i];
} else{
arr[oddcount++] = array[i];
}
}
return arr;
}
14 链表中倒数第k个节点
题目描述
输入一个链表,输出该链表中倒数第k个结点。
题目分析
代码
我的解法
/*function ListNode(x){
this.val = x;
this.next = null;
}*/
function FindKthToTail(head, k)
{
// write code here
if (head == null || k <= 0) return null;
let arr = [];
while(head) {
arr.push(head);
head = head.next;
}
return arr[arr.length - k];
}
引用类型不是共享内存吗?应使用浅拷贝
/* function ListNode(x){
this.val = x;
this.next = null;
}*/
function FindKthToTail(head, k) {
if (head === null || k <= 0) return null;
let pNode1 = head;
let pNode2 = head;
while (--k) {
if (pNode2.next !== null) {
pNode2 = pNode2.next;
} else {
return null;
}
}
while (pNode2.next !== null) {
pNode1 = pNode1.next;
pNode2 = pNode2.next;
}
return pNode1;
}
15 反转链表
题目描述
输入一个链表,反转链表后,输出新链表的表头。
题目分析
所以第一步要把当前节点的next记住
定义3个指针
- 当前遍历到的节点
- 它的前一个节点
- 它的后一个节点
代码
/*function ListNode(x){
this.val = x;
this.next = null;
}*/
function ReverseList(pHead)
{
// write code here
// 链表题都要判断边界条件,下句比较通用,都可以写
if (!pHead || !pHead.next) return pHead;
// 记录当前节点
let current = pHead;
// 记录当前节点的前一个节点
let pre = null;
// 记录当前节点的后一个节点
let next;
while(current) {
// 先记录当前节点的下一个节点,到时候断掉就找不到了
next = current.next;
// 将当前节点的下一节点指向前一个节点
current.next = pre;
// 前一个节点后移1位
pre = current;
// 当前节点后移1位
current = next;
}
return pre;
}
16 合并两个排序的链表
题目描述
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调递增规则。
题目分析
运用递归
代码
/*function ListNode(x){
this.val = x;
this.next = null;
}*/
function Merge(pHead1, pHead2)
{
// write code here
if(!pHead1) return pHead2;
if(!pHead2) return pHead1;
let pMergeHead = null;
if (pHead1.val < pHead2.val) {
pMergeHead = pHead1;
pMergeHead.next = Merge(pHead1.next, pHead2);
} else {
pMergeHead = pHead2;
pMergeHead.next = Merge(pHead1, pHead2.next);
}
return pMergeHead;
}
17 树的子结构❓
题目描述
输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)
题目分析
分析如何判断树B是不是树A的子结构,只需要两步。很容易看出来这是一个递归的过程。一般在树的求解方面都和递归有关。
- 在树A中找到和B的根结点的值一样的结点R
- 判断树A中以R为根结点的子树是不是包含和树B一样的结点
代码
/* function TreeNode(x) {
this.val = x;
this.left = null;
this.right = null;
} */
function HasSubtree(pRoot1, pRoot2) {
let res = false;
if (pRoot1 === null || pRoot2 === null) return false;
if (pRoot1.val === pRoot2.val) res = doesTree1HasTree2(pRoot1, pRoot2);
if (!res) res = HasSubtree(pRoot1.left, pRoot2);
if (!res) res = HasSubtree(pRoot1.right, pRoot2);
return res;
}
function doesTree1HasTree2(pRoot1, pRoot2) {
if (pRoot2 === null) return true;
if (pRoot1 === null) return false;
if (pRoot1.val !== pRoot2.val) return false;
return doesTree1HasTree2(pRoot1.left, pRoot2.left) && doesTree1HasTree2(pRoot1.right, pRoot2.right);
}
18 二叉树的镜像
题目描述
操作给定的二叉树,将其变换为源二叉树的镜像
二叉树的镜像定义:源二叉树
题目分析
递归交换左右节点
代码
/* function TreeNode(x) {
this.val = x;
this.left = null;
this.right = null;
} */
function Mirror(root)
{
// write code here
if(root === null) return;
if(root.left === null && root.right === null) return;
let temp = root.left;
root.left = root.right;
root.right = temp;
if(root.left) Mirror(root.left);
if(root.right) Mirror(root.right);
}
19 顺时针打印矩阵
题目描述
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如
题目分析
用左上和右下的坐标定位出一次要旋转打印的数据,一次旋转打印结束后,往对角分别前进和后退一个单位。
代码
function printMatrix(matrix)
{
// write code here
if(matrix == null) return;
const rows = matrix.length;
const cols = matrix[0].length;
let start = 0;
let result = [];
while (rows > start * 2 && cols > start * 2) {
result = result.concat(printMatrixInCircle(matrix, rows, cols, start));
start++;
}
return result;
}
function printMatrixInCircle(matrix, rows, cols, start) {
const endX = cols - 1 - start;
const endY = rows - 1 - start;
let result = [];
// 从左往右
for (let i = start; i <= endX; i++) {
result.push(matrix[start][i]);
}
// 从上往下
for(let i = start + 1; i <= endY; i++) {
result.push(matrix[i][endX]);
}
// 从右往左
for(let i = endX - 1; i >= start && endY > start; i--) {
result.push(matrix[endY][i]);
}
// 从下往上
for(let i = endY -1; i >= start + 1 && endX > start; i--) {
result.push(matrix[i][start]);
}
return result;
}
20 包含min函数的栈 中等 辅助栈
21 栈的压入、弹出序列 中等 辅助栈
22 从上往下打印二叉树 简单 广度遍历、队列
23 二叉树的后续遍历序列 中等 画图
24 二叉树和为某一值的遍历序列 中等 深度遍历、递归
25 复杂链表的复制 难 map保存<N,N’> || N->N’得S->S‘
26 二叉搜索树与双向链表 中等偏难 递归、中序遍历
27 字符串的排列 难 回溯法 || 递归全排列法
28 数组中出现次数超过一半的数 中等 partion法 || times变量变化法
29 最小的k个数 中等 partion法
30 连续子数组的最大值 中等 找规律、动态规划、注意判断条件
31 (~n整数中1出现的次数 中等 位运算 || 数学分析
把数组排成最小的数 简单偏难 改变排序规则
丑数 难 动态规划、注意判断条件
第一个只出现一次的字符 哈希表记录
数组中的逆序对 难+ 基于归并排序、临时数组
两个链表中的第一个公共节点 简单 双指针法
数字在排序数组中出现的次数 简单偏难 二分法改造
二叉树的深度 简单 递归
平衡二叉树 简单 递归
数组中只出现一次的数字 简单 indexOf || map记录 || 异或
和为S的连续正数序列 中等 数学分析
和为S的字符串 简单 双指针
左旋转字符串 简单 裁剪拼接
单次翻转序列 简单 转数组,对每项反序
扑克牌顺子 中等 注意题目条件、位运算判断数字重复
孩子们的游戏 难 数学分析得出公式 || 画图按题目做、注意下标
求1+2+3+…+n 中等 位运算、递归
不用加减乘除做加法 中等 位运算
把字符串转成整数 中等 位运算
数组中重复的数字 中等 将值放到对应位置上
构建乘积数组 中等偏上 借助中间变量存储后面的乘积
正则表达式的匹配 难 注意判断条件、递归
表示数值的字符串 中等 正则
字符流中第一个不重复的数字 中等 map记录 || indexOf法
链表中环的入口节点 中等 双指针法、数学分析
删除链表中重复的节点 中等 加头节点、注意多个重复
二叉树的下一个节点 中等 画图、分析各种情况
对称的二叉树 中等 递归、对称遍历
按之字形顺序打印二叉树 难 广度遍历、两个栈
把二叉树打印成多行 中等偏难 队列+两个记录变量
序列化二叉树 中等 数组代表流、递归
二叉搜索树的第k个节点 中等 中序遍历+计数变量
数据流的中位数 中等 partion法 || 维持排序 || 排序链表法 || AVL树 || 最大堆和最小堆
滑动窗口中的最大值 难 改变参考对象、双端队列、存下标
矩阵中的路径 中等 回溯法
机器人的运动范围 中等 回溯法
参考资料
[1] https://www.cnblogs.com/wuguanglin/p/code-interview.html
[2] https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/剑指 Offer 题解 - 目录.md
[3] https://www.cnblogs.com/wuguanglin/p/SummaryOfJSDoAlgorithmProblem.html