Skip to content

Commit

Permalink
Merge pull request #330 from cicirello/cache-hashcode
Browse files Browse the repository at this point in the history
Cache hash on first call to Permutation.hashCode()
  • Loading branch information
cicirello committed Apr 13, 2023
2 parents c71701a + 0c2d3c2 commit 95ef1cf
Show file tree
Hide file tree
Showing 16 changed files with 233 additions and 58 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased] - 2023-04-07
## [Unreleased] - 2023-04-13

### Added

### Changed
* Permutation now caches hash on first call to hashCode() to optimize applications that rely heavily on hashing.

### Deprecated

Expand Down
86 changes: 72 additions & 14 deletions src/main/java/org/cicirello/permutations/Permutation.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,27 @@
public final class Permutation
implements Serializable, Iterable<Permutation>, Copyable<Permutation> {

private static final long serialVersionUID = 1L;
private static final long serialVersionUID = 2L;

/**
* Raw permutation, which should consist of a permutation of the integers in [0,
* permutation.length).
*/
private final int[] permutation;

/*
* Class caches the hashCode the first time hashCode() is called
* to avoid cost of recomputing in applications that rely on HashSets or HashMaps
* of Permutations, etc with heavy use of the hashCode.
*/
private transient int hashCode;

/*
* Flag for validating/invalidating cache of hashCode. All methods
* that change state of Permutation must invalidate the cache.
*/
private transient boolean hashCodeIsCached;

/**
* Initializes a random permutation of n integers. Uses {@link ThreadLocalRandom} as the source of
* efficient random number generation.
Expand Down Expand Up @@ -159,6 +172,8 @@ private Permutation(int[] p, boolean validate) {
*/
public Permutation(Permutation p) {
permutation = p.permutation.clone();
hashCodeIsCached = p.hashCodeIsCached;
hashCode = p.hashCode;
}

/**
Expand Down Expand Up @@ -197,6 +212,7 @@ public Permutation(Permutation p, int length) {
*/
public void apply(PermutationUnaryOperator operator) {
operator.apply(permutation);
hashCodeIsCached = false;
}

/**
Expand All @@ -206,6 +222,7 @@ public void apply(PermutationUnaryOperator operator) {
*/
public void apply(PermutationFullUnaryOperator operator) {
operator.apply(permutation, this);
hashCodeIsCached = false;
}

/**
Expand All @@ -218,6 +235,8 @@ public void apply(PermutationFullUnaryOperator operator) {
*/
public void apply(PermutationBinaryOperator operator, Permutation other) {
operator.apply(permutation, other.permutation);
hashCodeIsCached = false;
other.hashCodeIsCached = false;
}

/**
Expand All @@ -230,6 +249,8 @@ public void apply(PermutationBinaryOperator operator, Permutation other) {
*/
public void apply(PermutationFullBinaryOperator operator, Permutation other) {
operator.apply(permutation, other.permutation, this, other);
hashCodeIsCached = false;
other.hashCodeIsCached = false;
}

/**
Expand All @@ -243,6 +264,7 @@ public void apply(PermutationFullBinaryOperator operator, Permutation other) {
public void applyThenValidate(PermutationUnaryOperator operator) {
try {
operator.apply(permutation);
hashCodeIsCached = false;
validate(permutation);
} catch (IllegalArgumentException exception) {
throw new IllegalPermutationStateException(
Expand All @@ -261,6 +283,7 @@ public void applyThenValidate(PermutationUnaryOperator operator) {
public void applyThenValidate(PermutationFullUnaryOperator operator) {
try {
operator.apply(permutation, this);
hashCodeIsCached = false;
validate(permutation);
} catch (IllegalArgumentException exception) {
throw new IllegalPermutationStateException(
Expand All @@ -282,6 +305,8 @@ public void applyThenValidate(PermutationFullUnaryOperator operator) {
public void applyThenValidate(PermutationBinaryOperator operator, Permutation other) {
try {
operator.apply(permutation, other.permutation);
hashCodeIsCached = false;
other.hashCodeIsCached = false;
validate(permutation);
validate(other.permutation);
} catch (IllegalArgumentException exception) {
Expand All @@ -305,6 +330,8 @@ public void applyThenValidate(PermutationBinaryOperator operator, Permutation ot
public void applyThenValidate(PermutationFullBinaryOperator operator, Permutation other) {
try {
operator.apply(permutation, other.permutation, this, other);
hashCodeIsCached = false;
other.hashCodeIsCached = false;
validate(permutation);
validate(other.permutation);
} catch (IllegalArgumentException exception) {
Expand Down Expand Up @@ -413,6 +440,7 @@ public Permutation getInversePermutation() {
*/
public void invert() {
System.arraycopy(getInverse(), 0, permutation, 0, permutation.length);
hashCodeIsCached = false;
}

/**
Expand Down Expand Up @@ -444,6 +472,7 @@ public void scramble(RandomGenerator r) {
permutation[j] = i;
}
}
hashCodeIsCached = false;
}
}

Expand Down Expand Up @@ -471,13 +500,14 @@ public void scramble(RandomGenerator r, boolean guaranteeDifferent) {
for (int i = permutation.length - 1; i > 1; i--) {
int j = RandomIndexer.nextInt(i + 1, r);
if (i != j) {
swap(i, j);
internalSwap(i, j);
changed = true;
}
}
if (permutation.length > 1 && (!changed || r.nextBoolean())) {
swap(0, 1);
internalSwap(0, 1);
}
hashCodeIsCached = false;
} else {
scramble(r);
}
Expand Down Expand Up @@ -509,22 +539,23 @@ public void scramble(int i, int j, RandomGenerator r) {
if (i == j) {
return;
}
int k = j;
if (i > j) {
int temp = i;
k = i;
i = j;
j = temp;
}
boolean changed = false;
for (int k = j; k > i + 1; k--) {
for (; k > i + 1; k--) {
int l = i + RandomIndexer.nextInt(k - i + 1, r);
if (l != k) {
swap(l, k);
internalSwap(l, k);
changed = true;
}
}
if (!changed || r.nextBoolean()) {
swap(i, i + 1);
internalSwap(i, i + 1);
}
hashCodeIsCached = false;
}

/**
Expand All @@ -544,13 +575,14 @@ public void scramble(int[] indexes, RandomGenerator r) {
for (int j = indexes.length - 1; j > 1; j--) {
int i = RandomIndexer.nextInt(j + 1, r);
if (i != j) {
swap(indexes[i], indexes[j]);
internalSwap(indexes[i], indexes[j]);
changed = true;
}
}
if (!changed || r.nextBoolean()) {
swap(indexes[0], indexes[1]);
internalSwap(indexes[0], indexes[1]);
}
hashCodeIsCached = false;
}
}

Expand Down Expand Up @@ -667,6 +699,7 @@ public void swap(int i, int j) {
int temp = permutation[i];
permutation[i] = permutation[j];
permutation[j] = temp;
hashCodeIsCached = false;
}

/**
Expand All @@ -688,6 +721,7 @@ public void cycle(int[] indexes) {
permutation[indexes[i - 1]] = permutation[indexes[i]];
}
permutation[indexes[indexes.length - 1]] = temp;
hashCodeIsCached = false;
}
}

Expand Down Expand Up @@ -718,14 +752,16 @@ public void swapBlocks(int a, int b, int i, int j) {
System.arraycopy(permutation, b + 1, temp, k, m);
System.arraycopy(permutation, a, temp, k + m, b - a + 1);
System.arraycopy(temp, 0, permutation, a, temp.length);
hashCodeIsCached = false;
}
}

/** Reverses the order of the elements in the permutation. */
public void reverse() {
for (int i = 0, j = permutation.length - 1; i < j; i++, j--) {
swap(i, j);
internalSwap(i, j);
}
hashCodeIsCached = false;
}

/**
Expand All @@ -739,13 +775,14 @@ public void reverse() {
public void reverse(int i, int j) {
if (i > j) {
for (; i > j; i--, j++) {
swap(i, j);
internalSwap(i, j);
}
} else {
for (; i < j; i++, j--) {
swap(i, j);
internalSwap(i, j);
}
}
hashCodeIsCached = false;
}

/**
Expand All @@ -762,10 +799,12 @@ public void removeAndInsert(int i, int j) {
int n = permutation[i];
System.arraycopy(permutation, i + 1, permutation, i, j - i);
permutation[j] = n;
hashCodeIsCached = false;
} else if (i > j) {
int n = permutation[i];
System.arraycopy(permutation, j, permutation, j + 1, i - j);
permutation[j] = n;
hashCodeIsCached = false;
}
}

Expand All @@ -784,6 +823,7 @@ public void rotate(int numPositions) {
System.arraycopy(
permutation, numPositions, permutation, 0, permutation.length - numPositions);
System.arraycopy(temp, 0, permutation, permutation.length - numPositions, numPositions);
hashCodeIsCached = false;
}
}

Expand All @@ -809,11 +849,13 @@ public void removeAndInsert(int i, int size, int j) {
System.arraycopy(permutation, j, temp, 0, i - j);
System.arraycopy(permutation, i, permutation, j, size);
System.arraycopy(temp, 0, permutation, j + size, i - j);
hashCodeIsCached = false;
} else { // Condition is implied by above: if (i < j)
int[] temp = new int[size];
System.arraycopy(permutation, i, temp, 0, size);
System.arraycopy(permutation, i + size, permutation, i, j - i);
System.arraycopy(temp, 0, permutation, j, size);
hashCodeIsCached = false;
}
}

Expand All @@ -831,6 +873,7 @@ public void set(int[] p) {
}
validate(p);
System.arraycopy(p, 0, permutation, 0, p.length);
hashCodeIsCached = false;
}

/**
Expand Down Expand Up @@ -892,7 +935,11 @@ public boolean equals(Object other) {
*/
@Override
public int hashCode() {
return Arrays.hashCode(permutation);
if (hashCodeIsCached) {
return hashCode;
}
hashCodeIsCached = true;
return hashCode = Arrays.hashCode(permutation);
}

private boolean validate(int[] p) {
Expand All @@ -909,4 +956,15 @@ private boolean validate(int[] p) {
}
return true;
}

/*
* Use internally, such as from reverse, etc to avoid
* repeatedly invalidating hashCode cache (as well as from
* the PermutationIterator).
*/
final void internalSwap(int i, int j) {
int temp = permutation[i];
permutation[i] = permutation[j];
permutation[j] = temp;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@
*/
public class PermutationIterator implements Iterator<Permutation> {

private Permutation p;
private int[] lastSwap;
private final Permutation p;
private final int[] lastSwap;
private boolean done;

/**
Expand Down Expand Up @@ -92,14 +92,14 @@ public Permutation next() {
done = true;
} else {
for (int i = lastSwap.length - 2; i >= 0; i--) {
if (lastSwap[i] != i) p.swap(i, lastSwap[i]);
if (lastSwap[i] != i) p.internalSwap(i, lastSwap[i]);
if (lastSwap[i] == lastSwap.length - 1) {
lastSwap[i] = i;
if (i == 0) done = true;
continue;
}
lastSwap[i]++;
p.swap(i, lastSwap[i]);
p.internalSwap(i, lastSwap[i]);
break;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* JavaPermutationTools: A Java library for computation on permutations and sequences
* Copyright 2005-2022 Vincent A. Cicirello, <https://www.cicirello.org/>.
* Copyright 2005-2023 Vincent A. Cicirello, <https://www.cicirello.org/>.
*
* This file is part of JavaPermutationTools (https://jpt.cicirello.org/).
*
Expand Down Expand Up @@ -155,6 +155,7 @@ public void testPermutationCopyConstructor() {
Permutation copy = new Permutation(p);
assertEquals(p, copy);
assertEquals(p.hashCode(), copy.hashCode());
assertEquals(p.hashCode(), copy.hashCode());
}
}
}
Expand All @@ -168,6 +169,7 @@ public void testPermutationCopyMethod() {
assertEquals(p, copy);
assertTrue(p != copy);
assertEquals(p.hashCode(), copy.hashCode());
assertEquals(p.hashCode(), copy.hashCode());
}
}
}
Expand Down
Loading

0 comments on commit 95ef1cf

Please sign in to comment.