前端复习企划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;
}
全部评论

相关推荐

11-02 09:49
已编辑
货拉拉_测试(实习员工)
热爱生活的仰泳鲈鱼求你们别卷了:没事楼主,有反转查看图片
点赞 评论 收藏
分享
Pandaileee:校友加油我现在也只有一个保底太难了
点赞 评论 收藏
分享
3 2 评论
分享
牛客网
牛客企业服务