青铜选手打怪记录——剑指offer(javascript版)

1,729 阅读28分钟

字符串的排列

  • 题目描述:

输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。

  • 输入描述:

输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。

实现思路推荐看全排列算法JavaScript实现 思路非常之清晰,我超级喜欢看这样的代码解答,但是他原文的代码是无法通过牛客网编译的,原因在于题目要求按字典顺序

  • 注意: 递归后的结果需要去重+排序
function Permutation(str)
{
    var result = [];
    // 初始条件:长度为1
    if (str.length < 2) {
        return str;
    } else {
        // 求剩余子串的全排列
        var preResult = Permutation(str.slice(1)); 
        // 遍历全排列数组
        for (var j = 0; j < preResult.length; j++) { 
            for (var k = 0; k < preResult[j].length + 1; k++) {
                // 将首字母插入k位置 
                var temp = preResult[j].slice(0, k) + str[0] + preResult[j].slice(k);
                result.push(temp);
            }
        }
        // 排序+去重
        return unique(result).sort();
    }
}
// 去重
function unique(arr){
    var res = [];
    for (var i = 0; i< arr.length; i++){
        if (res.indexOf(arr[i]) === -1){
            res.push(arr[i]);
        }
    }
    return res;
}

数组中出现次数超过一半的数字

  • 题目描述:

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。

  • 思路:
    1. 对数组排序
    2. 如果存在这样的数字,那一定位于排序后数组的中位数位置,因此找到数组中位数num
    3. 遍历数组,看看中位数出现的次数是否超过数组长度的一半
      • 若超过,返回num;
      • 若不超过,返回0;
function MoreThanHalfNum_Solution(numbers)
{
    var len = numbers.length;
    if (len < 1){
        return 0;
    }
    // 数组排序
    var sort_nums = numbers.sort();
    // 获取中位数
    var num = sort_nums[Math.floor(len/2)];
    // 遍历数组,查看中位数出现的次数
    var count = 0;
    for (var i = 0; i < len; i++){
        if (numbers[i] === num){
            count++;
        }
    }
    if (count > len/2){
        return num;
    } else {
        return 0;
    }
    
}

最小的K个数

题目描述:

输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。

function GetLeastNumbers_Solution(input, k)
{
    if (k < 1 || k > input.length){
        return [];
    }
    // 将数组由小到大排序;
    var sortArr = input.sort(function(a, b){
        return a - b;
    });
    return sortArr.slice(0,k);
}

连续子数组的最大和

  • 题目描述

HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)

  • 题目翻译:

输入一个数组,数组里有正数也有负数(注意有可能全是负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)

  • 注意: 初始max一定要是array[0],如果初始化为0,那么如果数组全部为负时就会返回0,这是错误的。
function FindGreatestSumOfSubArray(array)
{
    // 错误处理
    if (array === null || array.length < 1) {
        return false;
    }
    var max = array[0]; // 存储最大连续子序列的和
    var sum = 0; // 存储以第i个数字结尾的子序列的最大和
    for (var i = 0; i < array.length; i++) {
        if (sum < 0){
            sum = array[i];
        } else {
            sum += array[i];
        }
        
        if (max < sum) {
            max = sum;
        }
    }
    return max;
}

整数中1出现的次数(从1到n整数中1出现的次数)

题目描述

求出113的整数中1出现的次数,并算出1001300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。

  • 暴力解
function NumberOf1Between1AndN_Solution(n)
{
    var count = 0;
    for (var i = 0; i <= n; i++){
        let num = i;
        while(num){
            if (num%10 === 1){
                count++;
            }
            num = Math.floor(num/10);
        }
    }
    return count;
}

把数组排成最小的数

题目描述

输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。

  • 定义新的排序规则,也就是把前一个数和后一个数拼接起来的数,然后再与后一个数和前一个数拼接起来的数比较字典序
function PrintMinNumber(numbers)
{
    numbers.sort(function(a, b){
        let c1 = `${a}${b}`;
        let c2 = `${b}${a}`;
        return c1 > c2;
    })
    var min = '';
    numbers.forEach(item => {
        min += item;
    });
    return min;
}

丑数

题目描述

把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。

主要在于理解丑数的概念,只包含因子2、3和5的数称作丑数,那么我们可以先把因子2、3和5分离出来,那么剩下的就是其他因子,看是否为1,为1的话说明没有其他因子,那就为丑数。不是1的话说明有其他因子,那么就不是丑数。

  • 第一种暴力解法,缺点是连非丑数的也计算力,会超时。
function GetUglyNumber_Solution(index) {
  if (index <= 1) return 0;
  let count = 0;
  let num = 0;
  while (count < index) {
    num++;
    if (isUgly(num)) {
      count++;
    }
  }
  return num;
}
function isUgly(num) {
  while (num % 2 === 0) num /= 2;
  while (num % 3 === 0) num /= 3;
  while (num % 5 === 0) num /= 5;
  return num === 1;
}
  • 第二种用到了动态规划的思想,把前面的丑数存着,生成后面的丑数。count2,count3,count5是判断点,用于判断从何处开始选出并乘以对应因子肯定会大于当前数组中最大丑数,而前面的丑数不用考虑。
function GetUglyNumber_Solution(index)
{
    if (index < 1){
        return 0;
    }
    let res = [1];
    let count2 = 0, count3 = 0, count5 = 0;
    for(var i = 1; i < index; i++){
        res[i] = Math.min(res[count2]*2, res[count3]*3, res[count5]*5);
        if (res[i]===res[count2]*2){ count2++; }
        if (res[i]===res[count3]*3){ count3++; }
        if (res[i]===res[count5]*5){ count5++; }
    }
    return res[i-1];
}

第一个只出现一次的字符

题目描述

在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).

《剑指offer》中的思路是:建立一张哈希表,key-value的形式,key用来保存每个字符,value为该字符出现的次数。算法共需要遍历两次字符串:

  • 第一次:统计每个字符出现的次数
  • 第二次:找到第一个出现次数为1的字符进行返回

但是要用js做这件事情可就太简单了,因为我们有现成的方法。

function FirstNotRepeatingChar(str)
{
    var length = str.length;
    for(var i = 0; i < length; i++){
        if(str.indexOf(str[i])===str.lastIndexOf(str[i])){
            return i;
            break;
        }
    }
    return -1;
}

数组中的逆序对

题目描述

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007 输入描述: 题目保证输入的数组中没有的相同的数字

数据范围:

对于%50的数据,size<=10^4

对于%75的数据,size<=10^5

对于%100的数据,size<=2*10^5

题目分析:(这题有点难)

第一反应是采用暴力解法,不过肯定会超时,参考《剑指offer》利用归并排序

先上代码;

function InversePairs(data) {
  if (!data || data.length < 2) return 0;
  const copy = data.slice();
  let count = 0;
  count = mergeCount(data, copy, 0, data.length - 1);
  return count % 1000000007;
}
function mergeCount(data, copy, start, end) {
  if (start === end) return 0;
  const mid = end - start >> 1,
    left = mergeCount(copy, data, start, start + mid), // 注意参数,copy作为data传入
    right = mergeCount(copy, data, start + mid + 1, end); // 注意参数,copy作为data传入
  let [p, q, count, copyIndex] = [start + mid, end, 0, end];
  while (p >= start && q >= start + mid + 1) {
    if (data[p] > data[q]) {
      copy[copyIndex--] = data[p--];
      count = count + q - start - mid;
    } else {
      copy[copyIndex--] = data[q--];
    }
  }
  while (p >= start) {
    copy[copyIndex--] = data[p--];
  }
  while (q >= start + mid + 1) {
    copy[copyIndex--] = data[q--];
  }
  return count + left + right;
}

实现思路:

以数组{7,5,6,4}为例来分析统计逆序对的过程。每次扫描到一个数字的时候,我们不拿它和后面的每一个数字作比较,否则时间复杂度就是O(n^2),我们可以考虑先比较两个相邻的数字。

  • (a) 把长度为4的数组分解成两个长度为2的子数组;
  • (b) 把长度为2的数组分解成两个成都为1的子数组;
  • (c) 把长度为1的子数组合并、排序,并统计逆序对;
  • (d) 把长度为2的子数组合并、排序,并统计逆序对;

合并子数组并统计逆序对的过程如下图: 我们先用两个指针分别指向两个子数组的末尾,并每次比较两个指针指向的数字。

  • 如果第一个子数组中的数字大于第二个数组中的数字,则构成逆序对,并且逆序对的数目等于第二个子数组中剩余数字的个数,如下图(a)和(c)所示。
  • 如果第一个数组的数字小于或等于第二个数组中的数字,则不构成逆序对,如图b所示。

每一次比较的时候,我们都把较大的数字从后面往前复制到一个辅助数组中,确保 辅助数组(记为copy) 中的数字是递增排序的。在把较大的数字复制到辅助数组之后,把对应的指针向前移动一位,接下来进行下一轮比较。

代码一步一步写:

首先明确函数的输入和输出,输入是一个数组,输出是这个数组中的逆序对的总数P%1000000007,所以函数的大体框架应该是这样的:

function InversePairs(data) {
  if (!data || data.length < 2) return 0;
  let count = 0;
  
  
  return count % 1000000007;
}

然后,确定是用归并排序来解决问题的。 这里需要有一个辅助数组copy, 并且假设我们函数mergeCount可以计算出data中的子数组的逆序对总数count。

function InversePairs(data) {
  if (!data || data.length < 2) return 0;
  const copy = data.slice();
  let count = 0;
  // mergeCount(data、copy、start、end)
  count = mergeCount(data, copy, 0, data.length - 1); 
  return count % 1000000007;
}

接下来就是如何实现mergecount(),这里要进行的是数组的拆分、拆到拆不开后开始一边计数一边排序合并、先不管计数和合并,这里首先给个递归结束条件,也就是子数组就一个元素了,无法拆开了。

function mergeCount(data, copy, start, end) {
  if (start === end) return 0;
}

下面我们考虑合并计数,先别管那么细致,一个子数组当中的逆序对总数一共可由三部分组成

  • 拆分后的左子数组中的逆序对
  • 拆分后的右子数组中的逆序对
  • 左边比右边的大形成的逆序对 别忘了mergecount()是干嘛的,给我一个子数组,就能返回这个子数组的逆序对数。
function mergeCount(data, copy, start, end) {
  if (start === end) return 0;
  const mid = Math.floor((end - start)/2);
  var left = mergecount(copy, data, start, start + mid); // 注意参数,copy作为data传入
  var right = mergeCount(copy, data, start + mid + 1, end); // 注意参数,copy作为data传入
  var count = 0;
  
  return count + left + right;
}

下面就是要专注于归并排序,并统计左子数组比右子数组大形成的逆序对数。

如上图所示,这里需要三个指针p、q、copyIndex,初始分别指向两个子数组的末尾和辅助数组的末尾

function mergeCount(data, copy, start, end) {
  if (start === end) return 0;
  const mid = Math.floor((end - start)/2);
  var left = mergecount(copy, data, start, start + mid); // 注意参数,copy作为data传入
  var right = mergeCount(copy, data, start + mid + 1, end); // 注意参数,copy作为data传入
  var [p, q, count, copyIndex] = [start + mid, end, 0, end];
  
  return count + left + right;
}

如果第一个子数组中的数字大于第二个数组中的数字,则构成逆序对,并且逆序对的数目等于第二个子数组中剩余数字的个数,如下图(a)和(c)所示。如果第一个数组的数字小于或等于第二个数组中的数字,则不构成逆序对,如图b所示。

function mergeCount(data, copy, start, end) {
  if (start === end) return 0;
  const mid = end - start >> 1,
    left = mergeCount(copy, data, start, start + mid), // 注意参数,copy作为data传入
    right = mergeCount(copy, data, start + mid + 1, end); // 注意参数,copy作为data传入
  let [p, q, count, copyIndex] = [start + mid, end, 0, end];
  while (p >= start && q >= start + mid + 1) {
    if (data[p] > data[q]) {
      copy[copyIndex--] = data[p--]; // 更新辅助数组
      count = count + q - start - mid; // 统计逆序对
    } else {
      copy[copyIndex--] = data[q--];
    }
  }
 
  return count + left + right;
}

当一个子数组已经遍历完以后,若另一个子数组中还有元素未遍历,那么剩余的元素依次放入辅助数组即可。到次为止,copy当中从start到end是一个由小到大的有序数组。

function mergeCount(data, copy, start, end) {
  if (start === end) return 0;
  const mid = end - start >> 1,
    left = mergeCount(copy, data, start, start + mid), // 注意参数,copy作为data传入
    right = mergeCount(copy, data, start + mid + 1, end); // 注意参数,copy作为data传入
  let [p, q, count, copyIndex] = [start + mid, end, 0, end];
  while (p >= start && q >= start + mid + 1) {
    if (data[p] > data[q]) {
      copy[copyIndex--] = data[p--];
      count = count + q - start - mid;
    } else {
      copy[copyIndex--] = data[q--];
    }
  }
  while (p >= start) {
    copy[copyIndex--] = data[p--];
  }
  while (q >= start + mid + 1) {
    copy[copyIndex--] = data[q--];
  }
  return count + left + right;
}

这道题就结束了

两个链表的第一个公共结点

题目描述

输入两个链表,找出它们的第一个公共结点。

算法解析

比较简单,先在长的链表上跑,直到长的和短的一样长,再一起跑,判断节点相等的时候就可以了

具体步骤

第一步. 首先找到两个链表的长度len1和len2

第二步.然后确定哪条为长链表哪条为短链表,之后确定长度差

第三步.长的链表先走长度差步数,这样能保证两个链表能同时走在尾

第四步.一层循环直到两条链表走到相同的结点就返回

/*function ListNode(x){
    this.val = x;
    this.next = null;
}*/
function FindFirstCommonNode(pHead1, pHead2)
{
    var len1 = getListLength(pHead1);
    var len2 = getListLength(pHead2);
    var pLongHead, pShortHead, nLengthDif;
    if (len1 > len2){
        pLongHead = pHead1;
        pShortHead = pHead2;
        nLengthDif = len1 - len2;
    } else {
        pLongHead = pHead2;
        pShortHead = pHead1;
        nLengthDif = len2 - len1;
    }
    // 长链表先多走几个节点
    for (var i = 0; i < nLengthDif; i++){
        pLongHead = pLongHead.next;
    }
    // 一起走
    while(pLongHead!==null && !pShortHead!==null && pLongHead !== pShortHead){
        pLongHead = pLongHead.next;
        pShortHead = pShortHead.next;
    }
    return pLongHead;
    
}
// 计算链表长度
function getListLength(head){
    var len = 0;
    var node = head;
    while(node !== null){
        len++;
        node = node.next;
    }
    return len;
}

还有一种更加简单的方法,很巧妙,最短的代码,不用记长度

用两个指针扫描”两个链表“,最终两个指针到达 null 或者到达公共结点。

一开始我不是很理解,后来我是这样理解的: 这个办法不需要计算两个链表的长度,是由于它进行了链表拼接,两个链表的长度和一定是相等的

  • 第一个指针p1扫描的是:第一个链表->null->第二个链表
  • 第二个指针p2扫描的是:第二个链表->null->第一个链表

事实上,算法在很巧妙的补齐长度,那么最终两个指针一定会同时到达公共结点或者null

function FindFirstCommonNode(pHead1, pHead2)
{
    // write code here
    var p1 = pHead1;
    var p2 = pHead2;
    while(p1 != p2){
        p1 = (p1 == null ? pHead2 : p1.next);
        p2 = (p2 == null ? pHead1 : p2.next);
    }
    return p1;
}

数字在排序数组中出现的次数

题目描述

统计一个数字在排序数组中出现的次数。

方法一:利用js现成api

function GetNumberOfK(data, k)
{
    var index = data.indexOf(k);
    if (index === -1){
        return 0;
    } else {
        return data.lastIndexOf(k) - data.indexOf(k) + 1;
    }
}

方法二:利用二分查找,确定开始位置和结束位置

function GetNumberOfK(data, k)
{
    var number = 0;
    if (data.length == 0){
        return number;
    }
    var firstIndex = getFirstIndex(data, k, 0, data.length);
    if (firstIndex === -1) {
        return number;
    }
    var lastIndex = getLastIndex(data, k, firstIndex, data.length);
    if (firstIndex !== -1 && lastIndex !== -1){
        return lastIndex - firstIndex + 1;
	}
    return number;
};

function getFirstIndex(data, k, start, end){
    if (start > end){
        return -1;
    }
    var mid = parseInt((end + start)/2);
    if (data[mid] > k){
        // 第一个k在数组前半段
        end = mid - 1;
    } else if (data[mid] < k){
        // 第一个k在数组后半段
        start = mid + 1;
    } else {
        if ((mid > 0 && data[mid - 1] !== k) || mid === 0){
            return mid;
        } else {
            end = mid - 1;
        }
    }
    return getFirstIndex(data, k, start, end);
}
function getLastIndex(data, k, start, end){
    if (start > end){
        return -1;
    }
    var mid = parseInt((end + start)/2);
    if (data[mid] > k){
        // 第一个k在数组前半段
        end = mid - 1;
    } else if (data[mid] < k){
        // 第一个k在数组后半段
        start = mid + 1;
    } else {
        if ((mid < data.length - 1 && data[mid + 1] !== k) || mid === data.length - 1){
            return mid;
        } else {
            start = mid + 1;
        }
    }
    return getLastIndex(data, k, start, end);
}

二叉树的深度

题目描述

输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。

方法一:递归

/* function TreeNode(x) {
    this.val = x;
    this.left = null;
    this.right = null;
} */
function TreeDepth(pRoot)
{
    if(!pRoot) return 0;
    let nLeft = TreeDepth(pRoot.left);
    let nRight = TreeDepth(pRoot.right);
    return Math.max(nLeft, nRight) + 1;
}

方法二:层次遍历

思路:利用队列结构先进先出的特性,维护一个队列,每次弹出第n层全部结点,第n+1层全部入队列,具体实现:

  • 每一层,使用round记录该层的结点数,即当前arr的长度,然后依次访问队列的第count个结点,将其从队列中弹出,将其左右子结点加入队列。
  • 当count ==== round时,说明已经遍历完当前层最后一个结点了,此时重置计数器count = 0, 重置队列长度round
  • 每遍历一层,二叉树的深度res++;
function TreeDepth(pRoot)
{
    if (!pRoot) return 0;
    
    let arr = [pRoot]; // 初始化队列
    let res = 0; // 初始化深度=0
    let count = 0, round = 1; // 初始化计数器=0,队列长度=1
    while(arr.length){ 
        count++;
        let parent = arr.shift();
        if(parent.left){arr.push(parent.left)};
        if(parent.right){arr.push(parent.right)};
        if(count === round){ // 遍历完一层了
            count = 0;
            round = arr.length;
            res++;
        }
    }
    return res;
}

平衡二叉树

题目描述

输入一棵二叉树,判断该二叉树是否是平衡二叉树。 在这里,我们只需要考虑其平衡性,不需要考虑其是不是排序二叉树

题目解析

  • 平衡二叉树的性质
    1. 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1
    2. 左右两个子树都是一棵平衡二叉树

思路:利用上题二叉树深度函数求解左右子树的深度,再进行递归

/* function TreeNode(x) {
    this.val = x;
    this.left = null;
    this.right = null;
} */
function IsBalanced_Solution(pRoot)
{
    if (!pRoot) return true;
    
    if (Math.abs(TreeDepth(pRoot.left) - TreeDepth(pRoot.right)) > 1){
        return false;
    } else {
        return IsBalanced_Solution(pRoot.left) && IsBalanced_Solution(pRoot.right);
    }
}

// 二叉树深度
function TreeDepth(pRoot)
{
    if(!pRoot) return 0;
    let nLeft = TreeDepth(pRoot.left);
    let nRight = TreeDepth(pRoot.right);
    return Math.max(nLeft, nRight) + 1;
}

数组中只出现了一次的数字

题目描述

一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。

function FindNumsAppearOnce(array)
{    
    let res = [];
    for (let i = 0; i < array.length; i++){
        if(array.indexOf(array[i]) === array.lastIndexOf(array[i])){
            res.push(array[i]);
        }
    }
    return res;
}

和为S的连续正数序列

题目描述

小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck!

输出描述

输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序

题目解析:所求连续正数数列为等差数列,等差数列的求和公式为sum = (a1 + an) * n/2。求解等差数列可以转化为求解数列的第一个数字first和最后一个数字last。初始化first = 1, last = 2

  1. [left, ..., right]的和<sum时,说明last偏小,因此last++
  2. [left, ..., right]的和>sum时,说明该数列过长,要去掉最小的元素,因此first++
  3. [left, ..., right]的和=sum时,找到了正确的等差数列,并增大left重新寻找新的数列
  4. left = right时,说明不再有满足条件的数列。
function FindContinuousSequence(sum)
{
    let res = [];
    let first = 1;
    let last = 2;
    while(first < last){
        if((first + last) * (last - first + 1) < 2 * sum){
            last++;
        } else if ((first + last) * (last - first + 1) > 2 * sum){
            first++;
        } else {
            let arr = [];
            for(let i = first; i <= last; i++){
                arr.push(i);
            }
            res.push(arr);
            first++;
        }
    }
    return res;
}

和为S的两个数字

题目描述

输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。

输出描述:

对应每个测试案例,输出两个数,小的先输出。

题目解析:解题思路参考上题《和为S的连续正数序列》,不同的是此题为两个数的和。

  • 递增数组,利用双指针,一个指向数组头,一个指向数组尾。
function FindNumbersWithSum(array, sum)
{
    let res = [];
    let i = 0, j = array.length - 1;
    while(i < j){
        if(array[i] + array[j] > sum){
            j--;
        }
        
        if(array[i] + array[j] < sum){
            i++;
        }
        
        if(array[i] + array[j] === sum){
            res.push([array[i], array[j]]);
            i++;
        }
    }
    
    let min = res.length && res[0];
    for(let i = 0; i < res.length; i++){
        if(res[i][0]*res[i][1] < min[0]*min[1]){
            min = res[i];
        }
    }
    return min;
}

左旋转字符串

题目描述

汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!

function LeftRotateString(str, n)
{
    if(!str) return '';
    const len = str.length;
    const num = n % len;
    const start = str.slice(0, num);
    const end = str.slice(num);
    return end + start;
}

翻转单词顺序

题目描述

牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?

function ReverseSentence(str)
{
    if(!str) return '';
    let arr = str.split(' ');
    const len = arr.length;
    for(let i = 0; i < Math.floor(len/2); i++){
        let temp = arr[i];
        arr[i] = arr[len - i - 1];
        arr[len - i - 1] = temp;
    }
    return arr.join(' ');
}

扑克牌顺子

题目描述

LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张^_^)...他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子.....LL不高兴了,他想了想,决定大\小 王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们LL的运气如何, 如果牌能组成顺子就输出true,否则就输出false。为了方便起见,你可以认为大小王是0。

题目抽象

给定一个长度为5(排除空vector),包含0-13的数组,判断公差是否为1.

解题思路

set+遍历

我们分两种情况考虑,

一. 如果vector中不包含0的情况:没有重复值,最大值与最小值的差值应该小于5.

二. 如果vector中包含0:除去0后的值,判断方法和1中是一样的。

所以根据如上两个条件,算法过程如下:

  1. 初始化一个set,最大值max_ = 0, 最小值min_ = 14
  2. 遍历数组, 对于大于0的整数,没有在set中出现,则加入到set中,同时更新max_, min_
  3. 如果出现在了set中,直接返回false
  4. 数组遍历完,最后再判断一下最大值与最小值的差值是否小于5
function IsContinuous(numbers)
{
    // write code here
    if(numbers.length !== 5) return false;
    let set = new Set();
    let max = 0, min = 14;
    for(let item of numbers){
        if(item>0){
            if(set.has(item)) return false;
            set.add(item);
            max = Math.max(max, item);
            min = Math.min(min, item);
        }
    }
    return (max-min)<5;
}

孩子们的游戏(圆圈中剩下最后的数)

题目描述

每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0...m-1报数....这样下去....直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!^_^)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1)

如果没有小朋友,请返回-1

题目翻译:n个人(编号0~(n-1)),从0开始报数,报到(m-1)的退出,剩下的人继续从0开始报数。求胜利者的编号。

有点小难

解析:

数学归纳法

第一个出列人的编号为m%n-1,剩下n-1个人,形成新的约瑟夫环(从编号k=m%n开始)

k  k+1  k+2  ... n-2, n-1, 0, 1, 2, ... k-2 并且从k开始报0

我们把他们的编号做一下转换, 变成n-1个人报数的子问题

k     --> 0
k+1   --> 1
k+2   --> 2
...
...
k-2   --> n-2
k-1   --> n-1

假如我们知道这个子问题的解f(n-1),反推回转换前(人数为n时)胜利者的编号应该为

f(n) = [f(n-1)+ k] % n

带入k = m % nf(n) = [f(n-1)+ m % n] % n = [f(n-1)+ m] % n

最终代码如下:

function LastRemaining_Solution(n, m)
{
    // write code here
    if(n === 0){
        return -1;
    }
    if(n === 1){
        return 0;
    }
    return (LastRemaining_Solution(n-1, m) + m)%n;
}

求1+2+3+...+n

题目描述

求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

// 递归
function Sum_Solution(n)
{
    return n ? Sum_Solution(n - 1) + n: 0;
}

不用加减乘除做加法

题目描述

写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。

虽然一看就知道是使用位运算做这个题目,但还是没做出来

(1)十进制加法分三步:(以5+17=22为例)

  1. 只做各位相加不进位,此时相加结果为12(个位数5和7相加不进位是2,十位数0和1相加结果是1);
  2. 做进位,5+7中有进位,进位的值是10;
  3. 重复1,2,直到没有进位制产生,将前面两个结果相加,12+10=22

(2)这三步同样适用于二进制位运算

由于二进制的特殊,还可以用位运算替代加法运算和乘法运算。

  • 按位异或:1^1=0 0^0=0 1^0=1 0^1=1
  • 按位与 :1&1=1 0&0=0 1&0=0 0&1=0

小结: 按位异或等价于不考虑进位的加法,按位与左移1位等价于进位值

  1. 不考虑进位对每一位相加==>异或运算;
  2. 考虑进位==>位与运算,然后向左移动一位;
  3. 相加过程重复前两步,直到不产生进位为止。
function Add(num1, num2)
{
    // num2存储进位值,num1存储不考虑进位的加法结果
    while(num2){
        var t = num1 ^ num2    //不进位的相加
        num2 = (num1 & num2) << 1   //同1则进位
        num1 = t
    }
    return num1
}

把字符串转换成整数

题目描述

将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0

输入描述:

输入一个字符串,包括数字字母符号,可以为空

输出描述:

如果是合法的数值表达则返回该数字,否则返回0

方法一:利用现成的API

function StrToInt(str)
{
    return Number(str)? parseInt(str):0
}

方法二:原生实现一个类似Number() 或者 parseInt()的函数

  1. 边界条件:字符串不存在或空,要return 0
  2. 判断首位:不为 + - 数字的,直接return 0,若是正负位则要标记
  3. 判断其余位:不是数字字符直接return 0(通过charCodeAt(0)判断)
  4. 数字字符串要转换为数字
function StrToInt(str)
{
    // write code here
    // 边界条件
    if (!str) {
        return 0
    }
    // 正常情况
    let plus = true
    let result = ''
    // 首位判断
    if (str[0] === '+') {
        result += ''
    } else if (str[0] === '-') {
        result += ''
        plus = false
    } else if (str[0].charCodeAt(0) >= 48 && str[0].charCodeAt(0) <= 57) {
        result += str[0]
    } else {
        return 0
    }
    // 其余为只要不是数字字符就返回0
    for (let i = 1; i < str.length; i++) {
        if (str[i].charCodeAt(0) >= 48 && str[i].charCodeAt(0) <= 57) {
            result += str[i]
        } else {
            return 0
        }
    }
    // 转成数字
    let front = 1
    let sum = 0
    for (let j = result.length - 1; j >= 0; j--) {
        let current = (result[j].charCodeAt(0) - 48) * front
        front *= 10
        sum += current
    }
    return plus ? sum : -sum
}

数组中重复的数字

题目描述

在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。

function duplicate(numbers, duplication)
{
    // write code here
    //这里要特别注意~找到任意重复的一个值并赋值到duplication[0]
    //函数返回True/False
    for(let i = 0; i < numbers.length; i++){
        let num = numbers[i];
        if(numbers.indexOf(num) !== numbers.lastIndexOf(num)){
            duplication[0] = num;
            return true;
        }
    }
    return false;
}

构建乘积数组

题目描述

给定一个数组A[0,1,...,n-1],请构建一个数组B[0,1,...,n-1],其中B中的元素B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1]。不能使用除法。(注意:规定B[0] = A[1] * A[2] * ... * A[n-1],B[n-1] = A[0] * A[1] * ... * A[n-2];) 对于A长度为1的情况,B无意义,故而无法构建,因此该情况不会存在

方法一:

无法使用除法,正常连乘的话时间复杂度为O(n^2),效率非常低。

function multiply(array)
{
    let B = [];
    for(let i = 0; i < array.length; i++){
        B[i] = 1;
        for(let j = 0; j < array.length; j++){
            if(j === i){
                continue;
            }
            B[i] *= array[j];
        }
    }
    return B;
}

方法二:

考虑到计算每个B[i]时都会有重复,思考B[i]之间的联系,找出规律,提高效率。 如上图所示,可以发现:

  • B[i]的左半部分(红色部分)和B[i-1]有关(将B[i]的左半部分乘积看成C[i],有C[i]=C[i-1]*A[i-1])
  • B[i]的右半部分(紫色部分)与B[i+1]有关(将B[i]的右半部分乘积看成D[i],有D[i]=D[i+1]*A[i+1])

思路:先从0到n-1遍历,计算每个B[i]的左半部分; 然后定义一个变量temp代表右半部分的乘积,从n-1到0遍历,令B[i]*=temp,而每次的temp与上次的temp关系即为temp*=A[i+1]

function multiply(array)
{
    if(!array || array.length < 2){ return null }
    let B = [];
    B[0] = 1;
    for (let i = 1; i < array.length; i++){
        B[i] = B[i-1]*array[i-1];
    }
    
    let temp = 1;
    for(let i = array.length - 2; i >= 0; i--){
        temp *= array[i+1];
        B[i] *= temp;
    }
    return B;
}

正则表达式匹配

题目描述

请实现一个函数用来匹配包括'.'和''的正则表达式。模式中的字符'.'表示任意一个字符,而''表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但是与"aa.a"和"ab*a"均不匹配

解析

  1. 当模式中的第二个字符不是“*”时:
  • 如果字符串第一个字符和模式中的第一个字符相匹配,那么字符串和模式都后移一个字符,然后匹配剩余的。
  • 如果字符串第一个字符和模式中的第一个字符相不匹配,直接返回false。
  1. 而当模式中的第二个字符是“*”时:
  • 如果字符串第一个字符跟模式第一个字符不匹配(还有机会,忽略字符串第一个字符,*代表x出现0次),则模式后移2个字符,继续匹配。

  • 如果字符串第一个字符跟模式第一个字符x匹配,可以有3种匹配方式:

    a. 字符串后移1字符,模式后移2字符(x出现1次)

    b. 字符串后移1字符,模式不动(x出现多次)

    c. 字符串不动,模式后移两个字符(x出现0次)

注意: 第二个字符是*和第二次字符不是*并不互斥, 还存在没有第二个字符的情况,因此要先处理第二个字符是*

function match(s, pattern)
{
    if (s == null || pattern == null) return false;
    if(pattern === '.*') return true;
    return check(s, pattern, 0, 0);
}

function check(s, pattern, i, j){
    // 刚好匹配完,么有下一位要check的字符了
    if(i === s.length && j === pattern.length) return true;
    
    // 开始 check
    if(i !== s.length && j === pattern.length) {
        // pattern已匹配完,s还有未匹配字符,false
        return false
    };
    
    // 第二个字符为*
    if(pattern[j + 1] && pattern[j + 1] === '*'){
        if(pattern[j] === s[i] || pattern[j] === '.' && i !== s.length){
            // 第一个字符匹配,三种情况
            return check(s, pattern, i, j + 2) || check(s, pattern, i + 1, j + 2) || check(s, pattern, i + 1, j);
        } else {
            // 第一个字符不匹配
            return check(s, pattern, i, j + 2)
        }
    }
    
    // 第二个字符不为* || 没第二个字符
    if(pattern[j] === s[i] || pattern[j] === '.' && i !== s.length){
        // 第一个字符匹配, pattern 和 s 各自后移一位check
        return check(s, pattern, i + 1, j + 1);
    } else {
        // 第一个字符不匹配
        return false;
    }
}

表示数值的字符串

题目描述

请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100","5e2","-123","3.1416"和"-1E-16"都表示数值。 但是"12e","1a3.14","1.2.3","+-5"和"12e+4.3"都不是。

解题思路

正则匹配

  • * 0次或多次
  • ? 0次或1次
  • + 一次以上
  • [] 或
//s字符串
function isNumeric(s)
{
    let reg = new RegExp(/^[+-]?\d*\.?\d*(e[+-]?\d+)?$/i);
    return reg.test(s);
}

字符流中第一个不重复的字符

题目描述

请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。

输出描述:

如果当前字符流没有存在出现一次的字符,返回#字符。

//Init module if you need
let map;
function Init()
{
    // write code here
    map = {};
}
//Insert one char from stringstream
function Insert(ch)
{
    // write code here
    if(map[ch]){
        map[ch] += 1;
    } else {
        map[ch] = 1;
    }
}
//return the first appearence once char in current stringstream
function FirstAppearingOnce()
{
    // write code here
    for(var key in map){
        if(map[key] === 1) {
            return key;
        }
    }
    return '#';
}

链表中环的入口结点

题目描述

给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。

题目解析

关于单链表的环,一般涉及以下这些问题:

  1. 给一个单链表,判断其中是否有环的存在;
  2. 如果存在环,找出环的入口点;
  3. 如果存在环,求出环上节点的个数;
  4. 如果存在环,求出链表的长度;
  5. 如果存在环,求出环上距离任意一个节点最远的点(对面节点);
  6. (扩展)如何判断两个无环链表是否相交;
  7. (扩展)如果相交,求出第一个相交的节点;

有关这些问题判断链表中是否有环 ----- 有关单链表中环的问题给出了所有问题的解决方法

本题解决思路只涉及到前两个问题

1.判断是否有环

采用“快慢指针”的方法。 就是有两个指针fast和slow,开始的时候两个指针都指向链表头head,然后在每一步操作中slow向前走一步即:slow = slow->next,而fast每一步向前两步即:fast = fast->next->next。

由于fast要比slow移动的快,如果有环,fast一定会先进入环,而slow后进入环。当两个指针都进入环之后,经过一定步的操作之后二者一定能够在环上相遇,并且此时slow还没有绕环一圈,也就是说一定是在slow走完第一圈之前相遇。

ALT

  • 证明“二者一定能够在环上相遇”

当slow刚进入环时每个指针可能处于上面的情况,接下来slow和fast分别向前走即:

if (slow != NULL && fast->next != NULL) 
{ 
         slow = slow -> next ; 
         fast = fast -> next -> next ; 
} 

也就是说,slow每次向前走一步,fast向前追了两步,因此每一步操作后fast到slow的距离缩短了1步,这样继续下去就会使得两者之间的距离逐渐缩小:...、5、4、3、2、1、0 -> 相遇。

  • 证明“是在slow走完第一圈之前相遇”

又因为在同一个环中fast和slow之间的距离不会大于环的长度,因此到二者相遇的时候slow一定还没有走完一周(或者正好走完以后,这种情况出现在开始的时候fast和slow都在环的入口处)。

let slow = fast = head ; 
while (slow != NULL && fast -> next != NULL) 
{ 
    slow = slow -> next ; 
    fast = fast -> next -> next ; 
    if (slow == fast) 
        return true ; 
} 
return false ; 
  1. 找到环的入口 ALT

从上面的分析知道,当fast和slow相遇时,slow还没有走完链表,假设fast已经在环内循环了n(1<= n)圈。假设slow走了s步,则fast走了2s步, 又由于fast走过的步数 = s + n*r(s + 在环上多走的n圈),则有下面的等式:

2*s = s + n * r ; (1)

=> s = n*r (2)

如果假设整个链表的长度是L,入口和相遇点的距离是x(如上图所示),起点到入口点的距离是a(如上图所示),则有:

a + x = s = n * r; (3) 由(2)推出

a + x = (n - 1) * r + r = (n - 1) * r + (L - a) (4) 由环的长度 = 链表总长度 - 起点到入口点的距离求出

a = (n - 1) * r + (L -a -x) (5)

集合式子(5)以及上图我们可以看出,从链表起点head开始到入口点的距离a,与从slow和fast的相遇点(如图)到入口点的距离相等。

因此我们就可以分别用一个指针(ptr1, prt2),同时从head与相遇点出发,每一次操作走一步,直到ptr1 == ptr2,此时的位置也就是入口点!

到此第二个问题也已经解决。

本题最终代码

/*function ListNode(x){
    this.val = x;
    this.next = null;
}*/
function EntryNodeOfLoop(pHead)
{
    if(!pHead || !pHead.next || !pHead.next.next) return null;
    let slow = pHead.next;
    let fast = pHead.next.next;
    while(slow&&fast){
        if(slow !== fast){
             slow = slow.next;
             fast = fast.next.next;
        } else {
            break;
        }
    }
    
    // 判断是否有环
    if(!slow || !fast) return null;
    
    fast = pHead;
    while(fast !== slow){
        fast = fast.next;
        slow = slow.next;
    }
    
    return fast;
}

删除链表中重复的结点

题目描述

在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5

题目解析

  1. 因为链表是单向的,如果是第一个、第二个节点就重复的话,删除就比较麻烦。因此我们可以额外添加头节点来解决
  2. 因为重复的节点不一定是重复两个,可能重复很多个,需要循环处理下。
****/*function ListNode(x){
    this.val = x;
    this.next = null;
}*/
function deleteDuplication(pHead)
{
    // write code here
    if(!pHead || !pHead.next) return pHead;
    
    // 添加头结点
    const Head = new ListNode(null);
    Head.next = pHead;
    let pre = Head;
    let cur = pHead;
    
    while(cur){
        if(cur.next && cur.val === cur.next.val){
            // 可能重复多个
            while(cur.next && cur.val === cur.next.val){
                cur = cur.next;
            }
            // pre链表中不包含重复的结点
            pre.next = cur.next;
            cur = cur.next;
        } else {
            pre = pre.next;
            cur = cur.next;
        }
    }
    return Head.next;
}