剑指 Offer 35. 复杂链表的复制
Object.assign //Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
const returnedTarget = Object.assign(target, source);
console.log(target);
// expected output: Object { a: 1, b: 4, c: 5 }
console.log(returnedTarget);
// expected output: Object { a: 1, b: 4, c: 5 }
console.log(target===returnedTarget)
// true
回溯+哈希表
本题要求我们对一个特殊的链表进行深拷贝。如果是普通链表,我们可以直接按照遍历的顺序创建链表节点。而本题中因为随机指针的存在,当我们拷贝节点时,「当前节点的随机指针指向的节点」可能还没创建,因此我们需要变换思路。一个可行方案是,我们利用回溯的方式,让每个节点的拷贝操作相互独立。对于当前节点,我们首先要进行拷贝,然后我们进行「当前节点的后继节点」和「当前节点的随机指针指向的节点」拷贝,拷贝完成后将创建的新节点的指针返回,即可完成当前节点的两指针的赋值。
具体地,我们用哈希表记录每一个节点对应新节点的创建情况。遍历该链表的过程中,我们检查「当前节点的后继节点」和「当前节点的随机指针指向的节点」的创建情况。如果这两个节点中的任何一个节点的新节点没有被创建,我们都立刻递归地进行创建。当我们拷贝完成,回溯到当前层时,我们即可完成当前节点的指针赋值。注意一个节点可能被多个其他节点指向,因此我们可能递归地多次尝试拷贝某个节点,为了防止重复拷贝,我们需要首先检查当前节点是否被拷贝过,如果已经拷贝过,我们可以直接从哈希表中取出拷贝后的节点的指针并返回即可。
在实际代码中,我们需要特别判断给定节点为空节点的情况。
var copyRandomList = function(head, cachedNode = new Map()) {
if (head === null) {
return null;
}
if (!cachedNode.has(head)) {
cachedNode.set(head, {val: head.val}), Object.assign(cachedNode.get(head), {next: copyRandomList(head.next, cachedNode), random: copyRandomList(head.random, cachedNode)})
}
return cachedNode.get(head);
}
//这里是这样的 首先通过对next的赋值 使得先递归一遍 建立完整的哈希表 这时整个链表已经建立完成 这样后续的random就可以正常的执行了
5. 最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:“aba” 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
这道题使用动态规划来处理,其基本思想为如果abba为回文串,那么bb就一定是回文串 反过来说 当s[i,j] 表示字符串中i到j的片段 其中如果s[i+1,j-1]是回文字符串的话,如果s[i] == s[j]
则 s[i,j] 为回文字符串
因此我门可以定义公式
p
[
i
,
j
]
=
{
t
r
u
e
如
果
s
[
i
,
j
]
是
回
文
串
f
a
l
s
e
如
果
s
[
i
,
j
]
不
是
回
文
串
p[i,j]= \left\{ \begin{aligned} true 如果s[i,j]是回文串 \\ false 如果s[i,j]不是回文串 \\ \end{aligned} \right.
p[i,j]={true如果s[i,j]是回文串false如果s[i,j]不是回文串
这里的「其它情况」包含两种可能性:
-
s[i, j] 本身不是一个回文串;
-
i > j 此时s[i, j]s[i,j] 本身不合法。
那么我们就可以写出动态规划的状态转移方程:
dp[i,j] = dp[i+1,j-1]&&s[i] == s[j]
优化:
我们可以使用一维数组来进行处理 利用滚动原理 因为我们只需要最长的序列 所以我们可以使用dp[i+1] 来代替dp[i+1, j-1]的值
/**
* @param {string} s
* @return {string}
*/
var longestPalindrome = function(s) {
let list = []
let Max_len = 0
let start = 0
for(j=0;j<s.length;++j) {
for(i=0;i<=j;++i) {
list[i] = (s[i] == s[j]&&(j-i < 2||list[i+1]))
if(list[i]&&(j-i+1)>Max_len)
{
Max_len = (j-i+1)
start = i
}
}
}
return s.slice(start,start+Max_len)
};
22. 括号生成
数字 n
代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
使用回溯法 + 剪枝
我们可以只在序列仍然保持有效时才添加 '('
or ')'
,而不是像 方法一 那样每次添加。我们可以通过跟踪到目前为止放置的左括号和右括号的数目来做到这一点,
如果左括号数量不大于 n
,我们可以放一个左括号。如果右括号数量小于左括号的数量,我们可以放一个右括号。
/**
* @param {number} n
* @return {string[]}
*/
var generateParenthesis = function(n,list=new Array(),d=true) {
let temp = []
var dp = function(a=new Array(),left=0,right=0) {
if(a.length == 2*n) {
temp.push(a.join(''))
return
}
if(left < n) {
a.push('(')
dp(a, left+1, right)
a.pop()
}
if(left > right) {
a.push(')')
dp(a, left, right+1)
a.pop()
}
}
dp()
return temp
};
剑指 Offer 04. 二维数组中的查找
在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
示例:
现有矩阵 matrix 如下:
[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
给定target = 5
返回 true
给定 target = 20
,返回 false
。
思路如下 我们可以直接从矩阵的右上角开始排起 因为右上角的元素下面的值比它大,左边的值比它小 是一个特殊的分界点 所以当这个点比目标值大的时候 我们将其纵坐标加一 反之横坐标加一 相等的话就返回真 知道超界 返回假
/**
* @param {number[][]} matrix
* @param {number} target
* @return {boolean}
*/
var findNumberIn2DArray = function(matrix, target) {
let n = 0
let m = 1
if (matrix.length) {
m = matrix[0].length-1
}
else
return false
while(1) {
if (matrix[n][m] < target) {
n += 1
} else if (matrix[n][m] == target) {
return true
} else if (matrix[n][m] > target) {
m -= 1
}
if (m<0||n==matrix.length) {
return false
}
}
};
45. 跳跃游戏 II(动态规划)
给你一个非负整数数组 nums
,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
假设你总是可以到达数组的最后一个位置。
示例 1:
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
示例 2:
输入: nums = [2,3,0,1,4]
输出: 2
我用的动态规划的思想来做的 但是
执行用时和内存消耗都很大
执行用时:216 ms, 在所有 JavaScript 提交中击败了14.38%
的用户
内存消耗:43.4 MB, 在所有 JavaScript 提交中击败了15.19%的用户
哭。。。
具体思路是 建立一个同等大小的数组 然后遍历给的数组 将从当前位置出发能到达的范围内赋于当前值加一 代表从当前位置到达最远的位置需要的最小步数为当前最小值加一
如果要赋值的位置有值 那么就与原值比较 取最小值
/**
* @param {number[]} nums
* @return {number}
*/
var jump = function(nums) {
let list = new Array(nums.length)
list[0] = 0
for(i=0;i<nums.length;++i) {
for(j=1;j<=nums[i];++j) {
if (list[i+j])
list[i+j] = list[i] + 1 > list[i+j] ? list[i+j] : list[i] + 1
else list[i+j] = list[i] + 1
}
}
return list[nums.length-1]
}
优化思路
可以取消使用列表
剑指 Offer 32 - II. 从上到下打印二叉树 II
从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。
3
/ \
9 20
/ \
15 7
返回其层次遍历结果:
[
[3],
[9,20],
[15,7]
]
使用先序遍历 加上每一层做标记来实现
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number[][]}
*/
function temp(root,list,floor=0) {
if(root) {
if(!list[floor])
list.push([])
list[floor].push(root.val)
}else
return
temp(root.left, list, floor+1)
temp(root.right, list, floor+1)
}
var levelOrder = function(root) {
let list = []
temp(root,list)
return list
};
剑指 Offer 47. 礼物的最大价值
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物
方法: 既然只能想右 和向左 那么用f(x,y) 表示到达x,y这个点的最大值 就等于
max(f(x-1,y),f(x,y-1)) + v(x,y)
根据这个可以得出代码
/**
* @param {number[][]} grid
* @return {number}
*/
function dfs(grid, x, y, sum, m, n, Max) { // 一开始用的方法 会超时 弃用
if(x==m-1&&y==n-1) {
Max.val = Math.max(Max.val, sum)
return
}
if(x==m-1&&y<n-1) {
dfs(grid, x, y+1, sum+grid[x][y+1], m, n, Max)
} else if(x<m-1&&y<n-1) {
dfs(grid, x+1, y, sum+grid[x+1][y], m, n, Max)
dfs(grid, x, y+1, sum+grid[x][y+1], m, n, Max)
}else if(x<m-1&&y==n-1) {
dfs(grid, x+1, y, sum+grid[x+1][y], m, n, Max)
}
}
var maxValue = function(grid) {
let [m, n] = [grid.length, grid[0].length]
let temp = new Array(m)
for(i=0;i<m;++i) {
temp[i] = new Array(n)
}
temp[0][0] = grid[0][0]
for(i=0;i<m;++i) {
for(j=0;j<n;++j) {
if (!i&&!j)
continue
if(!i)
temp[i][j] = temp[i][j-1] + grid[i][j]
else if(!j)
temp[i][j] = temp[i-1][j] + grid[i][j]
else
temp[i][j] = Math.max(temp[i-1>=0?i-1:0][j], temp[i][j-1>=0?j-1:0]) + grid[i][j]
}
}
return temp[m-1][n-1]
};
剑指 Offer 46. 把数字翻译成字符串
给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", “bwfi”, “bczi”, “mcfi"和"mzi”
结题思路: 这个题其实思考过后就是加了一些判断条件的斐波那契数列 就是只有两位数的值小于26才加上f(n-2) 但是如果你只想到这里就错了 因为按照题意 两位数0的前面的还不算 所以正确的判断条件应该还要加上 值大于10才行
代码是
/**
* @param {number} num
* @return {number}
*/
var translateNum = function(num) {
num = num.toString()
let list = new Array(num.length)
list[0] = 1
for(i=1;i<num.length;++i) {
if (i-2>=0&&Number(num.substr(i-1, 2))<26&&Number(num.substr(i-1, 2))>=10)
list[i] = list[i-1] + list[i-2]
else if(Number(num.substr(i-1,2))>=26) {
list[i] = list[i-1]
}else if (Number(num.substr(i-1, 2))<10)
list[i] = list[i-1]
else if(i-2<0) {
if(Number(num.substr(i-1,2))<26&&Number(num.substr(i-1, 2))>=10)
list[i] = list[i-1] + 1
else
list[i] = list[i-1]
}
}
return list[num.length-1]
};
其实可以用滚动数组来做 但是懒得做了
就现在效果也挺好的 时间和空间均达到了70多
剑指 Offer 48. 最长不含重复字符的子字符串
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
使用滑动窗口来解决 即我们在遍历字符串的时候维护一个只有不重复字母的子序列S,如果当前值s与维护的子序列S中的字符v重复,那我们就删除子序列S中v之前包括自身的值,并且将当前子序列S变更为之前v+1 - s 因为这个范围内的字符都是不相同的。
判断子序列中是否有相同字符可以使用哈希表来判断。
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
let list = new Map()
let Max = 0
let temp = 0
for(i=0;i<s.length;++i) {
if(!list.has(s[i])) {
list.set(s[i],i)
++temp
}
else {
for(j=i-temp;j<list.get(s[i]);++j)
list.delete(s[j])
temp = i - list.get(s[i])
list.set(s[i], i)
}
Max = Math.max(Max, temp)
}
return Max
};
剑指 Offer 12. 矩阵中的路径
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
例如,在下面的 3×4 的矩阵中包含单词 “ABCCED”(单词中的字母已标出)。
就是dfs加剪纸
这里有个巨坑的地方
创建二维数组的时候
let map = new Array(n).fill(new Array(m).fill(true))
这样创建会有一个问题,因为如果fill中传入的参数为引入类型 会导致执行都是一个引入类型 这会导致 我们每次修改某一行某一列的数据的时候 会导致这个一列的数据就会更改
所以推荐使用下面的创建方法
let map = new Array(m).fill(true).map(()=>
new Array(m).fill(true))
/**
* @param {character[][]} board
* @param {string} word
* @return {boolean}
*/
var exist = function(board, word) {
const [m, n] = [board.length, board[0].length];
var dfs =function(board, word, i, j, k) {
//越界,字符不匹配返回false
if(i >= m || i < 0 || j >=n || j < 0 || board[i][j] !=word[k]) return false;
//k代表正搜索的word[k-1],如果k等于单词长度-1,说明全匹配上了
if(k == word.length - 1) return true;
//将当前字符设置为空,防止四个方向dfs再次遍历到
board[i][j] = '';
//四个方向遍历
const res = dfs(board, word, i + 1, j, k+1) || dfs(board, word, i, j + 1, k + 1) || dfs(board, word, i - 1, j, k +1) || dfs(board, word, i, j - 1, k + 1);
//恢复当前字符
board[i][j] = word[k]
return res;
};
for(let i = 0; i < m; i++){
for(let j = 0; j < n; j++) {
if(dfs(board, word, i, j, 0)) return true;
}
}
return false;
};
//function dfs(board,row,col,m_row,m_col,word,len,k,flag) {
// if(row>=0&&row<m_row&&col>=0&&col<m_col) {
// if(board[row][col]==word[len]&&flag[row][col]){
// if(len==word.length-1){
// k.val = true
// return
// }
// flag[row][col] = false
// dfs(board,row+1,col,m_row,m_col,word,len+1,k,flag)
// dfs(board,row,col+1,m_row,m_col,word,len+1,k,flag)
// dfs(board,row-1,col,m_row,m_col,word,len+1,k,flag)
// dfs(board,row,col-1,m_row,m_col,word,len+1,k,flag)
// flag[row][col] = true
// }
// else
// return
// }
// else
// return
// }
// var exist = function(board, word) {
// let m_row = board.length
// let m_col = board[0].length
// let k = {
// val: false
// }
// let flag = new Array(m_row).fill(true).map(()=> new Array(m_col).fill(true))
// for(i=0;i<m_row;++i) {
// for(j=0;j<m_col;++j) {
// if(board[i][j] == word[0]) {
// dfs(board,i,j,m_row,m_col,word,0,k,flag)
// if(k.val)
// return true
// }
// }
// }
// return false
// };
剑指 Offer 68 - I. 二叉搜索树的最近公共祖先
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @param {TreeNode} p
* @param {TreeNode} q
* @return {TreeNode}
*/
var lowestCommonAncestor = function(root, p, q) {
let temp = root
let dfs = function(root) {
if(!root)
return false
let left = dfs(root.left)
let right = dfs(root.right)
if((root === p||root === q) && (left||right)||(left&&right)) {
// console.log(left,right)
temp = root
return true
} else if(root === p||root === q || left || right) {
return true
} else
return false
}
dfs(root)
return temp
};
剑指 Offer 16. 数值的整数次方
实现 pow(x, n) ,即计算 x 的 n 次幂函数(即,xn)。不得使用库函数,同时不需要考虑大数问题。
示例 1:
输入:x = 2.00000, n = 10
输出:1024.00000
示例 2:
输入:x = 2.10000, n = 3
输出:9.26100
示例 3:
输入:x = 2.00000, n = -2
输出:0.25000
解释:2-2 = 1/22 = 1/4 = 0.25
/**
* @param {number} x
* @param {number} n
* @return {number}
*/
var myPow = function(x, n) {
var mul = function(x,n) {
if(n==0)
return 1.0
let X = mul(x,Math.floor(n/2))
return n%2 ? X*X*x : X*X
}
if(!n)
return 1.0
return n>=0 ? mul(x,n) : 1 / mul(x,-n)
};
使用 快速幂算法
比如 2的20次幂 可以拆分称为 2的10次幂 乘以2 的10次幂
这样就不用连续乘以20次
如果是奇数的话需要额外乘以一个2
剑指 Offer 56 - I. 数组中数字出现的次数
一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
示例 1:
输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]
示例 2:
输入:nums = [1,2,10,4,1,4,3,3]
输出:[2,10] 或 [10,2]
算法
先对所有数字进行一次异或,得到两个出现一次的数字的异或值。
在异或结果中找到任意为 1 的位。
根据这一位对所有的数字进行分组。
在每个组内进行异或操作,得到两个数字。
/**
* @param {number[]} nums
* @return {number[]}
*/
var singleNumbers = function(nums) {
let num = nums.reduce((pre,now)=> {
return pre^now
}, 0)
let div = 1
console.log(num)
while((num&div)==0) {
div = div << 1
}
console.log(div)
let a = 0,b = 0
for(let i in nums) {
if(nums[i]&div) {
console.log("*", nums[i])
a ^= nums[i]
} else {
b ^= nums[i]
console.log("%", nums[i])
}
}
return [a,b]
};
剑指 Offer 59 - II. 队列的最大值
请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。
若队列为空,pop_front 和 max_value 需要返回 -1
输入:
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]
这个题非常的巧妙
我使用的思路为 使用两个栈来模拟队列 在使用一个保存当前节点最大值得一个栈,同时再使用一个值来保存push数据的栈的最大值,这样相当于把队列分为两个部分,
当我们push数据的时候 把数据存入listin栈中,同时更新最大值,然后是用用栈表示队列的想法,然后在输出当前最大值的时候比较当前节点最大值和listin的最大值进行输出
代码如下:
var MaxQueue = function() {
this.listout = new Array()
this.listin = new Array()
this.Max_list = []
this.Max = 0
};
/**
* @return {number}
*/
MaxQueue.prototype.max_value = function() {
if(this.Max_list.length==0) {
let max_val = 0
if(this.listin.length==0)
return -1
while(this.listin.length!=0) {
this.Max = 0
max_val = Math.max(max_val,this.listin[this.listin.length-1])
this.Max_list.push(max_val)
this.listout.push(this.listin.pop())
}
}
return this.Max_list[this.Max_list.length-1] > this.Max ? this.Max_list[this.Max_list.length-1] : this.Max
};
/**
* @param {number} value
* @return {void}
*/
MaxQueue.prototype.push_back = function(value) {
this.listin.push(value)
this.Max = Math.max(this.Max, value)
};
/**
* @return {number}
*/
MaxQueue.prototype.pop_front = function() {
if(this.listout.length != 0) {
this.Max_list.pop()
return this.listout.pop()
}
else {
let max_val = 0
while(this.listin.length!=0) {
this.Max = 0
max_val = Math.max(max_val,this.listin[this.listin.length-1])
this.Max_list.push(max_val)
this.listout.push(this.listin.pop())
}
if(this.listout.length==0)
return -1
this.Max_list.pop()
return this.listout.pop()
}
};
/**
* Your MaxQueue object will be instantiated and called as such:
* var obj = new MaxQueue()
* var param_1 = obj.max_value()
* obj.push_back(value)
* var param_3 = obj.pop_front()
*/