[面试][白板题]设计一个简易的抽奖程序

白板题跟系统设计问题的相同点,就是重视厘清问题沟通

相比於系统设计,白板题往往需要写出能够运行的程序,或者提供面试官认为可行性高的演算法;大部分的白板题难度会落在 LeeCode Easy 的等级,有程序基础的人基本上都能够给出答案,但除了给出答案外,程序的正确率与执行效率也会列入评分。

本篇文章笔主要是分享答题思路以及完善程序,演算法并非笔者所擅长;如果想提升演算法的能力,除了买一本书好好阅读外,同时也要去 LeetCode 刷题累积实战经验。

有些公司是考 LeeCode Medium/Hard 等级的题目,除非天赋异禀;否则这个难度需要花大量时间(可能半年以上)练习,才能在现场稳定发挥。


大纲

  1. 初探白板题

    • 1.1 白板题的类型
    • 1.2 过程有时比解答更重要
  2. 设计一个简易的抽奖程序

    • 2.1 问题叙述
    • 2.2 面试官为什麽会问?
    • 2.3 面试官想从答案确认什麽?
    • 2.4 笔者提供的简答
  3. 衍伸问题

    • 3.1 如果使用者输入的机率有小数点呢?
    • 3.2 你有考虑到使用者可能输入不合理的机率吗?
    • 3.3 你会用什麽方式来验证程序的正确性?
    • 3.4 留给读者优化与思考的问题

1. 初探白板题

1.1 白板题的类型

白板题大概分成以下几种形式:

  • 在白板回答
    通常用白板回答的只要写出演算法跟架构就好。

    相较於其他形式,更注重与面试官的「沟通」与「确认问题」。

  • 纸上作业
    用来观察求职者在没有 IDE 辅助下,是否具备写程序的能力。

    现在的 IDE 实在太方便了,函式会自动补全、错误会直接 Highlight,很多工程师脱离 IDE 後真的是武功全废。

  • 上机考
    通常会给你一个时间限制,要求你在时间内写出一个可以 Run 的 MVP;大多数的面试官会看着你写 Code 的过程。

    有些人会因为不适应有人盯着写程序,导致现场发挥失常。


1.2 解题过程比解答更重要

也许你已经非常熟悉 LeeCode 的解题模式;但白板题跟 LeeCode 不同的地方在於通常没有测资、题目相对模糊

如果你接到题目就直接解题,可能会忽略一些隐性条件;在了解题目时如果发现模糊地带,请与面试官厘清问题,有时自己想像出来的条件反而会增加题目的复杂度。

通常在你给出第一版的解答後,面试官会以你的解答为基础循序渐进问一些延伸性的问题,这部分除了考核程序能力外,也是判断求职者在开发过程中能否与团队成员沟通;毕竟随着专案规模的扩增,单打独斗的情境已不多见。

如果遇上你无法解决的白板题,请以积极的态度与面试官沟通你想到的解题方式和卡住的点;相比於求职者的程序能力,有些面试官更加看重求职者的人格特质;有些人也许程序能力不足,但能理解面试官的问题,并在讨论过程中作出改善;若能在解题过程中被面试官视为有潜力的人才,也是有机会获得 Offer 的。

备注 1:解题过程中并不是所有面试官都是以「讨论」的形式与你沟通,有些是以「质问」的态度面对求职者;遇到用「质问」态度的面试官,你也要思考自己是否能够与这样的同事一起共事

备注 2:白板题需要有良好的程序基础与抗压性,才可能在面试现场表现优良。


2. 设计一个简易的抽奖程序

为了方便读者阅读与验证,本篇文章以上机考的形式向大家分享。

2.1 问题叙述

请你设计一个抽奖系统,奖品有「一奖、二奖、三奖」,其余都是感谢奖。
一奖、二奖、三奖的中奖机率可以自由输入,若没输入则默认一奖(1%)、二奖(2%)、三奖(3%)。

时间限制:5 分钟


2.2 面试官为什麽会问?

这个题目虽然简单,却有很多延伸的议题可以讨论;是一个了解求职者基本功的经典题目。


2.3 面试官想从答案确认什麽?

  • 你的程序是否保留扩充空间
  • 你会用什麽演算法解决这个问题
  • 是否有验证使用者输入的参数
  • 如何验证你的程序是符合需求的

2.4 笔者提供的简答

在面对白板题时,笔者是先求有再求好;如果一昧追求高效但自己不熟悉的演算法,有时会有意想不到的错误。

笔者的逻辑是先把签放入签筒,再透过乱数抽奖印出结果,以符合题目的基础要求为目标:

  • 使用者可以透过调整参数变更中奖机率。
  • 默认的中奖机率:一奖(1%)、二奖(2%)、三奖(3%)。
  • Math.random() 函式模拟乱数抽奖。
  • 会印出中奖结果
// 写一个函式,有默认的抽奖机率:一奖(1%)、二奖(2%)、三奖(3%)
function lottery(first_prize = 1, second_prize = 2, third_prize = 3) {
  let lickbox = [];
  // 放入签筒
  for (var i = 0; i < 100; i++) {
    if (first_prize !== 0) {
      lickbox.push("一奖");
      first_prize--;
    } else if (second_prize !== 0) {
      lickbox.push("二奖");
      second_prize--;
    } else if (third_prize !== 0) {
      lickbox.push("三奖");
      third_prize--;
    } else {
      lickbox.push("感谢奖");
    }
  }
  // 抽奖
  console.log(lickbox[Math.floor(Math.random() * 99)]);
}
lottery();

上面的程序先不论演算法优劣,还有许多细节是没有被考虑到的;在完成 MVP 後有些面试官会先问你:「之前有写过类似的程序吗?」如果没有相关经验就老实说没有吧;接下来面试官会以这个程序为基础,跟你讨论哪里还可以做改善。


3. 衍伸问题

3.1 如果使用者输入的机率有小数点呢?

考点:求职者对浮点数的处理

很显然,如果使用者输入的机率包含小数点,现在的函式无法应付,下面是笔者的处理方式:

  • 取得输入得奖机率中最长的小数点有几位(N)。
  • 将所有得奖机率乘以 10 的 N 次方,这样机率就没有小数点了。
  • 将 10 的 N 次方再乘以 100,以此为签筒的总数量
function lottery(first_prize = 1, second_prize = 2, third_prize = 3) {
  // 取得最长小数点
  let tmp = [first_prize, second_prize, third_prize];
  let longest = 0;
  tmp.forEach((prize) => {
    let after_point = prize.toString().split(".")[1];
    if (after_point) {
      if (after_point.length > longest) {
        longest = after_point.length;
      }
    }
  });

  // 让机率变整数
  let multiple = Math.pow(10, longest);
  // 没有用 Math.floor 会出现 js 小数点结尾的 bug,ex:11100.000000000002
  first_prize = Math.floor(first_prize * multiple);
  second_prize = Math.floor(second_prize * multiple);
  third_prize = Math.floor(third_prize * multiple);
  // 签筒的总数量
  let max = multiple * 100;

  let lickbox = [];
  // 放入签筒
  for (var i = 0; i < max; i++) {
    if (first_prize !== 0) {
      lickbox.push("一奖");
      first_prize--;
    } else if (second_prize !== 0) {
      lickbox.push("二奖");
      second_prize--;
    } else if (third_prize !== 0) {
      lickbox.push("三奖");
      third_prize--;
    } else {
      lickbox.push("感谢奖");
    }
  }
  // 抽奖
  console.log(lickbox[Math.floor(Math.random() * (max - 1))]);
}
lottery(1.11, 2.34, 3.567);

3.2 你有考虑到使用者可能输入不合理的机率吗?

考点:能举例会发生错误的情境,并设计程序来验证传入的参数

如果没有对输入参数做验证,程序的稳定性真的是极低,下面列举几个要做的基础验证:

  • 中奖机率不可能超过 100%,如果一奖、二奖、三奖加起来超过 100 是不合理的。
  • 中奖机率要是正数,输入负的机率也是不合理。
  • 如果参数输入文字或是符号就一定是错的

这边笔者透过撰写一个函式(validate_input)来验证输入的参数,如果不符规则就会跳出错误讯息

function validate_input(first_prize, second_prize, third_prize) {
  // 确认是否为正数
  var reg = /^(?=.+)(?:[1-9]\d*|0)?(?:\.\d+)?$/;
  if (!(reg.test(first_prize) && reg.test(second_prize) && reg.test(third_prize))) {
    console.log("请确认输入参数皆为正数!");
    return false;
  }

  // 确认没爆表
  if (first_prize + second_prize + third_prize > 100) {
    console.log("中奖机率超过 100 %,爆表了!");
    return false;
  }
  return true;
}
const argsArray = [
  [100, 2.34, 3.567], //中奖率不可能超过100
  [-1, 2.34, 3.567], //机率不可为负
  [1, "错误", 3.567], //含有文字
  [1, 2.34, 3.567],
];
argsArray.forEach((args) => {
  console.log("验证: " + args);
  validate_input.apply(this, args);
});

3.3 你会用什麽方式来验证程序的正确性?

考点:是否有自动化的测试方案来验证程序

程序的正确性并不是用嘴说的,面试官想了解你会用什麽方法验证程序的正确性

笔者这边会写一个测试函式来蒐集抽奖结果,并对原本程序做如下调整:

  • 签筒会反覆使用,所以把它独立成函式(initLickbox)。
  • lottery 函式改为纯粹抽签用。
  • 建立测试函式(qa),可调整测试次数,在执行时会印出实际机率与期望机率的比对。

这里附上调整过的完整程序:

function initLickbox(first_prize = 1, second_prize = 2, third_prize = 3) {
  // 先验证输入参数
  if (!validate_input(first_prize, second_prize, third_prize)) {
    return;
  }
  // 取得最长小数点
  let tmp = [first_prize, second_prize, third_prize];
  let longest = 0;
  tmp.forEach((prize) => {
    let after_point = prize.toString().split(".")[1];
    if (after_point) {
      if (after_point.length > longest) {
        longest = after_point.length;
      }
    }
  });

  // 让机率变整数
  let multiple = Math.pow(10, longest);
  // 没有用 Math.floor 会出现 js 小数点结尾的 bug,ex:11100.000000000002
  first_prize = Math.floor(first_prize * multiple);
  second_prize = Math.floor(second_prize * multiple);
  third_prize = Math.floor(third_prize * multiple);
  // 签筒的总数量
  let max = multiple * 100;

  let lickbox = [];
  // 放入签筒
  for (var i = 0; i < max; i++) {
    if (first_prize !== 0) {
      lickbox.push("一奖");
      first_prize--;
    } else if (second_prize !== 0) {
      lickbox.push("二奖");
      second_prize--;
    } else if (third_prize !== 0) {
      lickbox.push("三奖");
      third_prize--;
    } else {
      lickbox.push("感谢奖");
    }
  }
  return lickbox;
}

function lottery(lickbox = []) {
  let max = lickbox.length;
  // 抽奖
  let result = lickbox[Math.floor(Math.random() * (max - 1))];
  return result;
}
function validate_input(first_prize, second_prize, third_prize) {
  // 确认是否为正数
  var reg = /^(?=.+)(?:[1-9]\d*|0)?(?:\.\d+)?$/;
  if (!(reg.test(first_prize) && reg.test(second_prize) && reg.test(third_prize))) {
    console.log("请确认输入参数皆为正数!");
    return false;
  }

  // 确认没爆表
  if (first_prize + second_prize + third_prize > 100) {
    console.log("中奖机率超过 100 %,爆表了!");
    return false;
  }
  return true;
}
function qa(test_times = 10000) {
  // 设定中奖机率
  let first_prize = 1.5,
    second_prize = 2.33,
    third_prize = 3.98;
  let thanks_prize = 100 - first_prize - second_prize - third_prize;
  let lickbox = initLickbox(first_prize, second_prize, third_prize);
  let first = 0,
    second = 0,
    third = 0,
    thanks = 0;
  for (var i = 0; i < test_times; i++) {
    let result = lottery(lickbox);
    if (result == "一奖") {
      first++;
    } else if (result == "二奖") {
      second++;
    } else if (result == "三奖") {
      third++;
    } else {
      thanks++;
    }
  }
  console.log("一奖:" + ((first / test_times) * 100).toFixed(2) + "% (" + first_prize + "%)");
  console.log("二奖:" + ((second / test_times) * 100).toFixed(2) + "% (" + second_prize + "%)");
  console.log("三奖:" + ((third / test_times) * 100).toFixed(2) + "% (" + third_prize + "%)");
  console.log("感谢奖:" + ((thanks / test_times) * 100).toFixed(2) + "% (" + thanks_prize + "%)");
}
qa();

// console.log(lottery(initLickbox()))

图片为程序测试的结果:
https://ithelp.ithome.com.tw/upload/images/20211006/20103256E3NQ1zqCBa.png


3.4 留给读者优化与思考的问题

上面只是笔者在面对白板题时的简单发想,这边还有一些问题留给读者思考:

  • 需不需要限制浮点数在小数点後的位数?
  • 笔者的解决方案超级土法炼钢,有没有更好的演算法可以优化?
  • 如果改成抽签不放回的机制,程序要如何调整?
  • 假使有四奖、五奖、六奖...程序架构要如何优化?

即使是抽奖系统这种看似简单的题目,如果深入探讨还是有许多的细节需要完善。

笔者上面所提出的衍伸问题,其实是求职者一开始要跟面试官厘清的细节,如果能够在开始作业前就定义好需求,在开发的过程中就能避免不合适的设计

不过有些面试官会倾向等求职者完成 MVP 後再做细节讨论,笔者就曾遇过面试官出完白板题後就离开 15 分钟让求职者作答;所以实际状况还是需要读者在现场随机应变


感谢大家的阅读,如果喜欢我的文章可以订阅接收通知;如果有帮助到你,按Like可以让我更有写文的动力,我们明天见~

我在 Medium 平台 也分享了许多技术文章
❝ 主题涵盖「MIS & DEVOPS资料库前端後端MICROSFT 365GOOGLE 云端应用自我修炼」希望可以帮助遇到相同问题、想自我成长的人。❞


<<:  Day30 完赛心得

>>:  Day29值的型态(JavaScript)

前端工程师也能开发全端网页:挑战 30 天用 React 加上 Firebase 打造社群网站|Day28 留言电子邮件通知

连续 30 天不中断每天上传一支教学影片,教你如何用 React 加上 Firebase 打造社群...

D18. 学习基础C、C++语言

D18. 函式库 #include<stdio.h> main(){ printf(&q...

[想试试看JavaScript ] 陈述式与表达式

程序语言,也是一种语言,所以也有一些语法可以归类。 像学英文一样,知道语法可以帮助我们学会如何可以组...

【PHP Telegram Bot】Day29 - 社群按赞机器人(1):让频道出现按赞按钮

今天来做这个很实用的东东,很多频道都有这个功能 将机器人加入频道 机器人要加入频道的话只能加成管理...

什麽是擅长编程?

今天在学习3d建模的时候,发现一个有趣的东西:3d建模的模型,如果要方便使用在各种软件当中的话,需要...