标签:toolbar ase type critical sum mat 缓冲 ffffff 面试问题
虽说我们很多时候前端很少有机会接触到算法。实际上学习数据结构与算法对于工程师去理解和分析问题都是有帮助的。如果将来当我们面对较为复杂的问题,这些基础知识的积累可以帮助我们更好的优化解决思路。
回文是指把相同的词汇或句子,在下文中调换位置或颠倒过来,产生首尾回环的情趣,叫做回文,也叫回环。比如 mamam redivider .
很多人拿到这样的题目非常容易想到用for 将字符串颠倒字母顺序然后匹配就行了。其实重要的考察的就是对于reverse的实现。其实我们可以利用现成的函数,将字符串转换成数组,这个思路很重要,我们可以拥有更多的自由度去进行字符串的一些操作。
function checkPalindrom(str) { return str == str.split(‘‘).reverse().join(‘‘); }
比如 输入: [1,13,24,11,11,14,1,2], 输出: [1,13,24,11,14,2] ,需要去掉重复的11 和 1 这两个元素。
主要考察个人对Object的使用,利用key来进行筛选。
** * unique an array **/ let unique = function(arr) { let hashTable = {}; let data = []; for(let i=0,l=arr.length;i<l;i++) { if(!hashTable[arr[i]]) { hashTable[arr[i]] = true; data.push(arr[i]); } } return data } module.exports = unique;
给出一段英文连续的英文字符窜,找出重复出现次数最多的字母
比如: 输入:afjghdfraaaasdenas 输出 : a
前面出现过去重的算法,这里需要是统计重复次数。
function findMaxDuplicateChar(str) { if(str.length == 1) { return str; } let charObj = {}; for(let i=0;i<str.length;i++) { if(!charObj[str.charAt(i)]) { charObj[str.charAt(i)] = 1; }else{ charObj[str.charAt(i)] += 1; } } let maxChar = ‘‘, maxValue = 1; for(var k in charObj) { if(charObj[k] >= maxValue) { maxChar = k; maxValue = charObj[k]; } } return maxChar; } module.exports = findMaxDuplicateChar;
如果说到算法题目的话,应该大多都是比较开放的题目,不限定算法的实现,但是一定要求掌握其中的几种,所以冒泡排序,这种较为基础并且便于理解记忆的算法一定需要熟记于心。冒泡排序算法就是依次比较大小,小的的大的进行位置上的交换。
function bubbleSort(arr) { for(let i = 0,l=arr.length;i<l-1;i++) { for(let j = i+1;j<l;j++) { if(arr[i]>arr[j]) { let tem = arr[i]; arr[i] = arr[j]; arr[j] = tem; } } } return arr; } module.exports = bubbleSort;
除了冒泡排序外,其实还有很多诸如 插入排序,快速排序,希尔排序 等。每一种排序算法都有各自的特点。全部掌握也不需要,但是心底一定要熟悉几种算法。 比如快速排序,其效率很高,而其基本原理如图(来自wiki):
算法参考某个元素值,将小于它的值,放到左数组中,大于它的值的元素就放到右数组中,然后递归进行上一次左右数组的操作,返回合并的数组就是已经排好顺序的数组了。
function quickSort(arr) { if(arr.length<=1) { return arr; } let leftArr = []; let rightArr = []; let q = arr[0]; for(let i = 1,l=arr.length; i<l; i++) { if(arr[i]>q) { rightArr.push(arr[i]); }else{ leftArr.push(arr[i]); } } return [].concat(quickSort(leftArr),[q],quickSort(rightArr)); } module.exports = quickSort;
学习地址,通过动画演示算法的实现。
HTML5 Canvas Demo: Sorting Algorithms
举例:输入 a = 2, b = 4 输出 a = 4, b =2
这种问题非常巧妙,需要大家跳出惯有的思维,利用 a , b进行置换。
主要是利用 + - 去进行运算,类似 a = a + ( b - a) 实际上等同于最后 的 a = b;
function swap(a , b) { b = b - a; a = a + b; b = a - b; return [a,b]; } module.exports = swap;
数列长度限定在9.
斐波那契数列
,又称黄金分割数列,指的是这样一个数列:0、1、1、2、3、5、8、13、21、34、……在数学上,斐波纳契数列主要考察递归的调用。我们一般都知道定义
fibo[i] = fibo[i-1]+fibo[i-2];
生成斐波那契数组的方法
function getFibonacci(n) { var fibarr = []; var i = 0; while(i<n) { if(i<=1) { fibarr.push(i); }else{ fibarr.push(fibarr[i-1] + fibarr[i-2]) } i++; } return fibarr; }
剩余的工作就是利用canvas arc
方法进行曲线绘制了
比如: 输入 [10,5,11,7,8,9] 输出 6
这是通过一道题目去测试对于基本的数组的最大值的查找,很明显我们知道,最大差值肯定是一个数组中最大值与最小值的差。
function getMaxProfit(arr) { var minPrice = arr[0]; var maxProfit = 0; for (var i = 0; i < arr.length; i++) { var currentPrice = arr[i]; minPrice = Math.min(minPrice, currentPrice); var potentialProfit = currentPrice - minPrice; maxProfit = Math.max(maxProfit, potentialProfit); } return maxProfit; }
实现一个算法,随机生成指制定长度的字符窜。
比如:给定 长度 8 输出 4ldkfg9j
function randomString(n) { let str = ‘abcdefghijklmnopqrstuvwxyz9876543210‘; let tmp = ‘‘, i = 0, l = str.length; for (i = 0; i < n; i++) { tmp += str.charAt(Math.floor(Math.random() * l)); } return tmp; } module.exports = randomString;
自己实现一个函数,查找某个DOM节点下面的包含某个class的所有DOM节点?不允许使用原生提供的 getElementsByClassName querySelectorAll 等原生提供DOM查找函数。
function queryClassName(node, name) { var starts = ‘(^|[ \n\r\t\f])‘, ends = ‘([ \n\r\t\f]|$)‘; var array = [], regex = new RegExp(starts + name + ends), elements = node.getElementsByTagName("*"), length = elements.length, i = 0, element; while (i < length) { element = elements[i]; if (regex.test(element.className)) { array.push(element); } i += 1; } return array; }
一般叫全部写完的概率比较少,但是重点考察你对它的理解和一些基本特点的实现。 二叉查找树,也称二叉搜索树、有序二叉树(英语:ordered binary tree)是指一棵空树或者具有下列性质的二叉树:
在写的时候需要足够理解二叉搜素树的特点,需要先设定好每个节点的数据结构
class Node { constructor(data, left, right) { this.data = data; this.left = left; this.right = right; } }
树是有节点构成,由根节点逐渐延生到各个子节点,因此它具备基本的结构就是具备一个根节点,具备添加,查找和删除节点的方法.
class BinarySearchTree { constructor() { this.root = null; } insert(data) { let n = new Node(data, null, null); if (!this.root) { return this.root = n; } let currentNode = this.root; let parent = null; while (1) { parent = currentNode; if (data < currentNode.data) { currentNode = currentNode.left; if (currentNode === null) { parent.left = n; break; } } else { currentNode = currentNode.right; if (currentNode === null) { parent.right = n; break; } } } } remove(data) { this.root = this.removeNode(this.root, data) } removeNode(node, data) { if (node == null) { return null; } if (data == node.data) { // no children node if (node.left == null && node.right == null) { return null; } if (node.left == null) { return node.right; } if (node.right == null) { return node.left; } let getSmallest = function(node) { if(node.left === null && node.right == null) { return node; } if(node.left != null) { return node.left; } if(node.right !== null) { return getSmallest(node.right); } } let temNode = getSmallest(node.right); node.data = temNode.data; node.right = this.removeNode(temNode.right,temNode.data); return node; } else if (data < node.data) { node.left = this.removeNode(node.left,data); return node; } else { node.right = this.removeNode(node.right,data); return node; } } find(data) { var current = this.root; while (current != null) { if (data == current.data) { break; } if (data < current.data) { current = current.left; } else { current = current.right } } return current.data; } } module.exports = BinarySearchTree;
Q:你将如何验证一个素数?
A:一个素数只能被它自己和1整除。所以,我将运行一个while循环并加1。(看代码示例,如果你无法理解,那这不是你的菜。先回去学习javaScript基础知识然后再回来吧。)
function isPrime(n){
var divisor =2;
while(n > divisor){
if(n % divisor ==0){
returnfalse;
}
else
divisor++;
}
returntrue;
}
isPrime(137);// = true
isPrime(237);// = false
Q:你能做得更好吗?
A:可以。除数一次增加1个。 在3之后我可以增加2.如果一个数可以被任何偶数整除,它将被2整除。
补充:如果你不需要把除数增加到这个数。你可以更早停止。让我在下面的步骤中解释一下(如果需要可以多读几遍)
第一步,任何数字都不能被大于它的一半的数字整除。 例如,13将永远不能被7,8,9整除……它甚至可以是偶数的一半。 例如,16将被8整除,但永远不会被9,10,11,12 ……
结论:一个数字将永远不能被一个大于其一半数值的数字整除。 所以,我们可以少循环50%。第二步,现在,如果一个数字不能被3整除。(如果它可被3整除,那么它就不是质数)。然后,它不可以被大于其值1/3的任何数整除。例如,35不能被3整除。因此,它永远不会被大于35/3的数整除,永远不能被12, 13, 14整除…如果你取一个像36一样的偶数,它将永远不能被13, 14, 15整除。
结论: 一个数字可以被其数值的三分之一整除。第三步,例如,你有一个数字127。127不能被2整除,因此你最多应该检查63.5。其次,127不能被3整除。因此,您将检查到127/3大约42。它不能被5整除,除数应该小于127/5大约25,而不是7。那么,我们该在哪里停下来?
结论: 除数将小于math.sqrt(N)
如果你不能理解也不用担心,别管它。如果那你不是一个研究人员就没关系。
function isPrime(n)
{
var divisor =3,
limit =Math.sqrt(n);
//check simple cases
if(n ==2|| n ==3)
returntrue;
if(n %2==0)
returnfalse;
while(divisor <= limit)
{
if(n % divisor ==0)
returnfalse;
else
divisor +=2;
}
returntrue;
}
isPrime(137);// = true
isPrime(237);// = false
Q:如何求出一个数的所有素数因子?
A:执行一个while循环。开始除以2,如果不能整除,记录这个除数,直到完成。
function primeFactors(n){
var factors =[],
divisor =2;
while(n>2){
if(n % divisor ==0){
factors.push(divisor);
n= n/ divisor;
}
else{
divisor++;
}
}
return factors;
}
primeFactors(69);// = [3, 23]
Q:运行时间复杂度是多少? 你能做得更好吗?
A:O(n)。可以将除数从3开始,累加2。因为,如果一个数被任何偶数整除,它将被2整除。因此,你不需要除以偶数。此外,你不会有一个大于其价值一半的因素。如果你想让它变得复杂,那就用第一题的补充概念吧。
Q:如何获得第n个斐波纳契数字?
A: 我创建一个数组并从迭代开始。
斐波那契系列是面向初学者的最受欢迎的面试问题之一。 所以,你必须学习这一个。
function fibonacci(n){
var fibo =[0,1];
if(n <=2)return1;
for(var i =2; i <=n; i++){
fibo[i]= fibo[i-1]+fibo[i-2];
}
return fibo[n];
}
fibonacci(12);// = 144
Q: 运行时间复杂度是多少?
A: O(n);
Q: 你能让它递归吗?
function fibonacci(n){
if(n <=1){
return n;
}else{
return fibonacci(n-1)+ fibonacci (n-2);
}
}
fibonacci(12);// = 144
Q: 运行时间复杂度是多少?
A: O(2n);关于时间复杂度的细节
Q: 你会如何找到两个数字的最大公约数?
function greatestCommonDivisor(a, b){
var divisor =2,
greatestDivisor =1;
//if u pass a -ve number this will not work. fix it dude!!
if(a <2|| b <2)
return1;
while(a >= divisor && b >= divisor){
if(a %divisor ==0&& b% divisor ==0){
greatestDivisor = divisor;
}
divisor++;
}
return greatestDivisor;
}
greatestCommonDivisor(14,21);// 7
greatestCommonDivisor(69,169);// = 1
很抱歉。我也无法解释它。 因为我自己80%的情况下都不能理解它。 我的算法分析教练告诉我这个,又从课堂笔记偷走(我是一个好学生,顺便说一句!)
function greatestCommonDivisor(a, b){
if(b ==0)
return a;
else
return greatestCommonDivisor(b, a%b);
}
注意:用你的大脑来理解它。
Q:你将如何从数组中删除重复的成员?
A: 执行一个循环,并保存一个对象/关联数组。如果我第一次找到一个元素,我会设置它的值为真(这将告诉我元素添加一次)。如果我在对象中找到这个元素,我不会将它插入到返回数组中。
function removeDuplicate(arr){
var exists ={},
outArr =[],
elm;
for(var i =0; i<arr.length; i++){
elm = arr[i];
if(!exists[elm]){
outArr.push(elm);
exists[elm]=true;
}
}
return outArr;
}
removeDuplicate([1,3,3,3,1,5,6,7,8,1]);// = [1, 3, 5, 6, 7, 8]
Q: 怎样合并两个已排序数组?
A: 我将为每个数组保留一个指针(看代码,并注意这个)。
function mergeSortedArray(a, b){
var merged =[],
aElm = a[0],
bElm = b[0],
i =1,
j =1;
if(a.length ==0)
return b;
if(b.length ==0)
return a;
/*
if aElm or bElm exists we will insert to merged array
(will go inside while loop)
to insert: aElm exists and bElm doesn‘t exists
or both exists and aElm < bElm
this is the critical part of the example
*/
while(aElm || bElm){
if((aElm &&!bElm)|| aElm < bElm){
merged.push(aElm);
aElm = a[i++];
}
else{
merged.push(bElm);
bElm = b[j++];
}
}
return merged;
}
mergeSortedArray([2,5,6,9],[1,2,3,29]);// = [1, 2, 2, 3, 5, 6, 9, 29]
Q:如何在不使用临时变量的情况下交换两个数字?
function swapNumb(a, b){
console.log(‘before swap: ‘,‘a: ‘, a,‘b: ‘, b);
b = b -a;
a = a+ b;
b = a-b;
console.log(‘after swap: ‘,‘a: ‘, a,‘b: ‘, b);
}
swapNumb(2,3);
// = before swap: a: 2 b: 3
// = after swap: a: 3 b: 2
位操作:对不起,我无法向你解释这一点。 Kinjal Dave建议到 logical conjunction理解它。将浪费您30分钟。
function swapNumb(a, b){
console.log("a: "+ a +" and b: "+ b);
a = a ^ b;
b = a ^ b;
a = a ^ b;
console.log("a: "+ a +" and b: "+ b);
}
swapNumb(2,3);
// = a: 2 and b: 3
// = a: 3 and b: 2
Q:如何在JavaScript中反转字符串?
A:可以遍历字符串并将字母连接到新字符串。
function reverse(str){
var rtnStr =‘‘;
for(var i = str.length-1; i>=0;i--){
rtnStr +=str[i];
}
return rtnStr;
}
reverse(‘you are a nice dude‘);
// = "edud ecin a era uoy"
Q:你知道在现代浏览器中串联效果很好,但在像IE8这样的旧浏览器中会变慢。 还有什么不同的方法,可以扭转一个字符串?
A:当然.我可以使用数组,也可以添加一些检查。如果字符串是NULL或其他字符串,这将失败。让我也做一些类型检查。使用此数组类似于在某些服务器端语言中使用字符串缓冲区。
function reverse(str){
var rtnStr =[];
if(!str ||typeof str !=‘string‘|| str.length <2)return str;
for(var i = str.length-1; i>=0;i--){
rtnStr.push(str[i]);
}
return rtnStr.join(‘‘);
}
Q: 运行时间复杂度是多少?
A: O(n);
Q:可以做得更好?
A:我可以遍历索引的一半,它会节省一点点。 (这是没用的,可能不会打动面试官)
function reverse(str){
str = str.split(‘‘);
var len = str.length,
halfIndex =Math.floor(len /2)-1,
revStr;
for(var i =0; i <= halfIndex; i++){
revStr = str[len - i -1];
str[len - i -1]= str[i];
str[i]= revStr;
}
return str.join(‘‘);
}
Q:这有效,但你可以做递归的方式吗?
A:可以。
function reverse (str){
if(str ===""){
return"";
}else{
return reverse(str.substr(1))+ str.charAt(0);
}
}
Q:你可以在方法中使用任何构建,使它更清洁吗?
function reverse(str){
if(!str || str.length <2)return str;
return str.split(‘‘).reverse().join(‘‘);
}
Q:你可以做反向函数作为字符串扩展吗?
A:我需要将这个函数添加到String.prototype,而不是使用str作为参数,我需要使用this
String.prototype.reverse =function(){
if(!this||this.length <2)returnthis;
returnthis.split(‘‘).reverse().join(‘‘);
}
‘abc‘.reverse();
// = ‘cba‘
Q:你如何在句子中颠倒单词?
A:您必须检查整个字符串的空白区域。确定是否可能有多个空格。
//have a tailing white space
//fix this later
//now i m sleepy
function reverseWords(str){
var rev =[],
wordLen =0;
for(var i = str.length-1; i>=0; i--){
if(str[i]==‘ ‘|| i==0){
rev.push(str.substr(i,wordLen+1));
wordLen =0;
}
else
wordLen++;
}
return rev.join(‘ ‘);
}
内置方法的快速解决方案:
function reverseWords(str){
return str.split(‘ ‘).reverse();
}
Q: 如果你有一个字符串如”I am the good boy”, 怎样变为 “I ma eht doog yob”? 注意这些单词位置不变但是被反转了。
A: 要做到这一点,我必须做字符串反向和字反转。
function reverseInPlace(str){
return str.split(‘ ‘).reverse().join(‘ ‘).split(‘‘).reverse().join(‘‘);
}
reverseInPlace(‘I am the good boy‘);// = "I ma eht doog yob"
Q: ok。好的,你能不使用内置反向函数做到吗?
A: (内心独白)有没有搞错!!
//sum two methods.
//you can simply split words by ‘ ‘
//and for each words, call reverse function
//put reverse in a separate function
//if u cant do this,
//have a glass of water, and sleep
Q: 怎么在字符串中找到第一个非重复字符?
A: 有什么条件吗?
A: 比如是否区分大小写?
面试官可能会说No。
A: 是长字符串还是短字符串?
Q: 这些有什么关系吗?
A:例如,如果它是一个非常长的字符串,说一百万个字符,我想检查是否有26个英文字符正在重复。 我可能会检查是否所有字符都在每200个字母中重复(例如),而不是循环遍历整个字符串。 这将节省计算时间。
Q: 简单起见, 这个字符串是 “the quick brown fox jumps then quickly blow air”。
function firstNonRepeatChar(str){
var len = str.length,
char,
charCount ={};
for(var i =0; i<len; i++){
char= str[i];
if(charCount[char]){
charCount[char]++;
}
else
charCount[char]=1;
}
for(var j in charCount){
if(charCount[j]==1)
return j;
}
}
firstNonRepeatChar(‘the quick brown fox jumps then quickly blow air‘);// = "f"
这有一个问题,不能再循环中及时退出。
Q: 怎样删除字符串中的重复字符?
A: 这与第一个非重复字符非常相似。你应该问一些类似的问题。它是区分大小写的吗?。
如果面试官说,这是区分大小写的,那么你就很轻松了。 如果他说不。你可以使用string.toLowercase()来把字符串。面试官可能不喜欢这个方法。 因为返回字符串不会拥有相同的大小写。 所以
function removeDuplicateChar(str){
var len = str.length,
char,
charCount ={},
newStr =[];
for(var i =0; i<len; i++){
char= str[i];
if(charCount[char]){
charCount[char]++;
}
else
charCount[char]=1;
}
for(var j in charCount){
if(charCount[j]==1)
newStr.push(j);
}
return newStr.join(‘‘);
}
removeDuplicateChar(‘Learn more javascript dude‘);// = "Lnmojvsciptu"
Q: 如何检查一个字符串是否是回文?
A: 把字符串反转,如果反转前后相等,那么它就是回文。
function isPalindrome(str){
var i, len = str.length;
for(i =0; i<len/2; i++){
if(str[i]!== str[len -1-i])
returnfalse;
}
returntrue;
}
isPalindrome(‘madam‘)
// = true
isPalindrome(‘toyota‘)
// = false
或者
function checkPalindrom(str){
return str == str.split(‘‘).reverse().join(‘‘);
}
类似的:在 O(n)时间复杂度内判断一个字符串是否包含在回文字符串内。你能在O(1)时间解决问题吗?
Q: 在一个1到100的未排序数组中找到缺失的数,你怎么做?
说明:数组中的数字为1到100。 数组中只有一个数字缺失。数组未排序。找到缺少的数字。
A: 你必须表现得像是在想很多。然后讨论n=n(n+1)/2的线性级数之和
function missingNumber(arr){
var n = arr.length+1,
sum =0,
expectedSum = n *(n+1)/2;
for(var i =0, len = arr.length; i < len; i++){
sum += arr[i];
}
return expectedSum - sum;
}
missingNumber([5,2,6,1,3]);
// = 4
注意: 这个会返回任意长度数组中缺失的那个
Q: 在一个未排序的数组中找出是否有任意两数之和等于给定的数?
A: 简单!双重循环。
function sumFinder(arr, sum){
var len = arr.length;
for(var i =0; i<len-1; i++){
for(var j = i+1;j<len; j++){
if(arr[i]+ arr[j]== sum)
returntrue;
}
}
returnfalse;
}
sumFinder([6,4,3,2,1,7],9);
// = true
sumFinder([6,4,3,2,1,7],2);
// = false
Q: 时间复杂度?
A: O(n2)。
Q: 有更优解?
A: 我想想。我可以用一个对象来存储当前元素和和值的差值。当我拿到一个新元素,如果这个元素的差值在对象中存在,那么我就能判断出是否存在。
function sumFinder(arr, sum){
var differ ={},
len = arr.length,
substract;
for(var i =0; i<len; i++){
substract = sum - arr[i];
if(differ[substract])
returntrue;
else
differ[arr[i]]=true;
}
returnfalse;
}
sumFinder([6,4,3,2,1,7],9);
// = true
sumFinder([6,4,3,2,1,7],2);
// = false
Q: 找到任意两个元素的最大总和?
A: 这实际上非常简单直接。 找到两个最大的数字并返回它们的总和
function topSum(arr){
var biggest = arr[0],
second = arr[1],
len = arr.length,
i =2;
if(len<2)returnnull;
if(biggest<second){
biggest = arr[1];
second = arr[0];
}
for(; i<len; i++){
if(arr[i]> biggest){
second = biggest;
biggest = arr[i];
}
elseif(arr[i]>second){
second = arr[i];
}
}
return biggest + second;
}
Q: 统计从1到n的零总数?
A: 如果 n = 100,则0的数目将是11(0,10,20,30,40,50,60,70,80,90,100)。 请注意,100有两个0.这个看起来很简单,但有点棘手
说明:所以这里的重点是。 如果你有一个1到50的数字,那么这个数值就是5,就是50除以10.然而,如果这个数值是100,这个数值是11,你将得到100/10 = 10和 10/10 = 1。 那就是你将如何在一个数字中得到更多的零,如(100,200,1000);
function countZero(n){
var count =0;
while(n>0){
count +=Math.floor(n/10);
n = n/10;
}
return count;
}
countZero(2014);
// = 223
Q: 在字符串中匹配子字符串?
A: 在迭代字符串时将使用指针(一个用于字符串,另一个用于子字符串)。 然后用另一个变量来保存初始匹配的起始索引。
function subStringFinder(str, subStr){
var idx =0,
i =0,
j =0,
len = str.length,
subLen = subStr.length;
for(; i<len; i++){
if(str[i]== subStr[j])
j++;
else
j =0;
//check starting point or a match
if(j ==0)
idx = i;
elseif(j == subLen)
return idx;
}
return-1;
}
subStringFinder(‘abbcdabbbbbck‘,‘ab‘)
// = 0
subStringFinder(‘abbcdabbbbbck‘,‘bck‘)
// = 9
//doesn‘t work for this one.
subStringFinder(‘abbcdabbbbbck‘,‘bbbck‘)
// = -1
Q: 如何获取字符串中的所有排列?
A: 根据您对算法的了解程度,这可能会很困难。、
function permutations(str){
var arr = str.split(‘‘),
len = arr.length,
perms =[],
rest,
picked,
restPerms,
next;
if(len ==0)
return[str];
for(var i=0; i<len; i++)
{
rest =Object.create(arr);
picked = rest.splice(i,1);
restPerms = permutations(rest.join(‘‘));
for(var j=0, jLen = restPerms.length; j< jLen; j++)
{
next = picked.concat(restPerms[j]);
perms.push(next.join(‘‘));
}
}
return perms;
}
标签:toolbar ase type critical sum mat 缓冲 ffffff 面试问题
原文地址:https://www.cnblogs.com/ygunoil/p/12094469.html