基数排序再预习——解答一道面试题
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 解答吗?