常见的一个问题: 给定一个整形数组, 统计其中有多少唯一的元素.
常见的思路有哪些呢?
以上空间复杂度均与元素数量关联, 如果允许损失精度, 是否可以使用较低的空间占用来统计呢?
但是, 我在这篇文章看到了一种全新的思路. 尽管并不建议在生产环境中使用, 但仍不失为一种思路.
这种思路简单说就是: “采样”. 就像是统计一个湖泊中鱼的数量, 并不会一次性将所有的鱼都捞上来. 而是先钓n 条鱼上来, 给他们都做上记号. 几天后再钓 n 条鱼上来, 看其中有多少个有记号的鱼. 从而来估算整个湖泊中鱼的总数.
这个思路是否也可以在这个问题上借鉴呢?
通常的统计方式如下:
func count(arr []int) int {
m := make(map[int]bool)
for _, i := range arr {
m[i] = true
}
return len(m)
}
好, 现在开始利用"采样"的思路, 丢失精确度, 降低空间复杂度.
我们将一半的元素放到hash
表中保存:
func count(arr []int) int {
m := make(map[int]bool)
for _, i := range arr {
if rand.Intn(2) == 0 {
m[i] = true
}
}
return len(m) * 2
}
但, 如此有个问题, 元素在数组中出下的次数, 会影响其最终放入hash
的概率. 次数越多, 概率越大. 这显然会影响计算的公平性.
一个非常简单的解决思路: 在随机之前, 我们将该元素从hash
中先删除. 这样, 影响此元素是否在hash
中出现的, 就只是其最后一次随机的结果了.
func count(arr []int) int {
m := make(map[int]bool)
for _, i := range arr {
delete(m, i)
if rand.Intn(2) == 0 {
m[i] = true
}
}
return len(m) * 2
}
当然, 现在的内存使用应该是数组长度的一半. 我们可以增加随机率来进一步降低内存占用.
func count(arr []int, p int) int {
m := make(map[int]bool)
for _, i := range arr {
delete(m, i)
if rand.Intn(p) == 0 {
m[i] = true
}
}
return len(m) * p
}
至此, 基本思路已经介绍完了, 但实际的内存占用仍然与数组大小关联. 可以看到, 随机率越高, hash
中保存的元素就会越少. 我们是否可以动态的来调整呢? 让随机率随着数组长度的增加而增加.
func count(arr []int, maxSize int) int {
p := 2
m := make(map[int]bool)
for _, i := range arr {
delete(m, i)
if rand.Intn(p) == 0 {
m[i] = true
}
if len(m) >= maxSize {
p *= 2
// 因为随机率增大了一倍
// 之前的元素也需要再次进行随机, 使得其满足现有的随机率
for k, _ := range m {
if rand.Intn(2) == 0 {
delete(m, k)
}
}
}
}
return len(m) * p
}
以上, 就是整个算法的全部内容了. 这是一个实现简单, 思路也简单的算法. 它给我打开了新的视野