青蛙跳台阶 1. 题目描述 一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
2. 思路分析 跳到 n 阶假设有 f(n)种方法。
往前倒退,如果青蛙最后一次是跳了 2 阶,那么之前有 f(n-2)种跳法; 如果最后一次跳了 1 阶,那么之前有 f(n-1)种跳法。
所以:f(n) = f(n-1) + f(n-2)。就是斐波那契数列。
3. 代码 这里利用缓存模式(又称备忘录模式)实现了代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 const fibonacci = (() => { let mem = new Map (); mem.set (1 , 1 ); mem.set (2 , 1 ); const _fibonacci = (n ) => { if (n <= 0 ) { throw new Error ("Unvalid param" ); } if (mem.has (n)) { return mem.get (n); } mem.set (n, _fibonacci (n - 1 ) + _fibonacci (n - 2 )); return mem.get (n); }; return _fibonacci; })(); let start = new Date ().getTime (), end = null ; fibonacci (8000 );end = new Date ().getTime (); console .log (`耗时为${end - start} ms` );start = end; fibonacci (8000 );end = new Date ().getTime (); console .log (`耗时为${end - start} ms` );
数值的整次方 1. 题目描述 题目:实现函数 double Power(double base, intexponent),求 base 的 exponent 次方。不得使用库函数,同时不需要考虑大数问题
2. 思路分析 简单思路 :最简单的做法是循环,但是要考虑异常值的检验。比如指数是负数,底数为 0。
优化思路 :书上提供了一种复杂度为 $O(logN)$ 的做法。比如我们要求 32 次方,那么只要求出 16 次方再平方即可。依次类推,是递归函数的结构。
递推公式如下:
$$ a^n=\left{ \begin{aligned} a^{n/2}*a^{n/2} ; n为偶数\ a^{(n - 1)/2}*a^{(n - 1)/2} ; n为奇数 \end{aligned} \right. $$
需要注意的是,如果幂是奇数,例如 5 次方,可以先计算 2 次方,结果平方后(4 次方),再乘以自身(5 次方)。按照此思路处理。
代码实现-简单思路 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 function pow (base, exp ) { if (!base) { return 0 ; } let result = 1 , absExp = Math .abs (exp); for (let i = 0 ; i < absExp; ++i) { result *= base; } if (exp < 0 ) { result = 1 / result; } return result; } console .log (pow (2 , -2 ));console .log (pow (2 , 2 ));console .log (pow (2 , 0 ));console .log (pow (0 , -9 ));
代码实现-优化思路 在 Js 中整数除 2 不会自动取整,可以使用Math.floor()
。但更好的做法是使用>>
位运算。
判断奇数可以用%2
判断。但更好的做法是和1
进行&
运算后(除了最后 1 位,都被置 0 了),判断是不是 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 function unsignedPow (base, exp ) { if (exp === 0 ) { return 1 ; } else if (exp === 1 ) { return base; } let result = pow (base, exp >> 1 ); result *= result; if (exp & (1 === 1 )) { result *= base; } return result; } function pow (base, exp ) { if (!base) { return 0 ; } let absExp = Math .abs (exp); return exp < 0 ? 1 / unsignedPow (base, absExp) : unsignedPow (base, absExp); } console .log (pow (2 , 2 ));console .log (pow (2 , 0 ));console .log (pow (0 , -9 ));console .log (pow (2 , -2 ));
打印从1到最大的n位数 1. 题目描述 题目:输入数字 n,按顺序打印出从 1 最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数即 999。
2. 思路分析 主要的坑点在:大数的溢出。当然,es6 提供了BigInt
数据类型,可以直接相加不用担心溢出。
除此之外,这题显然是要我们模拟“大数相加”:将最低位加 1,然后每次检查是否进位,如果不进位,直接退出循环;如果进位,需要保留进上来的 1,然后加到下一位,直到不进位或者超出了我们规定的范围。
3. 代码实现 js 中不方便操作字符串中指定位置的字符,因此用数组对象来模拟。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 function increase (arr ) { let length = arr.length , over = 0 ; for (let i = length - 1 ; i >= 0 ; --i) { arr[i] = arr[i] + over; if (i === length - 1 ) { arr[i] += 1 ; } if (arr[i] >= 10 ) { if (i === 0 ) { return true ; } arr[i] = arr[i] - 10 ; over = 1 ; } else { break ; } } return false ; } function printMaxDigits (n ) { if (n <= 0 ) { return ; } let arr = new Array (n).fill (0 ); while (!increase (arr)) { console .log (arr); } } printMaxDigits (2 );printMaxDigits (3 );printMaxDigits (10 );
顺时针打印矩阵 1. 题目描述 输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
2. 思路分析 既然是顺时针打印,其实就是由外向内一圈圈打印 ,将过程分为 2 步:
第一步:printMatrix
函数,确定要打印的圈的左上角坐标(比较简单)
第二步:printMatrixInCircle
函数,根据左上角坐标,顺时针打印这一圈的信息。这个过程又分为四步:左上 -> 右上 -> 右下 -> 左下 -> 左上。
3. 代码实现 如果觉得,函数printMatrixInCircle
的条件判断不清楚,可以配合下面这张图一起看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 function printMatrixInCircle (arr, cols, rows, start ) { let endX = cols - start - 1 , endY = rows - start - 1 , result = "" ; for (let i = start; i <= endX; ++i) { result = result + " " + arr[start][i]; } if (start < endY) { for (let i = start + 1 ; i <= endY; ++i) { result = result + " " + arr[i][endX]; } } if (start < endX && start < endY) { for (let i = endX - 1 ; i >= start; --i) { result = result + " " + arr[endY][i]; } } if (start < endX && start < endY - 1 ) { for (let i = endY - 1 ; i >= start + 1 ; --i) { result = result + " " + arr[i][start]; } } console .log (result); } function printMatrix (arr ) { if (!Array .isArray (arr) || !Array .isArray (arr[0 ])) { return ; } let start = 0 , cols = arr[0 ].length , rows = arr.length ; while (cols > start * 2 && rows > start * 2 ) { console .log (`第${start + 1 } 层: ` ); printMatrixInCircle (arr, cols, rows, start); ++start; } } printMatrix ([ [1 , 2 , 3 ], [4 , 5 , 6 ], [7 , 8 , 9 ], ]); printMatrix ([ [1 , 2 , 3 , 4 ], [4 , 5 , 6 , 7 ], [8 , 9 , 10 , 11 ], ]);
数组中出现次数超过一半的数字 1. 题目描述 数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为 9 的数组{1,2,3,2,2,2,5,4,2}。
由于数字 2 在数组中出现了 5 次,超过数组长度的一半,因此输出 2。
2. 思路分析 数组中有一个数字出现的次数超过数组长度的一半,说明它出现的次数比其他所有数字出现次数的和还要多 。
在遍历的过程中保存两个变量:一个数字 + 一个次数。遍历到每个元素都会更新次数,元素 = 数字,加次数;否则,减次数;如果次数为 0,当前元素赋值给数字。
需要注意的是,最后结果不一定符合条件,比如数组 [1, 2, 3]
,结果是 3。所以要再统计一下最后数字的次数,是否有一半那么多。
3. 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 function checkMoreThanHalf (nums = [], target ) { let times = 0 ; nums.forEach ((num ) => num === target && ++times); return times * 2 >= nums.length ; } function moreThanHalfNum (nums = [] ) { if (!Array .isArray (nums) || !nums.length ) { return null ; } let times = 1 , result = nums[0 ]; for (let i = 1 ; i < nums.length ; ++i) { if (times === 0 ) { times = 1 ; result = nums[i]; } else if (result === nums[i]) { ++times; } else { --times; } } return checkMoreThanHalf (nums, result) ? result : null ; } console .log (moreThanHalfNum ([3 , 1 , 3 , 2 , 2 ])); console .log (moreThanHalfNum ([1 , 2 , 3 , 2 , 2 , 2 , 5 , 4 , 2 ]));
最小的k个数 1. 题目描述 输入 n 个整数,找出其中最小的 k 个数。例如输入 4、5、1、6、2、7、3、8 这 8 个数字,则最小的 4 个数字是 1、2、3、4。
2. 思路分析 利用“快速排序”的中的 partition 操作:返回 index,小于 index 对应元素的元素都放在了左边,大于 index 对应元素的元素都放在右边。
利用这个特性,只要我们的 partition 返回值是 k - 1,那么数组中前 k 个元素已经被摆放到了正确位置,直接遍历输出即可。
由于不需要排序全部,整体的时间复杂度是 O(N)。但美中不足的是:要在原数组操作,除非用 O(N)的空间来做拷贝。除此之外,针对海量动态增加的数据,也不能很好处理。这种情况需要用到“最大堆”,请前往《堆》章节查看。
3. 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 function partiton (arr = [], start, end ) { const length = arr.length ; if (!length) { return null ; } let v = arr[start], left = start + 1 , right = end; while (1 ) { while (left <= end && arr[left] <= v) ++left; while (right >= start + 1 && arr[right] >= v) --right; if (left >= right) { break ; } [arr[left], arr[right]] = [arr[right], arr[left]]; ++left; --right; } [arr[right], arr[start]] = [arr[start], arr[right]]; return right; } function getKthNumbers (nums = [], k ) { if (k <= 0 ) { return null ; } const length = nums.length ; const result = new Array (k); let start = 0 , end = length - 1 ; let index = partiton (nums, start, end); while (index !== k - 1 ) { if (index > k - 1 ) { end = index - 1 ; index = partiton (nums, start, end); } else { start = index + 1 ; index = partiton (nums, start, end); } } for (let i = 0 ; i < k; ++i) { result[i] = nums[i]; } return result; } console .log (getKthNumbers ([4 , 5 , 1 , 6 , 2 , 7 , 3 , 8 ], 4 )); console .log (getKthNumbers ([10 , 2 ], 1 ));
和为s的两个数字 1. 题目描述 输入一个递增排序的数组和一个数字 s,在数组中查找两个数,使得它们的和正好是 s。如果有多对数字的和等于 s,输出任意一对即可。
2. 解题思路 如果这个数组不是递增的,就得用哈希表来解决,空间复杂度是 O(N)。
但是题目条件是“递增数组”,因此可以使用“双指针”的思路来实现:即一个指针指向开头,另一个指向结尾。
比较指针对应的 2 个元素的和与给定数组 s:
元素和 > s: 后指针向前移动
元素和 < s: 前指针向后移动
元素和 = s: 返回指针对应的 2 个元素
3. 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 function findNumsWithSum (data, sum ) { if (!Array .isArray (data) || data.length <= 1 ) { return [null , null ]; } let i = 0 , j = data.length - 1 ; while (i < j) { let now = data[i] + data[j]; if (now === sum) { return [data[i], data[j]]; } else if (now > sum) { --j; } else { ++i; } } return [null , null ]; } console .log (findNumsWithSum ([1 , 2 , 4 , 7 , 11 , 15 ], 15 ));
和为s的连续正数序列 1. 题目描述 输入一个正数 s,打印出所有和为 s 的连续正数序列(至少含有两个数)。例如输入 15,由于 1 + 2 + 3 + 4 + 5 = 4 + 5 + 6 = 7 + 8 = 15,所以结果打印出 3 个连续序列 1 ~ 5、4 ~ 6 和 7 ~ 8。
2. 思路分析 和前面题目很相似,这里也是“双指针”的思路。不同的地方有 2 个点:
指针是从第 0 个和第 1 个位置开始的(下面称为 a 和 b)
这里要计算指针范围内的所有元素和(题目要求是“连续序列”)
每次移动 a、b 之前,都要计算一下当前[a,b]
范围内的所有元素和。如果等于 s,打印并且 b 右移;如果小于 s,b 右移;如果大于 s,a 右移。
至于为什么相等的时候 b 右移而不是 a 右移?因为 a 右移会漏掉情况,而且指针可能重叠。比如对于数组 [1, 2, 2]
,给定 s 是 3。
3. 算法实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 function print (data, seq ) { const [start, end] = seq; for (let i = start; i <= end; ++i) { process.stdout .write (data[i] + ", " ); } process.stdout .write ("\\n" ); } function findSequenceWithSum (data, sum ) { let small = 0 , big = 1 , cur = data[small] + data[big]; const middle = (data.length + 1 ) >> 1 ; while (small < middle) { if (cur <= sum) { cur === sum && print (data, [small, big]); ++big; cur += data[big]; } else { cur -= data[small]; ++small; } } } findSequenceWithSum ([1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ], 9 );
n个骰子的点数 1. 题目描述 把 n 个骰子扔在地上,所有骰子朝上一面的点数之和为 s。输入 n,打印出 s 的所有可能的值出现的概率。
2. 思路分析 递归的思路就是组合出所有情况,然后每种情况记录出现次数,最后除以 6^n 即可。其中,6^n 就是所有情况的总数。
书中提出的方法是使用循环来优化递归 ,递归是自顶向下,循环是自底向上,思考起来有难度。
技巧性很强,准备 2 个数组,假想每次投掷一个骰子,出现和为 n 的次数,就是之前骰子和为 n-1, n-2, …, n-6 的次数和。依次类推,每次存储结果都和之前的数组不同。
3. 算法实现 注释中都有详细说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 const gMaxValue = 6 ; function printProbability (number ) { if (number < 1 ) { return ; } const probabilities = [ new Array (gMaxValue * number + 1 ), new Array (gMaxValue * number + 1 ), ]; let flag = 0 ; for (let i = 0 ; i < gMaxValue * number + 1 ; ++i) { probabilities[0 ][i] = probabilities[1 ][i] = 0 ; } for (let i = 1 ; i <= gMaxValue; ++i) { probabilities[flag][i] = 1 ; } for (let k = 2 ; k <= number ; ++k) { for (let i = 0 ; i < k; ++i) { probabilities[1 - flag][i] = 0 ; } for (let i = k; i < gMaxValue * k + 1 ; ++i) { probabilities[1 - flag][i] = 0 ; for (let j = 1 ; j < i && j <= gMaxValue; ++j) { probabilities[1 - flag][i] += probabilities[flag][i - j]; } } flag = 1 - flag; } const total = Math .pow (gMaxValue, number ); for (let i = number ; i < gMaxValue * number + 1 ; ++i) { console .log (`sum is ${i} , ratio is ${probabilities[flag][i] / total} ` ); } } printProbability (6 );
扑克牌的顺子 1. 题目描述 从扑克牌中随机抽 5 张牌,判断是不是一个顺子,即这 5 张牌是不是连续的。
2 ~ 10 为数字本身,A 为 1,J 为 11,Q 为 12,K 为 13,而大、小王可以看成任意数字。
2. 思路分析 难度不大,可以将大小王看成数字 0,可以在任何不连续的两个数字之间做填充。
首先将原数组排序,然后统计任意数字(0)的出现次数。再遍历之后的数字,找出不相邻数字之间总共差多少个数字。
最后比较 0 的出现次数和总共差多少个数字,两者的大小关系。
注意 :连续两个相同的数字是对子,不符合要求。
3. 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function isContinuous (numbers ) { numbers.sort (); const length = numbers.length ; let zeroNum = 0 ; for (let i = 0 ; i < length && !numbers[i]; ++i) { ++zeroNum; } let interval = 0 ; for (let i = zeroNum + 1 ; i < length - 1 ; ++i) { if (numbers[i] === numbers[i + 1 ]) { return false ; } interval += numbers[i + 1 ] - numbers[i] - 1 ; } return interval <= zeroNum; } console .log (isContinuous ([3 , 8 , 0 , 0 , 1 ])); console .log (isContinuous ([8 , 10 , 0 , 6 , 0 ]));
圆圈中最后剩下的数字 1. 题目 0,1,…,n-1 这 n 个数字排成一个圆圈,从数字 0 开始每次从这个圆圈里删除第 m 个数字。求出这个圆圈里剩下的最后一个数字。
2. 思路分析 这个其实是经典的“约瑟夫环”问题。常用解法就是“循环取余”。每次都把下标移动 m 个位置,然后移除当前元素。直到只剩最后一个元素。
3. 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function lastRemain (n, m ) { const nums = new Array (n); for (let i = 0 ; i < n; ++i) { nums[i] = i; } let start = 0 ; while (nums.length > 1 ) { start = (start + m) % nums.length ; nums.splice (start, 1 ); } return nums.shift (); } console .log (lastRemain (5 , 2 ));