【问题标题】:Calculate mean of HashMap values after insertion计算插入后 HashMap 值的平均值
【发布时间】:2014-07-05 00:15:58
【问题描述】:

我想在每次插入新的键/值对时有效地计算 HashMap 的两种值的均值。

假设我们目前有这个HashMap<Double, Double>

3 4
5 6
8 8
1 3
6 8 <- Latest insertion

最新插入的是键6,值为8

要计算的第一个平均值包括所有键小于插入键的值,即6

这些是键3,5,1 的值4,6,3,所以平均值是(4+6+3)/3=4.3...

第二个平均值是“相反”,因此所有大于6的键的所有值的平均值。

带有值1 的键8 给出了这个平均值为8/1=8

现在,插入一个新的密钥/对:

3 4
5 6
6 8
8 8
1 3
4 9 <- Latest insertion

同样,我们需要计算所有键值小于4 的值的平均值。

这些是键 3,1 的值 4,3,因此“较小的平均值”现在是 (4+3)/2=3.5

对于键/值对 5/6,6/8,8/8,“更大的平均值”现在是 (6+8+8)/3=7.3...

一个简单的实现可能是这样的:

public class CalculateMapMean {

        private double smallerMean = 0.0;
        private double greaterMean = 0.0;

        private HashMap<Double, Double> someMap = new HashMap<Double, Double>();

        public void calculateMeans(double latestInsertedKey) {
            double sumGreater = 0;
            double sumSmaller = 0;
            double sumGreaterCount = 0;
            double sumSmallerCount = 0;
            for (Map.Entry<Double, Double> entry : someMap.entrySet()) {
                double key = entry.getKey();
                double value = entry.getValue();
                if (key > latestInsertedKey) {
                    sumGreater += value;
                    ++sumGreaterCount;
                }
                else if (key < latestInsertedKey) {
                    sumSmaller += value;
                    ++sumSmallerCount;
                }
            }
            if (sumGreaterCount != 0) {
                greaterMean = sumGreater / sumGreaterCount;
            }
            else {
                greaterMean = 0.0;
            }
            if (sumSmallerCount != 0) {
                smallerMean = sumSmaller / sumSmallerCount;
            }
            else {
                smallerMean = 0.0;
            }
        }
    }

问题是,是否可以使用TreeMap 或其他数据结构显着改进均值的计算,这样就不会在每次插入时遍历所有键。

有没有优雅的方式重用以前的计算?

【问题讨论】:

    标签: java performance algorithm map mean


    【解决方案1】:

    我能想到的让每次更改地图的时间都低于O(n) 的唯一方法是使用键保持平衡二叉搜索树 (BBST)。在每个节点中,您需要保留一些额外的字段

    • 以该节点为根的子树中的节点数
    • 以该节点为根的所有节点的值的总和

    在插入/删除后重新平衡 BBST 需要 O(log n) 时间。在相同的余额操作中,您也可以在 O(log n) 时间内更新计数和总和(因为您执行 O(log n) 操作需要 O(1) 时间)。

    要获得正确的意味着您需要遍历树并添加正确的计数。让我们举一个简单的例子。假设我有以下 7 个键值对。我希望你能想象对应的 BBST 会是什么样子。

    (3, 5) (4, 3) (7, 1) (8, 4) (11, 3) (12, 1)(13, 3)
    

    在根 - (8, 4) - 存储总数和总和:[7, 20]。在左子树的根 - (4, 3) - 存储该子树的总计数和总和:[3, 9]。我现在将这些额外的值绘制为树中深度的函数:

    [         7, 20        ]
    [   3, 9   ][   3, 7   ]
    [1, 5][1, 1][1, 3][1, 3]
    

    假设我现在添加一个键为 10 的新元组。我开始遍历树根。因为8 &lt; 10,我不需要遍历左子树:该子树中的所有键都小于10,所以我们可以使用缓存值[3, 9]。对于右子树,我们需要递归,因为有些键可能小于 10,有些可能更大。那里我们不用遍历右子树,因为12 &gt; 10,所以我们可以直接使用[1, 3]

    在树的每一层中,我们可以忽略一个分支并递归另一个分支。因此,查找小于最后插入的键和大于最后插入的键的键的总值和计数也需要O(log n) 时间。

    【讨论】:

    • @downvoter:想解释一下怎么回事?如果这种方法存在概念上的问题,如果仍然没有说出来,它对任何人都无济于事。
    • 感谢您的建议。我想到了类似的东西,并对开箱即用的有效解决方案感到好奇。
    • @Juergen 当然我不能确定,但​​是如果存在针对这个问题的开箱即用的数据结构,我会感到非常惊讶。当然,您仍然可以找到一个 BBST 实现并对其进行修改,因为正确实现它绝对不是一件容易的事!
    【解决方案2】:

    是的,TreeSet 会有所帮助。

    假设有一个带有e=(k,v) 的元素进来。如果将元组保存在树集中,则可以使用tailSet(e) 来获取所有值大于v 的元素。 headSet(e) 也是如此。然后,您通常可以在成本为O(n*log(n)) 的情况下找到这些集合中数字的平均值,然后插入成本为O(log(n)) 的新元组。

    我相信您可以通过使用平衡二叉树来加快速度,除了键和值之外,它还跟踪具有较低键的元素的数量及其平均值。同样对于具有较高值的​​右分支的元素。然后,当一个新元素进来时,你对它的插入点进行二分搜索,并跟踪你遇到的平均值,适当地构造更高和更低数字的平均值。我认为实现平衡位会很棘手,因为一切都会移动,您必须确保average 标签的完整性。

    也就是说,我建议您只使用 TreeSet。

    【讨论】:

    • 不是在枚举TreeSet 线性时间吗?如果您以Austern et al. 提出的模块化方式实现二叉树,则维护平均标签(更可能是总和标签和计数标签)并不难。
    • 我想我反应太快了,你是对的,这根本没有帮助,忽略答案的第一部分,因为无论如何我们都会查看树集的所有元素。我认为最好的方法是使用我上面解释的二叉树。
    【解决方案3】:

    您可以将这些值存储在您的实现中,例如:

    public class MyHashMap extends HashMap<Double, Double> {
        private double sum = 0;
    
        @Override
        public void put(Double key, Double value) {
            super (key, value);
            if (containsKey(key)) {
                sum -= get(key);
            }
            sum += value;
            super(key, value);
        }
    
        @Override
        public void putAll(Map<? extends Double, ? extends Double> map) {
            for (Map.Entry<? extends Double, ? extends Double> entry: map) {
                put(entry.getKey(), entry.getValue());
            }
        }
    
        @Override
        public void remove(Object key) {
            Double value = get(key);
            if (value != null)
                sum -= value;
            super(key);
        }
    
        public double getMean() {
            return sum / size();
        }
    }
    

    【讨论】:

    • 这是不正确的。 OP 想要 two 意味着:小于最后插入的键的所有键的平均值和大于最后插入的键的所有键的平均值。没有办法通过保持一个总和来实现这一点。
    • 但这是除插入值之外的所有值的平均值,不是吗?我需要两种方法,具体取决于密钥。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-02-06
    • 2012-06-19
    • 2013-01-14
    • 1970-01-01
    相关资源
    最近更新 更多