剑指offer算法——js版(一)

79 阅读5分钟

写这篇文章主要是方便自己复习,mark

剑指 Offer 03. 数组中重复的数字

找出数组中重复的数字。

在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

示例 1:

输入:
[2, 3, 1, 0, 2, 5, 3]
输出:23 
// 方法一:如果数组下标与其数组对应值相等,这个数字可以说就在其对应的位置上了,所以我们要找的就是重复遇到的那个数字。
/**
 * @param {number[]} nums
 * @return {number}
 */
var findRepeatNumber = function(nums) {
    const len = nums.length;
    for(let i = 0; i < len; i++){
        // 判断i是否已经在i位置上
        while((num = nums[i]) !== i){
          // i之前的数字已经在其对应的位置上了,如果发生重复,则说明当前数字为所要找的数字
            if(num === nums[num]){
                return num;
            }
            [nums[i], nums[num]] = [nums[num], nums[i]];
        }        
    }
};

// 方法二:利用set
var findRepeatNumber = function(nums) {
    let s = new Set();
    for(var i in nums){
        let curLen = s.size;
        s.add(nums[i]);
        if(curLen == s.size) return nums[i];
    }
};

/*
  for in遍历的是数组的索引(即键名),而for of遍历的是数组元素值。
  for of遍历的只是数组内的元素,而不包括数组的原型属性。
*/

剑指 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}
 */
 // 方法一:数组扁平化,利用es6特性
var findNumberIn2DArray = function(matrix, target) {
    //使用 Infinity 作为深度,展开任意深度的嵌套数组
    return matrix.flat(Infinity).includes(target);
};

// 方法二:利用本题数组的特点
var findNumberIn2DArray = function(matrix, target) {
    const rowNum = matrix.length;
    if(!rowNum) return false;
    const colNum = matrix[0].length;
    if(!colNum) return false;

    let row = 0;
    let col = colNum-1;

    while(row < rowNum && col >= 0){
        if(matrix[row][col] === target){
            return true;
        }else if(matrix[row][col] > target){
            col--;
        }else {
            row++;
        }
    }

    return false;
};

//方法三:二分查找提高搜索速率
var findNumberIn2DArray = function(matrix, target) {
    let len = matrix.length;
    if(!len) return false;

    for(let i = 0; i < len; i++){
        let item = matrix[i];
        if(target >= item[0]){
            if(bs(item, target)) return true;
        }else {
            return false;
        }
    }

    return false;
};
// 二分查找函数
function bs(arr, target){
    let i = 0, j = arr.length;
    while(i < j){
        let middle = i + ((j - i) >> 1);
        if(arr[middle] === target){
            return true;
        }else if(arr[middle] > target){
            j = middle;
        }else {
            i = middle + 1;
        }
    }
    return false;
}

剑指 Offer 05. 替换空格

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

示例 1:

输入:s = "We are happy."
输出:"We%20are%20happy."
/**
* @param {string} s
* @return {string}
*/
// 方法一:利用split和join  
// split()方法用于把一个字符串分割成字符串数组。 join()方法用于把数组中的所有元素放入一个字符串。
var replaceSpace = function(s) {
   return s.split(" ").join("%20");
    // 方法二:利用正则
 // return s.replace(/ /g, "%20");
};

// 方法三:老老实实搜索到空格的地方就进行替换
var replaceSpace = function(s) {
   if(!s || !s.length){
       return '';
   }

   const len = s.length;
   let empty = 0;

   for(let i = 0; i < len; i++){
       if(s[i] == ' '){
           empty++;
       }
   }

   const newLen = empty*2+len;
   let news = new Array(newLen);
   for(let i = 0, j = 0; j < len; j++){
          if(s[j] == ' '){
              news[i++] = '%';
              news[i++] = '2';
              news[i++] = '0';
          }else {
              news[i++] = s[j];
          }
   }

   return news.join("");

};

剑指 Offer 06. 从尾到头打印链表

输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。

示例 1:

输入:head = [1,3,2]
输出:[2,3,1] 
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} head
 * @return {number[]}
 */
// 方法一:直接利用reverse函数,不过要先将链表转换为数组
var reversePrint = function(head) {
    if(!head) return [];

    let arr = [];

    while(head){
        arr.push(head.val);
        head = head.next;
    }

    return arr.reverse();
};

// 方法二:使用递归
var reversePrint = function(head) {
    if(!head){
        return [];
    }
	   
    var arr = reversePrint(head.next);
    //一层一层进入到最后一个节点,然后再由最后面将值一层一层推进数组里面
    arr.push(head.val);

    return arr;
};

剑指 Offer 07. 重建二叉树

输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

例如,给出

前序遍历 preorder = [3,9,20,15,7] 中序遍历 inorder = [9,3,15,20,7] 返回如下的二叉树:

    3
   / \
  9  20
    /  \
   15   7
/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {number[]} preorder
 * @param {number[]} inorder
 * @return {TreeNode}
 */
// 方法一: (常规)递归法
/*
  前序遍历:根节点,左子树,右子树
  中序遍历:左子树,根节点,右子树
*/
var buildTree = function(preorder, inorder) {
    if(!preorder.length || !inorder.length){
        return null;
    }

    const rootVal = preorder[0];
    const node = new TreeNode(rootVal);
	
    // 既是左子树的节点数目,又是中序遍历根节点的下标
    let i = 0;
    for(; i < inorder.length; i++){
        if(inorder[i] == rootVal){
            break;
        }
    }

    node.left = buildTree(preorder.slice(1, i+1), inorder.slice(0, i));
    node.right = buildTree(preorder.slice(i+1), inorder.slice(i+1));

    return node;
};

// 方法二:迭代法
var buildTree = function(preorder, inorder) {
    if(!preorder.length || !inorder.length){
        return null;
    }

    const root = new TreeNode(preorder[0]);
    const len = preorder.length;
    // 栈,存放根节点和左子树,遇到右节点就把根节点和左子树都推出去
    let stack = [root];
    let inorderIndex = 0;

    for(let i = 1; i < len; i++) {
        let pre = preorder[i];
        let node = stack[stack.length-1];
        if(node.val !== inorder[inorderIndex]){
            node.left = new TreeNode(pre);
            stack.push(node.left);
        }else {
            while(stack.length && (stack[stack.length-1].val === inorder[inorderIndex])){
                node = stack.pop();
                inorderIndex++;
            }
            node.right = new TreeNode(pre);
            stack.push(node.right);
        }
    }
    
    return root;
};

剑指 Offer 09. 用两个栈实现队列

用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )

示例 1:

输入:
["CQueue","appendTail","deleteHead","deleteHead"]
[[],[3],[],[]]
输出:[null,null,3,-1]
示例 2:

输入:
["CQueue","deleteHead","appendTail","appendTail","deleteHead","deleteHead"]
[[],[],[5],[2],[],[]]
输出:[null,-1,null,null,5,2]
提示:

1 <= values <= 10000
最多会对 appendTail、deleteHead 进行 10000 次调用
var CQueue = function() {
    this.stack1 = [];
    this.stack2 = [];
};

/** 
 * @param {number} value
 * @return {void}
 */
CQueue.prototype.appendTail = function(value) {
    this.stack1.push(value);
};

/**
 * @return {number}
 */
CQueue.prototype.deleteHead = function() {
    if(this.stack2.length) return this.stack2.pop();
    while(this.stack1.length){
        this.stack2.push(this.stack1.pop());
    }
    return this.stack2.pop() || -1;
};

/**
 * Your CQueue object will be instantiated and called as such:
 * var obj = new CQueue()
 * obj.appendTail(value)
 * var param_2 = obj.deleteHead()
 */

延伸阅读
实际上现实中也有使用两个栈来实现队列的情况,那么为什么我们要用两个stack来实现一个queue?

其实使用两个栈来替代一个队列的实现是为了在多进程中分开对同一个队列对读写操作。一个栈是用来读的,另一个是用来写的。当且仅当读栈满时或者写栈为空时,读写操作才会发生冲突。

当只有一个线程对栈进行读写操作的时候,总有一个栈是空的。在多线程应用中,如果我们只有一个队列,为了线程安全,我们在读或者写队列的时候都需要锁住整个队列。而在两个栈的实现中,只要写入栈不为空,那么push操作的锁就不会影响到pop。

剑指 Offer 10- I. 斐波那契数列

写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项。斐波那契数列的定义如下:

F(0) = 0,   F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 01 开始,之后的斐波那契数就是由之前的两数相加而得出。

答案需要取模 1e9+71000000007),如计算初始结果为:1000000008,请返回 1。

示例 1:

输入:n = 2
输出:1
示例 2:

输入:n = 5
输出:5
/**
 * @param {number} n
 * @return {number}
 */

// 方法一:普通的尾递归调用自身+ es6尾调用优化解法
// 优点:不创建新的栈帧,现有的栈帧被重复利用,不会爆栈。
// 缺点:需要重复的清除栈帧的数据,性能不如下面的循环解法。
var fib = function(n) {
    return f(n);
};

function f(n, a=1, b=1){
    if(n === 0) return 0;
    if(n <= 2) return b;
    
    return f(n-1, b, (a+b)%(1e9+7));
}

// 方法二:动态规划法 :思想是通过保存中间的计算结果,减少结果计算时间。
// 优点:空间换时间,所有的计算结果都被缓存,下一次计算直接读取缓存结果。
// 缺点:需要额外的存储空间,空间复杂度高。

var fib = function(n) {
    var dp = [0, 1, 1];

    function f(n, a=1, b=1){
        if(dp[n] != undefined){
            return dp[n];
        }

        dp[n] = f(n-1) + f(n-2);

        return dp[n]%(1e9+7);
    }

    return f(n);
};


// 方法三:循环计算解法
// 优点:每一次的计算结果都能得到利用,只保存前面的两个计算结果。
var fib = function(n) {
    var a = 0, b = 1, c = 1;

    if(n <= 1) return n;

    while(n-- > 1){       
        c = (a + b)%(1e9+7);
        a = b;
        b = c;
      
      // [a, b] = [b, (a + b)%(1e9+7)]; 解构赋值,不用变量c
    }

    return c;
};

剑指 Offer 10- II. 青蛙跳台阶问题

一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

示例 1:
输入:n = 2
输出:2

示例 2:
输入:n = 7
输出:21

示例 3:
输入:n = 0
输出:1
/*
动态规划:
    分析:
    跳到第 5 个台阶的方法:
    1.从第 4 个跳过来
    2.从第 3 个跳过来
    3.所以 dp[5] = dp[5 - 1] + dp[5 - 2]

    即类比得到状态转移方程:
    dp[n] = dp[n - 1] + dp[n - 2]
*/
/**
 * @param {number} n
 * @return {number}
 */
// 方法一:建立一个长度为n+1的数组
var numWays = function(n) {
    let dp = [1, 1];

    for(let i = 2; i <= n; i++){
        let c = dp[i-1] + dp[i-2];
        dp[i] = c > (1e9+7)? c%(1e9+7): c;
    }

    return dp[n];
};
// 方法二:
/*
  按照上面的状态转移方程需要创建一个n+1的数组,但是从上面的转移方程可发现 dp[i] 只与dp[i-2] dp[i-1]有关,
  所以我们用两个常量来存储即可,也就是采用滚动数组的思想;
  举个例子:第一次循环时:a=1;b=2,c=a+b=3;进入下一次循环时把b赋值给a,c赋值给b,a+b赋值给c,直到循环结束。
*/
var numWays = function(n) {
    if(n === 0) return 1;
    if(n < 3) return n;

    let a = 1, b = 2;

    for(let i = 3; i <= n; i++){
        let temp = b;
        b = (a + b)%(1e9+7);
        a = temp;
    }

    return b;
};