基数排序再预习——解答一道面试题

Posted hans774882968

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基数排序再预习——解答一道面试题相关的知识,希望对你有一定的参考价值。

前端面试官:你不是说你算法很厉害嘛,怎么连基数排序的过程都记不得?

本文并不是基数排序教程,只是一个备忘录,动机就是上面那句QAQ。

网上很容易搜到那种朴素的基数排序实现:设基数为B,则维护一个大小为B的队列数组。每次循环先按位放入队列数组,然后不断取队首。这种实现的复杂度看上去很对,但跑起来不知道为什么就是很慢。

回忆一下:正统的实现,是基于“桶求和”的。我们把每个数字v扔进计数桶:c[v]++,并求其前缀和。则c[v]的含义变为:数组里小于等于v的数的个数。当我们倒序遍历数组,遍历到a[j],则它在新数组的位置可以表示为c[getBit(a[j], i)]-1(0-indexed),getBit表示取B进制的第i位,并把c数组当前位置减1,表示这一元素已经取出。

因为比a[j]下标更大且与它第i位相同的所有元素都已取出,所以倒序遍历能够求出正确的位置。

面试官问到这么一个问题:有1e5个数字要排序,值域4e9(原来说的是“int范围”,但加上偏移量即可为正整数,所以不妨假设这里都是正整数),内存无限,问哪种算法最合适。

我写了如下代码,实现了基数排序,并试图解决面试官的问题。

"use strict";

const radixSort = function (a, M = 65536) {
  if (!Number.isInteger(M) || M <= 1) throw new Error('Radix should be an integer >= 2!')
  const n = a.length
  let mx = Math.max(...a)
  let digit = 0
  for (; mx; mx = Math.floor(mx / M)) ++digit
  let pM = [1]
  for (let i = 1; i < digit; ++i) pM.push(pM[i - 1] * M)
  let c = new Array(M), tmp = new Array(n)
  const getBit = (v, i) => Math.floor(v / pM[i]) % M
  for (let i = 0; i < digit; ++i) {
    c.fill(0)
    a.forEach(v => ++c[getBit(v, i)])
    for (let j = 1; j < M; ++j) c[j] += c[j - 1]
    for (let j = n - 1; ~j; --j) tmp[--c[getBit(a[j], i)]] = a[j]
    a = tmp.slice()
  }
  return a
}

Array.prototype.isSorted = function () {
  if (typeof this !== 'object' || !(this instanceof Array)) {
    throw new Error('expected Array type!')
  }
  const a = this
  for (let i = 1; i < a.length; ++i) if (a[i] < a[i - 1]) return false
  return true
}

const getPerformance = function () {
  let ans = Infinity
  const n = 100000
  let idx = 0
  const lnM = Math.log(4e9)
  for (let i = 2; i <= 200000; ++i) {
    let v = Math.ceil((3 * n + i) * (Math.floor(lnM / Math.log(i)) + 1))
    if (ans > v) {
      ans = v
      idx = i
    }
  }
  return [idx, ans]
}

console.log(getPerformance())//[63246, 726492]
let arr = [
  [197, 164, 4684, 465, 1486, 4684, 1499, 461, 859, 458, 5610],
  [1980777, 167574, 4678684, 46787075, 14207086, 467632084, 1407599, 460781, 85709, 45078, 5607010]
]
for (let a of arr) {
  const b = radixSort(a, 10), c = radixSort(a)
  console.log(b, c)
  if (!b.isSorted() || !c.isSorted()) throw new Error('hans774882968好鶸啊!')
}

getPerformance就是求解面试官问的那个问题的。我们得到,基数取63246,计算次数约为726492。

因此结论是:如果用65536这个基数(和63246挺接近的!),位运算+循环展开(2轮就够了),应该会比快排跑得快一些!

以上是关于基数排序再预习——解答一道面试题的主要内容,如果未能解决你的问题,请参考以下文章

测试开发实战|一道有趣的大厂测试面试题,你能用 Python or Shell 解答吗?

一道linux shell面试题:如何用shell命令统计排序网站域名的访问量。

用C++编程思想解决一道题

面试官:给我手撕一下基数排序,再考虑一下如何进行改进呢?

每天一道大厂SQL题Day04大数据排序统计

每天一道大厂SQL题Day04大数据排序统计