Skip to content

Instantly share code, notes, and snippets.

@jackhftang
Last active May 6, 2024 19:44
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jackhftang/709ec0f7fe6a6565c07b778d1c538a7f to your computer and use it in GitHub Desktop.
Save jackhftang/709ec0f7fe6a6565c07b778d1c538a7f to your computer and use it in GitHub Desktop.
Oursky Developer Pre-Test

Question 1

Write a function that takes two arrays as input, each array contains a list of A-Z; Your program should return True if the 2nd array is a subset of 1st array, or False if not.

For example: isSubset([A,B,C,D,E], [A,E,D]) = true isSubset([A,B,C,D,E], [A,D,Z]) = false isSubset([A,D,E], [A,A,D,E]) = true

Answer 1

one-liner

This one is handy, sometimes may directly used inline. When size of superset is small, it works fine.

const isSubset = (a,b) => b.every(x => a.indexOf(x) >= 0);

radix

function isSubset(a,b){
	const s = 'A'.charCodeAt(0);
	let t = Array(26).fill(true);
	for(let x of a) t[x.charCodeAt(0) - s] = false;
	for(let x of b) if(t[x.charCodeAt(0) -s]) return false;
	return true;
}

list merge

The technique is the same as used in merge sort or finding unique element in an array. This modifies the original array, which may or may not be important.

function isSubset(a,b){
	a.sort((x,y) => x-y);
	b.sort((x,y) => x-y);
	let i=0,j=0;
	while(i<a.length && j<b.length){
		if( a[i] < b[i]) i++;
		else if( a[i] === b[j] ) i++, j++;
		// if the array do not contain incompatible type 
		// e.g. 3 < undefined // always yield false
		// this should be the case a[i] > b[i] 
		else return false;
	}
	return b.length === j;
}

binary search

Assume the set of elements are orderable.

function bsearchMax(a,b,test){
  // a is true and valid
  while(b-a>1){
    let mid = Math.floor((a+b)/2);
    test(mid) ? a = mid : b = mid;
  }
  return a;
}

function isSubset(a,b){
	a.sort( (x,y) => x-y);
	for(let x of b){
		let ix = bsearchMax(-1, a.length, y => a[y] <= x)
		if( ix < 0 || a[ix] !== b ) return false;
	}
	return true;
}

set

Depend on implementation, usually it is a hash set.

function isSubset(a,b){
	let s = new Set(a);
	for(let x of b) if(!s.has(x)) return false;
	return true;
}

Instead of checking has() on superset, we can also delete() on subset.

function isSubset(a,b){
	let s = new Set(b);
	for(let x of a) s.delete(x);
	return s.size === 0;
}

It may be worth mention that it is particularly easy to code in python.

def isSubset(a,b):
	return set(b) <= set(a)

divide and conquer

function isSubset(a,b){
	a.sort((a,b) => a-b);
	b.sort((a,b) => a-b);

function run(ai,aj,bi,bj){
    if(ai === aj) return bj-bi===0;
    if(bi === bj) return true;

    // out bound O( 1 ) 
    if( b[bi] < a[ai] || a[aj] < b[bj] ) return false; 

    // divide both superset and subset into half
    let bk = Math.floor((bi+bj)/2);
    let ak = bsearchMin(ai, aj, i => b[bk] <= a[i] );

    // cannot find b[bk] in a
    if( ak === aj || b[bk] !== a[ak] ) return false;

    return run(ai,ak,bi,bk) && run(ak,aj,bk+1,bj);
  } 

  return run(0,a.length,0,b.length);
}

van Emde Boas tree

function isSubset(a,b){
  // pesudo code 
  let tree = new vEBTree();
  for(let x of a) tree.add(x);
  return b.every(x => tree.has(x));
}

bloom filter

A probabilistic data structure. No false negative, but false positive. In this case, there may be wrong isSubset, but no isNotSubset.

Similar to hash set method, but with more than one hash. When superset is very large, and pre-processing is allowed. This method be handy to pass around as fast screening.

function isSubset(a,b){
  // pesudo code
  let s = new Set();
  for(let x of a){
    for(let hash of hashes) s.add(hash(x));
  }
  return b.every( x => hashes.every(hash => s.has(hash(x))));
}

parallel

Obvisouly, subset can be divided and checked in parallel.

const cluster = require('cluster');
const nWorker = require('os').cpus().length;
const print = console.log.bind(console);

if (cluster.isMaster) {
  // initialize input argument here
  const a = new Array(1e6);
  for (let i = 0; i < a.length; i++) a[i] = Math.floor(Math.random() * 1e10);
  const b = a.slice(0, 1e4);

  const batchSize = Math.ceil(b.length / nWorker);

  // fork and initialize workers
  let workers = [];
  for (let i = 0; i < nWorker; i++) {
    const worker = cluster.fork();
    worker.send(['superset', a]);
    workers.push(worker);
  }

  let doneCnt = 0;
  workers.forEach((worker, i) => {
    worker.on('message', isSubset => {
      if (isSubset) {
        doneCnt++;
        if (doneCnt === nWorker) {
          workers.forEach(w => w.kill());
          print(true);
        }
      }
      else {
        workers.forEach(w => w.kill());
        print(false);
      }
    });
    worker.send(['subset', b.slice(i * batchSize, (i + 1) * batchSize)]);
  });
}
else if (cluster.isWorker) {
  let a = new Set();
  process.on('message', ([cmd, arg]) => {
    if (cmd === 'superset') a = new Set(arg);
    else if (cmd === 'subset') {
      process.send(arg.every(x => a.has(x)));
    }
  });
}

parallel (divide and conquer)

Similar to divide and conquer, but call the two subcall in parallel.

// pesudo code 

function isSubset(a,b,callback){
	a.sort((a,b) => a-b);
	b.sort((a,b) => a-b);

  function run(ai,aj,bi,bj,done){
    if(ai === aj) return bj-bi===0;
    if(bi === bj) return true;

    // out bound O( 1 ) 
    if( b[bi] < a[ai] || a[aj] < b[bj] ) return false; 

    // divide both superset and subset into half
    let bk = Math.floor((bi+bj)/2);
    let ak = bsearchMin(ai, aj, i => b[bk] <= a[i] );

    // cannot find b[bk] in a
    if( ak === aj || b[bk] !== a[ak] ) return false;
    
    // pesudo code 
    let resCnt = 0;
    let thread1 = new Thread(_ => run(ai,ak,bi,bk), (err, res) => {
      if(err) return done(err);
      if(!res) done(null, false);
      if(++resCnt === 2) done(null, true);
    });
    let thread2 = new Thread(_ => run(ak,aj,bk+1,bj), (err, res) => {
      if(err) return done(err);
      if(!res) done(null, false);
      if(++resCnt === 2) done(null, true);
    });
  } 

  return run(0,a.length,0,b.length, callback);
}

Question 2

What is the computational complexity of your answer in Question 1?

Can you explain why?

Answer 2

Let A be the superset to test. Let B be the subset to test. Let size of A be M, size of B be N. Let P be the number of parallel workers.

one-liner

For each element in B, we search A one by one. So, time complexity is O(M*N). Space complexity is O(1)

Note: the coefficient is small. If M or N is small, this works well.

list merge

Sorting A and B take O(Mlog(M) + Nlog(N)) The merging part take at most O(M+N).

So, time complexity is O(Mlog(M) + Nlog(N)). If the array is already sorted, then O(M+N).

Space complexity depend on sorting algorithm. Regardless of that, Space complexity is O(1).

binary search

Sorting A take O(Mlog(M)) Binary search for N element take O(Nlog(M))

So, time complexity O((M + N)*log(M)) Space complexity is O(1)

set

Depend on implementation of set, if it is hash set, the time complexity is O(N) in amortized sense. if it is tree set, the time complexity is O(N*log(M))

Space complexity, again depend on implementation, should be at least O(M)

divide and conquer

Each element in B at most do binary search once. This similar to binary search, but the range is smaller and also with early break; So, time complexity is O(N * log(M)) and Space complexity O(1)

van Emde Boas tree

With vEB tree, all operations are O(log(log(N))) Build tree take O(Mlog(log(M))) Subset check take O(Nlog(log(M))) in total, O( (M + N) * log(log(M)) )

bloom filter

Same as hash set, time complexity is O(N), but due to probabilistic nature, usually, there is another algorithm to further confirm. space complexity is adjustable, but should be somehow O(M).

parallel

Worker initialization take O(P*M) If shared memory is supported, then O(1)

Distribute array B to workers take O(N) If shared memory is supported, then O(1)

And workers can any isSubset method above At best, take O(N/P) to complete isSubset check. passing result back take O(1)

Overall, O(P*M + N + N/P). Ideally, if shared memory is supported, O(M + N/P)

parallel (divide and conquer)

Since each subcall divide subset into half and superset roughly into half. The dept of subcall at most Max(log(M), log(N)). Each call take O(log(N)) to complete. If assume shared memory (no copy) and infinite parallelism The time complexity is O( Max(log(M), log(N)) * log(N))

const lookup = (function () {
let a = 1, b = 1, arr = [];
for (let n = 78; n--;) {
arr.push(b);
let t = b;
b += a;
a = t;
}
return arr;
})();
function bsearchMin(a, b, test) {
// test(b) is vritually true
while (b - a > 1) {
let mid = Math.floor((a + b) / 2);
test(mid) ? b = mid : a = mid;
}
return b;
}
function nextFibIxSmall(n) {
return bsearchMin(0, lookup.length, i => n < lookup[i]);
}
/**
*
* fast Fibonacci number mod m
*
* @param mod
* @param n
* @returns {number}
*/
function modFib(mod, n) {
function mul(a, b) {
return [
(a[0] * b[0] + a[1] * b[2]) % mod,
(a[0] * b[1] + a[1] * b[3]) % mod,
(a[2] * b[0] + a[3] * b[2]) % mod,
(a[2] * b[1] + a[3] * b[3]) % mod
]
}
function exp(m, n) {
// m^n
if (n === 0) return [1, 0, 0, 1];
let n2 = Math.floor(n / 2);
let t = exp(m, n2);
let t2 = mul(t, t);
if (n % 2 === 0) return t2;
return mul(t2, m);
}
if (n <= 2) return 1;
let m = [1, 1, 1, 0];
let ans = exp(m, n - 2);
return (ans[0] + ans[1]) % mod;
}
/**
*
* inverse of Fibonacci number
*
* @param intStr
* @returns {number}
*/
function ifib(intStr) {
let n = intStr.length - 1;
let x = parseFloat(intStr[0] + '.' + intStr.slice(1));
const a = 2.0780869212350273;
const b = 1.6722759381845549;
const ln10 = 2.302585092994046;
return a * (n * ln10 + Math.log(x)) + b;
}
function nextFibIxLarge(intStr) {
const MOD = 1e7;
let x = ifib(intStr);
let xround = Math.round(x);
let y1 = parseInt(intStr.slice(-7));
let EPS = 1e-13;
let err = Math.abs(x - xround);
if (err > EPS) return xround + 1;
let y2 = modFib(MOD, xround);
// how far y1 away from y2
let diff = (y1 - y2 + MOD ) % MOD;
if (diff < MOD / 2) {
// assume to be y1 >= y2
return xround + 1;
}
// assume to be y1 < y2
return xround;
}
function nextFibIx(n) {
if (typeof n === 'string') {
if (n.length > 12) return nextFibIxLarge(n);
return nextFibIxSmall(parseInt(n));
}
return nextFibIxSmall(n);
}
function nextFibonacci(arr) {
return arr.map(n => {
let ix = nextFibIx(n);
if (ix < lookup.length) return lookup[ix];
return `fib(${ix})`;
});
}
module.exports = {
nextFibIxSmall,
nextFibIxLarge,
nextFibIx,
nextFibonacci
};

Question 3

Write a function that takes an array of integers as input. For each integer, output the next fibonacci number.

Fibonacci number of Fn is defined by: Fn = Fn-1 + Fn-2 F1 = 1, F2 = 1

Your program should run correctly for the first 60 Fibonacci Numbers.

For example: nextFibonacci([1,9,22])
Output: 2
13
34

Explanation: 2 is the next fibonacci number great than 1. The fibonacci number after 9 is 13, and after 22 is 34.

Answer 3

At first glance, it is straight forward and reasonable to make use of lookup array and binary search.

let lookup = (function () {
  let a = 1, b = 1, arr = [];
  for (let n = 78; n--;) {
    arr.push(b);
    let t = b;
    b += a;
    a = t;
  }
  return arr;
})();

function bsearchMin(a, b, test) {
  // test(b) is vritually true
  while (b - a > 1) {
    let mid = Math.floor((a + b) / 2);
    test(mid) ? b = mid : a = mid;
  }
  return b;
}

function nextFibonacci(arr) {
  return arr.map(i => {
    let ix = bsearchMin(0, lookup.length, j => {
      return i < lookup[j];
    });
    return lookup[ix];
  })
}

The question requires that the program should run correctly for the first 60 Fibonacci number. But it may be interesting to ask how large beyond 60 can we handle (without BigInteger library)?

For programming language like javascript, it has only double as its numerical primitive and double has 52+1 bit of significant, which can at most represent the 78th Fibonacci number exactly. It is because:

fib(78) = 8,944,394,323,791,464 < 2^53 = 9,007,199,254,740,992 < fib(79) = 14,472,334,024,676,221

Is it mean that we have no ways to handle input larger that fib(79) ? Not exactly. To see how, let define ifib(x) as inverse of fib (i.e. ifib(fib(x)) = x), since fib(x) is strictly increasing for x > 4, therefore inverse of fib exists.

The core of this question is to calculate

answer = fib( floor( ifib(x) + 1 ) )

The exact formula for fib(x) = (a^x - b^x)/sqrt(5) where a = (1+sqrt(5))/2, b = (1-sqrt(5))/2 And there is no closed form of ifib(x). But for x > 40, fib(x) is almost equal to a^x/sqrt(5), (because b = -0.618 < 1, b^x tend to 0 and a=1.618, a^x grow very large) and hence

	ifib(x) ~= ln(sqrt(5)*x)/ln(a) where a = (1+sqrt(5))/2
        ~= 2.0780869212350273 * ln(x) + 1.6722759381845549

in code:

function ifib(intStr){
  let n = intStr.length-1;
  let x = parseFloat(intStr[0] + '.' + intStr.slice(1));
  const a = 2.0780869212350273;
  const b = 1.6722759381845549;
  const ln10 = 2.302585092994046;
  return  a * (n * ln10 + Math.log(x)) + b;
}

Let's see an example:

x = 142078217164707271763357915451
(NOTE: x is randomly chosen and x > 2^96) 

ifib('142078217164707271763357915451') = 141.16630278089832

answer = fib( floor(141.16630278089832 + 1) ) = fib(142) 

This example us shows that most of time (assume we have only 45-bit accuracy, we have 1-0.5^45 > 99.99999999999% of time), we only need the information of the number of digits and the most significant digits, and would be enough to determine the next Fibonacci number.

However there is still one issue: When x is close to some Fibonacci number, we have precision problem. Take an example:

x = 2880067194370816119 = fib(90)-1
ifib(x) = 90
answer = fib( floor( 90 + 1 ) ) = fib(91)

(NOTE: the correct answer should be fib(90))

To mitigate the issue, we compare the least significant digits of fib(round(ifib(x))) and x. Using matrix exponential, it is fast O(ln(x)) to compute the exact value fib(x)%m for some m.

function modFib(mod, n) {

  function mul(a, b) {
    return [
      (a[0] * b[0] + a[1] * b[2]) % mod,
      (a[0] * b[1] + a[1] * b[3]) % mod,
      (a[2] * b[0] + a[3] * b[2]) % mod,
      (a[2] * b[1] + a[3] * b[3]) % mod
    ]
  }

  function exp(m, n) {
    // m^n
    if (n === 0) return [1, 0, 0, 1];
    let n2 = Math.floor(n / 2);
    let t = exp(m, n2);
    let t2 = mul(t, t);
    if (n % 2 === 0) return t2;
    return mul(t2, m);
  }

  if (n <= 2) return 1;
  let m = [1, 1, 1, 0];
  let ans = exp(m, n - 2);
  
  return (ans[0] + ans[1]) % mod;
}

function nextFibIx(intStr) {
  const EPS = 1e-14;
  const MOD = 1e7;
  let x = ifib(intStr);
  let xround = Math.round(x);
  let y1 = parseInt(intStr.slice(-7));

  if (Math.abs(x - xround) > EPS) return xround + 1;

  let y2 = modFib(MOD, xround);

  // how far y1 away from y2
  let diff = (y1 - y2 + MOD ) % MOD;

  if (diff < MOD / 2) {
    // assume to be y1 >= y2
    return xround + 1;
  }
  // assume to be y1 < y2
  return xround
}

Now re-test the above example:

nextFibIx('2880067194370816119') // => 90
nextFibIx('2880067194370816120') // => 91

Good! However, the precision problem is not completely solved. I have used most-significant and least-significant digits to determine the answer, but still there some precision loss in the middle digits. There are probably other methods to mitigate the issue, but I want to stop here. Thought I did have rigorous analysis, I would expect the above two ideas should give correct answer over the whole range of int64. (roughly estimation of accuracy: 53 + lg(5e6) ~= 75.2 bits, with some loss of precision, should have still have 64-bits )

(see answer3.js for full source code)

Finally, let's close this question with an naive BigInteger library, which is easy to implement and work well =]

def nextFib(x):
    a,b = 1,1
    while b <= x:
        t = b;
        b = a + b;
        a = t; 
    return b
  
def nextFibonacci(arr):
    return [ nextFib(x) for x in arr]
const assert = require('assert');
const print = console.log.bind(console);
function createArrayOfFunctions(y) {
var arr = [];
for (let i = 0; i < y; i++) {
arr[i] = function (x) {
return x + i;
}
}
return arr;
}
function createArrayOfFunctions1(y) {
var arr = [];
for (var i = 0; i < y; i++)(function (i) {
arr[i] = function (x) {
return x + i;
}
})(i);
return arr;
}
function createArrayOfFunctions2(y) {
var arr = [];
for (var i = 0; i < y; i++) {
arr[i] = (function (i) {
return function (x) {
return x + i;
};
})(i)
}
return arr;
}
function createArrayOfFunctions3(y) {
var arr = [];
for (var i = 0; i < y; i++) {
arr[i] = function f(x) {
return x + arr.indexOf(f);
}
}
return arr;
}
function createArrayOfFunctions4(y) {
var arr = [];
for (var i = 0; i < y; i++) {
arr[i] = eval('(function(x) { return x + z; })'.replace('z', i));
}
return arr;
}
function createArrayOfFunctions5(y) {
var arr = [];
for (var i = 0; i < y; i++) {
arr[i] = (function (x,y) { return x + y; }).bind(null, i);
}
return arr;
}
function createArrayOfFunctions6(y) {
var arr = [];
for (var i = 0; i < y; i++) {
arr[i] = (function (x) {
return x + this;
}).bind(i)
}
return arr;
}
function createArrayOfFunctions7(y) {
var arr = [];
for (var i = 0; i < y; i++) {
arr[i] = (function (x) {
return this.call ? 1 + this(x) : x;
}).bind(arr[i-1]);
}
return arr;
}
let createArrayOfFunctions8 = n => Array(n).fill().map((_, i) => x => x + i);
const toTest = [
createArrayOfFunctions1,
createArrayOfFunctions2,
createArrayOfFunctions3,
createArrayOfFunctions4,
createArrayOfFunctions5,
createArrayOfFunctions6,
createArrayOfFunctions7,
createArrayOfFunctions8,
];
for (let f of toTest) {
let n = 100;
let arr = f(n);
for (let i = 0; i < n; i++) {
for (let j = -100; j < 100; j++) {
assert(arr[i](j) === i + j);
}
}
}
print('done');

Question 4

Please explain what is the bug in the following Javascript function, and how to correct it.

function createArrayOfFunctions(y) {
  var arr = [];
  for(var i = 0; i<y; i++) {
    arr[i] = function(x) { return x + i; }
  }
  return arr; 
}

Answer 4

Strictly speaking, to say whether there is a bug, I need to know the intention of code.
And since the question do not specify any intention, I would assume that the code is to create an array of function such that the i-th function add i to its input

Let's start with a test

describe('question 4', function(){
  
  describe('test 1: validation', function(){
    
    it('n=100', function(){
      let n = 100;
      let arr = createArrayOfFunctions(n);
      for(let i=0; i<n; i++){
        for(let j=-100; j<100; j++){
          assert(arr[i](j) === i+j);
        }
      }
    })
    
  })
})

The critical problem of this question is scoping. Before ES6, there is only function scope and an implicit global one. The variable i live in the scope of function createArrayOfFunctions, And all functions in arr reference the same variable i in the scope of createArrayOfFunctions. This means once i change, the value of i seen from all functions also change.

ES6 introduce block scope of variable and defined by keyword let. So, a simple solution is

function createArrayOfFunctions(y) {
  var arr = [];
  for(let i = 0; i<y; i++) {
    arr[i] = function(x) { return x + i; }
  }
  return arr; 
}

If ES5 is required, a common technique is to wrap the whole block statements with a function.

function createArrayOfFunctions(y) {
  var arr = [];
  for(var i = 0; i<y; i++)(function(i){
    arr[i] = function(x) { return x + i; }
  })(i);
  return arr;
}

or only wrap the function

function createArrayOfFunctions2(y) {
  var arr = [];
  for(var i = 0; i<y; i++){
    arr[i] = (function(i){
      return function(x) { return x + i; };
    })(i)
  }
  return arr;
}

The former is more general and later one may look nicer in some cases. For practical use, the above ways is fairly enough. But as an interview question, we may look into other interesting solutions.

indexOf

function createArrayOfFunctions3(y){
  var arr = [];
  for (var i = 0; i < y; i++) {
    arr[i] = function f(x){ return x + arr.indexOf(f);}
  }
  return arr;
}

eval

function createArrayOfFunctions4(y) {
  var arr = [];
  for (let i = 0; i < y; i++) {
    arr[i] = eval('(function(x) { return x + z; })'.replace('z', i));
  }
  return arr;
}

bind

function createArrayOfFunctions5(y) {
  var arr = [];
  for (let i = 0; i < y; i++) {
    arr[i] = (function (x,y) { return x + y; }).bind(null, i);
  }
  return arr;
}

bind, this

function createArrayOfFunctions6(y) {
  var arr = [];
  for (let i = 0; i < y; i++) {
    arr[i] = (function (x) { return x + this; }).bind(i);
  }
  return arr;
}

bind, this, recursion

function createArrayOfFunctions7(y) {
  var arr = [];
  for (var i = 0; i < y; i++) {
    arr[i] = (function (x) {
      return this.call ? 1 + this(x) : x;
    }).bind(arr[i-1]);
  }
  return arr;
}

one-liner

let createArrayOfFunctions7 = n => Array(n).fill().map( (_,i) => x => x+i ); 
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment