前端复习企划15-常见算法题及剑指offer
常见算法题
反转字符串
给定一个字符串,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。
示例 1:
输入: "Let's take LeetCode contest"
输出: "s'teL ekat edoCteeL tsetnoc"
注意:在字符串中,每个单词由单个空格分隔,并且字符串中不会有任何额外的空格。
思路:先用利用空格分割每个单词,单词变数组,再在每个数组内进行反转
let reverseWord=(str) => { return str.split(' ').map(item => { return item.split('').reverse().join('') }).join(' ') }
eetcode最快的思路:
先把每个字符都颠倒过来,tsetnoc edoCteeL ekat s'teL
之后按照空格分隔开, 重新排序
let reverseWord=(str) => { return str.split('').reverse().join('').split(' ').reverse().join(' ') }
最长连续子串
给定一个字符串 s,计算具有相同数量0和1的非空(连续)子字符串的数量,并且这些子字符串中的所有0和所有1都是组合在一起的。
重复出现的子串要计算它们出现的次数。
示例 1 :
输入: "00110011"
输出: 6
解释: 有6个子串具有相同数量的连续1和0:“0011”,“01”,“1100”,“10”,“0011” 和 “01”。
请注意,一些重复出现的子串要计算它们出现的次数。
另外,“00110011”不是有效的子串,因为所有的0(和1)没有组合在一起。
/** * 思路1:利用正则表达式来写 * 0011001 每次都从第一个字符开始,直到遇到非它的。比如说第一次,00截止,长度为2,那么就从后检查有没有满足0011的,输出0011。 * 第二次,从第二个0开始,后面遇到1就截止,长度为1.那么就从后面match有没有满足01的,输出01 * 第三次,从1开始,11截止,长度为2,那么就从后检测有没有1100的,输出1100 * 只需要把上述过程实现即可,需要每次减1位 */ export default (str) => { // 保存数据 let r = [] // 给定任意子输入都返回第一个符合条件的子串 let match = (str) => { let j = str.match(/^(0+|1+)/)[0] let o = (j[0] ^ 1).toString().repeat(j.length) let reg = new RegExp(`^(${j}${o})`) if (reg.test(str)) { return RegExp.$1 } else { return '' } } for (let i = 0, len = str.length - 1; i < len; i++) { let sub = match(str.slice(i)) if (sub) { r.push(sub) } } return r } /** * 思路2 * 000111必定有三个子串 * 00011必定有两个子串 * 0111必定有1个子串 * 以此类推, 每两组数据之间长度最短的值为子串的数量 */ export default (str) => { let n = 0, arr = s.match(/([1]+)|([0]+)/g) for (let i = 0; i < arr.length - 1; i++) { n += Math.min(arr[i].length, arr[i + 1].length) } return n } /** * 常规思路:判断个数,迭代输出 */ export default (str) => { var prev = 0, curr = 1, res = 0 for (var i = 1; i < s.length; i++) { if (s[i] === s[i - 1]) { curr++ } else { prev = curr curr = 1 } if (prev >= curr) { res++ } } return res }
重复的子串
给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。
示例 1:
输入: "abab"
输出: True
解释: 可由子字符串 "ab" 重复两次构成。
export default (s) => { //正则表达式 // \w字符 + 至少一个 ()\1 模式匹配()内的 var reg = new RegExp(/^(\w+)\1+$/); return reg.test(s) }
判断数组是否连续的问题
[0,10] [8,10] [10,30] true
[0,10] [25 30] [8 20] false
写一个方法来实现这个功能,判断数组是不是连续的?
思路一:
先排序,每个子数组中的第一个元素跟前一个数组最大值做比较,比它小就说明是连续的。
思路二:
用一个新的数组来判断,直接根据每个数组的首尾,给一个新的数组全部赋值,比如都是1。最后只需要判断数组includes(undefined)有没有,就可以判断是不是连续的了。
思路三:
就是找子数组起始坐标的最大值和结束坐标的最小值
//思路2: function check(lines){ let result=[]; for(let i=0;i<lines.length;i++){ let start=lines[i][0]; let end=lines[i][1]; for(let j=start;j<=end;j++){ result[j]=1; } } //此时获取数组中有值的起始坐标,因为不一定数组是从0开始的 let begin =result.indexOf(1); //把数组截取出来,然后判断是不是连续的 return !result.slice(begin).includes(undefined); } //思路3: function check(lines){ //取起始点最大值、结束点最小值 let maxstart=0,minend=0; for(let i=0;i<lines.length;i++){ if(lines[i][0]> maxstart){ maxstart=lines[i][0]; }if(lines[i][1]<minend){ minend=lines[i][0]; } } //起始点最大比结束点最小大,那么一定是重合的 return maxstart>minend ? true:false }
九宫格手机键盘字母排列组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射(九宫格)。注意 1 不对应任何字母。
示例:
输入:"23"
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
let phonenum= (str) => { //为空返回空 if (!str) { return [] } //map映射关系 let map = ['', 1, 'abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz']; //输入字符按字符分隔变成数组 let num = str.split(''); //保存键盘映射后的字母内容 23=>['abc','def'] let code = [] num.forEach(item => { if (map[item]) { code.push(map[item]) } }) //只有一个数字的情况 if (code.length <= 1) { return code[0].split('') } //字符组合 let comb = (arr) => { //保存前两个组合的结果 let tmp = [] for (let i = 0, il = arr[0].length; i < il; i++) { for (let j = 0, jl = arr[1].length; j < jl; j++) { tmp.push(`${arr[0][i]}${arr[1][j]}`) } } //去掉组合后前两个,插入新组合 arr.splice(0, 2, tmp) if (arr.length > 1) { comb(arr) } else { return tmp } return arr[0] } return comb(code) }
格雷编码
格雷编码是一个二进制数字系统,在该系统中,两个连续的数值仅有一个位数的差异。
给定一个代表编码总位数的非负整数 n,打印其格雷编码序列。格雷编码序列必须以 0 开头。
示例:
输入: 2
输出: [0,1,3,2]
解释:
00 - 0
01 - 1
11 - 3
10 - 2
let GrayCode= (num) => { if (num === 0) return ['0'] //递归计算为n的格雷编码 let getGrayCode = (n) => { if (n === 1) { return ['0', '1'] } else { let prev = getGrayCode(n - 1) let TempResult = [] let maxlength = Math.pow(2, n) - 1 for (let i = 0; i < prev.length; i++) { TempResult[i] = `0${prev[i]}` TempResult[maxlength - i] = `1${prev[i]}` } return TempResult } } return getGrayCode(num).map((item) => { return parseInt(item, 2) }); }
种花问题
假设你有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花卉不能种植在相邻的地块上,它们会争夺水源,两者都会死去。
给定一个花坛(表示为一个数组包含0和1,其中0表示没种植花,1表示种植了花),和一个数 n 。能否在不打破种植规则的情况下种入 n 朵花?能则返回True,不能则返回False。
示例 1:
输入: flowerbed = [1,0,0,0,1], n = 1
输出: True
示例 2:
输入: flowerbed = [1,0,0,0,1], n = 2
输出: False
let palceFlower= (arr, n) => { let max = 0 // 右边界补充[0,0,0],最后一块地能不能种只取决于前面的是不是1,所以默认最后一块地的右侧是0(无须考虑右侧边界有阻碍) arr.push(0) for (let i = 0; i < arr.length - 1; i++) { if (arr[i] === 0) { if (i === 0 && arr[1] === 0) { max++ i = i + 1 } else if (arr[i - 1] === 0 && arr[i + 1] === 0) { max++ i = i + 1 } } } return max >= n }
卡牌游戏
给定一副牌,每张牌上都写着一个整数。 此时,你需要选定一个数字 X,使我们可以将整副牌按下述规则分成 1 组或更多组:
每组都有 X 张牌。组内所有的牌上都写着相同的整数。仅当你可选的 X >= 2 时返回 true。
示例 1:
输入:[1,2,3,4,4,3,2,1]
输出:true
解释:可行的分组是 [1,1],[2,2],[3,3],[4,4]
示例 2:
输入:[1,1,1,2,2,2,3,3]
输出:false
解释:没有满足要求的分组。
let deckCard= (arr) => { //其实这道题就是在求相同数字数目的最大公约数的问题 //先写求最大公约数的方法 let gcd = (a, b) => { if (b === 0) { return a } else { return gcd(b, a % b) } } //记录每张牌的数目 let group = [] let tmp = {} arr.forEach(element => { tmp[element] = tmp[element] ? tmp[element] + 1 : 1 }) for (let v of Object.values(tmp)) { group.push(v) } while (group.length > 1) { let a = group.shift() let b = group.shift() let v = gcd(a, b) if (v === 1) { //最小公约数是1,false return false } else { //放回前两个数的最大公约数继续计算 group.unshift(v) } } //判断最后得出的最小公约数是否是1 return group.length ? group[0] > 1 : false }
子串匹配
给定一个字符串 s 和一些长度相同的单词 words。
找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。
注意子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序。
示例 1:
输入:
s = "barfoothefoobarman",words = ["foo","bar"]
输出:[0,9]
解释:
从索引 0 和 9 开始的子串分别是 "barfoor" 和 "foobar" 。
输出的顺序不重要, [9,0] 也是有效答案。
示例 2:
输入:
s = "wordgoodgoodgoodbestword",words = ["word","good","best","word"]
输出:[]
//思路:words[a,b,c] 递归排列组合[a] 第一位 b c中选一位放在第二位 // [b] 第一位 a c中选一位放在第二位 // [c] 第一位 a b中选一位放在第二位 //先排列组合生成子串组合,然后看看对应字符串有无对应的即可 export default (str, words) => { //数组长度 let num = words.length //结果 let result = [] //生成递归的函数 let range = (r, _arr) => { //边界,计算完毕 if (r.length === num) { result.push(r) } else { _arr.forEach((item, idx) => { let tmp = [].concat(_arr); tmp.splice(idx, 1); //丢出第一位,生成第二位待选数组 range(r.concat(item), tmp); }) } } range([], words); //子串获取完成,检查有没有匹配的子串 return result.map(item => { return str.indexOf(item.join('')) }).filter(item => item !== -1).sort() }
// 利用查找每个单词在字符串的位置,然后通过计算这些位置是不是连续的。 // 比如 abforbarcd,[for,bar],那么for的起始位置是2,bar的起始位置是5; // 说明这两个单词是连续的2+3(for的长度)=5 // for:[{start:2,end:5}] // bar:[{start:5,end:8}] // 判断上一个单词的end和下一个单词的start是不是相同来计算两个单词是不是挨着 export default (str, words) => { // 计算字符串的总长度 let strLen = str.length // 计算所有的单词数量 let wordsLen = words.length // 计算所有单词出现的起始位置和截止位置 let pos = {} // 如果字符串的长度小于所有单词的总长度直接返回 if (strLen < words.join('').length) { return [] } // 遍历所有单词查找在字符串中的起始位置和截止位置 words.every(word => { if (pos[word]) { return true } let wl = word.length let tmp = [] for (let i = 0, len = strLen - wl, idx; i <= len; i++) { idx = str.slice(i).indexOf(word) if (idx > -1) { if (idx === 0) { tmp.push({ start: i, end: i + wl }) } else if (str[i + 1] !== word[0]) { i += idx - 1 } } else { break } } // 如果没有匹配到单词终止遍历 if (tmp[0] === undefined) { return false } else { // 保存当前单词的位置,遍历下一个单词 pos[word] = tmp.sort((a, b) => a.start - b.start) return true } }) // 只要有一个单词没找到说明不能匹配到连续的字符串 if (words.find(item => !pos[item])) { return [] } let result = [] // 计算所有单词的位置 let match = (poses) => { // 记录是不是所有单词都被匹配到了,每一次都应该把所有单词都包括进来并且是相邻的 let record = [] let len = Object.keys(poses).length // 如果没有单词的位置说明处理结束了 if (len < 1) { return -1 } while (1) { // 每次循环应该把记录清空 record.length = 0 // 按照起始位置进行升序排序 let minV = Number.MAX_SAFE_INTEGER let minK = '' // 优先找到所有单词其实位置最小的单词开始匹配 for (let [k, v] of Object.entries(poses)) { if (!v.length) { return false } else { if (v[0].start < minV) { minK = k minV = v[0].start } } } if (!minK) { return false } // 起始位置最小的单词 let first = poses[minK].shift() if (!first) { return false } // 记录下这个起始位置 let start = first.start // 记录words列表中的单词 record.push(words.findIndex(item => item === minK)) // 每次循环要匹配到所有单词 for (let i = 1; i < wordsLen; i++) { for (let j = 0, next; j < wordsLen; j++) { if (record.includes(j)) { continue } if (poses[words[j]][0] === undefined) { return false } next = poses[words[j]].find(item => item.start === first.end) if (next) { record.push(j) first = next break } } } // 如果所有单词的顺序是挨着的,记录下当前的起始位置 if (record.length === wordsLen && !record.find(item => item === undefined)) { result.push(start) } } } match(pos) // 对 result 去重,如 result=[1,1,2,3] [...new Set(result)]===[1,2,3] return [...new Set(result)] }
复原IP地址
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
示例:
输入: "25525511135"
输出: ["255.255.11.135", "255.255.111.35"]
//思路: // IP三个点分成四部分 每部分0-255 每部分最多不超过3位 // 示例 25525511135 第一部分可能是 2 25 255 // 如果第一部分是2 那么 第二部分 5 55 // 如果第二部分 5 那么 第三部分 剩下的超过6位 不满足 export default (str) => { //保存所有可能的结果 let r = [] //递归函数 上次处理结果,待处理字符串 let search = (cur, sub) => { //已经分为四部分且四部分长度相等 if (cur.length === 4 && cur.join('') === str) { r.push(cur.join('.')); } else { //每部分长度最多是3位 for (let i = 0, tmp, len = Math.min(3, sub.length); i < len; i++) { //待处理子串,取 1位 2位 3位 tmp = sub.substr(0, i + 1); if (tmp < 256) { //之前的合并进去 子串位置+1 search(cur.concat([tmp]), sub.substr(i + 1)) } } } } search([], str); return r; }
完全二叉搜索树
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
节点的左子树只包含小于当前节点的数。
节点的右子树只包含大于当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入:
2
/ \
1 3
输出: true
示例 2:
输入:
5
/ \
1 4
/ \
3 6
输出: false
解释: 输入为: [5,1,4,null,null,3,6]。
class Node { constructor(val) { this.val = val; this.left = this.right = undefined; } } class Tree { constructor(data) { let root = new Node(data.shift()); //遍历数据,插入树 data.forEach(item => { this.insert(root, item); }); return root; } insert(node, data) { if (node.val > data) { if (node.left === undefined) { node.left = data; } else { this.insert(node.left, data); } } else { if (node.right === undefined) { node.right = data; } else { this.insert(node.right, data); } } } static walk(root) { if (!root.left && !root.right) { return true; } else if ((root.left && root.left.val > root.val) || (root.right && root.right.val < root.val)) { return false; } else { return Tree.walk(root.left) && Tree.walk(root.right); } } }
镜像对称树
给定一个二叉树,检查它是否是镜像对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
1
/ \
2 2
/ \ / \
3 4 4 3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
1
/ \
2 2
\ \
3 3
class Node { constructor(val) { this.value = val; this.left = this.right = undefined; } } //构造二叉树 class Tree { constructor(data) { //临时存储所有节点,方便寻找父子节点 let nodeList = []; //顶点节点 let root; for (let i = 0; i < data.length; i++) { let node = new Node(data[i]); nodeList.push(node); if (i > 0) { //计算属于哪一层 let n = Math.floor(Math.sqrt(i + 1)); //当前层起始点的索引 let q = Math.pow(2, n) - 1; //上一层起始点的索引 let p = Math.pow(2, n - 1) - 1; //当前节点的父节点 let parent = nodeList[p + Math.floor((i - q) / 2)]; if (parent.left) { parent.right = node; } else { parent.left = node; } } } root = nodeList.shift(); nodeList = null; return root; } static isSymmetry(root) { if (!root) { return false } let walk = (left, right) => { if (!left && !right) { return true } if ((left && !right) || (!left && right) || left.value !== right.value) { return false; } return walk(left.left, right.right) && walk(left.right, right.left); } return walk(root.left, root.right) } }
常见应用题
Promise超时处理
//Promise 超时处理 //if < 1s return as usual //if >1s return timeout //直接通过一个promise里面抢resolve和reject的执行权 const myFetch=()=>{ return new Promise((resolve,reject)=>{ let timer=setTimeout(()=>{ reject('timeout') },1000); fetch(url,{headers:{"content-type":"application/text"}}) .then(res=>res.json()).then( res=>{ clearTimeout(timer); resolve(res); timer=null; } ) }) } //利用Promise.race()先返回最快的那个resolve const myFetch=()=>{ Promise.race([ new Promise((resolve,reject)=>{ setTimeout(resolve('timeout'),1000); }), fetch(url,{headers:{"content-type":"application/text"}}) .then(res.res.json()).then( res=>{ resolve(res); }) ] ) } //用async 真呀么真快乐 const myFetch=async ()=>{ let start = new Date().getTime() let result = await (await fetch(url)).json() let time = new Date().getTime() -start if (time > 1000) return time; else return result }
有N个Promise,要求按顺序执行
//n个promise的数组,按要执行的顺序排好 var promiseArr=[new Promise(()=>{console.log('1')}), new Promise(()=>{console.log('2')}), new Promise(()=>{console.log('3')}), new Promise(()=>{console.log('4')}) ]; function* donebyOrder(arr){ let len=arr.length; for(let i=0;i<len;i++){ yield arr[i]; } } let gen=donebyOrder(promiseArr) for(let i=1;i<promiseArr.length;i++){ gen.next(); }
剑指Offer(未完)
正则表达式匹配
请实现一个函数用来匹配包括'.'和''的正则表达式。模式中的字符'.'表示任意一个字符,而''表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"abaca"匹配,但是与"aa.a"和"ab*a"均不匹配
function match(s, pattern) { //正常模式:完全匹配 //.模式:跳过一位匹配即可 //* 比较复杂,可能0个,可能很多个 //考虑每次匹配完,就把前面的丢掉,继续递归看后面的满不满足条件 let isMatch=(s,p)=>{ //边界是字符串和正则都为空 if(p.length<=0){ return !s.length; } //开始判断当前第一个字符是不是匹配的 let match=false; if(s.length>0&&(s[0]===p[0]||p[0]==='.')){ match=true; } //处理有*的情况 if(p.length>1&&p[1]==='*'){ //匹配0个,或者丢弃一个继续匹配 sssa s*a 丢掉一个s 匹配ssa s*a return isMatch(s,p.slice(2))||(match&&isMatch(s.slice(1),p)); }else{ //正常匹配 return match&&isMatch(s.slice(1),p.slice(1)); } } return isMatch(s,pattern); }
替换空格
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
//正则表达 function replaceSpace(str){ return str.replace(new RegExp(/\s/g),'%20'); }
数组中重复的数字
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
function duplicate(numbers, duplication) { // write code here //这里要特别注意~找到任意重复的一个值并赋值到duplication[0] //函数返回True/False if(numbers===null&&numbers<0){return false;} for(let i=0,tmp;i<numbers.length;i++){ if(numbers[i]<0&&numbers[i]>length-1) return false; //不等于就交换 let temp=undefined;//临时变量保存交换结果 while(numbers[i]!==i){ //找到第一个就返回 if(numbers[i]===numbers[numbers[i]]){ duplication[0]=numbers[i]; return true; } temp=numbers[i]; numbers[i]=numbers[temp]; numbers[temp]=temp; } } return false; }
不修改数组找出重复的数字
在一个长度为n+1的数组里,,所有数字都在1-n的范围中,所以数组中至少有一个数是重复的。 请找出数组中任意一个重复的数字,但不可以修改这个数组。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是2或者3。
思路一就是申请一个新的数组,先复制,然后和上面那道题一样。
但如果要避免使用O(n)的辅助空间,那就可以借鉴二分查找的思路。从1-n的数字从中间数字m分为两个部分,如果1-m部分数字数目超过m,那么这一半的区间里一半为m+1n。如果1m的数字的数目超过m,那么这一半的区间里一定包含重复数字。
{2,3,5,4,3,2,6,7},数字在1-7的范围内,从中间3开始,1-3 和4-7;统计1-3这3个数字在在数组中出现的次数。一共出现了4次,那么一定有重复的。接着再1-3分为1-2和3-3,统计1-2在数组中出现的次数,有2次;统计3-3在数组中出现的次数,有2次,那么3重复了。
//输入一个数组,数字的范围 function duplicate(arr,nums){ if(arr==null&&nums<=0)return -1; let start=1,end=nums; while(end>=start){ let middle=Math.ceil((end-start)/2)+start; let count=countnum(arr,nums,start,middle); if(end ===start){ if(count>1) return start; } if(count>middle-start+1){ end=middle; }else{ start=middle+1; } } return -1; } //计算区间内数字出现的个数 function countnum(arr,nums,start,end){ if(nums==null) return 0; let counts=0; for(let i=0;i<nums;i++){ if(nums[i]>=start&&nums[i]<=end){ counts++; } } return counts; }
二维数组中的查找
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
//暴力 function Find(target, array) { let result=false; let len=array.length; for(let i=0;i<len;i++){ for(let j=0;j<array[i].length;j++){ if (array[i][j]===target){ result=true; } } } return result; }
这道题说了,每一行都是从左到右递增的顺序,每一列都是从上到下的递增的顺序,所以完全可以利用这一点进行。比如从二维矩阵的右上角开始,目标比它小,只需要在它右边查找,目标比它大,那直接向下查找即可。
当target小于元素a[row][col]
时,那么target必定在元素a所在行的左边,即col--;当target大于元素a[row][col]
时,那么target必定在元素a所在列的下边,即row++;
function Find(target,array){ //右上角a[0][col] let row=0,col=array[0].length-1; while(row<array.length&&col>=0){ if (target==array[row][col]){ return true; } if(target<array[row][col]){ col--; }else{ row++; } } return false; }
从尾到头打印链表
输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。
剑指Offer书上的题解是:从尾到头输出可以想象是一个递归的过程,每次都先输出后面一个节点的值。
不过用js不用这么麻烦,正常遍历,每次都从数组头部插入,这样结果就是逆序的了。
/*function ListNode(x){ this.val = x; this.next = null; }*/ function printListFromTailToHead(head) { let result=[]; while(head){ result.unshift(head.val); head=head.next; } return result; }
重建二叉树
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{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) { let node=null; if(pre.length>1){ let head=pre[0]; let vinheadindex=vin.indexOf(head); let vinleft=vin.slice(0,vinheadindex); let vinright=vin.slice(vinheadindex+1); pre.shift();//头节点丢掉 let preleft=pre.slice(0,vinleft.length); let preright=pre.slice(vinleft.length); node={ val:head, left:reConstructBinaryTree(preleft,vinleft), right:reConstructBinaryTree(preright,vinright) } }else if (pre.length===1){ node={ val:pre[0], left:null, right:null } } return node; }
二叉树的下一个节点
给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
思路:
如果这个节点有右子树,那么下一个节点就是右子树的最左子节点。
如果节点没有右子树:
节点是父节点的左子节点,那下一个节点是父节点。
节点是父节点的右子节点,那就向上找到一个是它父节点的左子节点的节点,如果节点存在,这个节点的父节点就是下一个节点。
/*function TreeLinkNode(x){ this.val = x; this.left = null; this.right = null; this.next = null; }*/ function GetNext(pNode) { // write code here if(!pNode) return null; let p; //存在右子树 if(pNode.right){ p=pNode.right; while(p.left){ p=p.left; } }else{ //父节点 p=pNode.next; if(p&&p.right==pNode){ //是父节点的右节点,向上查一个父亲节点的左节点 while(p.next && p.next.right == p){ p = p.next; } if(p.next == null){ p = null; }else{ p = p.next; } } } return p; }
用两个栈实现队列
用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
思路:
先依次把 a,b,c 插stack1中,如果要弹出a,把c,b压入stack2中,弹出a,此时stack1为空,再从stack2弹出b……依此类推。如果在中途插入新的元素,那么必须等stack2全部为空之后再进行操作。
let stack1=[], stack2=[]; function push(node) { stack1.push(node); } function pop() { if(stack2.length==0){ //stack2为空,说明全部在stack1中,需要从那边压过来 if(stack1.length==0){ return null; }else{ //依次压入 let len =stack1.length; for(let i=0;i<len;i++){ stack2.push(stack1.pop()); } return stack2.pop(); } }else{ //stack2不为空直接弹 return stack2.pop(); } }
斐波拉契数列
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。
n<=39
//递归方法 function Fibonacci(n){ if(n<=0) return 0; if(n===1) return 1; return Fibonacci(n-1)+Fibonacci(n-2); }
这样做的话效率很低,其实做的过程中的中间数都是重复的,把中间值保存一下,需要计算的时候查找,就能解决这个问题。
function Fibonacci(n){ let res=[0,1]; if(n<=1){return res[n];} let n1=0,n2=1; let tmp; for(let i=2;i<=n;i++){ tmp=n1+n2; n1=n2; n2=tmp; } return tmp; }
变种题 青蛙跳台阶
一只青蛙一次可以跳1级台阶或者2级台阶,求问该青蛙跳上一个n级台阶有多少种跳法。
其实就是斐波拉契数列,因为n级台阶的跳法看成是n的函数,f(n)。那么有两种跳法,一种只跳1级,即f(n-1);一种跳两级,即f(n-2)。那么f(n)=f(n-1)+f(n-2)。
function jumpFloor(number) { let result=[0,1,2]; if(number<=2){return result[number];} let res; let n1=1,n2=2; for(let i=3;i<=number;i++){ res=n1+n2; n1=n2; n2=res; } return res; }
拓展
一只青蛙可以跳1级、2级……n级。此时青蛙跳上一个n级的台阶有多少种跳法。
数学归纳法是f(n)=2^(n-1)
function jumpFloorII(number) { return Math.pow(2,number-1); }
不过,用位运算更快
function jumpFloorII(number) { return 1<<(--number); }
变种题2 覆盖矩形
可以用2*1
的小矩形横着或者竖着去覆盖大矩形。请问用8个2*1
的小矩形无重叠覆盖一个2*8
的大矩阵总共有多少种方法?
先把2*8
的覆盖方法记为f(8),用第一个覆盖时,可以横着或者竖着。竖着放的时候,还剩下2*7
的矩形,记为f(7)。横着放的时候,另一个也只能横着放,还剩下2*6
的矩形,记为f(6)。所以还是斐波那契数列。
function rectCover(number) { let res=[0,1,2]; if(number<=2){return res[number];} let tmp,n1=1,n2=2; for(let i=3;i<=number;i++){ tmp=n1+n2; n1=n2; n2=tmp; } return tmp; }
旋转数组的最小数字
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
思路:
暴力破解其实也挺快的,但是算法复杂度为O(n)。这样肯定不够优雅。
优雅的解题思路是,把旋转之后的数组看做是两个排序的子数组,而且前面子数组的元素都大于或者等于后面子数组的元素。最小的元素就是这两个子数组的分界线。
可以用两个指针首尾二分法查找,以上面的数组为例,开始指针指向3,2。二分法中间为5,大于3,它一定位于递增数列第一个子数组。把头部指针指向5。此时位于中间的1,小于2,位于递增数组第二个子数组,尾指针指向1。此时指针差为1,输出尾指针的元素即可。
特殊情况是当中间三个数的数值都一样的时候,那只能遍历输出了。
function minNumberInRotateArray(rotateArray) { let start=0,end=rotateArray.length-1;//记录下标 let middle=0; while(rotateArray[start]>=rotateArray[end]){ if(end-start==1){ middle=end; break; } middle=Math.floor((end+start)/2); if(rotateArray[middle]>=rotateArray[start]){ start=middle; }else if(rotateArray[middle]<=rotateArray[end]){ end=middle; } //如果下标start end middle的值一样,那只能顺序查找了 if(rotateArray[start]===rotateArray[middle]&&rotateArray[start]===rotateArray[end]){ let res=rotateArray[start]; for(let i=start+1;i<=end;i++){ if(res>rotateArray[i]){ res=rotateArray[i]; } } return res; } } return rotateArray[middle]; }
常规算法思路是上面这样,但在js中可以利用数组的API来计算……但这样就没啥意思了不是么。
function minNumberInRotateArray(rotateArray) { return Math.min(...rotateArray); }
矩阵中的路径
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则之后不能再次进入这个格子。 例如 a b c e s f c s a d e e 这样的3 X 4 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。
思路:
首先,在矩阵中任选一个格子作为路径的起点。如果路径上的第i个字符不是ch,那么这个格子不可能处在路径上的第i个位置。如果路径上的第i个字符正好是ch,那么往相邻的格子寻找路径上的第i+1个字符。除在矩阵边界上的格子之外,其他格子都有4个相邻的格子。
重复这个过程直到路径上的所有字符都在矩阵中找到相应的位置。由于回朔法的递归特性,路径可以被开成一个栈。当在矩阵中定位了路径中前n个字符的位置之后,在与第n个字符对应的格子的周围都没有找到第n+1个字符,这个时候只要在路径上回到第n-1个字符,重新定位第n个字符。
由于路径不能重复进入矩阵的格子,还需要定义和字符矩阵大小一样的布尔值矩阵,用来标识路径是否已经进入每个格子。 当矩阵中坐标为(row,col)的
格子和路径字符串中相应的字符一样时,从4个相邻的格子(row,col-1),(row-1,col),(row,col+1)以及(row+1,col)中去定位路径字符串中下一个字符。如果4个相邻的格子都没有匹配字符串中下一个的字符,表明当前路径字符串中字符在矩阵中的定位不正确,我们需要回到前一个,然后重新定位。
一直重复这个过程,直到路径字符串上所有字符都在矩阵中找到合适的位置。
function hasPath(matrix, rows, cols, path) { if(path.length===0) return true; if(rows*cols< path.length) return false; let status=[]; for(let i=0;i<rows;i++){ status.push([]); for(let j=0;j<cols;j++){ status[i][j]=false; } } //找到第一个符合的path for(let i=0;i<rows;i++){ for(let j=0;j<cols;j++){ if(matrix[i*cols+j]===path[0]){ if(path.length===1){ return true; } status[i][j]=true; if(find(matrix,rows,cols,i,j,path.slice(1),status)){ return true; } status[i][j]=false; } } } return false; } function find(matrix,rows,cols,row,col,path,status){ if(row>0 && matrix[(row-1)*cols+col]===path[0]&&status[row-1][col]===false){ if(path.length===1){ return true; } status[row-1][col]=true; if(find(matrix,rows,cols,row-1,col,path.slice(1),status)){ return true; } status[row-1][col]=false; } if(row<rows-1&&matrix[(row+1)*cols+col]===path[0]&&status[row+1][col]===false){ if(path.length===1){ return true; } status[row+1][col]=true; if(find(matrix,rows,cols,row+1,col,path.slice(1),status)){ return true; } status[row+1][col]=false; } if(col>0&&matrix[row*cols+col-1]===path[0]&&status[row][col-1]===false){ if(path.length==1){ return true; } status[row][col-1]=true; if(find(matrix,rows,cols,row,col-1,path.slice(1),status)){ return true; } status[row][col-1]=false; } if(col<cols-1&&matrix[row*cols+col+1]===path[0]&&status[row][col+1]===false){ if(path.length===1){ return true; } status[row][col+1]=true; if(find(matrix,rows,cols,row,col+1,path.slice(1),status)){ return true; } status[row][col+1]=false; } return false; }
机器人的运动范围
地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?
从0,0开始移动,当它准备进入坐标为(i,j)的格子时,通过检查坐标的数位和来判断机器人能否进入,如果可以进入,再判断它是否能进入上下左右的四个格子。
function movingCount(threshold, rows, cols) { // write code here var flag = []; for(var i=0;i<rows;i++){ flag.push([]); for(var j =0;j<cols;j++){ flag[i][j] = 0; } } return count(0,0,rows,cols,threshold,flag); } function count(i,j,rows,cols,threshold,flag){ if(i<0||j<0||i>=rows||j>=cols||flag[i][j]||sumNum(i)+sumNum(j)>threshold){ return 0; } flag[i][j] = 1; return count(i+1,j,rows,cols,threshold,flag) +count(i-1,j,rows,cols,threshold,flag) +count(i,j+1,rows,cols,threshold,flag) +count(i,j-1,rows,cols,threshold,flag)+1; } function sumNum(num){ var sum =0; do{ sum+=num%10; }while((num = Math.floor(num/10))>0); return sum; }
剪绳子
有一根长度为n的绳子,把绳子剪成m段(m、n都是整数,n>1且m>1),每段绳子的长度记为k[0],k[1]……k[m]。请问他们长度的乘积最大是多少?例如,绳子长度是8的时候,把它剪成长度分别为2、3、3的三段,此时最大乘积是18。
动态规划思路:在剪第一刀的时候,有n-1种可能的选择(1,2,3,…n-1),因而f(n)=max(f(i)*f(n-i)) (0<i<n)。
先得到f(2) f(3) 再从它们得到f(4)……f(n)
function maxProductAfterCutting(length){ if(length<2) return 0; if(length==2)return 1; if(length==3)return 2; let product=[0,1,2,3]; for(let i=4,max;i<=length;i++){ max=0; for(let j=1;j<=i/2;j++){ let temp=product[j]*product[i-j]; if(max<temp){ max=temp; } product[i]=max; } } return product[length]; }
贪心思路:
如果按照这个策略来剪绳子:
当n大于等于5时,尽可能多的剪长度为3的绳子;剩下的长度为4时,把绳子剪成两段长度为2的绳子。
function maxProductAfterCutting(length){ if(length<2) return 0; if(length==2)return 1; if(length==3)return 2; //尽可能多剪3 let timesofthree=length/3; //绳子剩下的长度为4时,剪成2*2 if(length-timesofthree*3==1){ timesofthree-=1; } let timesoftwo=(length-timesofthree*3)/2; return Math.pow(3,timesofthree)*Math.pow(2,timesoftwo); }
二进制中1的个数
输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
思路:如果最后一位是1,那么减去1,最后一位变成0,其他不变;如果最后一位是0,减去1之后,最右边的1会变成0,后面一位变成1。
把一个整数减去1然后与它做与运算,就能把整数最右边的1变成0,这样的话,有多少个1就可以进行多少次操作。
function NumberOf1(n){ let count=0; while (n){ ++count; n=(n-1)&n } return count; }
数值的n次方
给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。
思路:假设指数为32,如果有16次方,只需要再平方一次就好;16次方是8次方的平方;8次方是4次方的平方……所以对偶数而言就是 a的二分之n方相乘;对奇数而言就是 a的二分之n-1方相乘再乘以a,于是可以用递归。
不过好像没有考虑到幂次是负数的情况,不过也不要紧:分开处理一下就好了。
function Power(base, exponent){ let result=power(base,Math.abs(exponent)); function power(base,exponent){ if(exponent===0){return 1;} if(exponent===1){return base;} let result=Power(base,exponent>>1); result *=result; if(exponent& 0x1===1){ //奇数再乘以a result *=base; } return result } return exponent>=0? result:1/result; }