package com.limegroup.gnutella.util;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
/**
* @Author Anurag Singla
*/
/**
* It stores only fixed number of entries as specified while constructing
* an instance of this class.
* It stores HashMap of Object => Weighable
* As it is of fixed size, it might need to remove some entry while inserting
* another one.
* The victim entry to be removed, when the hashmap is full,
* is found using an approximation algorithm, that doesnt weigh much.
* To be specific, the entry removed is the one that has weight which is
* less than k * (average weight in the map) (which is equivalent to
average-weight-in-the map + (k-1)*average-weight-in-the-map),
* where k > 1 (ideal value of k lies in the range (1,2]). Its better to
* choose it more towards 1. We have chosen in this implementation, value of
* k as 1.1
* This assures that the entries which are important (relatively more weighted)
* will not get chosen as the victim entry.
* Although its more complex than the actual HashMap class, but still all the
* supported operations in the class are O(1). This has been achieved by
* amortizing the operation to find victim. And the time complexity can be
* proved easily.
* Note: The instances of this class are not thread safe. Therefore access to
* it should be externally snchronized, if required
* @see Weighable
*/
public class WeightBasedHashMap
{
/**
* Underlying hash map storage
*/
HashMap hashMap;
/**
* The number of entries in the underlying hashMap
*/
private int numOfEntries = 0;
/**
* The max number of entries we should store
*/
private int maxEntries = 0;
/**
* Sum of the weights of all the entries in the underlying hashMap
*/
private long sumOfWeights = 0;
/**
* Stores probable removable entries (i.e ones having lesser value)
* from hashMap
*/
private HashSet probableRemovableEntries;
/**
* Max number of entries to be stored in probableRemovableEntries
* @see probableRemovableEntries
*/
private int maxProbableRemovableEntries = 0;
/**
* Allocate space to store sufficient number of entries
* @param maxSize maximum number of entries to be stored in the underlying
* datastructure
* @exception IllegalArgumentException if maxSize is less < 1,
*/
public WeightBasedHashMap(int maxSize)
{
//check for the valid value for size
if (maxSize < 1)
throw new IllegalArgumentException();
//create hashMap with sufficient capacity
hashMap = new HashMap((int)(maxSize / 0.75 + 10) , 0.75f);
//initialize maxEntries to maxSize
maxEntries = maxSize;
//allocate space for probableRemovableEntries
probableRemovableEntries =
new HashSet((int)(maxSize / 0.75 + 5), 0.75f); //~10%
//set maxProbableRemovableEntries to 1/10 of the total number of entries
//we store
maxProbableRemovableEntries = maxSize/10 + 1;
}
/**
* Increment the weight corresponding to the given key by 1
* @param key The key for which the count to be incremented
* @return true, if the entry was present as count got incremented,
* false otherwise
*/
public boolean incrementWeight(Object key)
{
Weighable weighable = null;
//get the old value for the given key
weighable = (Weighable)hashMap.get(key);
if(weighable != null)
{
//increment the weight
weighable.addWeight(1);
//Increment the sumOfWeights
sumOfWeights++;
//return true
return true;
}
else //if the mapping doesnt exist
{
//return false;
return false;
}
}
/**
* Returns the value to which this map maps the specified key
* Note: The weight associated with the returned Weighable value
* shouldnt be altered externally
* @param key key whose associated value is to be returned
* @return the value to which this map maps the specified key
*/
public Weighable get(Object key)
{
//return from the underlying hashMap
return (Weighable)hashMap.get(key);
}
/**
* Removes the mapping for this key from this map if present.
* @param key The key whose mapping to be removed
* @return previous value associated with specified key,
* or null if there was no mapping for key.
*/
public Weighable remove(Object key)
{
//remove the entry and store the value the removed key mapped to
Weighable value = (Weighable)hashMap.remove(key);
if(value != null)
{
//adjust sum of weights
sumOfWeights -= value.getWeight();
//adjust num of entries
numOfEntries--;
}
//return the value corresponding to the removed key
return value;
}
/**
* stores the given key-value. It might remove some other entry from the
* underlying data structure to make space for that.
* @param key The key for the mapping
* @param value The weighable value
* @return The entry(key) removed to make space for this new key, null
* otherwise
*/
public Object add(Object key, Weighable value)
{
Object entryRemoved = null;
//insert it into the hashMap
Weighable oldValue = (Weighable)hashMap.put(key, value);
//update sum of Weights with this new entry
sumOfWeights = sumOfWeights + value.getWeight();
if (oldValue == null) //ie we added a new key
{
//increment the numOfEntries of entries
numOfEntries++;
//if the numOfEntries is more than the maxEntries,
//we should delete some entry
if(numOfEntries > maxEntries)
{
//remove some less weighted entry
//it also adjustes sumOfWeights as well as numEntries
entryRemoved = removeSomeLessWeightedEntry();
}
}
else //we didnt add anything new, but updated the old mapping
{
//Adjust sum of Weights
sumOfWeights = sumOfWeights - oldValue.getWeight();
//no need to update the numOfEntries as we didnt add anything new
}
//return the removed entry
return entryRemoved;
}
/**
* It removes a low weigt entry
* @modifies sumOfWeights, numOfEntries
*/
private Object removeSomeLessWeightedEntry()
{
//see if there's anything in the probable list that we can remove
if(probableRemovableEntries.size() <= 0)
{
//fill the array from where we can pick some entry to be removed
fillProbableRemovableEntries();
}
//remove an entry
Object entryRemoved = probableRemovableEntries.iterator().next();
//store the value corresponding to the key removed
Weighable removedValue =
(Weighable)hashMap.remove(entryRemoved);
//remove it from probableRemovableEntries also
probableRemovableEntries.remove(entryRemoved);
//decrement the count of entries
numOfEntries--;
//update sum of weights
sumOfWeights = sumOfWeights - removedValue.getWeight();
//return the removed entry
return entryRemoved;
}
/**
* Checks if the given query is frequent enough
* @param value The Weighable to be tested for weight
* @return true, if the object has enough weigh (more than
* average + some constant), false
* otherwise
*/
public boolean isWeightedEnough(Weighable value)
{
//get the average
int average = (int)( sumOfWeights / numOfEntries) ;
//give some margin over average
if(value.getWeight() > average + 5)
{
return true;
}
else
{
return false;
}
}
/**
* checks if the hash Map is full or not
* @return true if the map is full, false otherwise
*/
public boolean isFull()
{
return numOfEntries >= maxEntries;
}
/**
* Fills the probableRemovableEntries set
* @see probableRemovableEntries
*/
private void fillProbableRemovableEntries()
{
//get the iterator for the entries in the hashMap
Iterator iterator = hashMap.entrySet().iterator();
//calculate the current average
float avg = sumOfWeights / numOfEntries;
int scaledAvg = (int)(1.1 * avg) + 1;
Weighable weighable;
Map.Entry entry;
//iterate over the elements till we fill up the cache
while(iterator.hasNext() && probableRemovableEntries.size() <
maxProbableRemovableEntries)
{
//get the next entry
entry = (Map.Entry)iterator.next();
//get the weighable
weighable = (Weighable)entry.getValue();
//if the value is less than or close to avg, we can put it in
//the removable list
if(weighable.getWeight() < scaledAvg)
{
probableRemovableEntries.add(entry.getKey());
}
}
}
/**
* Returns a collection view of the mappings contained in this map.
* Each element in the returned collection is a Map.Entry
* @return A collection view of the mappings contained in this map.
*/
public Set entrySet()
{
return hashMap.entrySet();
}
/**
* Returns the string representation of mapping
* @return The string representation of this
*/
public String toString()
{
return hashMap.toString();
}
}